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.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)

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):
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]

View file

@ -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()

View file

@ -9,19 +9,25 @@
<ul>
{% for x in events %}
<li id="{{x.event.id}}">
<h3>{{ x.event.date|date:"d F Y" }}, {{ x.event.time_str }}
{% if user.is_staff %}
<a href="/admin/events/event/{{ x.event.id }}/change/">Modificeren</a>
{% endif %}</h3>
<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>
<p>Verantwoordelijke: {{ x.event.responsible_person.username }}</p>
<p>Verantwoordelijke: {{ x.event.responsible_person.username }}</p>
{% if x.event.note %}
<p>Beschrijving: {{ x.event.note }}</p>
{% endif %}
{% if x.event.note %}
<p>Beschrijving: {{ x.event.note }}</p>
{% endif %}
{% include "events/registrations.html" with event=x.event my_registration=x.my_registration %}
</li>
<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>
{% else %}

View file

@ -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 %}

View file

@ -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"),
]

View file

@ -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}")

View file

@ -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