Add timeslots

This commit is contained in:
Maxime Bloch 2020-08-04 03:32:02 +02:00
parent db9fbb1d7e
commit 73b82ff771
9 changed files with 212 additions and 62 deletions

View file

@ -1,7 +1,15 @@
from django.contrib import admin
from django import forms 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): class RegistrationFormSet(forms.BaseInlineFormSet):
@ -12,12 +20,25 @@ class RegistrationFormSet(forms.BaseInlineFormSet):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class TimeSlotInline(admin.StackedInline):
model = TimeSlot
extra = 1
formset = TimeSlotFormSet
class RegistrationInline(admin.TabularInline): class RegistrationInline(admin.TabularInline):
model = EventRegistration model = EventRegistration
extra = 1 extra = 1
formset = RegistrationFormSet formset = RegistrationFormSet
#
class EventAdmin(admin.ModelAdmin): class EventAdmin(admin.ModelAdmin):
inlines = [TimeSlotInline]
class TimeSlotAdmin(admin.ModelAdmin):
inlines = [RegistrationInline] inlines = [RegistrationInline]
admin.site.register(Event, EventAdmin) admin.site.register(Event, EventAdmin)
admin.site.register(TimeSlot, TimeSlotAdmin)

View 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'}),
]

View file

@ -4,23 +4,37 @@ from users.models import CustomUser
class Event(models.Model): 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) MORNING, AFTERNOON, EVENING = range(3)
TIME_SLOTS = { TIME_SLOTS = {
MORNING: "voormiddag", MORNING: "voormiddag",
AFTERNOON: "namiddag", AFTERNOON: "namiddag",
EVENING: "avond", EVENING: "avond",
} }
date = models.DateField()
time = models.IntegerField(choices=TIME_SLOTS.items(), default=MORNING) time = models.IntegerField(choices=TIME_SLOTS.items(), default=MORNING)
capacity = models.IntegerField(default=6) event = models.ForeignKey(Event, on_delete=models.CASCADE)
note = models.TextField(max_length=1024, default="")
responsible_person = models.ForeignKey(CustomUser, null=True, on_delete=models.SET_NULL) class Meta:
constraints = [
models.UniqueConstraint(
fields=["time", "event"], name="no_duplicate_timeslots"
),
]
def __str__(self): 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): def time_str(self):
return Event.TIME_SLOTS[self.time] return TimeSlot.TIME_SLOTS[self.time]
def count_with_status(self, status): def count_with_status(self, status):
return self.eventregistration_set.filter(state=status).count() return self.eventregistration_set.filter(state=status).count()
@ -38,7 +52,7 @@ class Event(models.Model):
registrations = self.eventregistration_set.filter(user=user).all() registrations = self.eventregistration_set.filter(user=user).all()
if not registrations: if not registrations:
return None 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] return registrations[0]
@ -49,19 +63,19 @@ class EventRegistration(models.Model):
INTERESTED: "op wachtlijst", INTERESTED: "op wachtlijst",
ADMITTED: "bevestigd", 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) user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
state = models.CharField(max_length=1, choices=REGISTRATION_STATE.items()) state = models.CharField(max_length=1, choices=REGISTRATION_STATE.items())
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
fields=["event", "user"], name="register_only_once" fields=["time_slot", "user"], name="register_only_once_per_timeslot"
), ),
] ]
def __str__(self): 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): def state_str(self):
return EventRegistration.REGISTRATION_STATE[self.state] return EventRegistration.REGISTRATION_STATE[self.state]

View file

