Merge branch 'add_timeslots' into 'master'
Add timeslots See merge request bestuur/kers!10
This commit is contained in:
commit
f3347c7ff0
9 changed files with 212 additions and 62 deletions
|
@ -1,7 +1,15 @@
|
|||
from django.contrib import admin
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Event, EventRegistration
|
||||
from .models import Event, EventRegistration, TimeSlot
|
||||
|
||||
|
||||
class TimeSlotFormSet(forms.BaseInlineFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs["initial"] = [
|
||||
{"time": TimeSlot.EVENING}
|
||||
]
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RegistrationFormSet(forms.BaseInlineFormSet):
|
||||
|
@ -12,12 +20,25 @@ class RegistrationFormSet(forms.BaseInlineFormSet):
|
|||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class TimeSlotInline(admin.StackedInline):
|
||||
model = TimeSlot
|
||||
extra = 1
|
||||
formset = TimeSlotFormSet
|
||||
|
||||
|
||||
class RegistrationInline(admin.TabularInline):
|
||||
model = EventRegistration
|
||||
extra = 1
|
||||
formset = RegistrationFormSet
|
||||
#
|
||||
|
||||
class EventAdmin(admin.ModelAdmin):
|
||||
inlines = [TimeSlotInline]
|
||||
|
||||
|
||||
class TimeSlotAdmin(admin.ModelAdmin):
|
||||
inlines = [RegistrationInline]
|
||||
|
||||
|
||||
admin.site.register(Event, EventAdmin)
|
||||
admin.site.register(TimeSlot, TimeSlotAdmin)
|
||||
|
|
104
events/migrations/0002_auto_20200804_0004.py
Normal file
104
events/migrations/0002_auto_20200804_0004.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
# Generated by Django 3.0.8 on 2020-08-03 22:04
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
def forwards_migrate_data(apps, schema_editor):
|
||||
EventRegistration = apps.get_model('events', 'EventRegistration')
|
||||
TimeSlot = apps.get_model('events', 'TimeSlot')
|
||||
Event = apps.get_model('events', 'Event')
|
||||
|
||||
grouped = {}
|
||||
for event in Event.objects.all():
|
||||
if event.note in grouped:
|
||||
grouped[event.note].append(event)
|
||||
else:
|
||||
grouped[event.note] = [event]
|
||||
|
||||
for note, events in grouped.items():
|
||||
event = events[0]
|
||||
new_event = Event(date=event.date, capacity=event.capacity, note=event.note,
|
||||
responsible_person=event.responsible_person)
|
||||
new_event.save()
|
||||
|
||||
# Create a corresponding timeslot for every event with the same description
|
||||
for event in events:
|
||||
new_timeslot = TimeSlot(time=event.time, event=new_event)
|
||||
new_timeslot.save()
|
||||
|
||||
# Connect registrations to the timeslots instead of the events.
|
||||
for registration in EventRegistration.objects.filter(event_id=event.id).all():
|
||||
registration.time_slot = new_timeslot
|
||||
registration.save()
|
||||
|
||||
def forwards_remove_old_data(apps, schema_editor):
|
||||
# Remove old events here so the cascade doesnt remove the registrations.
|
||||
EventRegistration = apps.get_model('events', 'EventRegistration')
|
||||
TimeSlot = apps.get_model('events', 'TimeSlot')
|
||||
Event = apps.get_model('events', 'Event')
|
||||
|
||||
# Remove all events without timeslot
|
||||
for event in Event.objects.exclude(id__in=TimeSlot.objects.values('event_id').all()):
|
||||
event.delete()
|
||||
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('events', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TimeSlot',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('time', models.IntegerField(choices=[(0, 'voormiddag'), (1, 'namiddag'), (2, 'avond')], default=0)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Event'))
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='eventregistration',
|
||||
name='time_slot',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.TimeSlot', null=True),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='timeslot',
|
||||
constraint=models.UniqueConstraint(fields=('time', 'event'), name='no_duplicate_timeslots'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='eventregistration',
|
||||
constraint=models.UniqueConstraint(fields=('time_slot', 'user'), name='register_only_once_per_timeslot'),
|
||||
),
|
||||
|
||||
migrations.RunPython(forwards_migrate_data, hints={'target_db': 'default'}),
|
||||
|
||||
migrations.RemoveField(
|
||||
model_name='event',
|
||||
name='time',
|
||||
),
|
||||
|
||||
# Django bugs out when removing constraints that are made on fk's. We manualy fix it here 2 'AlterField' calls
|
||||
# https://code.djangoproject.com/ticket/31335
|
||||
migrations.AlterField(
|
||||
model_name='eventregistration',
|
||||
name='event',
|
||||
field=models.ForeignKey(
|
||||
db_constraint=False,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='eventregistrations',
|
||||
related_query_name='eventregistration',
|
||||
to='events.Event',
|
||||
),
|
||||
),
|
||||
migrations.RemoveConstraint(
|
||||
model_name='eventregistration',
|
||||
name='register_only_once',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='eventregistration',
|
||||
name='event',
|
||||
),
|
||||
|
||||
migrations.RunPython(forwards_remove_old_data, hints={'target_db': 'default'}),
|
||||
]
|
|
@ -4,23 +4,37 @@ from users.models import CustomUser
|
|||
|
||||
|
||||
class Event(models.Model):
|
||||
date = models.DateField()
|
||||
capacity = models.IntegerField(default=6)
|
||||
note = models.TextField(max_length=1024, default="")
|
||||
responsible_person = models.ForeignKey(CustomUser, null=True, on_delete=models.SET_NULL)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.date}"
|
||||
|
||||
|
||||
class TimeSlot(models.Model):
|
||||
MORNING, AFTERNOON, EVENING = range(3)
|
||||
TIME_SLOTS = {
|
||||
MORNING: "voormiddag",
|
||||
AFTERNOON: "namiddag",
|
||||
EVENING: "avond",
|
||||
}
|
||||
date = models.DateField()
|
||||
time = models.IntegerField(choices=TIME_SLOTS.items(), default=MORNING)
|
||||
capacity = models.IntegerField(default=6)
|
||||
note = models.TextField(max_length=1024, default="")
|
||||
responsible_person = models.ForeignKey(CustomUser, null=True, on_delete=models.SET_NULL)
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["time", "event"], name="no_duplicate_timeslots"
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.date} {self.TIME_SLOTS[self.time]}"
|
||||
return f"{self.event.date} {self.TIME_SLOTS[self.time]}"
|
||||
|
||||
def time_str(self):
|
||||
return Event.TIME_SLOTS[self.time]
|
||||
return TimeSlot.TIME_SLOTS[self.time]
|
||||
|
||||
def count_with_status(self, status):
|
||||
return self.eventregistration_set.filter(state=status).count()
|
||||
|
@ -38,7 +52,7 @@ class Event(models.Model):
|
|||
registrations = self.eventregistration_set.filter(user=user).all()
|
||||
if not registrations:
|
||||
return None
|
||||
assert len(registrations) == 1, "Registrations should be unique per user and event"
|
||||
assert len(registrations) == 1, "Registrations should be unique per user and timeslot"
|
||||
return registrations[0]
|
||||
|
||||
|
||||
|
@ -49,19 +63,19 @@ class EventRegistration(models.Model):
|
|||
INTERESTED: "op wachtlijst",
|
||||
ADMITTED: "bevestigd",
|
||||
}
|
||||
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
||||
time_slot = models.ForeignKey(TimeSlot, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
|
||||
state = models.CharField(max_length=1, choices=REGISTRATION_STATE.items())
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["event", "user"], name="register_only_once"
|
||||
fields=["time_slot", "user"], name="register_only_once_per_timeslot"
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Reservation[{self.user.username}:{self.event.date}:{self.state}]"
|
||||
return f"Reservation[{self.user.username}:{self.time_slot.event.date}:{self.state}]"
|
||||
|
||||
def state_str(self):
|
||||
return EventRegistration.REGISTRATION_STATE[self.state]
|
||||
|
|
|
@ -2,39 +2,42 @@ from datetime import date, timedelta
|
|||
from typing import List
|
||||
|
||||
from KeRS.celery import app
|
||||
from events.models import Event, EventRegistration
|
||||
from events.models import Event, EventRegistration, TimeSlot
|
||||
from users.models import CustomUser
|
||||
|
||||
|
||||
def calc_score(user: CustomUser):
|
||||
registrations_last_month = EventRegistration.objects.filter(user_id=user.id,
|
||||
event__date__gt=date.today() - timedelta(days=30),
|
||||
event__date__lte=date.today(),
|
||||
registrations_last_month: List[EventRegistration] = EventRegistration.objects \
|
||||
.filter(user_id=user.id,
|
||||
time_slot__event__date__gt=date.today() - timedelta(days=30),
|
||||
time_slot__event__date__lte=date.today(),
|
||||
state=EventRegistration.ADMITTED)
|
||||
score = 0
|
||||
for r in registrations_last_month:
|
||||
days_ago = (date.today() - r.event.date).days
|
||||
days_ago = (date.today() - r.time_slot.event.date).days
|
||||
score += 1 / (days_ago + 1)
|
||||
|
||||
return score
|
||||
|
||||
|
||||
@app.task(bind=True)
|
||||
def assign_reservations(self):
|
||||
def assign_reservations(self, dry_run=False):
|
||||
"""
|
||||
Check if there are any events the next day.
|
||||
If so, calculate the current score for every interested user and assign the ones with the lowest scores.
|
||||
:param self:
|
||||
:param dry_run: If this is set to true, no persistent changes will be made.
|
||||
:return:
|
||||
"""
|
||||
print("Assigning reservations")
|
||||
print("======================")
|
||||
# Get all events of tomorrow
|
||||
events = Event.objects.filter(date=date.today() + timedelta(days=1))
|
||||
events: List[Event] = Event.objects.filter(date=date.today() + timedelta(days=1))
|
||||
# Get all timeslots of tomorrow
|
||||
timeslots: List[TimeSlot] = TimeSlot.objects.filter(event_id__in=map(lambda e: e.id, events))
|
||||
|
||||
# Reservations
|
||||
registrations: List[EventRegistration] = EventRegistration.objects.filter(
|
||||
event_id__in=map(lambda event: event.id, events),
|
||||
time_slot_id__in=map(lambda timeslot: timeslot.id, timeslots),
|
||||
state=EventRegistration.INTERESTED)
|
||||
if len(registrations) == 0:
|
||||
print("NO REGISTRATIONS?")
|
||||
|
@ -46,17 +49,18 @@ def assign_reservations(self):
|
|||
print(f"Scores: {scores}")
|
||||
print(f"Queue: {queue}")
|
||||
|
||||
for event in events:
|
||||
print(f"EVENT: {event.date} - {event.capacity}")
|
||||
event_registrations = list(filter(lambda r: r.event == event, registrations))
|
||||
event_users = set(map(lambda r: r.user, event_registrations))
|
||||
event_queue = list(filter(lambda element: element[0] in event_users, queue))
|
||||
for timeslot in timeslots:
|
||||
print(f"EVENT: {timeslot.event.date} - {timeslot.event.capacity}")
|
||||
timeslot_registrations = list(filter(lambda r: r.time_slot == timeslot, registrations))
|
||||
timeslot_users = set(map(lambda r: r.user, timeslot_registrations))
|
||||
timeslot_queue = list(filter(lambda element: element[0] in timeslot_users, queue))
|
||||
|
||||
for user in event_queue[0:event.capacity]:
|
||||
for user in timeslot_queue[0:timeslot.event.capacity]:
|
||||
print(f"Selected {user[0]}")
|
||||
r = EventRegistration.objects.get(
|
||||
event_id=event.id,
|
||||
time_slot_id=timeslot.id,
|
||||
user_id=user[0].id
|
||||
)
|
||||
r.state = EventRegistration.ADMITTED
|
||||
if not dry_run:
|
||||
r.save()
|
||||
|
|
|
@ -9,10 +9,11 @@
|
|||
<ul>
|
||||
{% for x in events %}
|
||||
<li id="{{x.event.id}}">
|
||||
<h3>{{ x.event.date|date:"d F Y" }}, {{ x.event.time_str }}
|
||||
<h3>{{ x.event.date|date:"d F Y" }}
|
||||
{% if user.is_staff %}
|
||||
<a href="/admin/events/event/{{ x.event.id }}/change/">Modificeren</a>
|
||||
{% endif %}</h3>
|
||||
{% endif %}
|
||||
</h3>
|
||||
|
||||
<p>Verantwoordelijke: {{ x.event.responsible_person.username }}</p>
|
||||
|
||||
|
@ -20,7 +21,12 @@
|
|||
<p>Beschrijving: {{ x.event.note }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% include "events/registrations.html" with event=x.event my_registration=x.my_registration %}
|
||||
<ul>
|
||||
{% for y in x.timeslots %}
|
||||
{% include "events/registrations.html" with event=x.event timeslot=y.timeslot my_registration=y.my_registration %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
{% if not my_registration %}
|
||||
<form action="{% url 'events:register' event.id %}" method="post">
|
||||
<form action="{% url 'events:register' timeslot.id %}" method="post">
|
||||
{% else %}
|
||||
<form action="{% url 'events:deregister' event.id %}" method="post">
|
||||
<form action="{% url 'events:deregister' timeslot.id %}" method="post">
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<p>{{ event.count_admitted }}/{{ event.capacity }} bevestigd{% if event.count_interested %},
|
||||
<p>{{ timeslot.time_str }}, {{ timeslot.count_admitted }}/{{ event.capacity }} bevestigd{% if timeslot.count_interested %},
|
||||
{{ event.count_interested }} op wachtlijst
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if not user.is_authenticated %}
|
||||
<p>Je moet inloggen voor je je kan inschrijven.</p>
|
||||
<p><i>Je moet inloggen voor je je kan inschrijven.</i></p>
|
||||
{% elif not user.has_ugent_info %}
|
||||
<p class="text--important">Je moet je studentennummer en echte naam invullen in je profiel voor je je kan inschrijven.</p>
|
||||
{% elif not my_registration %}
|
||||
|
|
|
@ -5,6 +5,6 @@ from . import views
|
|||
app_name = "events"
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index"),
|
||||
path("<int:event_id>/register", views.register, name="register"),
|
||||
path("<int:event_id>/deregister", views.deregister, name="deregister"),
|
||||
path("<int:timeslot_id>/register", views.register, name="register"),
|
||||
path("<int:timeslot_id>/deregister", views.deregister, name="deregister"),
|
||||
]
|
||||
|
|
|
@ -1,46 +1,44 @@
|
|||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
|
||||
from django.utils import timezone
|
||||
from django.urls import reverse
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import Event, EventRegistration, CustomUser
|
||||
|
||||
from events.tasks import assign_reservations
|
||||
from .models import Event, EventRegistration, TimeSlot
|
||||
|
||||
|
||||
def index(request):
|
||||
events = Event.objects.filter(date__gte=timezone.now().date()).order_by("date")[:20]
|
||||
|
||||
events_data = [
|
||||
{ "event": event, "my_registration": event.registration_of(request.user) }
|
||||
{"event": event, "timeslots": [{"timeslot": timeslot,
|
||||
"my_registration": timeslot.registration_of(request.user)}
|
||||
for timeslot in TimeSlot.objects.filter(event_id=event.id)]}
|
||||
for event in events
|
||||
]
|
||||
return render(request, "events/index.html", {"events": events_data})
|
||||
|
||||
|
||||
def register(request, event_id):
|
||||
def register(request, timeslot_id):
|
||||
if request.method != "POST":
|
||||
return HttpResponse(status_code=405)
|
||||
|
||||
if not request.user.has_ugent_info:
|
||||
raise ValueError("User has missing UGent info missing")
|
||||
|
||||
event = get_object_or_404(Event, id=event_id)
|
||||
timeslot = get_object_or_404(TimeSlot, id=timeslot_id)
|
||||
|
||||
event.eventregistration_set.create(
|
||||
timeslot.eventregistration_set.create(
|
||||
state=EventRegistration.INTERESTED,
|
||||
event=event,
|
||||
time_slot=timeslot,
|
||||
user=request.user,
|
||||
)
|
||||
return HttpResponseRedirect(reverse("events:index") + f"#{event.id}")
|
||||
return HttpResponseRedirect(reverse("events:index") + f"#{timeslot.id}")
|
||||
|
||||
|
||||
def deregister(request, event_id):
|
||||
def deregister(request, timeslot_id):
|
||||
if request.method != "POST":
|
||||
return HttpResponse(status_code=405)
|
||||
|
||||
registration = get_object_or_404(EventRegistration, event=event_id, user=request.user)
|
||||
registration = get_object_or_404(EventRegistration, time_slot=timeslot_id, user=request.user)
|
||||
registration.delete()
|
||||
return HttpResponseRedirect(reverse("events:index") + f"#{event_id}")
|
||||
return HttpResponseRedirect(reverse("events:index") + f"#{timeslot_id}")
|
||||
|
|
|
@ -23,6 +23,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
|
|||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
def __repr__(self):
|
||||
return "<User: {}>".format(self.username)
|
||||
|
||||
@property
|
||||
def has_ugent_info(self):
|
||||
return self.real_name and self.student_number
|
||||
|
|
Loading…
Reference in a new issue