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 ``` 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..6db560f --- /dev/null +++ b/src/shiftings/accounts/management/commands/send_reminders.py @@ -0,0 +1,10 @@ +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.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 new file mode 100644 index 0000000..e27b6e2 --- /dev/null +++ b/src/shiftings/accounts/migrations/0002_alter_user_options_user_reminder_type_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.10 on 2026-01-31 18:36 + +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(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', + 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'), + ), + ] diff --git a/src/shiftings/accounts/models/user.py b/src/shiftings/accounts/models/user.py index 86d149c..3fa62f9 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 : list[tuple[str | None, str]] = [ + (None, '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,10 +48,14 @@ 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'), 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.')) class Meta: default_permissions = () - ordering = ['username'] + ordering = ['display_name', 'username'] def __str__(self): return self.display @@ -55,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: @@ -67,3 +88,47 @@ def shift_count(self) -> int: def get_absolute_url(self): return reverse('user_profile') + + def send_reminders(self) -> None: + from shiftings.shifts.models import Shift + + reminder_date = date.today() + timedelta(days=self.reminders_days_before_event) + + start_date=datetime.combine(date=reminder_date, time=MIN_TIME) + 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) + #case 'telegram': + #self.send_reminder_telegram() + case _: + # Do nothing + return + + 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: + 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=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/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 @@