From a67a15d22a87543b3e85e73bc24cd8130d939bbc Mon Sep 17 00:00:00 2001 From: Pablo Schmeiser Date: Mon, 12 Jan 2026 02:25:40 +0100 Subject: [PATCH 1/8] improving user-ordering --- src/shiftings/accounts/models/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shiftings/accounts/models/user.py b/src/shiftings/accounts/models/user.py index 86d149c..faed777 100644 --- a/src/shiftings/accounts/models/user.py +++ b/src/shiftings/accounts/models/user.py @@ -34,7 +34,7 @@ class User(BaseUser): class Meta: default_permissions = () - ordering = ['username'] + ordering = ['display_name', 'username'] def __str__(self): return self.display From e03d358904bb710073509144a9275abeb4d318d9 Mon Sep 17 00:00:00 2001 From: Pablo Schmeiser Date: Mon, 12 Jan 2026 02:27:17 +0100 Subject: [PATCH 2/8] Fix language --- docs/organizations/recurring.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/organizations/recurring.md b/docs/organizations/recurring.md index 9e7e87e..cd02364 100644 --- a/docs/organizations/recurring.md +++ b/docs/organizations/recurring.md @@ -30,7 +30,7 @@ if N is 1 and the first day of the selected month is a Sunday the shift will be ## Creating Shifts from Recurring Shifts Recurring Shifts have to be created using the "*create_recurring_shifts*" command. -Optimally this is executed daily as a cron job oder systemd timer: +Optimally this is executed daily as a cron job or systemd timer: ``` 0 0 * * * /path/to/manage.py create_recurring_shifts > /dev/null 2>&1 ``` From d25421bd8e6a0eedf5f3ba224051f5afcbeabcc3 Mon Sep 17 00:00:00 2001 From: Pablo Schmeiser Date: Mon, 12 Jan 2026 02:28:32 +0100 Subject: [PATCH 3/8] Fixing time_display to be consistent --- src/shiftings/shifts/models/shift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shiftings/shifts/models/shift.py b/src/shiftings/shifts/models/shift.py index 1157b7e..8dee7e3 100644 --- a/src/shiftings/shifts/models/shift.py +++ b/src/shiftings/shifts/models/shift.py @@ -69,7 +69,7 @@ def detailed_display(self) -> str: @property def time_display(self) -> str: if self.start.date() != self.end.date(): - return _('{start} to {end_time} on {end_date} ').format(name=self.name, + return _('{start} to {end_time} on {end_date}').format(name=self.name, start=self.start.strftime('%H:%M'), end_time=self.end.strftime('%H:%M'), end_date=self.end.date()) From 6bfe17af4cdb29a810bacffc94887470ad735721 Mon Sep 17 00:00:00 2001 From: Pablo Schmeiser Date: Mon, 12 Jan 2026 02:30:03 +0100 Subject: [PATCH 4/8] Adding email reminders --- src/shiftings/accounts/forms/user_form.py | 6 +- .../management/commands/send_reminders.py | 11 ++++ src/shiftings/accounts/models/user.py | 60 +++++++++++++++++++ .../templates/accounts/user_detail.html | 4 ++ 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 src/shiftings/accounts/management/commands/send_reminders.py diff --git a/src/shiftings/accounts/forms/user_form.py b/src/shiftings/accounts/forms/user_form.py index 7a4dc98..544688b 100644 --- a/src/shiftings/accounts/forms/user_form.py +++ b/src/shiftings/accounts/forms/user_form.py @@ -13,7 +13,7 @@ class UserCreateForm(forms.ModelForm): class Meta: model = User fields = ['username', 'display_name', 'first_name', 'last_name', 'email', 'phone_number', - 'password', 'confirm_password'] + 'password', 'confirm_password', 'reminder_type', 'reminders_days_before_event'] def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) @@ -35,7 +35,7 @@ def clean(self) -> Dict[str, Any]: class UserUpdateForm(forms.ModelForm): class Meta: model = User - fields = ['first_name', 'last_name', 'email', 'display_name', 'phone_number'] + fields = ['first_name', 'last_name', 'email', 'display_name', 'phone_number', 'reminder_type', 'reminders_days_before_event'] def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) @@ -46,4 +46,4 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: else: self.fields['first_name'].disabled = True self.fields['last_name'].disabled = True - self.fields['email'].disabled = True + self.fields['email'].disabled = True \ No newline at end of file diff --git a/src/shiftings/accounts/management/commands/send_reminders.py b/src/shiftings/accounts/management/commands/send_reminders.py new file mode 100644 index 0000000..1278e0c --- /dev/null +++ b/src/shiftings/accounts/management/commands/send_reminders.py @@ -0,0 +1,11 @@ +from django.core.management.base import BaseCommand + +from shiftings.accounts.models import User + +class Command(BaseCommand): + help = 'Send reminders to users x amount of days before events they are participating in.' + + def handle(self, *args, **options): + for user in User.objects.all(): + if user.reminders_days_before_event >= 0 and user.reminder_type != '': + user.send_reminders() diff --git a/src/shiftings/accounts/models/user.py b/src/shiftings/accounts/models/user.py index faed777..f610158 100644 --- a/src/shiftings/accounts/models/user.py +++ b/src/shiftings/accounts/models/user.py @@ -2,7 +2,11 @@ from typing import TYPE_CHECKING +from datetime import date, time, datetime, timedelta + +from django.conf import settings from django.contrib.auth.models import AbstractUser +from django.core.mail import EmailMessage from django.db import models from django.db.models import Q, QuerySet from django.urls import reverse @@ -13,6 +17,19 @@ from shiftings.events.models import Event from shiftings.organizations.models import Organization +REMINDER_TYPES = [ + ('', 'None'), + ("email", "Email"), + #("telegram", "Telegram"), + #('discord', 'Discord'), + #('whatsapp', 'WhatsApp'), + #('socket', 'Socket') +] + +MIN_TIME=time.fromisoformat('00:00:00') +MAX_TIME=time.fromisoformat('23:59:59,999999') + +REMINDER_SUBJECT_MSG='Reminder concerning shift(s) on ' class BaseUser(AbstractUser): class Meta: @@ -31,6 +48,10 @@ def display(self) -> str: class User(BaseUser): display_name = models.CharField(max_length=150, verbose_name=_('Display Name'), null=True, blank=True) phone_number = PhoneNumberField(verbose_name=_('Telephone Number'), blank=True, null=True) + reminder_type = models.CharField(max_length=32, verbose_name=_('Reminder Type'), default='', blank=True, + choices=REMINDER_TYPES, help_text='Leave this empty to disable reminders.') + reminders_days_before_event = models.IntegerField(verbose_name=_('Days before event reminders'), default=1, + help_text=_('I want to receive reminders this many days before an event.')) class Meta: default_permissions = () @@ -67,3 +88,42 @@ def shift_count(self) -> int: def get_absolute_url(self): return reverse('user_profile') + + def send_reminders(self): + from shiftings.shifts.models import Shift + if self.reminders_days_before_event < 0: + return + if self.reminder_type == '': + return + + reminder_date = date.today() + timedelta(days=self.reminder_emails_days_before_event) + + start_date=datetime.combine(date=reminder_date, time=MIN_TIME) + shifts = Shift.objects.filter( + start__date__gte=start_date, + start__date__lte=datetime.combine(date=reminder_date, time=MAX_TIME), + participants__user=self + ) + + match self.reminder_type: + case 'email': + self.send_reminder_emails(shifts, reminder_date) + #case 'telegram': + #self.send_reminder_telegram() + case _: + # Do nothing + return + + def send_reminder_emails(self, shifts, reminder_date): + subject = REMINDER_SUBJECT_MSG + reminder_date.__str__() + text = 'This email is a reminder concerning the following Shiftings ' + if shifts.__len__() > 1: + text += f'shifts on the {reminder_date.__str__()}: ' + for shift in shifts: + text += f'\n - {shift.display}: {shift.time_display} for {shift.organization}' + else: + shift = shifts[0] + text += f'shift on the {reminder_date.__str__()}: \n{shift.display}: {shift.time_display} for {shift.organization}' + + email = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [self.email], headers={'Reply-To': settings.DEFAULT_FROM_EMAIL}) + email.send() \ No newline at end of file diff --git a/src/shiftings/accounts/templates/accounts/user_detail.html b/src/shiftings/accounts/templates/accounts/user_detail.html index 20b4fef..cdc5573 100644 --- a/src/shiftings/accounts/templates/accounts/user_detail.html +++ b/src/shiftings/accounts/templates/accounts/user_detail.html @@ -63,6 +63,10 @@

