diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml index 078d0be2..c150276c 100644 --- a/.github/workflows/build_docker_image.yml +++ b/.github/workflows/build_docker_image.yml @@ -15,7 +15,7 @@ jobs: services: mariadb: - image: mariadb:10.4 + image: mariadb:10.5 env: MARIADB_USER: amelie_test MARIADB_PASSWORD: amelie_test diff --git a/.gitignore b/.gitignore index f327c031..5a4748e9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ venv/ .cache/ media/ amelie.db +amelie.egg-info +build/ # backups of local settings, but not the .default settings amelie/settings/local.py* diff --git a/Dockerfile b/Dockerfile index a3da2bd0..7d4c5566 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Build the amelie docker image based on Debian 11 (Bullseye) -FROM debian:bullseye +# Build the amelie docker image based on Debian 12 (Bookworm) +FROM debian:bookworm # Load some build variables from the pipeline ARG BUILD_BRANCH=unknown @@ -18,7 +18,7 @@ RUN echo "Updating repostitories..." && \ echo "Upgrading base debian system..." && \ apt-get upgrade -y && \ echo "Installing Amelie required packages..." && \ - apt-get install -y apt-utils git net-tools python3 python3-pip mariadb-client libmariadb-dev xmlsec1 libssl-dev libldap-dev libsasl2-dev libjpeg-dev zlib1g-dev gettext locales acl && \ + apt-get install -y apt-utils git net-tools python3 pkg-config default-libmysqlclient-dev python3-pip mariadb-client libmariadb-dev xmlsec1 libssl-dev libldap-dev libsasl2-dev libjpeg-dev zlib1g-dev gettext locales acl && \ echo "Enabling 'nl_NL' and 'en_US' locales..." && \ sed -i -e 's/# nl_NL.UTF-8 UTF-8/nl_NL.UTF-8 UTF-8/' /etc/locale.gen && \ sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ @@ -27,7 +27,7 @@ RUN echo "Updating repostitories..." && \ echo "Creating directories for amelie..." && \ mkdir -p /amelie /config /static /media /photo_upload /data_exports /homedir_exports /var/log /var/run && \ echo "Installing python requirements..." && \ - pip3 install . && \ + pip3 install . --break-system-packages && \ echo "Adding build variable files..." && \ echo "${BUILD_BRANCH}" > /amelie/BUILD_BRANCH && \ echo "${BUILD_COMMIT}" > /amelie/BUILD_COMMIT && \ diff --git a/amelie/calendar/migrations/0008_alter_event_participants.py b/amelie/calendar/migrations/0008_alter_event_participants.py new file mode 100644 index 00000000..13988f5e --- /dev/null +++ b/amelie/calendar/migrations/0008_alter_event_participants.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2 on 2025-11-11 19:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('calendar', '0007_alter_event_entire_day'), + ('members', '0016_person_unverified_picture'), + ] + + operations = [ + migrations.AlterField( + model_name='event', + name='participants', + field=models.ManyToManyField(blank=True, through='calendar.Participation', through_fields=('event', 'person'), to='members.person', verbose_name='Participants'), + ), + ] diff --git a/amelie/members/views.py b/amelie/members/views.py index de700fe7..89f9332a 100644 --- a/amelie/members/views.py +++ b/amelie/members/views.py @@ -5,6 +5,7 @@ import logging from datetime import date +from datetime import timezone as tz from decimal import Decimal from functools import lru_cache from io import BytesIO @@ -350,7 +351,7 @@ def _person_can_be_anonymized(person): year=membership.year, next_year=membership.year + 1, type=membership.type ))) # Date where new SEPA authorizations came into effect. - begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=timezone.utc) + begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) personal_tab_credit = Transaction.objects.filter(person=person, date__gte=begin).aggregate(Sum('price'))[ 'price__sum'] or Decimal('0.00') if personal_tab_credit != 0: diff --git a/amelie/personal_tab/alexia.py b/amelie/personal_tab/alexia.py index 42b2a759..0465c019 100644 --- a/amelie/personal_tab/alexia.py +++ b/amelie/personal_tab/alexia.py @@ -4,6 +4,7 @@ from django.conf import settings from django.utils import timezone +from datetime import timezone as tz def get_alexia(): @@ -43,7 +44,7 @@ def parse_datetime(datetimestr): if datetimestr[-6:] != '+00:00': raise ValueError('Datetime "%s" is not in UTC' % datetimestr) - return datetime.datetime.strptime(datetimestr[:-6], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=timezone.utc) + return datetime.datetime.strptime(datetimestr[:-6], "%Y-%m-%dT%H:%M:%S").replace(tzinfo=tz.utc) class AlexiaConnectionError(ConnectionError): diff --git a/amelie/personal_tab/debt_collection.py b/amelie/personal_tab/debt_collection.py index bc389646..b9890036 100644 --- a/amelie/personal_tab/debt_collection.py +++ b/amelie/personal_tab/debt_collection.py @@ -1,4 +1,5 @@ import datetime +from datetime import timezone as tz from django.db.models import Sum, Q from django.template.defaultfilters import date as _date @@ -11,6 +12,7 @@ from amelie.tools.encodings import normalize_to_ascii + def authorization_contribution(person): """ Returns the most appropriate contribution authorization for a given person. @@ -181,7 +183,7 @@ def generate_cookie_corner_instructions(end_date): all_transactions = Transaction.objects.filter(debt_collection=None) # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET - begin_date = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=timezone.utc) + begin_date = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) all_transactions = all_transactions.filter(date__gte=begin_date, date__lt=end_date) diff --git a/amelie/personal_tab/managers.py b/amelie/personal_tab/managers.py index fed3a70d..e4d99632 100644 --- a/amelie/personal_tab/managers.py +++ b/amelie/personal_tab/managers.py @@ -4,6 +4,7 @@ from django.db.models import Q from django.db.models.aggregates import Sum from django.utils import timezone +from datetime import timezone as tz from amelie.members.models import Person @@ -21,7 +22,7 @@ def _people_with_outstanding_balance(): Returns a QuerySet with all Persons having a non-zero cookie corner balance. """ # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET - begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=timezone.utc) + begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) return Person.objects.filter(transaction__date__gte=begin).annotate(balance=Sum('transaction__price')).filter( balance__gt=0) diff --git a/amelie/personal_tab/migrations/0002_auto_20190408_2228.py b/amelie/personal_tab/migrations/0002_auto_20190408_2228.py index 85c9e8c4..83cc7bb7 100644 --- a/amelie/personal_tab/migrations/0002_auto_20190408_2228.py +++ b/amelie/personal_tab/migrations/0002_auto_20190408_2228.py @@ -5,13 +5,14 @@ from django.db import migrations, models from django.conf import settings from django.utils import timezone +from datetime import timezone as tz def fill_old_rfid_created_used_dates(apps, schema_editor): RFIDCard = apps.get_model('personal_tab', 'RFIDCard') default_datetime = timezone.datetime(settings.DATE_OLD_RFID_CARDS.year, settings.DATE_OLD_RFID_CARDS.month, - settings.DATE_OLD_RFID_CARDS.day, 0, 0, 0, 0, timezone.utc) + settings.DATE_OLD_RFID_CARDS.day, 0, 0, 0, 0, tz.utc) default_datetime = default_datetime - timezone.timedelta(days=1) for card in RFIDCard.objects.all(): diff --git a/amelie/personal_tab/views.py b/amelie/personal_tab/views.py index f72d918b..dd93c471 100644 --- a/amelie/personal_tab/views.py +++ b/amelie/personal_tab/views.py @@ -50,6 +50,9 @@ from amelie.tools.logic import current_association_year from amelie.tools.mixins import RequirePersonMixin, RequireBoardMixin +from datetime import timezone as tz + + DATETIMEFORMAT = '%Y%m%d%H%M%S' logger = logging.getLogger(__name__) @@ -59,13 +62,13 @@ def _urlize(dt): """ Convert datetime to url format """ - return dt.astimezone(timezone.utc).strftime(DATETIMEFORMAT) + return dt.astimezone(tz.utc).strftime(DATETIMEFORMAT) def _parsedatetime(inputstr): if isinstance(inputstr, int): inputstr = str(inputstr) - return datetime.datetime.strptime(inputstr, DATETIMEFORMAT).replace(tzinfo=timezone.utc) + return datetime.datetime.strptime(inputstr, DATETIMEFORMAT).replace(tzinfo=tz.utc) @require_lid @@ -463,8 +466,8 @@ def transaction_overview(request, date_from=False, date_to=False): if request.method == 'POST': form = PeriodTimeForm(request.POST) if form.is_valid(): - start = form.cleaned_data['datetime_from'].astimezone(timezone.utc) - end = form.cleaned_data['datetime_to'].astimezone(timezone.utc) + start = form.cleaned_data['datetime_from'].astimezone(tz.utc) + end = form.cleaned_data['datetime_to'].astimezone(tz.utc) return HttpResponseRedirect(reverse('personal_tab:transactions', args=[_urlize(start), _urlize(end)])) if not date_from: @@ -608,7 +611,7 @@ def dashboard(request, pk, slug): personal_transactions = Transaction.objects.filter(person=person).order_by('-added_on')[:5] # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET - begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=timezone.utc) + begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) now = timezone.now() today = now.astimezone(timezone.get_default_timezone()).date() @@ -660,8 +663,8 @@ def person_transactions(request, pk, slug, date_from=None, date_to=None): if request.method == 'POST': form = PeriodTimeForm(request.POST) if form.is_valid(): - start = form.cleaned_data['datetime_from'].astimezone(timezone.utc) - end = form.cleaned_data['datetime_to'].astimezone(timezone.utc) + start = form.cleaned_data['datetime_from'].astimezone(tz.utc) + end = form.cleaned_data['datetime_to'].astimezone(tz.utc) return HttpResponseRedirect(reverse('personal_tab:person_transactions', kwargs={ 'pk': pk, 'slug': slug, 'date_from': _urlize(start), 'date_to': _urlize(end) })) @@ -727,8 +730,8 @@ def exam_cookie_credit(request, date_from=False, date_to=False): if request.method == 'POST': form = PeriodTimeForm(request.POST) if form.is_valid(): - start = form.cleaned_data['datetime_from'].astimezone(timezone.utc) - end = form.cleaned_data['datetime_to'].astimezone(timezone.utc) + start = form.cleaned_data['datetime_from'].astimezone(tz.utc) + end = form.cleaned_data['datetime_to'].astimezone(tz.utc) return HttpResponseRedirect(reverse('personal_tab:exam_cookie_credit', args=[_urlize(start), _urlize(end)])) @@ -757,8 +760,8 @@ def person_exam_cookie_credit(request, person_id, slug, date_from=None, date_to= if request.method == 'POST': form = PeriodTimeForm(request.POST) if form.is_valid(): - start = form.cleaned_data['datetime_from'].astimezone(timezone.utc) - end = form.cleaned_data['datetime_to'].astimezone(timezone.utc) + start = form.cleaned_data['datetime_from'].astimezone(tz.utc) + end = form.cleaned_data['datetime_to'].astimezone(tz.utc) return HttpResponseRedirect(reverse('personal_tab:person_exam_cookie_credit', kwargs={ 'person_id': person_id, 'slug': slug, 'date_from': _urlize(start), 'date_to': _urlize(end) })) @@ -803,8 +806,8 @@ def statistics_form(request): if request.method == 'POST': form = StatisticsForm(data=request.POST) if form.is_valid(): - start = form.cleaned_data['start_date'].astimezone(timezone.utc) - end = form.cleaned_data['end_date'].astimezone(timezone.utc) + start = form.cleaned_data['start_date'].astimezone(tz.utc) + end = form.cleaned_data['end_date'].astimezone(tz.utc) return HttpResponseRedirect(reverse('personal_tab:statistics', kwargs={ 'date_from': _urlize(start), 'date_to': _urlize(end), @@ -868,7 +871,7 @@ def balance(request, dt_str=False): if request.method == 'POST': form = DateTimeForm(request.POST) if form.is_valid(): - dt = form.cleaned_data['datetime'].astimezone(timezone.utc) + dt = form.cleaned_data['datetime'].astimezone(tz.utc) return HttpResponseRedirect(reverse('personal_tab:balance', args=[_urlize(dt)])) # Redirect to form if no date given @@ -888,7 +891,7 @@ def balance(request, dt_str=False): dt_url = _urlize(dt) # Date the SEPA debt collection went into effect: 2013-10-31 00:00 CET - begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=timezone.utc) + begin = datetime.datetime(2013, 10, 30, 23, 00, 00, tzinfo=tz.utc) all_transactions = Transaction.objects.filter(date__gte=begin, date__lt=dt) all_transactions_aggregated = all_transactions.aggregate(Sum('price')) @@ -942,8 +945,8 @@ def export(request, date_from=False, date_to=False): if request.method == 'POST': form = PeriodTimeForm(request.POST) if form.is_valid(): - start = form.cleaned_data['datetime_from'].astimezone(timezone.utc) - end = form.cleaned_data['datetime_to'].astimezone(timezone.utc) + start = form.cleaned_data['datetime_from'].astimezone(tz.utc) + end = form.cleaned_data['datetime_to'].astimezone(tz.utc) return HttpResponseRedirect(reverse('personal_tab:export', args=[_urlize(start), _urlize(end)])) # Redirect to form if no date given @@ -1253,7 +1256,7 @@ def debt_collection_new(request): if request.method == 'POST': form = DebtCollectionForm(minimal_execution_date, request.POST) if form.is_valid(): - end = form.cleaned_data['end'].astimezone(timezone.utc) + end = form.cleaned_data['end'].astimezone(tz.utc) contribution = form.cleaned_data['contribution'] cookie_corner = form.cleaned_data['cookie_corner'] diff --git a/amelie/settings/generic.py b/amelie/settings/generic.py index b955fd27..a0ffd5c5 100644 --- a/amelie/settings/generic.py +++ b/amelie/settings/generic.py @@ -191,6 +191,9 @@ } ] +# Override the default form rendering class to include our IA styling templates. +FORM_RENDERER = "amelie.style.forms.AmelieFormRenderer" + # Middleware classes that are used by the application MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', @@ -995,4 +998,4 @@ # Set language cookie settings for /graphql language switcher LANGUAGE_COOKIE_SAMESITE = "None" -LANGUAGE_COOKIE_SECURE = "True" # Cookie is only sent over HTTPS \ No newline at end of file +LANGUAGE_COOKIE_SECURE = "True" # Cookie is only sent over HTTPS diff --git a/amelie/style/forms.py b/amelie/style/forms.py index 04748fbe..d008fd63 100644 --- a/amelie/style/forms.py +++ b/amelie/style/forms.py @@ -1,54 +1,21 @@ -from django.forms.forms import BaseForm -from django.forms.utils import ErrorList +from django.forms.renderers import DjangoTemplates -def as_div(self): - """Render a form as a div with the correct CSS class""" +class AmelieFormRenderer(DjangoTemplates): + form_template_name = "style/forms/div.html" + field_template_name = "style/forms/field.html" - return self._html_output( - normal_row='
%s
' % ''.join(['%s' % e for e in self]) - - def __str__(self): - return self.as_divs() - - -def update_init(old_init): - """Override the __init__ so that the correct error_class is used""" - - def _update_init(self, *args, **kwargs): - kwargs_new = {'error_class': DivErrorList} - kwargs_new.update(kwargs) - old_init(self, *args, **kwargs_new) - - return _update_init + def render(self, template_name, context, request = None): + # Override the default template used to render errors. This template is determined by a different class, + # so we intercept the call in the render function itself. + if template_name == "django/forms/errors/list/default.html": + template_name = "style/forms/errors.html" + return super().render(template_name=template_name, context=context, request=request) def inject_style(*args): """ - Override methods and add new methods so that the new IA style is used everywhere in Forms, - without having to change it everywhere. + Empty method. Was previously used to inject our styling into forms but is unused now. + Stubbed out here because it's a pain to find and remove all usages. """ - - for form in args: - # Type checking - if not issubclass(form, BaseForm): - raise Exception("%s is not an instance of BaseForm" % form.__name__) - - # Inject - form.as_div = as_div - form.__init__ = update_init(form.__init__) - form.__str__ = as_div + return diff --git a/amelie/style/static/css/compiled.css b/amelie/style/static/css/compiled.css index 198cb5cf..4df9beb2 100644 --- a/amelie/style/static/css/compiled.css +++ b/amelie/style/static/css/compiled.css @@ -11259,6 +11259,11 @@ img.profile_picture { margin-top: 0; width: 368px; } +form.big .tabbed-content textarea, +form.big .tabbed-content input, +form.big .tabbed-content select { + max-width: 100%; +} .block { background: #FFFFFF; box-shadow: 0 0 2px rgba(0, 0, 0, 0.3); @@ -11625,12 +11630,18 @@ form.big select { padding: 4px 6px; border: 1px solid #aaa; } +form.big select { + max-width: calc(100% - 190px); +} form.big textarea { width: 100%; } form.big .form-row { padding: 5px 0px; } +form.big .form-row.errors { + color: #B8231F; +} form.big .form-row label { margin-top: 3px; width: 180px; diff --git a/amelie/style/static/less/classes/tabbed-input.less b/amelie/style/static/less/classes/tabbed-input.less index 977018c3..292a5577 100644 --- a/amelie/style/static/less/classes/tabbed-input.less +++ b/amelie/style/static/less/classes/tabbed-input.less @@ -51,3 +51,9 @@ width: 368px; } } + +form.big .tabbed-content { + textarea, input, select { + max-width: 100%; + } +} diff --git a/amelie/style/static/less/htmltags/form.less b/amelie/style/static/less/htmltags/form.less index 26888d25..047245ab 100644 --- a/amelie/style/static/less/htmltags/form.less +++ b/amelie/style/static/less/htmltags/form.less @@ -26,7 +26,9 @@ form { padding: 4px 6px; border: 1px solid #aaa; } - + select { + max-width: calc(100% ~"-" 190px); + } textarea { width: 100%; } @@ -34,6 +36,10 @@ form { .form-row { padding: 5px 0px; + &.errors { + color: @educolor; + } + label { margin-top: 3px; width: 180px; diff --git a/amelie/style/templates/style/forms/div.html b/amelie/style/templates/style/forms/div.html new file mode 100644 index 00000000..991cd2b1 --- /dev/null +++ b/amelie/style/templates/style/forms/div.html @@ -0,0 +1,15 @@ +{{ errors }} +{% if errors and not fields %} +{{ error }}
{% endfor %}{% endif %} diff --git a/amelie/style/templates/style/forms/field.html b/amelie/style/templates/style/forms/field.html new file mode 100644 index 00000000..5d81b79f --- /dev/null +++ b/amelie/style/templates/style/forms/field.html @@ -0,0 +1,4 @@ +{% if field.label %}{{ field.label_tag }}{% endif %} +{{ field }} +{% if field.help_text %}