diff --git a/app/base/migrations/0005_person_last_visit.py b/app/base/migrations/0005_person_last_visit.py new file mode 100644 index 0000000..a660720 --- /dev/null +++ b/app/base/migrations/0005_person_last_visit.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2 on 2023-06-08 08:07 + +from django.db import migrations +from app.base.forms.fields import DateField +from datetime import date + + +def calc_last_visit(apps, schema_editor): + Person = apps.get_model('base', 'Person') + Booking = apps.get_model('base', 'Booking') + CourseVisit = apps.get_model('base', 'CourseVisit') + + max_dates = {} + zero = date.fromtimestamp(0) + + for (pk, day) in Booking.objects.values_list('user', 'begin_time__date'): + max_dates[pk] = max(day, max_dates.get(pk, zero)) + + for (pk1, pk2, day) in CourseVisit.objects.values_list( + 'participant', 'teacher', 'date'): + max_dates[pk1] = max(day, max_dates.get(pk1, zero)) + max_dates[pk2] = max(day, max_dates.get(pk2, zero)) + + for person in Person.objects.all(): + person.last_visit = max(person.created, max_dates.get(person.pk, zero)) + person.save() + + +def noop(apps, schema_editor): + pass # allow reverse migrate because field will be just deleted + + +class Migration(migrations.Migration): + + dependencies = [ + ('base', '0004_person_created_merge_street'), + ] + + operations = [ + migrations.AddField( + model_name='person', + name='last_visit', + field=DateField(verbose_name='Letzter Besuch', editable=False, + default=date.fromtimestamp(0)), + preserve_default=False, + ), + migrations.RunPython(calc_last_visit, noop), + ] diff --git a/app/base/models/booking.py b/app/base/models/booking.py index 35740a1..93ad5f9 100755 --- a/app/base/models/booking.py +++ b/app/base/models/booking.py @@ -41,6 +41,26 @@ class Booking(models.Model): to_current_timezone(self.end_time).strftime('%H:%M') if self.end_time else '') + def save(self, *args, **kwargs): + # update last_visit time of all involved persons + prev = Booking.objects.get(pk=self.pk) if self.pk else None + + rv = super().save(*args, **kwargs) + + if prev and prev.user != self.user: + prev.user.update_last_visit(None) + if not prev or prev.user != self.user or \ + prev.begin_time.date() != self.begin_time.date(): + self.user.update_last_visit(self.begin_time.date()) + + return rv + + def delete(self, *args, **kwargs): + rv = super().delete(*args, **kwargs) + # update last_visit time for person + self.user.update_last_visit(None) + return rv + @property def duration(self) -> 'int|None': if self.end_time: @@ -54,11 +74,6 @@ class Booking(models.Model): traits = set(x[0] for x in traits) return self.type.price_with_traits(self.duration or 0, traits) - @staticmethod - def latest_checkin_query(for_user: 'Person|OuterRef'): - objects = Booking.objects.filter(user=for_user) - return objects.order_by('-begin_time').values('begin_time')[:1] - @staticmethod def currently_open_checkin(for_user: 'Person|OuterRef') -> 'Booking|None': return Booking.objects.filter( diff --git a/app/base/models/course_visit.py b/app/base/models/course_visit.py index 0648a6e..0520978 100755 --- a/app/base/models/course_visit.py +++ b/app/base/models/course_visit.py @@ -33,3 +33,39 @@ class CourseVisit(models.Model): def get_absolute_url(self): return reverse('course-visit:detail', kwargs={'pk': self.pk}) + + def save(self, *args, **kwargs): + # update last_visit time of all involved persons + old_date: 'date|None' = None + old_list: 'list[Person]' = [] + + if self.pk: + prev = CourseVisit.objects.get(pk=self.pk) + old_date = prev.date + old_list.append(prev.participant) + if prev.teacher: + old_list.append(prev.teacher) + + rv = super().save(*args, **kwargs) + + new_date = self.date + new_list: 'list[Person]' = [self.participant] + if self.teacher: + new_list.append(self.teacher) + + for person in old_list: + if person not in new_list: + person.update_last_visit(None) + for person in new_list: + if person not in old_list or old_date != new_date: + person.update_last_visit(new_date) + + return rv + + def delete(self, *args, **kwargs): + rv = super().delete(*args, **kwargs) + # update last_visit time of all involved persons + self.participant.update_last_visit(None) + if self.teacher: + self.teacher.update_last_visit(None) + return rv diff --git a/app/base/models/person.py b/app/base/models/person.py index f6666f4..8f435db 100755 --- a/app/base/models/person.py +++ b/app/base/models/person.py @@ -3,17 +3,20 @@ from django.db.models import Q from django.urls import reverse from app.base.forms.fields import DateField -from app.base.models.course import Course from app.base.models.booking import Booking +from app.base.models.course import Course +from app.base.models.course_visit import CourseVisit from datetime import datetime, date from typing import TYPE_CHECKING if TYPE_CHECKING: - from app.base.models import Account, Note, CourseVisit, TraitMapping + from app.base.models import Account, Note, TraitMapping class Person(models.Model): created: 'models.DateField[date]' = DateField('Angelegt', editable=False) + last_visit: 'models.DateField[date]' = DateField( + 'Letzter Besuch', editable=False) uuid = models.CharField('Karten-ID', max_length=200, blank=True) first_name = models.CharField('Vorname', max_length=200) @@ -55,6 +58,7 @@ class Person(models.Model): def save(self, *args, **kwargs): if not self.pk: self.created = date.today() + self.last_visit = date.today() return super().save(*args, **kwargs) @property @@ -76,15 +80,32 @@ class Person(models.Model): return self.traits_at_date(datetime.now()).values_list( 'pk', 'trait__key', 'trait__label') + @property + def current_checkin(self): + return Booking.currently_open_checkin(self) + def traits_at_date(self, date: datetime): return self.traits.filter( Q(valid_from__lte=date), Q(valid_until__gte=date) | Q(valid_until=None)) - def last_check_in(self) -> 'datetime|None': - obj = Booking.latest_checkin_query(self).first() - return obj['begin_time'] if obj else None + def lookup_last_visit(self) -> date: + ''' Return newest date from either booking, course, or created. ''' + last_booking = Booking.objects.filter( + user=self).order_by('-begin_time').first() + last_course = CourseVisit.objects.filter( + Q(participant=self) | Q(teacher=self)).order_by('-date').first() + available_dates = [self.created] + if last_booking: + available_dates.append(last_booking.begin_time.date()) + if last_course: + available_dates.append(last_course.date) + return max(available_dates) - @property - def current_checkin(self): - return Booking.currently_open_checkin(self) + def update_last_visit(self, new_visit: 'date|None') -> None: + if self.last_visit == new_visit: + return # no need to update + if not new_visit or new_visit < self.last_visit: + new_visit = self.lookup_last_visit() + self.last_visit = new_visit + self.save() diff --git a/app/base/views/model_views/person.py b/app/base/views/model_views/person.py index 5d188ba..11931eb 100755 --- a/app/base/views/model_views/person.py +++ b/app/base/views/model_views/person.py @@ -1,9 +1,6 @@ -from django.db.models import OuterRef, Subquery from django.urls import path -from app.base.models.booking import Booking -from app.base.models.transaction import Transaction -from app.base.models.person import Person +from app.base.models import Booking, Person, Transaction from app.base.views.login import LoginRequired from app.base.views.model_views.base import ( ModelDetailView, ModelListView, ModelCreateView, ModelUpdateView, @@ -37,30 +34,26 @@ class PersonOptions(ViewOptions[Person], LoginRequired): 'trait': 'traits__pk', 'course': 'courses__pk', } - list_columns = ['display_name', 'birth_date', 'last_check_in'] + list_columns = ['display_name', 'birth_date', 'last_visit'] list_render = { - 'display_name': {'verbose_name': 'Nutzer:in'}, + 'display_name': { + 'verbose_name': 'Nutzer:in' + }, 'birth_date': { 'verbose_name': 'Geburtsjahr', 'date_format': 'Y', }, - 'last_check_in': { - 'verbose_name': 'Letzter Besuch', - 'date_format': 'D. d. M y, H:i', + 'last_visit': { + 'date_format': 'D. d. M y', }, } class PersonListView(PersonOptions, ModelListView): icon = 'users' - ordering = ('-last_check_in',) + ordering = ('-last_visit',) search_fields = ['uuid', 'first_name', 'last_name'] - def get_queryset(self): - query = Subquery(Booking.latest_checkin_query(OuterRef('pk'))) - rv = Person.objects.annotate(last_check_in=query) - return rv.order_by(*self.ordering) - class PersonCreateView(PersonOptions, ModelCreateView): # on_success = 'person:detail', '{.pk}' # the default anyway