From 83f651d7434db9cc91d924ec90cfa92bddecc3d9 Mon Sep 17 00:00:00 2001 From: postgnostic Date: Sun, 15 Mar 2026 15:47:52 +0100 Subject: [PATCH 1/3] Add CalendarFilter model to persist calendar filter preferences --- src/shiftings/cal/migrations/0001_initial.py | 32 ++++++++++++++++++++ src/shiftings/cal/migrations/__init__.py | 0 src/shiftings/cal/models/__init__.py | 3 ++ src/shiftings/cal/models/filter.py | 13 ++++++++ 4 files changed, 48 insertions(+) create mode 100644 src/shiftings/cal/migrations/0001_initial.py create mode 100644 src/shiftings/cal/migrations/__init__.py create mode 100644 src/shiftings/cal/models/__init__.py create mode 100644 src/shiftings/cal/models/filter.py diff --git a/src/shiftings/cal/migrations/0001_initial.py b/src/shiftings/cal/migrations/0001_initial.py new file mode 100644 index 0000000..9e4bc31 --- /dev/null +++ b/src/shiftings/cal/migrations/0001_initial.py @@ -0,0 +1,32 @@ +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('organizations', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='CalendarFilter', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('hide_public_shifts', models.BooleanField(default=False)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, + related_name='calendar_filter', + to=settings.AUTH_USER_MODEL)), + ('hidden_public_organizations', models.ManyToManyField(blank=True, + related_name='hidden_calendar_filters', + to='organizations.organization')), + ], + options={ + 'default_permissions': (), + }, + ), + ] diff --git a/src/shiftings/cal/migrations/__init__.py b/src/shiftings/cal/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/shiftings/cal/models/__init__.py b/src/shiftings/cal/models/__init__.py new file mode 100644 index 0000000..434e4b8 --- /dev/null +++ b/src/shiftings/cal/models/__init__.py @@ -0,0 +1,3 @@ +from shiftings.cal.models.filter import CalendarFilter + +__all__ = ['CalendarFilter'] diff --git a/src/shiftings/cal/models/filter.py b/src/shiftings/cal/models/filter.py new file mode 100644 index 0000000..39a44b0 --- /dev/null +++ b/src/shiftings/cal/models/filter.py @@ -0,0 +1,13 @@ +from django.db import models + + +class CalendarFilter(models.Model): + user = models.OneToOneField('accounts.User', on_delete=models.CASCADE, + related_name='calendar_filter') + hide_public_shifts = models.BooleanField(default=False) + hidden_public_organizations = models.ManyToManyField( + 'organizations.Organization', blank=True, + related_name='hidden_calendar_filters') + + class Meta: + default_permissions = () From 0bbe1649c303702421fe7551d4efec8e96e39a4b Mon Sep 17 00:00:00 2001 From: postgnostic Date: Sun, 15 Mar 2026 15:48:04 +0100 Subject: [PATCH 2/3] Add public shift filtering with persistence to calendar filters --- src/shiftings/shifts/forms/filters.py | 17 ++++++ .../template/shift_url_filter_form.html | 26 +++++++++ src/shiftings/shifts/utils/filter_mixin.py | 53 +++++++++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/src/shiftings/shifts/forms/filters.py b/src/shiftings/shifts/forms/filters.py index 5454832..d48e5f4 100644 --- a/src/shiftings/shifts/forms/filters.py +++ b/src/shiftings/shifts/forms/filters.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib.contenttypes.models import ContentType from django.forms import BooleanField, CheckboxSelectMultiple, ModelMultipleChoiceField, TimeField from django.utils.translation import gettext_lazy as _ @@ -18,6 +19,9 @@ class ShiftFilterForm(forms.Form): start_after_time_field = TimeField(label=_('Time HH:MM'), required=False) end_before_field = DateFormField(label=_('Date YYYY-MM-DD'), required=False) end_before_time_field = TimeField(label=_('Time HH:MM'), required=False) + hide_public_shifts = BooleanField(widget=forms.CheckboxInput, label=_('Hide public shifts'), required=False) + exclude_public_orgs_field = ModelMultipleChoiceField(queryset=Organization.objects.none(), + widget=CheckboxSelectMultiple, required=False) def __init__(self, user: User, *args, **kwargs): super().__init__(*args, **kwargs) @@ -28,3 +32,16 @@ def __init__(self, user: User, *args, **kwargs): self.fields['select_event_field'].queryset = events self.has_events = events.count() > 0 + from shiftings.shifts.models import Shift + from shiftings.shifts.models.permission import ParticipationPermission, ParticipationPermissionType + shift_ct = ContentType.objects.get_for_model(Shift) + public_shift_ids = ParticipationPermission.objects.filter( + referred_content_type=shift_ct, + permission_type_field__gte=ParticipationPermissionType.Existence, + organization__isnull=True + ).values_list('referred_object_id', flat=True) + public_orgs = Organization.objects.filter( + shifts__id__in=public_shift_ids + ).exclude(id__in=orgs.values('id')).distinct() + self.fields['exclude_public_orgs_field'].queryset = public_orgs + self.has_public_orgs = public_orgs.exists() diff --git a/src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html b/src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html index d0f33c5..035d2c8 100644 --- a/src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html +++ b/src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html @@ -17,6 +17,32 @@
{% trans "Parameters" %}:
{% bootstrap_field shift_filter_form.own_shifts_checkbox layout='inline' %}
+
+ {% bootstrap_field shift_filter_form.hide_public_shifts layout='inline' %} +
+ {% if shift_filter_form.has_public_orgs %} +
+
+
+
+ +
+
+
+ {% bootstrap_field shift_filter_form.exclude_public_orgs_field show_label=False wrapper_class='' %} +
+
+
+
+
+ {% endif %} {% if shift_filter_form.has_orgs %}
diff --git a/src/shiftings/shifts/utils/filter_mixin.py b/src/shiftings/shifts/utils/filter_mixin.py index 8b1c7ca..e07f8d8 100644 --- a/src/shiftings/shifts/utils/filter_mixin.py +++ b/src/shiftings/shifts/utils/filter_mixin.py @@ -1,8 +1,10 @@ +import functools from datetime import datetime from django.db.models import Q from django.http import HttpRequest +from shiftings.cal.models import CalendarFilter from shiftings.shifts.forms.filters import ShiftFilterForm @@ -12,21 +14,49 @@ class ShiftFilterMixin: def get_filter_form_kwargs(self): kwargs = {arg: self.request.GET.get(arg) for arg in ['own_shifts_checkbox', 'start_after_field', 'end_before_field', - 'start_after_time_field', 'end_before_time_field'] + 'start_after_time_field', 'end_before_time_field', 'hide_public_shifts'] if self.request.GET.get(arg) is not None} kwargs.update({arg: self.request.GET.getlist(arg) - for arg in ['select_org_field', 'select_event_field'] + for arg in ['select_org_field', 'select_event_field', 'exclude_public_orgs_field'] if self.request.GET.get(arg) is not None}) return kwargs - def get_form(self): + @functools.cached_property + def _shift_filter_form(self): kwargs = self.get_filter_form_kwargs() - if len(kwargs) > 0: + if kwargs: form = ShiftFilterForm(data=kwargs, user=self.request.user) + if form.is_valid(): + self._persist_calendar_filter(form.cleaned_data) else: - form = ShiftFilterForm(user=self.request.user) + saved_kwargs = self._load_calendar_filter_kwargs() + form = ShiftFilterForm(data=saved_kwargs if saved_kwargs else None, user=self.request.user) return form + def _persist_calendar_filter(self, cleaned_data): + cal_filter, _ = CalendarFilter.objects.get_or_create(user=self.request.user) + cal_filter.hide_public_shifts = cleaned_data.get('hide_public_shifts', False) + cal_filter.save() + cal_filter.hidden_public_organizations.set( + cleaned_data.get('exclude_public_orgs_field') or [] + ) + + def _load_calendar_filter_kwargs(self): + try: + cal_filter = self.request.user.calendar_filter + except CalendarFilter.DoesNotExist: + return {} + kwargs = {} + if cal_filter.hide_public_shifts: + kwargs['hide_public_shifts'] = 'on' + orgs = list(cal_filter.hidden_public_organizations.values_list('id', flat=True)) + if orgs: + kwargs['exclude_public_orgs_field'] = orgs + return kwargs + + def get_form(self): + return self._shift_filter_form + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['shift_filter_form'] = self.get_form() @@ -58,4 +88,17 @@ def get_filters(self): shift_filter &= Q(end__date__lte=form.cleaned_data['end_before_field']) elif form.cleaned_data['end_before_time_field'] is not None: shift_filter &= Q(end__time__lte=form.cleaned_data['end_before_time_field']) + if form.cleaned_data['hide_public_shifts']: + user_orgs = self.request.user.organizations + shift_filter &= ( + Q(organization__in=user_orgs) | Q(participants__user=self.request.user) + ) + elif form.cleaned_data['exclude_public_orgs_field'].exists(): + excluded = form.cleaned_data['exclude_public_orgs_field'] + user_orgs = self.request.user.organizations + shift_filter &= ( + ~Q(organization__in=excluded) | + Q(organization__in=user_orgs) | + Q(participants__user=self.request.user) + ) return shift_filter From 5952026068d5b9ff4672b3d8121e60d668af72c7 Mon Sep 17 00:00:00 2001 From: postgnostic Date: Sun, 15 Mar 2026 15:50:29 +0100 Subject: [PATCH 3/3] Alter color fields on RecurringShift and ShiftType --- ...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..bf37572 --- /dev/null +++ b/src/shiftings/shifts/migrations/0007_alter_recurringshift_color_alter_shifttype_color.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.2 on 2026-03-15 14:17 + +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')]), + ), + ]