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 @@
Verantwoordelijke: {{ x.event.responsible_person.username }}
+Verantwoordelijke: {{ x.event.responsible_person.username }}
- {% if x.event.note %} -Beschrijving: {{ x.event.note }}
- {% endif %} + {% if x.event.note %} +Beschrijving: {{ x.event.note }}
+ {% endif %} - {% include "events/registrations.html" with event=x.event my_registration=x.my_registration %} -