From 73b82ff771118f7ce9a05d41b0a52ef08a1f3150 Mon Sep 17 00:00:00 2001 From: Maxime Bloch Date: Tue, 4 Aug 2020 03:32:02 +0200 Subject: [PATCH] Add timeslots --- events/admin.py | 25 ++++- events/migrations/0002_auto_20200804_0004.py | 104 +++++++++++++++++++ events/models.py | 34 ++++-- events/tasks.py | 40 +++---- events/templates/events/index.html | 26 +++-- events/templates/events/registrations.html | 10 +- events/urls.py | 4 +- events/views.py | 28 +++-- users/models.py | 3 + 9 files changed, 212 insertions(+), 62 deletions(-) create mode 100644 events/migrations/0002_auto_20200804_0004.py diff --git a/events/admin.py b/events/admin.py index 6d41955..2d80bf8 100644 --- a/events/admin.py +++ b/events/admin.py @@ -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) diff --git a/events/migrations/0002_auto_20200804_0004.py b/events/migrations/0002_auto_20200804_0004.py new file mode 100644 index 0000000..32a6f4f --- /dev/null +++ b/events/migrations/0002_auto_20200804_0004.py @@ -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'}), + ] diff --git a/events/models.py b/events/models.py index 89cdec7..2fdd95b 100644 --- a/events/models.py +++ b/events/models.py @@ -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] diff --git a/events/tasks.py b/events/tasks.py index f323745..6be220e 100644 --- a/events/tasks.py +++ b/events/tasks.py @@ -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(), - state=EventRegistration.ADMITTED) + 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 - r.save() + if not dry_run: + r.save() diff --git a/events/templates/events/index.html b/events/templates/events/index.html index 037cc86..91f3bc7 100644 --- a/events/templates/events/index.html +++ b/events/templates/events/index.html @@ -9,19 +9,25 @@ {% else %} diff --git a/events/templates/events/registrations.html b/events/templates/events/registrations.html index 6d4e807..07523ae 100644 --- a/events/templates/events/registrations.html +++ b/events/templates/events/registrations.html @@ -1,23 +1,23 @@ {% if not my_registration %} -
+ {% else %} - + {% endif %} {% csrf_token %} -

{{ event.count_admitted }}/{{ event.capacity }} bevestigd{% if event.count_interested %}, +

{{ timeslot.time_str }}, {{ timeslot.count_admitted }}/{{ event.capacity }} bevestigd{% if timeslot.count_interested %}, {{ event.count_interested }} op wachtlijst {% endif %}

{% if not user.is_authenticated %} -

Je moet inloggen voor je je kan inschrijven.

+

Je moet inloggen voor je je kan inschrijven.

{% elif not user.has_ugent_info %}

Je moet je studentennummer en echte naam invullen in je profiel voor je je kan inschrijven.

{% elif not my_registration %} {% else %}

Mijn status: {{ my_registration.state_str }}

- + {% endif %}
diff --git a/events/urls.py b/events/urls.py index 812d2d2..1b92767 100644 --- a/events/urls.py +++ b/events/urls.py @@ -5,6 +5,6 @@ from . import views app_name = "events" urlpatterns = [ path("", views.index, name="index"), - path("/register", views.register, name="register"), - path("/deregister", views.deregister, name="deregister"), + path("/register", views.register, name="register"), + path("/deregister", views.deregister, name="deregister"), ] diff --git a/events/views.py b/events/views.py index 7f093f3..b66d10b 100644 --- a/events/views.py +++ b/events/views.py @@ -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}") diff --git a/users/models.py b/users/models.py index e88fc4e..1490deb 100644 --- a/users/models.py +++ b/users/models.py @@ -23,6 +23,9 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): def __str__(self): return self.username + def __repr__(self): + return "".format(self.username) + @property def has_ugent_info(self): return self.real_name and self.student_number