@ -2,39 +2,42 @@ from datetime import date, timedelta
from typing import List from typing import List
from KeRS.celery import app from KeRS.celery import app
from events.models import Event, EventRegistration from events.models import Event, EventRegistration, TimeSlot
from users.models import CustomUser from users.models import CustomUser
def calc_score(user: CustomUser): def calc_score(user: CustomUser):
registrations_last_month = EventRegistration.objects.filter(user_id=user.id, registrations_last_month: List[EventRegistration] = EventRegistration.objects \
event__date__gt=date.today() - timedelta(days=30), .filter(user_id=user.id,
event__date__lte=date.today(), time_slot__event__date__gt=date.today() - timedelta(days=30),
time_slot__event__date__lte=date.today(),
state=EventRegistration.ADMITTED) state=EventRegistration.ADMITTED)
score = 0 score = 0
for r in registrations_last_month: 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) score += 1 / (days_ago + 1)
return score return score
@app.task(bind=True) @app.task(bind=True)
def assign_reservations(self): def assign_reservations(self, dry_run=False):
""" """
Check if there are any events the next day. 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. 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: :return:
""" """
print("Assigning reservations") print("Assigning reservations")
print("======================") print("======================")
# Get all events of tomorrow # 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 # Reservations
registrations: List[EventRegistration] = EventRegistration.objects.filter( 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) state=EventRegistration.INTERESTED)
if len(registrations) == 0: if len(registrations) == 0:
print("NO REGISTRATIONS?") print("NO REGISTRATIONS?")
@ -46,17 +49,18 @@ def assign_reservations(self):
print(f"Scores: {scores}") print(f"Scores: {scores}")
print(f"Queue: {queue}") print(f"Queue: {queue}")
for event in events: for timeslot in timeslots:
print(f"EVENT: {event.date} - {event.capacity}") print(f"EVENT: {timeslot.event.date} - {timeslot.event.capacity}")
event_registrations = list(filter(lambda r: r.event == event, registrations)) timeslot_registrations = list(filter(lambda r: r.time_slot == timeslot, registrations))
event_users = set(map(lambda r: r.user, event_registrations)) timeslot_users = set(map(lambda r: r.user, timeslot_registrations))
event_queue = list(filter(lambda element: element[0] in event_users, queue)) 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]}") print(f"Selected {user[0]}")
r = EventRegistration.objects.get( r = EventRegistration.objects.get(
event_id=event.id, time_slot_id=timeslot.id,
user_id=user[0].id user_id=user[0].id
) )
r.state = EventRegistration.ADMITTED r.state = EventRegistration.ADMITTED
if not dry_run:
r.save() r.save()

View file

@ -9,10 +9,11 @@
<ul> <ul>
{% for x in events %} {% for x in events %}
<li id="{{x.event.id}}"> <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 %} {% if user.is_staff %}
<a href="/admin/events/event/{{ x.event.id }}/change/">Modificeren</a> <a href="/admin/events/event/{{ x.event.id }}/change/">Modificeren</a>
{% endif %}</h3> {% endif %}
</h3>
<p>Verantwoordelijke: {{ x.event.responsible_person.username }}</p> <p>Verantwoordelijke: {{ x.event.responsible_person.username }}</p>
@ -20,7 +21,12 @@
<p>Beschrijving: {{ x.event.note }}</p> <p>Beschrijving: {{ x.event.note }}</p>
{% endif %} {% 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> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View file

@ -1,16 +1,16 @@
{% if not my_registration %} {% if not my_registration %}
<form action="{% url 'events:register' event.id %}" method="post"> <form action="{% url 'events:register' timeslot.id %}" method="post">
{% else %} {% else %}
<form action="{% url 'events:deregister' event.id %}" method="post"> <form action="{% url 'events:deregister' timeslot.id %}" method="post">
{% endif %} {% endif %}
{% csrf_token %} {% 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 {{ event.count_interested }} op wachtlijst
{% endif %} {% endif %}
</p> </p>
{% if not user.is_authenticated %} {% 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 %} {% 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> <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 %} {% elif not my_registration %}

View file

@ -5,6 +5,6 @@ from . import views
app_name = "events" app_name = "events"
urlpatterns = [ urlpatterns = [
path("", views.index, name="index"), path("", views.index, name="index"),
path("<int:event_id>/register", views.register, name="register"), path("<int:timeslot_id>/register", views.register, name="register"),
path("<int:event_id>/deregister", views.deregister, name="deregister"), path("<int:timeslot_id>/deregister", views.deregister, name="deregister"),
] ]

View file

@ -1,46 +1,44 @@
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from django.urls import reverse from django.urls import reverse
import datetime from django.utils import timezone
from .models import Event, EventRegistration, CustomUser from .models import Event, EventRegistration, TimeSlot
from events.tasks import assign_reservations
def index(request): def index(request):
events = Event.objects.filter(date__gte=timezone.now().date()).order_by("date")[:20] events = Event.objects.filter(date__gte=timezone.now().date()).order_by("date")[:20]
events_data = [ 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 for event in events
] ]
return render(request, "events/index.html", {"events": events_data}) return render(request, "events/index.html", {"events": events_data})
def register(request, event_id): def register(request, timeslot_id):
if request.method != "POST": if request.method != "POST":
return HttpResponse(status_code=405) return HttpResponse(status_code=405)
if not request.user.has_ugent_info: if not request.user.has_ugent_info:
raise ValueError("User has missing UGent info missing") 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, state=EventRegistration.INTERESTED,
event=event, time_slot=timeslot,
user=request.user, 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": if request.method != "POST":
return HttpResponse(status_code=405) 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() registration.delete()
return HttpResponseRedirect(reverse("events:index") + f"#{event_id}") return HttpResponseRedirect(reverse("events:index") + f"#{timeslot_id}")

View file

@ -23,6 +23,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
def __str__(self): def __str__(self):
return self.username return self.username
def __repr__(self):
return "<User: {}>".format(self.username)
@property @property
def has_ugent_info(self): def has_ugent_info(self):
return self.real_name and self.student_number return self.real_name and self.student_number