{% trans "User" %} {{ user }}

{{ user.email }}
{% trans "Phone Number" %}
{{ user.phone_number|default:_('Unknown') }}
+
{% trans "Receives Reminders of this type" %}
+
{{ user.get_reminder_type_display }}
+
{% trans "Receives reminders this many days before an Event" %}
+
{{ user.reminders_days_before_event }}
{% trans "Member since" %}
{{ user.date_joined|timesince }}
{% trans "Total Shifts" %}
From 01739449118ce78975b2b38d022bb5a6605e6abb Mon Sep 17 00:00:00 2001 From: Pablo Schmeiser Date: Wed, 28 Jan 2026 18:09:14 +0100 Subject: [PATCH 5/8] Improving reminders --- .../management/commands/send_reminders.py | 5 ++--- src/shiftings/accounts/models/user.py | 17 +++++++++++------ src/shiftings/mail/views/mail.py | 9 +++++++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/shiftings/accounts/management/commands/send_reminders.py b/src/shiftings/accounts/management/commands/send_reminders.py index 1278e0c..af389f0 100644 --- a/src/shiftings/accounts/management/commands/send_reminders.py +++ b/src/shiftings/accounts/management/commands/send_reminders.py @@ -6,6 +6,5 @@ class Command(BaseCommand): help = 'Send reminders to users x amount of days before events they are participating in.' def handle(self, *args, **options): - for user in User.objects.all(): - if user.reminders_days_before_event >= 0 and user.reminder_type != '': - user.send_reminders() + for user in User.objects.filter(reminder__type__ne='', reminder__days_before_event__gte=0): + user.send_reminders() \ No newline at end of file diff --git a/src/shiftings/accounts/models/user.py b/src/shiftings/accounts/models/user.py index f610158..3fbc524 100644 --- a/src/shiftings/accounts/models/user.py +++ b/src/shiftings/accounts/models/user.py @@ -76,7 +76,7 @@ def organizations(self) -> QuerySet[Organization]: def events(self) -> QuerySet[Event]: from shiftings.events.models import Event organizations = self.organizations - return Event.objects.filter(organization__in=organizations) + return Event.objects.filter(organization__in=organizations).distinct() @property def shift_count(self) -> int: @@ -99,11 +99,10 @@ def send_reminders(self): reminder_date = date.today() + timedelta(days=self.reminder_emails_days_before_event) start_date=datetime.combine(date=reminder_date, time=MIN_TIME) - shifts = Shift.objects.filter( + shifts = self.shifts.filter( start__date__gte=start_date, - start__date__lte=datetime.combine(date=reminder_date, time=MAX_TIME), - participants__user=self - ) + start__date__lte=datetime.combine(date=reminder_date, time=MAX_TIME) + ).distinct().order_by('start', 'end', 'name') match self.reminder_type: case 'email': @@ -125,5 +124,11 @@ def send_reminder_emails(self, shifts, reminder_date): shift = shifts[0] text += f'shift on the {reminder_date.__str__()}: \n{shift.display}: {shift.time_display} for {shift.organization}' - email = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [self.email], headers={'Reply-To': settings.DEFAULT_FROM_EMAIL}) + email = EmailMessage( + subject=subject, + body=text, + from_email=settings.DEFAULT_FROM_EMAIL, + to=[self.email], + headers={'Reply-To': settings.DEFAULT_FROM_EMAIL} + ) email.send() \ No newline at end of file diff --git a/src/shiftings/mail/views/mail.py b/src/shiftings/mail/views/mail.py index df7d360..5afb349 100644 --- a/src/shiftings/mail/views/mail.py +++ b/src/shiftings/mail/views/mail.py @@ -37,8 +37,13 @@ def form_valid(self, form: MailForm) -> HttpResponse: subject = form.cleaned_data['subject'].format(**replacements) text = form.cleaned_data['text'].format(**replacements) users = self.get_users(form) - email = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, bcc=[user.email for user in users], - headers={'Reply-To': settings.DEFAULT_FROM_EMAIL}) + email = EmailMessage( + subject=subject, + body=text, + from_email=settings.DEFAULT_FROM_EMAIL, + bcc=[user.email for user in users], + headers={'Reply-To': settings.DEFAULT_FROM_EMAIL} + ) for file in dict(form.files).get('attachments', list()): email.attach(file.name, file.file.read(), mimetype=file.content_type) email.send() From eddca6d24698e379676c626ef7110c496daa066d Mon Sep 17 00:00:00 2001 From: Pablo Schmeiser Date: Wed, 28 Jan 2026 18:44:25 +0100 Subject: [PATCH 6/8] Adding necessary migration --- ...ser_options_user_reminder_type_and_more.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/shiftings/accounts/migrations/0002_alter_user_options_user_reminder_type_and_more.py diff --git a/src/shiftings/accounts/migrations/0002_alter_user_options_user_reminder_type_and_more.py b/src/shiftings/accounts/migrations/0002_alter_user_options_user_reminder_type_and_more.py new file mode 100644 index 0000000..61eb5a7 --- /dev/null +++ b/src/shiftings/accounts/migrations/0002_alter_user_options_user_reminder_type_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.9 on 2026-01-12 01:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='user', + options={'default_permissions': (), 'ordering': ['display_name', 'username']}, + ), + migrations.AddField( + model_name='user', + name='reminder_type', + field=models.CharField(blank=True, choices=[('', 'None'), ('email', 'Email')], default='', help_text='Leave this empty to disable reminders.', max_length=32, verbose_name='Reminder Type'), + ), + migrations.AddField( + model_name='user', + name='reminders_days_before_event', + field=models.IntegerField(default=1, help_text='I want to receive reminders this many days before an event.', verbose_name='Days before event reminders'), + ), + ] From aa230db3fb4a116dea1f034c7031654e7b48e83f Mon Sep 17 00:00:00 2001 From: Pablo Schmeiser Date: Wed, 28 Jan 2026 18:45:11 +0100 Subject: [PATCH 7/8] Adding missing migration from older change --- ...urringshift_color_alter_shifttype_color.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/shiftings/shifts/migrations/0007_alter_recurringshift_color_alter_shifttype_color.py diff --git a/src/shiftings/shifts/migrations/0007_alter_recurringshift_color_alter_shifttype_color.py b/src/shiftings/shifts/migrations/0007_alter_recurringshift_color_alter_shifttype_color.py new file mode 100644 index 0000000..513aa15 --- /dev/null +++ b/src/shiftings/shifts/migrations/0007_alter_recurringshift_color_alter_shifttype_color.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.9 on 2026-01-12 01:32 + +import colorfield.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('shifts', '0006_recurringshift_auto_create_days'), + ] + + operations = [ + migrations.AlterField( + model_name='recurringshift', + name='color', + field=colorfield.fields.ColorField(default='#FD7E14', image_field=None, max_length=25, samples=[('#0D6EFD', 'Blue'), ('#6610F2', 'Indigo'), ('#6F42C1', 'Purple'), ('#D63384', 'Pink'), ('#DC3545', 'Red'), ('#FD7E14', 'Orange'), ('#FFC107', 'Yellow'), ('#198754', 'Green'), ('#20C997', 'Teal'), ('#0DCAF0', 'Cyan')]), + ), + migrations.AlterField( + model_name='shifttype', + name='color', + field=colorfield.fields.ColorField(default='#FD7E14', image_field=None, max_length=25, samples=[('#0D6EFD', 'Blue'), ('#6610F2', 'Indigo'), ('#6F42C1', 'Purple'), ('#D63384', 'Pink'), ('#DC3545', 'Red'), ('#FD7E14', 'Orange'), ('#FFC107', 'Yellow'), ('#198754', 'Green'), ('#20C997', 'Teal'), ('#0DCAF0', 'Cyan')]), + ), + ] From 391988b699e4313e0570b450b01413b1bb936a87 Mon Sep 17 00:00:00 2001 From: Pablo Schmeiser Date: Sat, 31 Jan 2026 19:38:26 +0100 Subject: [PATCH 8/8] Replacing falty inequality check --- .../management/commands/send_reminders.py | 2 +- ...ser_options_user_reminder_type_and_more.py | 4 ++-- src/shiftings/accounts/models/user.py | 22 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/shiftings/accounts/management/commands/send_reminders.py b/src/shiftings/accounts/management/commands/send_reminders.py index af389f0..6db560f 100644 --- a/src/shiftings/accounts/management/commands/send_reminders.py +++ b/src/shiftings/accounts/management/commands/send_reminders.py @@ -6,5 +6,5 @@ class Command(BaseCommand): help = 'Send reminders to users x amount of days before events they are participating in.' def handle(self, *args, **options): - for user in User.objects.filter(reminder__type__ne='', reminder__days_before_event__gte=0): + for user in User.objects.filter(reminder_type__isnull=False, reminders_days_before_event__gte=0): user.send_reminders() \ No newline at end of file diff --git a/src/shiftings/accounts/migrations/0002_alter_user_options_user_reminder_type_and_more.py b/src/shiftings/accounts/migrations/0002_alter_user_options_user_reminder_type_and_more.py index 61eb5a7..e27b6e2 100644 --- a/src/shiftings/accounts/migrations/0002_alter_user_options_user_reminder_type_and_more.py +++ b/src/shiftings/accounts/migrations/0002_alter_user_options_user_reminder_type_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.9 on 2026-01-12 01:32 +# Generated by Django 5.2.10 on 2026-01-31 18:36 from django.db import migrations, models @@ -17,7 +17,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='user', name='reminder_type', - field=models.CharField(blank=True, choices=[('', 'None'), ('email', 'Email')], default='', help_text='Leave this empty to disable reminders.', max_length=32, verbose_name='Reminder Type'), + field=models.CharField(choices=[(None, 'None'), ('email', 'Email')], help_text='Leave this empty to disable reminders.', max_length=32, null=True, verbose_name='Reminder Type'), ), migrations.AddField( model_name='user', diff --git a/src/shiftings/accounts/models/user.py b/src/shiftings/accounts/models/user.py index 3fbc524..3fa62f9 100644 --- a/src/shiftings/accounts/models/user.py +++ b/src/shiftings/accounts/models/user.py @@ -17,8 +17,8 @@ from shiftings.events.models import Event from shiftings.organizations.models import Organization -REMINDER_TYPES = [ - ('', 'None'), +REMINDER_TYPES : list[tuple[str | None, str]] = [ + (None, 'None'), ("email", "Email"), #("telegram", "Telegram"), #('discord', 'Discord'), @@ -48,7 +48,7 @@ def display(self) -> str: class User(BaseUser): display_name = models.CharField(max_length=150, verbose_name=_('Display Name'), null=True, blank=True) phone_number = PhoneNumberField(verbose_name=_('Telephone Number'), blank=True, null=True) - reminder_type = models.CharField(max_length=32, verbose_name=_('Reminder Type'), default='', blank=True, + reminder_type = models.CharField(max_length=32, verbose_name=_('Reminder Type'), null=True, choices=REMINDER_TYPES, help_text='Leave this empty to disable reminders.') reminders_days_before_event = models.IntegerField(verbose_name=_('Days before event reminders'), default=1, help_text=_('I want to receive reminders this many days before an event.')) @@ -89,21 +89,21 @@ def shift_count(self) -> int: def get_absolute_url(self): return reverse('user_profile') - def send_reminders(self): + def send_reminders(self) -> None: from shiftings.shifts.models import Shift - if self.reminders_days_before_event < 0: - return - if self.reminder_type == '': - return - reminder_date = date.today() + timedelta(days=self.reminder_emails_days_before_event) + reminder_date = date.today() + timedelta(days=self.reminders_days_before_event) start_date=datetime.combine(date=reminder_date, time=MIN_TIME) - shifts = self.shifts.filter( + shifts = Shift.objects.filter( + participants__user=self, start__date__gte=start_date, start__date__lte=datetime.combine(date=reminder_date, time=MAX_TIME) ).distinct().order_by('start', 'end', 'name') + if shifts.count() == 0: + return + match self.reminder_type: case 'email': self.send_reminder_emails(shifts, reminder_date) @@ -113,7 +113,7 @@ def send_reminders(self): # Do nothing return - def send_reminder_emails(self, shifts, reminder_date): + def send_reminder_emails(self, shifts, reminder_date : date) -> None: subject = REMINDER_SUBJECT_MSG + reminder_date.__str__() text = 'This email is a reminder concerning the following Shiftings ' if shifts.__len__() > 1: