Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/organizations/recurring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
6 changes: 3 additions & 3 deletions src/shiftings/accounts/forms/user_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
10 changes: 10 additions & 0 deletions src/shiftings/accounts/management/commands/send_reminders.py
Original file line number Diff line number Diff line change
@@ -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()
Original file line number Diff line number Diff line change
@@ -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'),
),
]
69 changes: 67 additions & 2 deletions src/shiftings/accounts/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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()
4 changes: 4 additions & 0 deletions src/shiftings/accounts/templates/accounts/user_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ <h4 class="text-center">{% trans "User" %} {{ user }}</h4>
<dd>{{ user.email }}</dd>
<dt>{% trans "Phone Number" %}</dt>
<dd>{{ user.phone_number|default:_('Unknown') }}</dd>
<dt>{% trans "Receives Reminders of this type" %}</dt>
<dd>{{ user.get_reminder_type_display }}</dd>
<dt>{% trans "Receives reminders this many days before an Event" %}</dt>
<dd>{{ user.reminders_days_before_event }}</dd>
<dt>{% trans "Member since" %}</dt>
<dd>{{ user.date_joined|timesince }}</dd>
<dt>{% trans "Total Shifts" %}</dt>
Expand Down
9 changes: 7 additions & 2 deletions src/shiftings/mail/views/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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')]),
),
]
2 changes: 1 addition & 1 deletion src/shiftings/shifts/models/shift.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down