From 78c117f30052b37e8f9ecb940a843ab334994e1d Mon Sep 17 00:00:00 2001 From: Christian Merten Date: Sat, 29 Nov 2025 15:03:32 +0100 Subject: [PATCH] chore(*): reformat using ruff (#22) This is the remainder except for `jdav_web/members/`. --- Jenkinsfile | 2 +- docker/production/nginx/kompass.nginx.conf | 1 - .../management/commands/ensuresuperuser.py | 16 +- jdav_web/contrib/templatetags/common.py | 1 + jdav_web/finance/migrations/0001_initial.py | 323 +++++--- .../migrations/0002_alter_permissions.py | 54 +- .../0003_alter_bill_options_and_more.py | 115 ++- .../migrations/0004_alter_bill_amount.py | 14 +- .../migrations/0005_alter_bill_proof.py | 13 +- ...6_statement_add_allowance_to_subsidy_to.py | 34 +- .../0007_alter_statement_allowance_to.py | 20 +- ...8_alter_statement_allowance_to_and_more.py | 34 +- .../migrations/0009_statement_ljp_to.py | 22 +- .../migrations/0010_statement_status.py | 20 +- ...emove_statement_confirmed_and_submitted.py | 11 +- .../0012_statementonexcursionproxy.py | 31 +- .../templates/admin/confirmed_statement.html | 4 +- .../admin/overview_submitted_statement.html | 6 +- .../templates/finance/statement_summary.tex | 2 +- jdav_web/finance/tests/__init__.py | 4 +- jdav_web/finance/tests/admin.py | 464 ++++++------ jdav_web/finance/tests/migrations.py | 44 +- jdav_web/finance/tests/models.py | 694 ++++++++++++------ jdav_web/finance/tests/rules.py | 48 +- jdav_web/logindata/migrations/0001_initial.py | 55 +- ...0002_alter_registrationpassword_options.py | 10 +- jdav_web/logindata/tests/__init__.py | 4 +- jdav_web/logindata/tests/oauth.py | 22 +- jdav_web/logindata/tests/views.py | 165 +++-- ...nitial_squashed_0007_alter_termin_group.py | 208 +++++- jdav_web/mailer/admin.py | 9 +- jdav_web/mailer/mailutils.py | 1 - .../management/commands/notify_active.py | 37 +- .../mailer/management/commands/reply_addrs.py | 23 +- ...nitial_squashed_0006_auto_20210924_1155.py | 168 +++-- .../migrations/0002_message_created_by.py | 21 +- .../migrations/0003_alter_message_options.py | 19 +- .../migrations/0004_alter_attachment_f.py | 11 +- .../0005_alter_emailaddress_name.py | 20 +- .../0006_emailaddress_allowed_senders.py | 20 +- .../0007_emailaddress_internal_only.py | 16 +- .../0008_alter_emailaddress_name.py | 21 +- jdav_web/mailer/models.py | 1 - .../mailer/templates/mailer/subscribe.html | 2 +- .../mailer/templates/mailer/unsubscribe.html | 2 +- jdav_web/mailer/tests/__init__.py | 8 +- jdav_web/mailer/tests/admin.py | 176 +++-- jdav_web/mailer/tests/mailutils.py | 36 +- jdav_web/mailer/tests/models.py | 129 ++-- jdav_web/mailer/tests/rules.py | 19 +- jdav_web/mailer/tests/utils.py | 32 +- jdav_web/mailer/tests/views.py | 34 +- jdav_web/material/admin.py | 1 + ...nitial_squashed_0002_auto_20171011_2045.py | 95 ++- jdav_web/startpage/migrations/0001_initial.py | 149 +++- .../0002_section_show_in_navigation.py | 12 +- .../migrations/0003_alter_post_section.py | 17 +- .../0004_internal_startpage_links.py | 40 +- .../templates/startpage/contact.html | 2 +- .../startpage/templatetags/markdown_extras.py | 20 +- 60 files changed, 2328 insertions(+), 1254 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 054dfef..33d3fc1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,5 @@ node { - checkout scm + checkout scm } pipeline { diff --git a/docker/production/nginx/kompass.nginx.conf b/docker/production/nginx/kompass.nginx.conf index 860e127..0e8edbe 100644 --- a/docker/production/nginx/kompass.nginx.conf +++ b/docker/production/nginx/kompass.nginx.conf @@ -27,4 +27,3 @@ server { } } - diff --git a/jdav_web/contrib/management/commands/ensuresuperuser.py b/jdav_web/contrib/management/commands/ensuresuperuser.py index d701d64..857d732 100644 --- a/jdav_web/contrib/management/commands/ensuresuperuser.py +++ b/jdav_web/contrib/management/commands/ensuresuperuser.py @@ -1,28 +1,26 @@ import os + from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand + class Command(BaseCommand): help = "Creates a super-user non-interactively if it doesn't exist." def handle(self, *args, **options): User = get_user_model() - username = os.environ.get('DJANGO_SUPERUSER_USERNAME', '') - password = os.environ.get('DJANGO_SUPERUSER_PASSWORD', '') + username = os.environ.get("DJANGO_SUPERUSER_USERNAME", "") + password = os.environ.get("DJANGO_SUPERUSER_PASSWORD", "") if not username or not password: - self.stdout.write( - self.style.WARNING('Superuser data was not set. Skipping.') - ) + self.stdout.write(self.style.WARNING("Superuser data was not set. Skipping.")) return if not User.objects.filter(username=username).exists(): User.objects.create_superuser(username=username, password=password) - self.stdout.write( - self.style.SUCCESS('Successfully created superuser.') - ) + self.stdout.write(self.style.SUCCESS("Successfully created superuser.")) else: self.stdout.write( - self.style.SUCCESS('Superuser with configured username already exists. Skipping.') + self.style.SUCCESS("Superuser with configured username already exists. Skipping.") ) diff --git a/jdav_web/contrib/templatetags/common.py b/jdav_web/contrib/templatetags/common.py index 4e2b350..7c3dbc2 100644 --- a/jdav_web/contrib/templatetags/common.py +++ b/jdav_web/contrib/templatetags/common.py @@ -3,6 +3,7 @@ from django.conf import settings register = template.Library() + # settings value @register.simple_tag def settings_value(name): diff --git a/jdav_web/finance/migrations/0001_initial.py b/jdav_web/finance/migrations/0001_initial.py index 5f37d7c..6934b07 100644 --- a/jdav_web/finance/migrations/0001_initial.py +++ b/jdav_web/finance/migrations/0001_initial.py @@ -1,136 +1,293 @@ # Generated by Django 4.0.1 on 2023-03-29 22:16 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - initial = True dependencies = [ - ('members', '0002_remove_member_not_waiting_and_more'), + ("members", "0002_remove_member_not_waiting_and_more"), ] operations = [ migrations.CreateModel( - name='Ledger', + name="Ledger", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=30, verbose_name='Name')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=30, verbose_name="Name")), ], options={ - 'verbose_name': 'Ledger', - 'verbose_name_plural': 'Ledgers', + "verbose_name": "Ledger", + "verbose_name_plural": "Ledgers", }, ), migrations.CreateModel( - name='Statement', + name="Statement", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('short_description', models.CharField(blank=True, max_length=30, verbose_name='Short description')), - ('explanation', models.TextField(blank=True, verbose_name='Explanation')), - ('night_cost', models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='Price per night')), - ('submitted', models.BooleanField(default=False, verbose_name='Submitted')), - ('submitted_date', models.DateTimeField(default=None, null=True, verbose_name='Submitted on')), - ('confirmed', models.BooleanField(default=False, verbose_name='Confirmed')), - ('confirmed_date', models.DateTimeField(default=None, null=True, verbose_name='Paid on')), - ('confirmed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confirmed_statements', to='members.member', verbose_name='Authorized by')), - ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_statements', to='members.member', verbose_name='Created by')), - ('excursion', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='members.freizeit', verbose_name='Associated excursion')), - ('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_statements', to='members.member', verbose_name='Submitted by')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "short_description", + models.CharField(blank=True, max_length=30, verbose_name="Short description"), + ), + ("explanation", models.TextField(blank=True, verbose_name="Explanation")), + ( + "night_cost", + models.DecimalField( + decimal_places=2, default=0, max_digits=5, verbose_name="Price per night" + ), + ), + ("submitted", models.BooleanField(default=False, verbose_name="Submitted")), + ( + "submitted_date", + models.DateTimeField(default=None, null=True, verbose_name="Submitted on"), + ), + ("confirmed", models.BooleanField(default=False, verbose_name="Confirmed")), + ( + "confirmed_date", + models.DateTimeField(default=None, null=True, verbose_name="Paid on"), + ), + ( + "confirmed_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="confirmed_statements", + to="members.member", + verbose_name="Authorized by", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="created_statements", + to="members.member", + verbose_name="Created by", + ), + ), + ( + "excursion", + models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="members.freizeit", + verbose_name="Associated excursion", + ), + ), + ( + "submitted_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="submitted_statements", + to="members.member", + verbose_name="Submitted by", + ), + ), ], options={ - 'verbose_name': 'Statement', - 'verbose_name_plural': 'Statements', - 'permissions': [('may_edit_submitted_statements', 'Is allowed to edit submitted statements')], + "verbose_name": "Statement", + "verbose_name_plural": "Statements", + "permissions": [ + ("may_edit_submitted_statements", "Is allowed to edit submitted statements") + ], }, ), migrations.CreateModel( - name='Transaction', + name="Transaction", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('reference', models.TextField(verbose_name='Reference')), - ('amount', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='Amount')), - ('confirmed', models.BooleanField(default=False, verbose_name='Paid')), - ('confirmed_date', models.DateTimeField(default=None, null=True, verbose_name='Paid on')), - ('confirmed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confirmed_transactions', to='members.member', verbose_name='Authorized by')), - ('ledger', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.ledger', verbose_name='Ledger')), - ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.member', verbose_name='Recipient')), - ('statement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='finance.statement', verbose_name='Statement')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("reference", models.TextField(verbose_name="Reference")), + ( + "amount", + models.DecimalField(decimal_places=2, max_digits=6, verbose_name="Amount"), + ), + ("confirmed", models.BooleanField(default=False, verbose_name="Paid")), + ( + "confirmed_date", + models.DateTimeField(default=None, null=True, verbose_name="Paid on"), + ), + ( + "confirmed_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="confirmed_transactions", + to="members.member", + verbose_name="Authorized by", + ), + ), + ( + "ledger", + models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="finance.ledger", + verbose_name="Ledger", + ), + ), + ( + "member", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="members.member", + verbose_name="Recipient", + ), + ), + ( + "statement", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="finance.statement", + verbose_name="Statement", + ), + ), ], options={ - 'verbose_name': 'Transaction', - 'verbose_name_plural': 'Transactions', + "verbose_name": "Transaction", + "verbose_name_plural": "Transactions", }, ), migrations.CreateModel( - name='Receipt', + name="Receipt", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('short_description', models.CharField(max_length=30, verbose_name='Short description')), - ('amount', models.DecimalField(decimal_places=2, max_digits=6)), - ('comments', models.TextField()), - ('ledger', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='finance.ledger', verbose_name='Ledger')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "short_description", + models.CharField(max_length=30, verbose_name="Short description"), + ), + ("amount", models.DecimalField(decimal_places=2, max_digits=6)), + ("comments", models.TextField()), + ( + "ledger", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="finance.ledger", + verbose_name="Ledger", + ), + ), ], ), migrations.CreateModel( - name='Bill', + name="Bill", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('short_description', models.CharField(max_length=30, verbose_name='Short description')), - ('explanation', models.TextField(blank=True, verbose_name='Explanation')), - ('amount', models.DecimalField(decimal_places=2, default=0, max_digits=6)), - ('costs_covered', models.BooleanField(default=False, verbose_name='Covered')), - ('refunded', models.BooleanField(default=False, verbose_name='Refunded')), - ('proof', models.ImageField(blank=True, upload_to='bill_images', verbose_name='Proof')), - ('paid_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='members.member', verbose_name='Paid by')), - ('statement', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='finance.statement', verbose_name='Statement')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "short_description", + models.CharField(max_length=30, verbose_name="Short description"), + ), + ("explanation", models.TextField(blank=True, verbose_name="Explanation")), + ("amount", models.DecimalField(decimal_places=2, default=0, max_digits=6)), + ("costs_covered", models.BooleanField(default=False, verbose_name="Covered")), + ("refunded", models.BooleanField(default=False, verbose_name="Refunded")), + ( + "proof", + models.ImageField(blank=True, upload_to="bill_images", verbose_name="Proof"), + ), + ( + "paid_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="members.member", + verbose_name="Paid by", + ), + ), + ( + "statement", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="finance.statement", + verbose_name="Statement", + ), + ), ], options={ - 'verbose_name': 'Bill', - 'verbose_name_plural': 'Bills', + "verbose_name": "Bill", + "verbose_name_plural": "Bills", }, ), migrations.CreateModel( - name='StatementConfirmed', - fields=[ - ], + name="StatementConfirmed", + fields=[], options={ - 'verbose_name': 'Paid statement', - 'verbose_name_plural': 'Paid statements', - 'permissions': (('may_manage_confirmed_statements', 'Can view and manage confirmed statements.'),), - 'proxy': True, - 'indexes': [], - 'constraints': [], + "verbose_name": "Paid statement", + "verbose_name_plural": "Paid statements", + "permissions": ( + ( + "may_manage_confirmed_statements", + "Can view and manage confirmed statements.", + ), + ), + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('finance.statement',), + bases=("finance.statement",), ), migrations.CreateModel( - name='StatementSubmitted', - fields=[ - ], + name="StatementSubmitted", + fields=[], options={ - 'verbose_name': 'Submitted statement', - 'verbose_name_plural': 'Submitted statements', - 'permissions': (('may_manage_submitted_statements', 'Can view and manage submitted statements.'),), - 'proxy': True, - 'indexes': [], - 'constraints': [], + "verbose_name": "Submitted statement", + "verbose_name_plural": "Submitted statements", + "permissions": ( + ( + "may_manage_submitted_statements", + "Can view and manage submitted statements.", + ), + ), + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('finance.statement',), + bases=("finance.statement",), ), migrations.CreateModel( - name='StatementUnSubmitted', - fields=[ - ], + name="StatementUnSubmitted", + fields=[], options={ - 'verbose_name': 'Statement in preparation', - 'verbose_name_plural': 'Statements in preparation', - 'proxy': True, - 'indexes': [], - 'constraints': [], + "verbose_name": "Statement in preparation", + "verbose_name_plural": "Statements in preparation", + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('finance.statement',), + bases=("finance.statement",), ), ] diff --git a/jdav_web/finance/migrations/0002_alter_permissions.py b/jdav_web/finance/migrations/0002_alter_permissions.py index b1a80d6..11b2635 100644 --- a/jdav_web/finance/migrations/0002_alter_permissions.py +++ b/jdav_web/finance/migrations/0002_alter_permissions.py @@ -4,46 +4,50 @@ from django.db import migrations class Migration(migrations.Migration): - - #replaces = [('finance', '0002_billonexcursionproxy_billonstatementproxy_and_more'), ('finance', '0003_alter_statementunsubmitted_options'), ('finance', '0004_alter_billonexcursionproxy_options'), ('finance', '0005_alter_billonstatementproxy_options'), ('finance', '0006_alter_statementsubmitted_options'), ('finance', '0007_alter_billonexcursionproxy_options_and_more')] + # replaces = [('finance', '0002_billonexcursionproxy_billonstatementproxy_and_more'), ('finance', '0003_alter_statementunsubmitted_options'), ('finance', '0004_alter_billonexcursionproxy_options'), ('finance', '0005_alter_billonstatementproxy_options'), ('finance', '0006_alter_statementsubmitted_options'), ('finance', '0007_alter_billonexcursionproxy_options_and_more')] dependencies = [ - ('finance', '0001_initial'), + ("finance", "0001_initial"), ] operations = [ migrations.AlterModelOptions( - name='statementsubmitted', - options={'permissions': [('process_statementsubmitted', 'Can manage submitted statements.')], 'verbose_name': 'Submitted statement', 'verbose_name_plural': 'Submitted statements'}, + name="statementsubmitted", + options={ + "permissions": [("process_statementsubmitted", "Can manage submitted statements.")], + "verbose_name": "Submitted statement", + "verbose_name_plural": "Submitted statements", + }, ), migrations.CreateModel( - name='BillOnExcursionProxy', - fields=[ - ], + name="BillOnExcursionProxy", + fields=[], options={ - 'verbose_name': 'Bill', - 'verbose_name_plural': 'Bills', - 'proxy': True, - 'indexes': [], - 'constraints': [], + "verbose_name": "Bill", + "verbose_name_plural": "Bills", + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('finance.bill',), + bases=("finance.bill",), ), migrations.CreateModel( - name='BillOnStatementProxy', - fields=[ - ], + name="BillOnStatementProxy", + fields=[], options={ - 'verbose_name': 'Bill', - 'verbose_name_plural': 'Bills', - 'proxy': True, - 'indexes': [], - 'constraints': [], + "verbose_name": "Bill", + "verbose_name_plural": "Bills", + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('finance.bill',), + bases=("finance.bill",), ), migrations.AlterModelOptions( - name='statementunsubmitted', - options={'verbose_name': 'Statement in preparation', 'verbose_name_plural': 'Statements in preparation'}, + name="statementunsubmitted", + options={ + "verbose_name": "Statement in preparation", + "verbose_name_plural": "Statements in preparation", + }, ), ] diff --git a/jdav_web/finance/migrations/0003_alter_bill_options_and_more.py b/jdav_web/finance/migrations/0003_alter_bill_options_and_more.py index e7f04b4..94d76a9 100644 --- a/jdav_web/finance/migrations/0003_alter_bill_options_and_more.py +++ b/jdav_web/finance/migrations/0003_alter_bill_options_and_more.py @@ -4,38 +4,121 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('finance', '0002_alter_permissions'), + ("finance", "0002_alter_permissions"), ] operations = [ migrations.AlterModelOptions( - name='bill', - options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Bill', 'verbose_name_plural': 'Bills'}, + name="bill", + options={ + "default_permissions": ( + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", + ), + "verbose_name": "Bill", + "verbose_name_plural": "Bills", + }, ), migrations.AlterModelOptions( - name='billonexcursionproxy', - options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Bill', 'verbose_name_plural': 'Bills'}, + name="billonexcursionproxy", + options={ + "default_permissions": ( + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", + ), + "verbose_name": "Bill", + "verbose_name_plural": "Bills", + }, ), migrations.AlterModelOptions( - name='billonstatementproxy', - options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Bill', 'verbose_name_plural': 'Bills'}, + name="billonstatementproxy", + options={ + "default_permissions": ( + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", + ), + "verbose_name": "Bill", + "verbose_name_plural": "Bills", + }, ), migrations.AlterModelOptions( - name='statement', - options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': [('may_edit_submitted_statements', 'Is allowed to edit submitted statements')], 'verbose_name': 'Statement', 'verbose_name_plural': 'Statements'}, + name="statement", + options={ + "default_permissions": ( + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", + ), + "permissions": [ + ("may_edit_submitted_statements", "Is allowed to edit submitted statements") + ], + "verbose_name": "Statement", + "verbose_name_plural": "Statements", + }, ), migrations.AlterModelOptions( - name='statementconfirmed', - options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': [('may_manage_confirmed_statements', 'Can view and manage confirmed statements.')], 'verbose_name': 'Paid statement', 'verbose_name_plural': 'Paid statements'}, + name="statementconfirmed", + options={ + "default_permissions": ( + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", + ), + "permissions": [ + ("may_manage_confirmed_statements", "Can view and manage confirmed statements.") + ], + "verbose_name": "Paid statement", + "verbose_name_plural": "Paid statements", + }, ), migrations.AlterModelOptions( - name='statementsubmitted', - options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': [('process_statementsubmitted', 'Can manage submitted statements.')], 'verbose_name': 'Submitted statement', 'verbose_name_plural': 'Submitted statements'}, + name="statementsubmitted", + options={ + "default_permissions": ( + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", + ), + "permissions": [("process_statementsubmitted", "Can manage submitted statements.")], + "verbose_name": "Submitted statement", + "verbose_name_plural": "Submitted statements", + }, ), migrations.AlterModelOptions( - name='statementunsubmitted', - options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'verbose_name': 'Statement in preparation', 'verbose_name_plural': 'Statements in preparation'}, + name="statementunsubmitted", + options={ + "default_permissions": ( + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", + ), + "verbose_name": "Statement in preparation", + "verbose_name_plural": "Statements in preparation", + }, ), ] diff --git a/jdav_web/finance/migrations/0004_alter_bill_amount.py b/jdav_web/finance/migrations/0004_alter_bill_amount.py index 127b3ee..21826f6 100644 --- a/jdav_web/finance/migrations/0004_alter_bill_amount.py +++ b/jdav_web/finance/migrations/0004_alter_bill_amount.py @@ -1,18 +1,20 @@ # Generated by Django 4.0.1 on 2024-12-02 00:22 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('finance', '0003_alter_bill_options_and_more'), + ("finance", "0003_alter_bill_options_and_more"), ] operations = [ migrations.AlterField( - model_name='bill', - name='amount', - field=models.DecimalField(decimal_places=2, default=0, max_digits=6, verbose_name='Amount'), + model_name="bill", + name="amount", + field=models.DecimalField( + decimal_places=2, default=0, max_digits=6, verbose_name="Amount" + ), ), ] diff --git a/jdav_web/finance/migrations/0005_alter_bill_proof.py b/jdav_web/finance/migrations/0005_alter_bill_proof.py index e87ad53..e8986c5 100644 --- a/jdav_web/finance/migrations/0005_alter_bill_proof.py +++ b/jdav_web/finance/migrations/0005_alter_bill_proof.py @@ -1,19 +1,20 @@ # Generated by Django 4.0.1 on 2024-12-26 09:45 -from django.db import migrations import utils +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('finance', '0004_alter_bill_amount'), + ("finance", "0004_alter_bill_amount"), ] operations = [ migrations.AlterField( - model_name='bill', - name='proof', - field=utils.RestrictedFileField(blank=True, upload_to='bill_images', verbose_name='Proof'), + model_name="bill", + name="proof", + field=utils.RestrictedFileField( + blank=True, upload_to="bill_images", verbose_name="Proof" + ), ), ] diff --git a/jdav_web/finance/migrations/0006_statement_add_allowance_to_subsidy_to.py b/jdav_web/finance/migrations/0006_statement_add_allowance_to_subsidy_to.py index df2d124..7eeb7e2 100644 --- a/jdav_web/finance/migrations/0006_statement_add_allowance_to_subsidy_to.py +++ b/jdav_web/finance/migrations/0006_statement_add_allowance_to_subsidy_to.py @@ -1,26 +1,38 @@ # Generated by Django 4.0.1 on 2025-01-18 19:08 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('members', '0032_member_upload_registration_form_key'), - ('members', '0033_freizeit_approved_extra_youth_leader_count'), - ('finance', '0005_alter_bill_proof'), + ("members", "0032_member_upload_registration_form_key"), + ("members", "0033_freizeit_approved_extra_youth_leader_count"), + ("finance", "0005_alter_bill_proof"), ] operations = [ migrations.AddField( - model_name='statement', - name='allowance_to', - field=models.ManyToManyField(help_text='The youth leaders to which an allowance should be paid. The count must match the number of permitted youth leaders.', related_name='receives_allowance_for_statements', to='members.Member', verbose_name='Pay allowance to'), + model_name="statement", + name="allowance_to", + field=models.ManyToManyField( + help_text="The youth leaders to which an allowance should be paid. The count must match the number of permitted youth leaders.", + related_name="receives_allowance_for_statements", + to="members.Member", + verbose_name="Pay allowance to", + ), ), migrations.AddField( - model_name='statement', - name='subsidy_to', - field=models.ForeignKey(help_text='The person that should receive the subsidy for night and travel costs. Typically the person who paid for them.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='receives_subsidy_for_statements', to='members.member', verbose_name='Pay subsidy to'), + model_name="statement", + name="subsidy_to", + field=models.ForeignKey( + help_text="The person that should receive the subsidy for night and travel costs. Typically the person who paid for them.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="receives_subsidy_for_statements", + to="members.member", + verbose_name="Pay subsidy to", + ), ), ] diff --git a/jdav_web/finance/migrations/0007_alter_statement_allowance_to.py b/jdav_web/finance/migrations/0007_alter_statement_allowance_to.py index 237ced2..35923c1 100644 --- a/jdav_web/finance/migrations/0007_alter_statement_allowance_to.py +++ b/jdav_web/finance/migrations/0007_alter_statement_allowance_to.py @@ -1,19 +1,25 @@ # Generated by Django 4.0.1 on 2025-01-18 22:00 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('members', '0033_freizeit_approved_extra_youth_leader_count'), - ('finance', '0006_statement_add_allowance_to_subsidy_to'), + ("members", "0033_freizeit_approved_extra_youth_leader_count"), + ("finance", "0006_statement_add_allowance_to_subsidy_to"), ] operations = [ migrations.AlterField( - model_name='statement', - name='allowance_to', - field=models.ManyToManyField(blank=True, help_text='The youth leaders to which an allowance should be paid. The count must match the number of permitted youth leaders.', related_name='receives_allowance_for_statements', to='members.Member', verbose_name='Pay allowance to'), + model_name="statement", + name="allowance_to", + field=models.ManyToManyField( + blank=True, + help_text="The youth leaders to which an allowance should be paid. The count must match the number of permitted youth leaders.", + related_name="receives_allowance_for_statements", + to="members.Member", + verbose_name="Pay allowance to", + ), ), ] diff --git a/jdav_web/finance/migrations/0008_alter_statement_allowance_to_and_more.py b/jdav_web/finance/migrations/0008_alter_statement_allowance_to_and_more.py index faf7504..e0529e1 100644 --- a/jdav_web/finance/migrations/0008_alter_statement_allowance_to_and_more.py +++ b/jdav_web/finance/migrations/0008_alter_statement_allowance_to_and_more.py @@ -1,25 +1,39 @@ # Generated by Django 4.0.1 on 2025-01-23 22:16 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('members', '0033_freizeit_approved_extra_youth_leader_count'), - ('finance', '0007_alter_statement_allowance_to'), + ("members", "0033_freizeit_approved_extra_youth_leader_count"), + ("finance", "0007_alter_statement_allowance_to"), ] operations = [ migrations.AlterField( - model_name='statement', - name='allowance_to', - field=models.ManyToManyField(blank=True, help_text='The youth leaders to which an allowance should be paid.', related_name='receives_allowance_for_statements', to='members.Member', verbose_name='Pay allowance to'), + model_name="statement", + name="allowance_to", + field=models.ManyToManyField( + blank=True, + help_text="The youth leaders to which an allowance should be paid.", + related_name="receives_allowance_for_statements", + to="members.Member", + verbose_name="Pay allowance to", + ), ), migrations.AlterField( - model_name='statement', - name='subsidy_to', - field=models.ForeignKey(blank=True, help_text='The person that should receive the subsidy for night and travel costs. Typically the person who paid for them.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='receives_subsidy_for_statements', to='members.member', verbose_name='Pay subsidy to'), + model_name="statement", + name="subsidy_to", + field=models.ForeignKey( + blank=True, + help_text="The person that should receive the subsidy for night and travel costs. Typically the person who paid for them.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="receives_subsidy_for_statements", + to="members.member", + verbose_name="Pay subsidy to", + ), ), ] diff --git a/jdav_web/finance/migrations/0009_statement_ljp_to.py b/jdav_web/finance/migrations/0009_statement_ljp_to.py index fc13323..c1279e9 100644 --- a/jdav_web/finance/migrations/0009_statement_ljp_to.py +++ b/jdav_web/finance/migrations/0009_statement_ljp_to.py @@ -1,20 +1,28 @@ # Generated by Django 4.2.20 on 2025-04-03 21:04 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('members', '0039_membertraining_certificate_attendance'), - ('finance', '0008_alter_statement_allowance_to_and_more'), + ("members", "0039_membertraining_certificate_attendance"), + ("finance", "0008_alter_statement_allowance_to_and_more"), ] operations = [ migrations.AddField( - model_name='statement', - name='ljp_to', - field=models.ForeignKey(blank=True, help_text='The person that should receive the ljp contributions for the participants. Should be only selected if an ljp request was submitted.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='receives_ljp_for_statements', to='members.member', verbose_name='Pay ljp contributions to'), + model_name="statement", + name="ljp_to", + field=models.ForeignKey( + blank=True, + help_text="The person that should receive the ljp contributions for the participants. Should be only selected if an ljp request was submitted.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="receives_ljp_for_statements", + to="members.member", + verbose_name="Pay ljp contributions to", + ), ), ] diff --git a/jdav_web/finance/migrations/0010_statement_status.py b/jdav_web/finance/migrations/0010_statement_status.py index 7c3a4d3..ed5987d 100644 --- a/jdav_web/finance/migrations/0010_statement_status.py +++ b/jdav_web/finance/migrations/0010_statement_status.py @@ -1,6 +1,7 @@ # Generated by Django 4.2.20 on 2025-10-11 15:43 -from django.db import migrations, models +from django.db import migrations +from django.db import models def set_status_from_old_fields(apps, schema_editor): @@ -10,7 +11,7 @@ def set_status_from_old_fields(apps, schema_editor): - If submitted is True but confirmed is False, status = SUBMITTED (1) - Otherwise, status = UNSUBMITTED (0) """ - Statement = apps.get_model('finance', 'Statement') + Statement = apps.get_model("finance", "Statement") UNSUBMITTED, SUBMITTED, CONFIRMED = 0, 1, 2 for statement in Statement.objects.all(): @@ -20,20 +21,23 @@ def set_status_from_old_fields(apps, schema_editor): statement.status = SUBMITTED else: statement.status = UNSUBMITTED - statement.save(update_fields=['status']) + statement.save(update_fields=["status"]) class Migration(migrations.Migration): - dependencies = [ - ('finance', '0009_statement_ljp_to'), + ("finance", "0009_statement_ljp_to"), ] operations = [ migrations.AddField( - model_name='statement', - name='status', - field=models.IntegerField(choices=[(0, 'In preparation'), (1, 'Submitted'), (2, 'Confirmed')], default=0, verbose_name='Status'), + model_name="statement", + name="status", + field=models.IntegerField( + choices=[(0, "In preparation"), (1, "Submitted"), (2, "Confirmed")], + default=0, + verbose_name="Status", + ), ), migrations.RunPython(set_status_from_old_fields, reverse_code=migrations.RunPython.noop), ] diff --git a/jdav_web/finance/migrations/0011_remove_statement_confirmed_and_submitted.py b/jdav_web/finance/migrations/0011_remove_statement_confirmed_and_submitted.py index 53e36a4..fffd505 100644 --- a/jdav_web/finance/migrations/0011_remove_statement_confirmed_and_submitted.py +++ b/jdav_web/finance/migrations/0011_remove_statement_confirmed_and_submitted.py @@ -4,18 +4,17 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('finance', '0010_statement_status'), + ("finance", "0010_statement_status"), ] operations = [ migrations.RemoveField( - model_name='statement', - name='confirmed', + model_name="statement", + name="confirmed", ), migrations.RemoveField( - model_name='statement', - name='submitted', + model_name="statement", + name="submitted", ), ] diff --git a/jdav_web/finance/migrations/0012_statementonexcursionproxy.py b/jdav_web/finance/migrations/0012_statementonexcursionproxy.py index ab95dca..5da8f5c 100644 --- a/jdav_web/finance/migrations/0012_statementonexcursionproxy.py +++ b/jdav_web/finance/migrations/0012_statementonexcursionproxy.py @@ -4,25 +4,30 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('finance', '0011_remove_statement_confirmed_and_submitted'), + ("finance", "0011_remove_statement_confirmed_and_submitted"), ] operations = [ migrations.CreateModel( - name='StatementOnExcursionProxy', - fields=[ - ], + name="StatementOnExcursionProxy", + fields=[], options={ - 'verbose_name': 'Statement', - 'verbose_name_plural': 'Statements', - 'abstract': False, - 'proxy': True, - 'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), - 'indexes': [], - 'constraints': [], + "verbose_name": "Statement", + "verbose_name_plural": "Statements", + "abstract": False, + "proxy": True, + "default_permissions": ( + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", + ), + "indexes": [], + "constraints": [], }, - bases=('finance.statement',), + bases=("finance.statement",), ), ] diff --git a/jdav_web/finance/templates/admin/confirmed_statement.html b/jdav_web/finance/templates/admin/confirmed_statement.html index 23e0c2c..7b05716 100644 --- a/jdav_web/finance/templates/admin/confirmed_statement.html +++ b/jdav_web/finance/templates/admin/confirmed_statement.html @@ -81,7 +81,7 @@ links.forEach(link => { imageContainer.innerHTML = ''; // Update the image element - + if(imageText == "") { imageContainer.innerHTML = '{% trans "No QR code can be displayed." %}'; } else { @@ -99,7 +99,7 @@ links.forEach(link => { link.text = '{% trans "Showing" %}'; }); }); - + diff --git a/jdav_web/finance/templates/admin/overview_submitted_statement.html b/jdav_web/finance/templates/admin/overview_submitted_statement.html index c8dc382..7814701 100644 --- a/jdav_web/finance/templates/admin/overview_submitted_statement.html +++ b/jdav_web/finance/templates/admin/overview_submitted_statement.html @@ -124,9 +124,9 @@ {% if statement.ljp_to %}

{% trans "LJP contributions" %}

-{% blocktrans %} The youth leaders have documented interventions worth of {{ total_seminar_days }} seminar -days for {{ participant_count }} eligible participants. Taking into account the maximum contribution quota -of 90% and possible taxes ({{ ljp_tax }}%), this results in a total of {{ paid_ljp_contributions }}€. +{% blocktrans %} The youth leaders have documented interventions worth of {{ total_seminar_days }} seminar +days for {{ participant_count }} eligible participants. Taking into account the maximum contribution quota +of 90% and possible taxes ({{ ljp_tax }}%), this results in a total of {{ paid_ljp_contributions }}€. Once their proposal was approved, the ljp contributions of should be paid to:{% endblocktrans %}
diff --git a/jdav_web/finance/templates/finance/statement_summary.tex b/jdav_web/finance/templates/finance/statement_summary.tex index 99657e4..0ceeb8e 100644 --- a/jdav_web/finance/templates/finance/statement_summary.tex +++ b/jdav_web/finance/templates/finance/statement_summary.tex @@ -78,7 +78,7 @@ Zuschüsse und Aufwandsentschädigung werden wie folgt abgerufen: \noindent\textbf{LJP-Zuschüsse} \noindent Der LJP-Zuschuss für die Teilnehmenden in Höhe von {{ statement.paid_ljp_contributions|esc_all }} € wird überwiesen an: -{{ statement.ljp_to.name|esc_all }} Dieser Zuschuss wird aus Landesmitteln gewährt und ist daher +{{ statement.ljp_to.name|esc_all }} Dieser Zuschuss wird aus Landesmitteln gewährt und ist daher in der Ausgabenübersicht gesondert aufgeführt. {% endif %} diff --git a/jdav_web/finance/tests/__init__.py b/jdav_web/finance/tests/__init__.py index 023e7d7..8b70455 100644 --- a/jdav_web/finance/tests/__init__.py +++ b/jdav_web/finance/tests/__init__.py @@ -1,4 +1,6 @@ +# ruff: noqa F403 + from .admin import * +from .migrations import * from .models import * from .rules import * -from .migrations import * diff --git a/jdav_web/finance/tests/admin.py b/jdav_web/finance/tests/admin.py index 996074d..72c0d47 100644 --- a/jdav_web/finance/tests/admin.py +++ b/jdav_web/finance/tests/admin.py @@ -1,32 +1,33 @@ -import unittest from http import HTTPStatus -from django.test import TestCase, override_settings + from django.contrib.admin.sites import AdminSite -from django.test import RequestFactory, Client -from django.contrib.auth.models import User, Permission from django.contrib.auth import models as authmodels -from django.utils import timezone -from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.auth.models import User +from django.contrib.messages import get_messages from django.contrib.messages.middleware import MessageMiddleware from django.contrib.messages.storage.fallback import FallbackStorage -from django.contrib.messages import get_messages +from django.contrib.sessions.middleware import SessionMiddleware +from django.test import Client +from django.test import RequestFactory +from django.test import TestCase +from django.urls import reverse +from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from django.urls import reverse, reverse_lazy -from django.http import HttpResponseRedirect, HttpResponse -from unittest.mock import Mock, patch -from django.test.utils import override_settings -from django.urls import path, include -from django.contrib import admin as django_admin - +from members.models import Freizeit +from members.models import GEMEINSCHAFTS_TOUR +from members.models import MALE +from members.models import Member +from members.models import MUSKELKRAFT_ANREISE from members.tests.utils import create_custom_user -from members.models import Member, MALE, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE -from ..models import ( - Ledger, Statement, StatementUnSubmitted, StatementConfirmed, Transaction, Bill, - StatementSubmitted -) -from ..admin import ( - LedgerAdmin, StatementAdmin, TransactionAdmin, BillAdmin -) + +from ..admin import StatementAdmin +from ..admin import TransactionAdmin +from ..models import Bill +from ..models import Ledger +from ..models import Statement +from ..models import StatementConfirmed +from ..models import StatementUnSubmitted +from ..models import Transaction class AdminTestCase(TestCase): @@ -35,17 +36,15 @@ class AdminTestCase(TestCase): self.model = model if model is not None and admin is not None: self.admin = admin(model, AdminSite()) - superuser = User.objects.create_superuser( - username='superuser', password='secret' - ) - standard = create_custom_user('standard', ['Standard'], 'Paul', 'Wulter') - trainer = create_custom_user('trainer', ['Standard', 'Trainings'], 'Lise', 'Lotte') - treasurer = create_custom_user('treasurer', ['Standard', 'Finance'], 'Lara', 'Litte') - materialwarden = create_custom_user('materialwarden', ['Standard', 'Material'], 'Loro', 'Lutte') + User.objects.create_superuser(username="superuser", password="secret") + create_custom_user("standard", ["Standard"], "Paul", "Wulter") + create_custom_user("trainer", ["Standard", "Trainings"], "Lise", "Lotte") + create_custom_user("treasurer", ["Standard", "Finance"], "Lara", "Litte") + create_custom_user("materialwarden", ["Standard", "Material"], "Loro", "Lutte") def _login(self, name): c = Client() - res = c.login(username=name, password='secret') + res = c.login(username=name, password="secret") # make sure we logged in assert res return c @@ -57,62 +56,64 @@ class StatementUnSubmittedAdminTestCase(AdminTestCase): def setUp(self): super().setUp(model=Statement, admin=StatementAdmin) - self.superuser = User.objects.get(username='superuser') + self.superuser = User.objects.get(username="superuser") self.member = Member.objects.create( - prename="Test", lastname="User", birth_date=timezone.now().date(), - email="test@example.com", gender=MALE, user=self.superuser + prename="Test", + lastname="User", + birth_date=timezone.now().date(), + email="test@example.com", + gender=MALE, + user=self.superuser, ) self.statement = StatementUnSubmitted.objects.create( - short_description='Test Statement', - explanation='Test explanation', - night_cost=25 + short_description="Test Statement", explanation="Test explanation", night_cost=25 ) # Create excursion for testing self.excursion = Freizeit.objects.create( - name='Test Excursion', + name="Test Excursion", kilometers_traveled=100, tour_type=GEMEINSCHAFTS_TOUR, tour_approach=MUSKELKRAFT_ANREISE, - difficulty=1 + difficulty=1, ) # Create confirmed statement with excursion self.statement_with_excursion = StatementUnSubmitted.objects.create( - short_description='With Excursion', - explanation='Test explanation', + short_description="With Excursion", + explanation="Test explanation", night_cost=25, excursion=self.excursion, ) def test_save_model_with_member(self): """Test save_model sets created_by for new objects""" - request = self.factory.post('/') + request = self.factory.post("/") request.user = self.superuser # Test with change=False (new object) - new_statement = Statement(short_description='New Statement') + new_statement = Statement(short_description="New Statement") self.admin.save_model(request, new_statement, None, change=False) self.assertEqual(new_statement.created_by, self.member) def test_has_delete_permission(self): """Test if unsubmitted statements may be deleted""" - request = self.factory.post('/') + request = self.factory.post("/") request.user = self.superuser self.assertTrue(self.admin.has_delete_permission(request, self.statement)) def test_get_fields(self): """Test get_fields when excursion is set or not set.""" - request = self.factory.post('/') + request = self.factory.post("/") request.user = self.superuser - self.assertIn('excursion', self.admin.get_fields(request, self.statement_with_excursion)) - self.assertNotIn('excursion', self.admin.get_fields(request, self.statement)) - self.assertNotIn('excursion', self.admin.get_fields(request)) + self.assertIn("excursion", self.admin.get_fields(request, self.statement_with_excursion)) + self.assertNotIn("excursion", self.admin.get_fields(request, self.statement)) + self.assertNotIn("excursion", self.admin.get_fields(request)) def test_get_inlines(self): """Test get_inlines""" - request = self.factory.post('/') + request = self.factory.post("/") request.user = self.superuser self.assertEqual(len(self.admin.get_inlines(request, self.statement)), 1) @@ -121,46 +122,44 @@ class StatementUnSubmittedAdminTestCase(AdminTestCase): # Mark statement as submitted self.statement.status = Statement.SUBMITTED readonly_fields = self.admin.get_readonly_fields(None, self.statement) - self.assertIn('status', readonly_fields) - self.assertIn('excursion', readonly_fields) - self.assertIn('short_description', readonly_fields) + self.assertIn("status", readonly_fields) + self.assertIn("excursion", readonly_fields) + self.assertIn("short_description", readonly_fields) def test_get_readonly_fields_not_submitted(self): """Test readonly fields when statement is not submitted""" readonly_fields = self.admin.get_readonly_fields(None, self.statement) - self.assertEqual(readonly_fields, ['status', 'excursion']) + self.assertEqual(readonly_fields, ["status", "excursion"]) def test_submit_view_insufficient_permission(self): - url = reverse('admin:finance_statement_submit', - args=(self.statement.pk,)) - c = self._login('standard') + url = reverse("admin:finance_statement_submit", args=(self.statement.pk,)) + c = self._login("standard") response = c.get(url, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Insufficient permissions.')) + self.assertContains(response, _("Insufficient permissions.")) def test_submit_view_get(self): - url = reverse('admin:finance_statement_submit', - args=(self.statement.pk,)) - c = self._login('superuser') + url = reverse("admin:finance_statement_submit", args=(self.statement.pk,)) + c = self._login("superuser") response = c.get(url, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Submit statement')) + self.assertContains(response, _("Submit statement")) def test_submit_view_get_with_excursion(self): - url = reverse('admin:finance_statement_submit', - args=(self.statement_with_excursion.pk,)) - c = self._login('superuser') + url = reverse("admin:finance_statement_submit", args=(self.statement_with_excursion.pk,)) + c = self._login("superuser") response = c.get(url, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Finance overview')) + self.assertContains(response, _("Finance overview")) def test_submit_view_post(self): - url = reverse('admin:finance_statement_submit', - args=(self.statement.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'apply': ''}) + url = reverse("admin:finance_statement_submit", args=(self.statement.pk,)) + c = self._login("superuser") + response = c.post(url, follow=True, data={"apply": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) - text = _("Successfully submited %(name)s. The finance department will notify the requestors as soon as possible.") % {'name': str(self.statement)} + text = _( + "Successfully submited %(name)s. The finance department will notify the requestors as soon as possible." + ) % {"name": str(self.statement)} self.assertContains(response, text) @@ -170,79 +169,86 @@ class StatementSubmittedAdminTestCase(AdminTestCase): def setUp(self): super().setUp(model=Statement, admin=StatementAdmin) - self.user = User.objects.create_user('testuser', 'test@example.com', 'pass') + self.user = User.objects.create_user("testuser", "test@example.com", "pass") self.member = Member.objects.create( - prename="Test", lastname="User", birth_date=timezone.now().date(), - email="test@example.com", gender=MALE, user=self.user + prename="Test", + lastname="User", + birth_date=timezone.now().date(), + email="test@example.com", + gender=MALE, + user=self.user, ) - self.finance_user = User.objects.create_user('finance', 'finance@example.com', 'pass') - self.finance_user.groups.add(authmodels.Group.objects.get(name='Finance'), - authmodels.Group.objects.get(name='Standard')) + self.finance_user = User.objects.create_user("finance", "finance@example.com", "pass") + self.finance_user.groups.add( + authmodels.Group.objects.get(name="Finance"), + authmodels.Group.objects.get(name="Standard"), + ) self.statement = Statement.objects.create( - short_description='Submitted Statement', - explanation='Test explanation', + short_description="Submitted Statement", + explanation="Test explanation", status=Statement.SUBMITTED, submitted_by=self.member, submitted_date=timezone.now(), - night_cost=25 + night_cost=25, ) self.statement_unsubmitted = StatementUnSubmitted.objects.create( - short_description='Submitted Statement', - explanation='Test explanation', - night_cost=25 + short_description="Submitted Statement", explanation="Test explanation", night_cost=25 ) self.transaction = Transaction.objects.create( - reference='verylonglong' * 14, + reference="verylonglong" * 14, amount=3, statement=self.statement, member=self.member, ) # Create commonly used test objects - self.ledger = Ledger.objects.create(name='Test Ledger') + self.ledger = Ledger.objects.create(name="Test Ledger") self.excursion = Freizeit.objects.create( - name='Test Excursion', + name="Test Excursion", kilometers_traveled=100, tour_type=GEMEINSCHAFTS_TOUR, tour_approach=MUSKELKRAFT_ANREISE, - difficulty=1 + difficulty=1, ) self.other_member = Member.objects.create( - prename="Other", lastname="Member", birth_date=timezone.now().date(), - email="other@example.com", gender=MALE + prename="Other", + lastname="Member", + birth_date=timezone.now().date(), + email="other@example.com", + gender=MALE, ) # Create statements for generate transactions tests self.statement_no_trans_success = Statement.objects.create( - short_description='No Transactions Success', - explanation='Test explanation', + short_description="No Transactions Success", + explanation="Test explanation", status=Statement.SUBMITTED, submitted_by=self.member, submitted_date=timezone.now(), - night_cost=25 + night_cost=25, ) self.statement_no_trans_error = Statement.objects.create( - short_description='No Transactions Error', - explanation='Test explanation', + short_description="No Transactions Error", + explanation="Test explanation", status=Statement.SUBMITTED, submitted_by=self.member, submitted_date=timezone.now(), - night_cost=25 + night_cost=25, ) # Create bills for generate transactions tests self.bill_for_success = Bill.objects.create( statement=self.statement_no_trans_success, - short_description='Test Bill Success', + short_description="Test Bill Success", amount=50, paid_by=self.member, - costs_covered=True + costs_covered=True, ) self.bill_for_error = Bill.objects.create( statement=self.statement_no_trans_error, - short_description='Test Bill Error', + short_description="Test Bill Error", amount=50, paid_by=None, # No payer will cause generate_transactions to fail costs_covered=True, @@ -252,57 +258,56 @@ class StatementSubmittedAdminTestCase(AdminTestCase): """Helper method to create a bill that matches transaction amount""" return Bill.objects.create( statement=statement or self.statement, - short_description='Test Bill', + short_description="Test Bill", amount=amount or self.transaction.amount, paid_by=self.member, - costs_covered=True + costs_covered=True, ) def _create_non_matching_bill(self, statement=None, amount=100): """Helper method to create a bill that doesn't match transaction amount""" return Bill.objects.create( statement=statement or self.statement, - short_description='Non-matching Bill', + short_description="Non-matching Bill", amount=amount, - paid_by=self.member + paid_by=self.member, ) def test_has_change_permission_with_permission(self): """Test change permission with proper permission""" - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.finance_user self.assertTrue(self.admin.has_change_permission(request)) def test_has_change_permission_without_permission(self): """Test change permission without proper permission""" - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user self.assertFalse(self.admin.has_change_permission(request)) def test_has_delete_permission(self): """Test that delete permission is disabled""" - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.finance_user self.assertFalse(self.admin.has_delete_permission(request)) def test_readonly_fields(self): - self.assertNotIn('explanation', - self.admin.get_readonly_fields(None, self.statement_unsubmitted)) + self.assertNotIn( + "explanation", self.admin.get_readonly_fields(None, self.statement_unsubmitted) + ) def test_change(self): - url = reverse('admin:finance_statement_change', - args=(self.statement.pk,)) - c = self._login('superuser') + url = reverse("admin:finance_statement_change", args=(self.statement.pk,)) + c = self._login("superuser") response = c.get(url) self.assertEqual(response.status_code, HTTPStatus.OK) def test_overview_view(self): - url = reverse('admin:finance_statement_overview', - args=(self.statement.pk,)) - c = self._login('superuser') + url = reverse("admin:finance_statement_overview", args=(self.statement.pk,)) + c = self._login("superuser") response = c.get(url) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('View submitted statement')) + self.assertContains(response, _("View submitted statement")) def test_overview_view_statement_not_found(self): """Test overview_view with statement that can't be found in StatementSubmitted queryset""" @@ -311,8 +316,8 @@ class StatementSubmittedAdminTestCase(AdminTestCase): self.statement.status = Statement.UNSUBMITTED self.statement.save() - url = reverse('admin:finance_statement_overview', args=(self.statement.pk,)) - c = self._login('superuser') + url = reverse("admin:finance_statement_overview", args=(self.statement.pk,)) + c = self._login("superuser") response = c.get(url, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) messages = list(get_messages(response.wsgi_request)) @@ -328,11 +333,13 @@ class StatementSubmittedAdminTestCase(AdminTestCase): # Create a bill that matches the transaction amount to make it valid self._create_matching_bill() - url = reverse('admin:finance_statement_overview', args=(self.statement.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'transaction_execution_confirm': ''}) + url = reverse("admin:finance_statement_overview", args=(self.statement.pk,)) + c = self._login("superuser") + response = c.post(url, follow=True, data={"transaction_execution_confirm": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) - success_text = _("Successfully confirmed %(name)s. I hope you executed the associated transactions, I wont remind you again.") % {'name': str(self.statement)} + success_text = _( + "Successfully confirmed %(name)s. I hope you executed the associated transactions, I wont remind you again." + ) % {"name": str(self.statement)} self.assertContains(response, success_text) self.statement.refresh_from_db() self.assertTrue(self.statement.confirmed) @@ -346,9 +353,9 @@ class StatementSubmittedAdminTestCase(AdminTestCase): # Create a bill that matches the transaction amount to make it valid self._create_matching_bill() - url = reverse('admin:finance_statement_overview', args=(self.statement.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'transaction_execution_confirm_and_send': ''}) + url = reverse("admin:finance_statement_overview", args=(self.statement.pk,)) + c = self._login("superuser") + response = c.post(url, follow=True, data={"transaction_execution_confirm_and_send": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) success_text = _("Successfully sent receipt to the office.") self.assertContains(response, success_text) @@ -363,24 +370,24 @@ class StatementSubmittedAdminTestCase(AdminTestCase): # Create a bill that matches the transaction amount to make total valid self._create_matching_bill() - url = reverse('admin:finance_statement_overview', - args=(self.statement.pk,)) - c = self._login('superuser') - response = c.post(url, data={'confirm': ''}) + url = reverse("admin:finance_statement_overview", args=(self.statement.pk,)) + c = self._login("superuser") + response = c.post(url, data={"confirm": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Statement confirmed')) + self.assertContains(response, _("Statement confirmed")) def test_overview_view_confirm_non_matching_transactions(self): """Test overview_view confirm with non-matching transactions""" # Create a bill that doesn't match the transaction self._create_non_matching_bill() - url = reverse('admin:finance_statement_overview', - args=(self.statement.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'confirm': ''}) + url = reverse("admin:finance_statement_overview", args=(self.statement.pk,)) + c = self._login("superuser") + response = c.post(url, follow=True, data={"confirm": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) - error_text = _("Transactions do not match the covered expenses. Please correct the mistakes listed below.") + error_text = _( + "Transactions do not match the covered expenses. Please correct the mistakes listed below." + ) self.assertContains(response, error_text) def test_overview_view_confirm_missing_ledger(self): @@ -392,14 +399,15 @@ class StatementSubmittedAdminTestCase(AdminTestCase): # Create a bill that matches the transaction amount to pass the first check self._create_matching_bill() - url = reverse('admin:finance_statement_overview', - args=(self.statement.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'confirm': ''}) + url = reverse("admin:finance_statement_overview", args=(self.statement.pk,)) + c = self._login("superuser") + response = c.post(url, follow=True, data={"confirm": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) # Check the Django messages for the error messages = list(get_messages(response.wsgi_request)) - expected_text = str(_("Some transactions have no ledger configured. Please fill in the gaps.")) + expected_text = str( + _("Some transactions have no ledger configured. Please fill in the gaps.") + ) self.assertTrue(any(expected_text in str(msg) for msg in messages)) def test_overview_view_confirm_invalid_allowance_to(self): @@ -420,24 +428,30 @@ class StatementSubmittedAdminTestCase(AdminTestCase): # Check validity obstruction is allowances self.assertEqual(self.statement_no_trans_success.validity, Statement.INVALID_ALLOWANCE_TO) - url = reverse('admin:finance_statement_overview', - args=(self.statement_no_trans_success.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'confirm': ''}) + url = reverse( + "admin:finance_statement_overview", args=(self.statement_no_trans_success.pk,) + ) + c = self._login("superuser") + response = c.post(url, follow=True, data={"confirm": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) # Check the Django messages for the error messages = list(get_messages(response.wsgi_request)) - expected_text = str(_("The configured recipients for the allowance don't match the regulations. Please correct this on the excursion.")) + expected_text = str( + _( + "The configured recipients for the allowance don't match the regulations. Please correct this on the excursion." + ) + ) self.assertTrue(any(expected_text in str(msg) for msg in messages)) def test_overview_view_reject(self): """Test overview_view reject statement""" - url = reverse('admin:finance_statement_overview', args=(self.statement.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'reject': ''}) + url = reverse("admin:finance_statement_overview", args=(self.statement.pk,)) + c = self._login("superuser") + response = c.post(url, follow=True, data={"reject": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) - success_text = _("Successfully rejected %(name)s. The requestor can reapply, when needed.") %\ - {'name': str(self.statement)} + success_text = _( + "Successfully rejected %(name)s. The requestor can reapply, when needed." + ) % {"name": str(self.statement)} self.assertContains(response, success_text) # Verify statement was rejected @@ -449,45 +463,53 @@ class StatementSubmittedAdminTestCase(AdminTestCase): # Ensure there's already a transaction self.assertTrue(self.statement.transaction_set.count() > 0) - url = reverse('admin:finance_statement_overview', args=(self.statement.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'generate_transactions': ''}) + url = reverse("admin:finance_statement_overview", args=(self.statement.pk,)) + c = self._login("superuser") + response = c.post(url, follow=True, data={"generate_transactions": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) - error_text = _("%(name)s already has transactions. Please delete them first, if you want to generate new ones") % {'name': str(self.statement)} + error_text = _( + "%(name)s already has transactions. Please delete them first, if you want to generate new ones" + ) % {"name": str(self.statement)} self.assertContains(response, error_text) def test_overview_view_generate_transactions_success(self): """Test overview_view generate transactions successfully""" - url = reverse('admin:finance_statement_overview', - args=(self.statement_no_trans_success.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'generate_transactions': ''}) + url = reverse( + "admin:finance_statement_overview", args=(self.statement_no_trans_success.pk,) + ) + c = self._login("superuser") + response = c.post(url, follow=True, data={"generate_transactions": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) - success_text = _("Successfully generated transactions for %(name)s") %\ - {'name': str(self.statement_no_trans_success)} + success_text = _("Successfully generated transactions for %(name)s") % { + "name": str(self.statement_no_trans_success) + } self.assertContains(response, success_text) def test_overview_view_generate_transactions_error(self): """Test overview_view generate transactions with error""" - url = reverse('admin:finance_statement_overview', - args=(self.statement_no_trans_error.pk,)) - c = self._login('superuser') - response = c.post(url, follow=True, data={'generate_transactions': ''}) + url = reverse("admin:finance_statement_overview", args=(self.statement_no_trans_error.pk,)) + c = self._login("superuser") + response = c.post(url, follow=True, data={"generate_transactions": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) messages = list(get_messages(response.wsgi_request)) - expected_text = str(_("Error while generating transactions for %(name)s. Do all bills have a payer and, if this statement is attached to an excursion, was a person selected that receives the subsidies?") %\ - {'name': str(self.statement_no_trans_error)}) + expected_text = str( + _( + "Error while generating transactions for %(name)s. Do all bills have a payer and, if this statement is attached to an excursion, was a person selected that receives the subsidies?" + ) + % {"name": str(self.statement_no_trans_error)} + ) self.assertTrue(any(expected_text in str(msg) for msg in messages)) def test_reduce_transactions_view(self): - url = reverse('admin:finance_statement_reduce_transactions', - args=(self.statement.pk,)) - c = self._login('superuser') - response = c.get(url, data={'redirectTo': reverse('admin:finance_statement_changelist')}, - follow=True) - self.assertContains(response, - _("Successfully reduced transactions for %(name)s.") %\ - {'name': str(self.statement)}) + url = reverse("admin:finance_statement_reduce_transactions", args=(self.statement.pk,)) + c = self._login("superuser") + response = c.get( + url, data={"redirectTo": reverse("admin:finance_statement_changelist")}, follow=True + ) + self.assertContains( + response, + _("Successfully reduced transactions for %(name)s.") % {"name": str(self.statement)}, + ) class StatementConfirmedAdminTestCase(AdminTestCase): @@ -496,24 +518,30 @@ class StatementConfirmedAdminTestCase(AdminTestCase): def setUp(self): super().setUp(model=Statement, admin=StatementAdmin) - self.user = User.objects.create_user('testuser', 'test@example.com', 'pass') + self.user = User.objects.create_user("testuser", "test@example.com", "pass") self.member = Member.objects.create( - prename="Test", lastname="User", birth_date=timezone.now().date(), - email="test@example.com", gender=MALE, user=self.user + prename="Test", + lastname="User", + birth_date=timezone.now().date(), + email="test@example.com", + gender=MALE, + user=self.user, ) - self.finance_user = User.objects.create_user('finance', 'finance@example.com', 'pass') - self.finance_user.groups.add(authmodels.Group.objects.get(name='Finance'), - authmodels.Group.objects.get(name='Standard')) + self.finance_user = User.objects.create_user("finance", "finance@example.com", "pass") + self.finance_user.groups.add( + authmodels.Group.objects.get(name="Finance"), + authmodels.Group.objects.get(name="Standard"), + ) # Create a base statement first base_statement = Statement.objects.create( - short_description='Confirmed Statement', - explanation='Test explanation', + short_description="Confirmed Statement", + explanation="Test explanation", status=Statement.CONFIRMED, confirmed_by=self.member, confirmed_date=timezone.now(), - night_cost=25 + night_cost=25, ) # StatementConfirmed is a proxy model, so we can get it from the base statement @@ -521,32 +549,34 @@ class StatementConfirmedAdminTestCase(AdminTestCase): # Create an unconfirmed statement for testing self.unconfirmed_statement = Statement.objects.create( - short_description='Unconfirmed Statement', - explanation='Test explanation', + short_description="Unconfirmed Statement", + explanation="Test explanation", status=Statement.SUBMITTED, - night_cost=25 + night_cost=25, ) # Create excursion for testing self.excursion = Freizeit.objects.create( - name='Test Excursion', + name="Test Excursion", kilometers_traveled=100, tour_type=GEMEINSCHAFTS_TOUR, tour_approach=MUSKELKRAFT_ANREISE, - difficulty=1 + difficulty=1, ) # Create confirmed statement with excursion confirmed_with_excursion_base = Statement.objects.create( - short_description='Confirmed with Excursion', - explanation='Test explanation', + short_description="Confirmed with Excursion", + explanation="Test explanation", status=Statement.CONFIRMED, confirmed_by=self.member, confirmed_date=timezone.now(), excursion=self.excursion, - night_cost=25 + night_cost=25, + ) + self.statement_with_excursion = StatementConfirmed.objects.get( + pk=confirmed_with_excursion_base.pk ) - self.statement_with_excursion = StatementConfirmed.objects.get(pk=confirmed_with_excursion_base.pk) def _add_session_to_request(self, request): """Add session to request""" @@ -560,20 +590,20 @@ class StatementConfirmedAdminTestCase(AdminTestCase): def test_has_change_permission(self): """Test that change permission is disabled""" - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.finance_user self.assertFalse(self.admin.has_change_permission(request, self.statement)) def test_has_delete_permission(self): """Test that delete permission is disabled""" - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.finance_user self.assertFalse(self.admin.has_delete_permission(request, self.statement)) def test_unconfirm_view_not_confirmed_statement(self): """Test unconfirm_view with statement that is not confirmed""" # Create request for unconfirmed statement - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.finance_user self._add_session_to_request(request) @@ -589,7 +619,7 @@ class StatementConfirmedAdminTestCase(AdminTestCase): def test_unconfirm_view_post_unconfirm_action(self): """Test unconfirm_view POST request with 'unconfirm' action""" # Create POST request with unconfirm action - request = self.factory.post('/', {'unconfirm': 'true'}) + request = self.factory.post("/", {"unconfirm": "true"}) request.user = self.finance_user self._add_session_to_request(request) @@ -612,7 +642,7 @@ class StatementConfirmedAdminTestCase(AdminTestCase): def test_unconfirm_view_get_render_template(self): """Test unconfirm_view GET request rendering template""" # Create GET request (no POST data) - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.finance_user self._add_session_to_request(request) @@ -626,33 +656,30 @@ class StatementConfirmedAdminTestCase(AdminTestCase): self.assertEqual(response.status_code, 200) # Check response content contains expected template elements - self.assertIn(str(_('Unconfirm statement')).encode('utf-8'), response.content) + self.assertIn(str(_("Unconfirm statement")).encode("utf-8"), response.content) self.assertIn(self.statement.short_description.encode(), response.content) def test_statement_summary_view_insufficient_permission(self): - url = reverse('admin:finance_statement_summary', - args=(self.statement_with_excursion.pk,)) - c = self._login('standard') + url = reverse("admin:finance_statement_summary", args=(self.statement_with_excursion.pk,)) + c = self._login("standard") response = c.get(url, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Insufficient permissions.')) + self.assertContains(response, _("Insufficient permissions.")) def test_statement_summary_view_unconfirmed(self): - url = reverse('admin:finance_statement_summary', - args=(self.unconfirmed_statement.pk,)) - c = self._login('superuser') + url = reverse("admin:finance_statement_summary", args=(self.unconfirmed_statement.pk,)) + c = self._login("superuser") response = c.get(url, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Statement not found.')) + self.assertContains(response, _("Statement not found.")) def test_statement_summary_view_confirmed_with_excursion(self): """Test statement_summary_view when statement is confirmed with excursion""" - url = reverse('admin:finance_statement_summary', - args=(self.statement_with_excursion.pk,)) - c = self._login('superuser') + url = reverse("admin:finance_statement_summary", args=(self.statement_with_excursion.pk,)) + c = self._login("superuser") response = c.get(url, follow=True) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertEqual(response.headers['Content-Type'], 'application/pdf') + self.assertEqual(response.headers["Content-Type"], "application/pdf") class TransactionAdminTestCase(TestCase): @@ -663,41 +690,44 @@ class TransactionAdminTestCase(TestCase): self.factory = RequestFactory() self.admin = TransactionAdmin(Transaction, self.site) - self.user = User.objects.create_user('testuser', 'test@example.com', 'pass') + self.user = User.objects.create_user("testuser", "test@example.com", "pass") self.member = Member.objects.create( - prename="Test", lastname="User", birth_date=timezone.now().date(), - email="test@example.com", gender=MALE, user=self.user + prename="Test", + lastname="User", + birth_date=timezone.now().date(), + email="test@example.com", + gender=MALE, + user=self.user, ) - self.ledger = Ledger.objects.create(name='Test Ledger') + self.ledger = Ledger.objects.create(name="Test Ledger") self.statement = Statement.objects.create( - short_description='Test Statement', - explanation='Test explanation' + short_description="Test Statement", explanation="Test explanation" ) self.transaction = Transaction.objects.create( member=self.member, ledger=self.ledger, amount=100, - reference='Test transaction', - statement=self.statement + reference="Test transaction", + statement=self.statement, ) def test_has_add_permission(self): """Test that add permission is disabled""" - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user self.assertFalse(self.admin.has_add_permission(request)) def test_has_change_permission(self): """Test that change permission is disabled""" - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user self.assertFalse(self.admin.has_change_permission(request)) def test_has_delete_permission(self): """Test that delete permission is disabled""" - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user self.assertFalse(self.admin.has_delete_permission(request)) diff --git a/jdav_web/finance/tests/migrations.py b/jdav_web/finance/tests/migrations.py index 5c8894a..9608061 100644 --- a/jdav_web/finance/tests/migrations.py +++ b/jdav_web/finance/tests/migrations.py @@ -1,5 +1,4 @@ import django.test -from django.apps import apps from django.db import connection from django.db.migrations.executor import MigrationExecutor @@ -7,9 +6,9 @@ from django.db.migrations.executor import MigrationExecutor class StatusMigrationTestCase(django.test.TransactionTestCase): """Test the migration from submitted/confirmed fields to status field.""" - app = 'finance' - migrate_from = [('finance', '0009_statement_ljp_to')] - migrate_to = [('finance', '0010_statement_status')] + app = "finance" + migrate_from = [("finance", "0009_statement_ljp_to")] + migrate_to = [("finance", "0010_statement_status")] def setUp(self): # Get the state before migration @@ -18,26 +17,20 @@ class StatusMigrationTestCase(django.test.TransactionTestCase): # Get the old models (before migration) old_apps = executor.loader.project_state(self.migrate_from).apps - self.Statement = old_apps.get_model(self.app, 'Statement') + self.Statement = old_apps.get_model(self.app, "Statement") # Create statements with different combinations of submitted/confirmed # created_by is nullable, so we don't need to create a Member self.unsubmitted = self.Statement.objects.create( - short_description='Unsubmitted Statement', - submitted=False, - confirmed=False + short_description="Unsubmitted Statement", submitted=False, confirmed=False ) self.submitted = self.Statement.objects.create( - short_description='Submitted Statement', - submitted=True, - confirmed=False + short_description="Submitted Statement", submitted=True, confirmed=False ) self.confirmed = self.Statement.objects.create( - short_description='Confirmed Statement', - submitted=True, - confirmed=True + short_description="Confirmed Statement", submitted=True, confirmed=True ) def test_status_field_migration(self): @@ -49,7 +42,7 @@ class StatusMigrationTestCase(django.test.TransactionTestCase): # Get the new models (after migration) new_apps = executor.loader.project_state(self.migrate_to).apps - Statement = new_apps.get_model(self.app, 'Statement') + Statement = new_apps.get_model(self.app, "Statement") # Constants from the Statement model UNSUBMITTED = 0 @@ -58,13 +51,22 @@ class StatusMigrationTestCase(django.test.TransactionTestCase): # Verify the migration worked correctly unsubmitted = Statement.objects.get(pk=self.unsubmitted.pk) - self.assertEqual(unsubmitted.status, UNSUBMITTED, - 'Statement with submitted=False, confirmed=False should have status=UNSUBMITTED') + self.assertEqual( + unsubmitted.status, + UNSUBMITTED, + "Statement with submitted=False, confirmed=False should have status=UNSUBMITTED", + ) submitted = Statement.objects.get(pk=self.submitted.pk) - self.assertEqual(submitted.status, SUBMITTED, - 'Statement with submitted=True, confirmed=False should have status=SUBMITTED') + self.assertEqual( + submitted.status, + SUBMITTED, + "Statement with submitted=True, confirmed=False should have status=SUBMITTED", + ) confirmed = Statement.objects.get(pk=self.confirmed.pk) - self.assertEqual(confirmed.status, CONFIRMED, - 'Statement with submitted=True, confirmed=True should have status=CONFIRMED') + self.assertEqual( + confirmed.status, + CONFIRMED, + "Statement with submitted=True, confirmed=True should have status=CONFIRMED", + ) diff --git a/jdav_web/finance/tests/models.py b/jdav_web/finance/tests/models.py index 2b640f7..e991594 100644 --- a/jdav_web/finance/tests/models.py +++ b/jdav_web/finance/tests/models.py @@ -1,16 +1,34 @@ -from unittest import skip +from decimal import Decimal + +from dateutil.relativedelta import relativedelta +from django.conf import settings from django.test import TestCase from django.utils import timezone -from django.conf import settings from django.utils.translation import gettext_lazy as _ -from decimal import Decimal -from finance.models import Statement, StatementUnSubmitted, StatementSubmitted, Bill, Ledger, Transaction,\ - StatementUnSubmittedManager, StatementSubmittedManager, StatementConfirmedManager,\ - StatementConfirmed, TransactionIssue, StatementManager -from members.models import Member, Group, Freizeit, LJPProposal, Intervention, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE, NewMemberOnList,\ - FAHRGEMEINSCHAFT_ANREISE, MALE, FEMALE, DIVERSE -from dateutil.relativedelta import relativedelta -from utils import get_member +from finance.models import Bill +from finance.models import Ledger +from finance.models import Statement +from finance.models import StatementConfirmed +from finance.models import StatementConfirmedManager +from finance.models import StatementManager +from finance.models import StatementSubmitted +from finance.models import StatementSubmittedManager +from finance.models import StatementUnSubmitted +from finance.models import StatementUnSubmittedManager +from finance.models import Transaction +from finance.models import TransactionIssue +from members.models import DIVERSE +from members.models import FAHRGEMEINSCHAFT_ANREISE +from members.models import Freizeit +from members.models import GEMEINSCHAFTS_TOUR +from members.models import Group +from members.models import Intervention +from members.models import LJPProposal +from members.models import MALE +from members.models import Member +from members.models import MUSKELKRAFT_ANREISE +from members.models import NewMemberOnList + # Create your tests here. class StatementTestCase(TestCase): @@ -22,82 +40,149 @@ class StatementTestCase(TestCase): def setUp(self): self.jl = Group.objects.create(name="Jugendleiter") - self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(), - email=settings.TEST_MAIL, gender=MALE) + self.fritz = Member.objects.create( + prename="Fritz", + lastname="Wulter", + birth_date=timezone.now().date(), + email=settings.TEST_MAIL, + gender=MALE, + ) self.fritz.group.add(self.jl) self.fritz.save() - self.personal_account = Ledger.objects.create(name='personal account') + self.personal_account = Ledger.objects.create(name="personal account") - self.st = Statement.objects.create(short_description='A statement', explanation='Important!', night_cost=0) - Bill.objects.create(statement=self.st, short_description='food', explanation='i was hungry', - amount=67.3, costs_covered=False, paid_by=self.fritz) - Transaction.objects.create(reference='gift', amount=12.3, - ledger=self.personal_account, member=self.fritz, - statement=self.st) + self.st = Statement.objects.create( + short_description="A statement", explanation="Important!", night_cost=0 + ) + Bill.objects.create( + statement=self.st, + short_description="food", + explanation="i was hungry", + amount=67.3, + costs_covered=False, + paid_by=self.fritz, + ) + Transaction.objects.create( + reference="gift", + amount=12.3, + ledger=self.personal_account, + member=self.fritz, + statement=self.st, + ) - self.st2 = Statement.objects.create(short_description='Actual expenses', night_cost=0) - Bill.objects.create(statement=self.st2, short_description='food', explanation='i was hungry', - amount=67.3, costs_covered=True, paid_by=self.fritz) + self.st2 = Statement.objects.create(short_description="Actual expenses", night_cost=0) + Bill.objects.create( + statement=self.st2, + short_description="food", + explanation="i was hungry", + amount=67.3, + costs_covered=True, + paid_by=self.fritz, + ) - ex = Freizeit.objects.create(name='Wild trip', kilometers_traveled=self.kilometers_traveled, - tour_type=GEMEINSCHAFTS_TOUR, - tour_approach=MUSKELKRAFT_ANREISE, - difficulty=1) - self.st3 = Statement.objects.create(night_cost=self.night_cost, excursion=ex, subsidy_to=self.fritz) + ex = Freizeit.objects.create( + name="Wild trip", + kilometers_traveled=self.kilometers_traveled, + tour_type=GEMEINSCHAFTS_TOUR, + tour_approach=MUSKELKRAFT_ANREISE, + difficulty=1, + ) + self.st3 = Statement.objects.create( + night_cost=self.night_cost, excursion=ex, subsidy_to=self.fritz + ) for i in range(self.participant_count): - m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL, gender=MALE) + m = Member.objects.create( + prename="Fritz {}".format(i), + lastname="Walter", + birth_date=timezone.now().date(), + email=settings.TEST_MAIL, + gender=MALE, + ) mol = NewMemberOnList.objects.create(member=m, memberlist=ex) ex.membersonlist.add(mol) for i in range(self.staff_count): - m = Member.objects.create(prename='Fritz {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL, gender=MALE) - Bill.objects.create(statement=self.st3, short_description='food', explanation='i was hungry', - amount=42.69, costs_covered=True, paid_by=m) + m = Member.objects.create( + prename="Fritz {}".format(i), + lastname="Walter", + birth_date=timezone.now().date(), + email=settings.TEST_MAIL, + gender=MALE, + ) + Bill.objects.create( + statement=self.st3, + short_description="food", + explanation="i was hungry", + amount=42.69, + costs_covered=True, + paid_by=m, + ) m.group.add(self.jl) ex.jugendleiter.add(m) if i < self.allowance_to_count: self.st3.allowance_to.add(m) # Create a small excursion with < 5 theoretic LJP participants for LJP contribution test - small_ex = Freizeit.objects.create(name='Small trip', kilometers_traveled=100, - tour_type=GEMEINSCHAFTS_TOUR, - tour_approach=MUSKELKRAFT_ANREISE, - difficulty=1) + small_ex = Freizeit.objects.create( + name="Small trip", + kilometers_traveled=100, + tour_type=GEMEINSCHAFTS_TOUR, + tour_approach=MUSKELKRAFT_ANREISE, + difficulty=1, + ) # Add only 3 participants (< 5 for theoretic_ljp_participant_count) for i in range(3): # Create young participants (< 6 years old) so they don't count toward LJP birth_date = timezone.now().date() - relativedelta(years=4) - m = Member.objects.create(prename='Small {}'.format(i), lastname='Participant', - birth_date=birth_date, - email=settings.TEST_MAIL, gender=MALE) + m = Member.objects.create( + prename="Small {}".format(i), + lastname="Participant", + birth_date=birth_date, + email=settings.TEST_MAIL, + gender=MALE, + ) NewMemberOnList.objects.create(member=m, memberlist=small_ex) # Create LJP proposal for the small excursion - ljp_proposal = LJPProposal.objects.create(title='Small LJP', category=LJPProposal.LJP_STAFF_TRAINING) + ljp_proposal = LJPProposal.objects.create( + title="Small LJP", category=LJPProposal.LJP_STAFF_TRAINING + ) small_ex.ljpproposal = ljp_proposal small_ex.save() self.st_small = Statement.objects.create(night_cost=10, excursion=small_ex) - ex = Freizeit.objects.create(name='Wild trip 2', kilometers_traveled=self.kilometers_traveled, - tour_type=GEMEINSCHAFTS_TOUR, - tour_approach=MUSKELKRAFT_ANREISE, - difficulty=2) - self.st4 = Statement.objects.create(night_cost=self.night_cost, excursion=ex, subsidy_to=self.fritz) + ex = Freizeit.objects.create( + name="Wild trip 2", + kilometers_traveled=self.kilometers_traveled, + tour_type=GEMEINSCHAFTS_TOUR, + tour_approach=MUSKELKRAFT_ANREISE, + difficulty=2, + ) + self.st4 = Statement.objects.create( + night_cost=self.night_cost, excursion=ex, subsidy_to=self.fritz + ) for i in range(2): - m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', - birth_date=timezone.now().date() - relativedelta(years=30), - email=settings.TEST_MAIL, gender=DIVERSE) + m = Member.objects.create( + prename="Peter {}".format(i), + lastname="Walter", + birth_date=timezone.now().date() - relativedelta(years=30), + email=settings.TEST_MAIL, + gender=DIVERSE, + ) mol = NewMemberOnList.objects.create(member=m, memberlist=ex) ex.membersonlist.add(mol) base = timezone.now() - ex = Freizeit.objects.create(name='Wild trip with old people', kilometers_traveled=self.kilometers_traveled, - tour_type=GEMEINSCHAFTS_TOUR, - tour_approach=MUSKELKRAFT_ANREISE, - difficulty=2, date=timezone.datetime(2024, 1, 2, 8, 0, 0, tzinfo=base.tzinfo), end=timezone.datetime(2024, 1, 5, 17, 0, 0, tzinfo=base.tzinfo) ) + ex = Freizeit.objects.create( + name="Wild trip with old people", + kilometers_traveled=self.kilometers_traveled, + tour_type=GEMEINSCHAFTS_TOUR, + tour_approach=MUSKELKRAFT_ANREISE, + difficulty=2, + date=timezone.datetime(2024, 1, 2, 8, 0, 0, tzinfo=base.tzinfo), + end=timezone.datetime(2024, 1, 5, 17, 0, 0, tzinfo=base.tzinfo), + ) settings.EXCURSION_ORG_FEE = 20 settings.LJP_TAX = 0.2 @@ -106,207 +191,335 @@ class StatementTestCase(TestCase): self.st5 = Statement.objects.create(night_cost=self.night_cost, excursion=ex) for i in range(9): - m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date() - relativedelta(years=i+21), - email=settings.TEST_MAIL, gender=DIVERSE) + m = Member.objects.create( + prename="Peter {}".format(i), + lastname="Walter", + birth_date=timezone.now().date() - relativedelta(years=i + 21), + email=settings.TEST_MAIL, + gender=DIVERSE, + ) mol = NewMemberOnList.objects.create(member=m, memberlist=ex) ex.membersonlist.add(mol) ljpproposal = LJPProposal.objects.create( - title='Test proposal', + title="Test proposal", category=LJPProposal.LJP_STAFF_TRAINING, goal=LJPProposal.LJP_ENVIRONMENT, - goal_strategy='my strategy', + goal_strategy="my strategy", not_bw_reason=LJPProposal.NOT_BW_ROOMS, - excursion=self.st5.excursion) + excursion=self.st5.excursion, + ) for i in range(3): - int = Intervention.objects.create( - date_start=timezone.datetime(2024, 1, 2+i, 12, 0, 0, tzinfo=base.tzinfo), - duration = 2+i, - activity = 'hi', - ljp_proposal=ljpproposal - ) + Intervention.objects.create( + date_start=timezone.datetime(2024, 1, 2 + i, 12, 0, 0, tzinfo=base.tzinfo), + duration=2 + i, + activity="hi", + ljp_proposal=ljpproposal, + ) self.b1 = Bill.objects.create( statement=self.st5, - short_description='covered bill', - explanation='hi', - amount='300', + short_description="covered bill", + explanation="hi", + amount="300", paid_by=self.fritz, costs_covered=True, - refunded=False + refunded=False, ) self.b2 = Bill.objects.create( statement=self.st5, - short_description='non-covered bill', - explanation='hi', - amount='900', + short_description="non-covered bill", + explanation="hi", + amount="900", paid_by=self.fritz, costs_covered=False, - refunded=False + refunded=False, ) self.st6 = Statement.objects.create(night_cost=self.night_cost) - Bill.objects.create(statement=self.st6, amount='42', costs_covered=True) + Bill.objects.create(statement=self.st6, amount="42", costs_covered=True) def test_org_fee(self): # org fee should be collected if participants are older than 26 - self.assertEqual(self.st5.excursion.old_participant_count, 3, 'Calculation of number of old people in excursion is incorrect.') + self.assertEqual( + self.st5.excursion.old_participant_count, + 3, + "Calculation of number of old people in excursion is incorrect.", + ) - total_org = 4 * 3 * 20 # 4 days, 3 old people, 20€ per day + total_org = 4 * 3 * 20 # 4 days, 3 old people, 20€ per day - self.assertEqual(self.st5.total_org_fee_theoretical, total_org, 'Theoretical org_fee should equal to amount per day per person * n_persons * n_days if there are old people.') - self.assertEqual(self.st5.total_org_fee, 0, 'Paid org fee should be 0 if no allowance and subsidies are paid if there are old people.') + self.assertEqual( + self.st5.total_org_fee_theoretical, + total_org, + "Theoretical org_fee should equal to amount per day per person * n_persons * n_days if there are old people.", + ) + self.assertEqual( + self.st5.total_org_fee, + 0, + "Paid org fee should be 0 if no allowance and subsidies are paid if there are old people.", + ) self.assertIsNone(self.st5.org_fee_payant) # now collect subsidies self.st5.subsidy_to = self.fritz - self.assertEqual(self.st5.total_org_fee, total_org, 'Paid org fee should equal to amount per day per person * n_persons * n_days if subsidies are paid.') + self.assertEqual( + self.st5.total_org_fee, + total_org, + "Paid org fee should equal to amount per day per person * n_persons * n_days if subsidies are paid.", + ) # now collect allowances self.st5.allowance_to.add(self.fritz) self.st5.subsidy_to = None - self.assertEqual(self.st5.total_org_fee, total_org, 'Paid org fee should equal to amount per day per person * n_persons * n_days if allowances are paid.') + self.assertEqual( + self.st5.total_org_fee, + total_org, + "Paid org fee should equal to amount per day per person * n_persons * n_days if allowances are paid.", + ) # now collect both self.st5.subsidy_to = self.fritz - self.assertEqual(self.st5.total_org_fee, total_org, 'Paid org fee should equal to amount per day per person * n_persons * n_days if subsidies and allowances are paid.') + self.assertEqual( + self.st5.total_org_fee, + total_org, + "Paid org fee should equal to amount per day per person * n_persons * n_days if subsidies and allowances are paid.", + ) - self.assertEqual(self.st5.org_fee_payant, self.fritz, 'Org fee payant should be the receiver allowances and subsidies.') + self.assertEqual( + self.st5.org_fee_payant, + self.fritz, + "Org fee payant should be the receiver allowances and subsidies.", + ) # return to previous state self.st5.subsidy_to = None self.st5.allowance_to.remove(self.fritz) - def test_ljp_payment(self): - expected_intervention_hours = 2 + 3 + 4 - expected_seminar_days = 0 + 0.5 + 0.5 # >=2.5h = 0.5days, >=5h = 1.0day - expected_ljp = (1-settings.LJP_TAX) * expected_seminar_days * settings.LJP_CONTRIBUTION_PER_DAY * 9 + expected_seminar_days = 0 + 0.5 + 0.5 # >=2.5h = 0.5days, >=5h = 1.0day + expected_ljp = ( + (1 - settings.LJP_TAX) * expected_seminar_days * settings.LJP_CONTRIBUTION_PER_DAY * 9 + ) # (1 - 20% tax) * 1 seminar day * 20€ * 9 participants - self.assertEqual(self.st5.excursion.total_intervention_hours, expected_intervention_hours, 'Calculation of total intervention hours is incorrect.') - self.assertEqual(self.st5.excursion.total_seminar_days, expected_seminar_days, 'Calculation of total seminar days is incorrect.') + self.assertEqual( + self.st5.excursion.total_intervention_hours, + expected_intervention_hours, + "Calculation of total intervention hours is incorrect.", + ) + self.assertEqual( + self.st5.excursion.total_seminar_days, + expected_seminar_days, + "Calculation of total seminar days is incorrect.", + ) - self.assertEqual(self.st5.paid_ljp_contributions, 0, 'No LJP contributions should be paid if no receiver is set.') + self.assertEqual( + self.st5.paid_ljp_contributions, + 0, + "No LJP contributions should be paid if no receiver is set.", + ) # now we want to pay out the LJP contributions self.st5.ljp_to = self.fritz - self.assertEqual(self.st5.paid_ljp_contributions, expected_ljp, 'LJP contributions should be paid if a receiver is set.') + self.assertEqual( + self.st5.paid_ljp_contributions, + expected_ljp, + "LJP contributions should be paid if a receiver is set.", + ) # now the total costs paid by trip organisers is lower than expected ljp contributions, should be reduced automatically - self.b2.amount=100 + self.b2.amount = 100 self.b2.save() - self.assertEqual(self.st5.total_bills_not_covered, 100, 'Changes in bills should be reflected in the total costs paid by trip organisers') - self.assertGreaterEqual(self.st5.total_bills_not_covered, self.st5.paid_ljp_contributions, 'LJP contributions should be less than or equal to the costs paid by trip organisers') + self.assertEqual( + self.st5.total_bills_not_covered, + 100, + "Changes in bills should be reflected in the total costs paid by trip organisers", + ) + self.assertGreaterEqual( + self.st5.total_bills_not_covered, + self.st5.paid_ljp_contributions, + "LJP contributions should be less than or equal to the costs paid by trip organisers", + ) self.st5.ljp_to = None def test_staff_count(self): - self.assertEqual(self.st4.admissible_staff_count, 0, - 'Admissible staff count is not 0, although not enough participants.') + self.assertEqual( + self.st4.admissible_staff_count, + 0, + "Admissible staff count is not 0, although not enough participants.", + ) for i in range(2): - m = Member.objects.create(prename='Peter {}'.format(i), lastname='Walter', birth_date=timezone.now().date(), - email=settings.TEST_MAIL, gender=DIVERSE) + m = Member.objects.create( + prename="Peter {}".format(i), + lastname="Walter", + birth_date=timezone.now().date(), + email=settings.TEST_MAIL, + gender=DIVERSE, + ) mol = NewMemberOnList.objects.create(member=m, memberlist=self.st4.excursion) self.st4.excursion.membersonlist.add(mol) - self.assertEqual(self.st4.admissible_staff_count, 2, - 'Admissible staff count is not 2, although there are 4 participants.') + self.assertEqual( + self.st4.admissible_staff_count, + 2, + "Admissible staff count is not 2, although there are 4 participants.", + ) def test_reduce_transactions(self): self.st3.generate_transactions() - self.assertTrue(self.st3.allowance_to_valid, 'Configured `allowance_to` field is invalid.') + self.assertTrue(self.st3.allowance_to_valid, "Configured `allowance_to` field is invalid.") # every youth leader on `st3` paid one bill, the first three receive the allowance # and one receives the subsidies - self.assertEqual(self.st3.transaction_set.count(), self.st3.real_staff_count + self.staff_count + 1, - 'Transaction count is not twice the staff count.') + self.assertEqual( + self.st3.transaction_set.count(), + self.st3.real_staff_count + self.staff_count + 1, + "Transaction count is not twice the staff count.", + ) self.st3.reduce_transactions() - self.assertEqual(self.st3.transaction_set.count(), self.st3.real_staff_count + self.staff_count + 1, - 'Transaction count after reduction is not the same as before, although no ledgers are configured.') + self.assertEqual( + self.st3.transaction_set.count(), + self.st3.real_staff_count + self.staff_count + 1, + "Transaction count after reduction is not the same as before, although no ledgers are configured.", + ) for trans in self.st3.transaction_set.all(): trans.ledger = self.personal_account trans.save() self.st3.reduce_transactions() # the three yls that receive an allowance should only receive one transaction after reducing, # the additional one is the one for the subsidies - self.assertEqual(self.st3.transaction_set.count(), self.staff_count + 1, - 'Transaction count after setting ledgers and reduction is incorrect.') + self.assertEqual( + self.st3.transaction_set.count(), + self.staff_count + 1, + "Transaction count after setting ledgers and reduction is incorrect.", + ) self.st3.reduce_transactions() - self.assertEqual(self.st3.transaction_set.count(), self.staff_count + 1, - 'Transaction count did change after reducing a second time.') + self.assertEqual( + self.st3.transaction_set.count(), + self.staff_count + 1, + "Transaction count did change after reducing a second time.", + ) def test_confirm_statement(self): - self.assertFalse(self.st3.confirm(confirmer=self.fritz), 'Statement was confirmed, although it is not submitted.') + self.assertFalse( + self.st3.confirm(confirmer=self.fritz), + "Statement was confirmed, although it is not submitted.", + ) self.st3.submit(submitter=self.fritz) - self.assertTrue(self.st3.submitted, 'Statement is not submitted, although it was.') - self.assertEqual(self.st3.submitted_by, self.fritz, - 'Statement was not submitted by fritz.') + self.assertTrue(self.st3.submitted, "Statement is not submitted, although it was.") + self.assertEqual(self.st3.submitted_by, self.fritz, "Statement was not submitted by fritz.") - self.assertFalse(self.st3.confirm(), 'Statement was confirmed, but is not valid yet.') + self.assertFalse(self.st3.confirm(), "Statement was confirmed, but is not valid yet.") self.st3.generate_transactions() for trans in self.st3.transaction_set.all(): trans.ledger = self.personal_account trans.save() - self.assertEqual(self.st3.validity, Statement.VALID, - 'Statement is not valid, although it was setup to be so.') - self.assertTrue(self.st3.confirm(confirmer=self.fritz), - 'Statement was not confirmed, although it submitted and valid.') - self.assertEqual(self.st3.confirmed_by, self.fritz, 'Statement not confirmed by fritz.') + self.assertEqual( + self.st3.validity, + Statement.VALID, + "Statement is not valid, although it was setup to be so.", + ) + self.assertTrue( + self.st3.confirm(confirmer=self.fritz), + "Statement was not confirmed, although it submitted and valid.", + ) + self.assertEqual(self.st3.confirmed_by, self.fritz, "Statement not confirmed by fritz.") for trans in self.st3.transaction_set.all(): - self.assertTrue(trans.confirmed, 'Transaction on confirmed statement is not confirmed.') - self.assertEqual(trans.confirmed_by, self.fritz, 'Transaction on confirmed statement is not confirmed by fritz.') + self.assertTrue(trans.confirmed, "Transaction on confirmed statement is not confirmed.") + self.assertEqual( + trans.confirmed_by, + self.fritz, + "Transaction on confirmed statement is not confirmed by fritz.", + ) def test_excursion_statement(self): - self.assertEqual(self.st3.excursion.staff_count, self.staff_count, - 'Calculated staff count is not constructed staff count.') - self.assertEqual(self.st3.excursion.participant_count, self.participant_count, - 'Calculated participant count is not constructed participant count.') - self.assertLess(self.st3.admissible_staff_count, self.staff_count, - 'All staff members are refinanced, although {} is too much for {} participants.'.format(self.staff_count, self.participant_count)) - self.assertFalse(self.st3.transactions_match_expenses, - 'Transactions match expenses, but currently no one is paid.') - self.assertGreater(self.st3.total_staff, 0, - 'There are no costs for the staff, although there are enough participants.') - self.assertEqual(self.st3.total_nights, 0, - 'There are costs for the night, although there was no night.') - self.assertEqual(self.st3.real_night_cost, settings.MAX_NIGHT_COST, - 'Real night cost is not the max, although the given one is way too high.') + self.assertEqual( + self.st3.excursion.staff_count, + self.staff_count, + "Calculated staff count is not constructed staff count.", + ) + self.assertEqual( + self.st3.excursion.participant_count, + self.participant_count, + "Calculated participant count is not constructed participant count.", + ) + self.assertLess( + self.st3.admissible_staff_count, + self.staff_count, + "All staff members are refinanced, although {} is too much for {} participants.".format( + self.staff_count, self.participant_count + ), + ) + self.assertFalse( + self.st3.transactions_match_expenses, + "Transactions match expenses, but currently no one is paid.", + ) + self.assertGreater( + self.st3.total_staff, + 0, + "There are no costs for the staff, although there are enough participants.", + ) + self.assertEqual( + self.st3.total_nights, 0, "There are costs for the night, although there was no night." + ) + self.assertEqual( + self.st3.real_night_cost, + settings.MAX_NIGHT_COST, + "Real night cost is not the max, although the given one is way too high.", + ) # changing means of transport changes euro_per_km epkm = self.st3.euro_per_km self.st3.excursion.tour_approach = FAHRGEMEINSCHAFT_ANREISE - self.assertNotEqual(epkm, self.st3.euro_per_km, 'Changing means of transport did not change euro per km.') + self.assertNotEqual( + epkm, self.st3.euro_per_km, "Changing means of transport did not change euro per km." + ) self.st3.generate_transactions() - self.assertTrue(self.st3.transactions_match_expenses, - "Transactions don't match expenses after generating them.") - self.assertGreater(self.st3.total, 0, 'Total is 0.') + self.assertTrue( + self.st3.transactions_match_expenses, + "Transactions don't match expenses after generating them.", + ) + self.assertGreater(self.st3.total, 0, "Total is 0.") def test_generate_transactions(self): # self.st2 has an unpaid bill - self.assertFalse(self.st2.transactions_match_expenses, - 'Transactions match expenses, but one bill is not paid.') + self.assertFalse( + self.st2.transactions_match_expenses, + "Transactions match expenses, but one bill is not paid.", + ) self.st2.generate_transactions() # now transactions should match expenses - self.assertTrue(self.st2.transactions_match_expenses, - "Transactions don't match expenses after generating them.") + self.assertTrue( + self.st2.transactions_match_expenses, + "Transactions don't match expenses after generating them.", + ) # self.st2 is still not valid - self.assertEqual(self.st2.validity, Statement.MISSING_LEDGER, - 'Statement is valid, although transaction has no ledger setup.') + self.assertEqual( + self.st2.validity, + Statement.MISSING_LEDGER, + "Statement is valid, although transaction has no ledger setup.", + ) for trans in self.st2.transaction_set.all(): trans.ledger = self.personal_account trans.save() - self.assertEqual(self.st2.validity, Statement.VALID, - 'Statement is still invalid, after setting up ledger.') + self.assertEqual( + self.st2.validity, + Statement.VALID, + "Statement is still invalid, after setting up ledger.", + ) # create a new transaction issue by manually changing amount t1 = self.st2.transaction_set.all()[0] t1.amount = 123 t1.save() - self.assertFalse(self.st2.transactions_match_expenses, - 'Transactions match expenses, but one transaction was tweaked.') + self.assertFalse( + self.st2.transactions_match_expenses, + "Transactions match expenses, but one transaction was tweaked.", + ) def test_generate_transactions_not_covered(self): bill = self.st2.bill_set.all()[0] @@ -334,19 +547,28 @@ class StatementTestCase(TestCase): def test_detect_unallowed_gift(self): # there is a bill - self.assertGreater(self.st.total_bills_theoretic, 0, 'Theoretic bill total is 0 (should be > 0).') + self.assertGreater( + self.st.total_bills_theoretic, 0, "Theoretic bill total is 0 (should be > 0)." + ) # but it is not covered - self.assertEqual(self.st.total_bills, 0, 'Real bill total is not 0.') - self.assertEqual(self.st.total, 0, 'Total is not 0.') - self.assertGreater(self.st.total_theoretic, 0, 'Total in theorey is 0.') + self.assertEqual(self.st.total_bills, 0, "Real bill total is not 0.") + self.assertEqual(self.st.total, 0, "Total is not 0.") + self.assertGreater(self.st.total_theoretic, 0, "Total in theorey is 0.") self.st.generate_transactions() - self.assertEqual(self.st.transaction_set.count(), 1, 'Generating transactions did produce new transactions.') + self.assertEqual( + self.st.transaction_set.count(), + 1, + "Generating transactions did produce new transactions.", + ) # but there is a transaction anyway - self.assertFalse(self.st.transactions_match_expenses, - 'Transactions match expenses, although an unreasonable gift is paid.') + self.assertFalse( + self.st.transactions_match_expenses, + "Transactions match expenses, although an unreasonable gift is paid.", + ) # so statement must be invalid - self.assertFalse(self.st.is_valid(), - 'Transaction is valid, although an unreasonable gift is paid.') + self.assertFalse( + self.st.is_valid(), "Transaction is valid, although an unreasonable gift is paid." + ) def test_allowance_to_valid(self): self.assertEqual(self.st3.excursion.participant_count, self.participant_count) @@ -391,20 +613,18 @@ class StatementTestCase(TestCase): def test_template_context(self): # with excursion - self.assertTrue('euro_per_km' in self.st3.template_context()) + self.assertTrue("euro_per_km" in self.st3.template_context()) # without excursion - self.assertFalse('euro_per_km' in self.st2.template_context()) + self.assertFalse("euro_per_km" in self.st2.template_context()) def test_grouped_bills(self): bills = self.st2.grouped_bills() - self.assertTrue('amount' in bills[0]) + self.assertTrue("amount" in bills[0]) def test_euro_per_km_no_excursion(self): """Test euro_per_km when no excursion is associated""" statement = Statement.objects.create( - short_description="Test Statement", - explanation="Test explanation", - night_cost=25 + short_description="Test Statement", explanation="Test explanation", night_cost=25 ) self.assertEqual(statement.euro_per_km, 0) @@ -414,7 +634,7 @@ class StatementTestCase(TestCase): short_description="Test Statement", explanation="Test explanation", night_cost=25, - created_by=self.fritz + created_by=self.fritz, ) self.assertFalse(statement.submitted) @@ -431,12 +651,12 @@ class StatementTestCase(TestCase): """Test statement template context when excursion is present""" # Use existing excursion from setUp context = self.st3.template_context() - self.assertIn('euro_per_km', context) - self.assertIsInstance(context['euro_per_km'], (int, float, Decimal)) + self.assertIn("euro_per_km", context) + self.assertIsInstance(context["euro_per_km"], (int, float, Decimal)) def test_title_with_excursion(self): title = self.st3.title - self.assertIn('Wild trip', title) + self.assertIn("Wild trip", title) def test_transaction_issues_with_org_fee(self): issues = self.st4.transaction_issues @@ -455,14 +675,17 @@ class StatementTestCase(TestCase): self.st4.save() # Verify org fee is calculated - self.assertGreater(self.st4.total_org_fee, 0, "Org fee should be > 0 with subsidies and old participants") + self.assertGreater( + self.st4.total_org_fee, 0, "Org fee should be > 0 with subsidies and old participants" + ) initial_count = Transaction.objects.count() self.st4.generate_transactions() final_count = Transaction.objects.count() self.assertGreater(final_count, initial_count) - org_fee_transaction = Transaction.objects.filter(statement=self.st4, - reference__icontains=_('reduced by org fee')).first() + org_fee_transaction = Transaction.objects.filter( + statement=self.st4, reference__icontains=_("reduced by org fee") + ).first() self.assertIsNotNone(org_fee_transaction) def test_generate_transactions_ljp(self): @@ -472,7 +695,9 @@ class StatementTestCase(TestCase): self.st3.generate_transactions() final_count = Transaction.objects.count() self.assertGreater(final_count, initial_count) - ljp_transaction = Transaction.objects.filter(statement=self.st3, member=self.fritz, reference__icontains='LJP').first() + ljp_transaction = Transaction.objects.filter( + statement=self.st3, member=self.fritz, reference__icontains="LJP" + ).first() self.assertIsNotNone(ljp_transaction) def test_subsidies_paid_property(self): @@ -485,8 +710,11 @@ class StatementTestCase(TestCase): self.st_small.save() # Verify that the small excursion has < 5 theoretic LJP participants - self.assertLess(self.st_small.excursion.theoretic_ljp_participant_count, 5, - "Should have < 5 theoretic LJP participants") + self.assertLess( + self.st_small.excursion.theoretic_ljp_participant_count, + 5, + "Should have < 5 theoretic LJP participants", + ) ljp_contrib = self.st_small.paid_ljp_contributions self.assertEqual(ljp_contrib, 0) @@ -499,25 +727,29 @@ class StatementTestCase(TestCase): class LedgerTestCase(TestCase): def setUp(self): - self.personal_account = Ledger.objects.create(name='personal account') + self.personal_account = Ledger.objects.create(name="personal account") def test_str(self): - self.assertTrue(str(self.personal_account), 'personal account') + self.assertTrue(str(self.personal_account), "personal account") class ManagerTestCase(TestCase): def setUp(self): - self.st = Statement.objects.create(short_description='A statement', - explanation='Important!', - night_cost=0) - self.st_submitted = Statement.objects.create(short_description='A statement', - explanation='Important!', - night_cost=0, - status=Statement.SUBMITTED) - self.st_confirmed = Statement.objects.create(short_description='A statement', - explanation='Important!', - night_cost=0, - status=Statement.CONFIRMED) + self.st = Statement.objects.create( + short_description="A statement", explanation="Important!", night_cost=0 + ) + self.st_submitted = Statement.objects.create( + short_description="A statement", + explanation="Important!", + night_cost=0, + status=Statement.SUBMITTED, + ) + self.st_confirmed = Statement.objects.create( + short_description="A statement", + explanation="Important!", + night_cost=0, + status=Statement.CONFIRMED, + ) def test_get_queryset(self): # TODO: remove this manager, since it is not used @@ -527,30 +759,43 @@ class ManagerTestCase(TestCase): mgr_unsubmitted = StatementUnSubmittedManager() mgr_unsubmitted.model = StatementUnSubmitted - self.assertQuerysetEqual(mgr_unsubmitted.get_queryset(), Statement.objects.filter(pk=self.st.pk)) + self.assertQuerysetEqual( + mgr_unsubmitted.get_queryset(), Statement.objects.filter(pk=self.st.pk) + ) mgr_submitted = StatementSubmittedManager() mgr_submitted.model = StatementSubmitted - self.assertQuerysetEqual(mgr_submitted.get_queryset(), Statement.objects.filter(pk=self.st_submitted.pk)) + self.assertQuerysetEqual( + mgr_submitted.get_queryset(), Statement.objects.filter(pk=self.st_submitted.pk) + ) mgr_confirmed = StatementConfirmedManager() mgr_confirmed.model = StatementConfirmed - self.assertQuerysetEqual(mgr_confirmed.get_queryset(), Statement.objects.filter(pk=self.st_confirmed.pk)) + self.assertQuerysetEqual( + mgr_confirmed.get_queryset(), Statement.objects.filter(pk=self.st_confirmed.pk) + ) class TransactionTestCase(TestCase): def setUp(self): - self.st = Statement.objects.create(short_description='A statement', - explanation='Important!', - night_cost=0) - self.personal_account = Ledger.objects.create(name='personal account') - self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(), - email=settings.TEST_MAIL, gender=MALE) - self.trans = Transaction.objects.create(reference='foobar', - amount=42, - member=self.fritz, - ledger=self.personal_account, - statement=self.st) + self.st = Statement.objects.create( + short_description="A statement", explanation="Important!", night_cost=0 + ) + self.personal_account = Ledger.objects.create(name="personal account") + self.fritz = Member.objects.create( + prename="Fritz", + lastname="Wulter", + birth_date=timezone.now().date(), + email=settings.TEST_MAIL, + gender=MALE, + ) + self.trans = Transaction.objects.create( + reference="foobar", + amount=42, + member=self.fritz, + ledger=self.personal_account, + statement=self.st, + ) def test_str(self): self.assertTrue(str(self.trans.pk) in str(self.trans)) @@ -558,9 +803,9 @@ class TransactionTestCase(TestCase): def test_escape_reference(self): """Test transaction reference escaping with various special characters""" test_cases = [ - ('harmless', 'harmless'), - ('äöüÄÖÜß', 'aeoeueAeOeUess'), - ('ha@r!?mless+09', 'har?mless+09'), + ("harmless", "harmless"), + ("äöüÄÖÜß", "aeoeueAeOeUess"), + ("ha@r!?mless+09", "har?mless+09"), ("simple", "simple"), ("test@email.com", "testemail.com"), ("ref!with#special$chars%", "refwithspecialchars"), @@ -574,26 +819,26 @@ class TransactionTestCase(TestCase): def test_code(self): self.trans.amount = 0 # amount is zero, so empty - self.assertEqual(self.trans.code(), '') + self.assertEqual(self.trans.code(), "") self.trans.amount = 42 # iban is invalid, so empty - self.assertEqual(self.trans.code(), '') + self.assertEqual(self.trans.code(), "") # a valid (random) iban - self.fritz.iban = 'DE89370400440532013000' - self.assertNotEqual(self.trans.code(), '') + self.fritz.iban = "DE89370400440532013000" + self.assertNotEqual(self.trans.code(), "") def test_code_with_zero_amount(self): """Test transaction code generation with zero amount""" transaction = Transaction.objects.create( reference="test-ref", - amount=Decimal('0.00'), + amount=Decimal("0.00"), member=self.fritz, ledger=self.personal_account, - statement=self.st + statement=self.st, ) # Zero amount should return empty code - self.assertEqual(transaction.code(), '') + self.assertEqual(transaction.code(), "") def test_code_with_invalid_iban(self): """Test transaction code generation with invalid IBAN""" @@ -602,36 +847,33 @@ class TransactionTestCase(TestCase): transaction = Transaction.objects.create( reference="test-ref", - amount=Decimal('100.00'), + amount=Decimal("100.00"), member=self.fritz, ledger=self.personal_account, - statement=self.st + statement=self.st, ) # Invalid IBAN should return empty code - self.assertEqual(transaction.code(), '') + self.assertEqual(transaction.code(), "") class BillTestCase(TestCase): def setUp(self): - self.st = Statement.objects.create(short_description='A statement', - explanation='Important!', - night_cost=0) - self.bill = Bill.objects.create(statement=self.st, - short_description='foobar') + self.st = Statement.objects.create( + short_description="A statement", explanation="Important!", night_cost=0 + ) + self.bill = Bill.objects.create(statement=self.st, short_description="foobar") def test_str(self): - self.assertTrue('€' in str(self.bill)) + self.assertTrue("€" in str(self.bill)) def test_pretty_amount(self): - self.assertTrue('€' in self.bill.pretty_amount()) + self.assertTrue("€" in self.bill.pretty_amount()) def test_pretty_amount_formatting(self): """Test bill pretty_amount formatting with specific values""" bill = Bill.objects.create( - statement=self.st, - short_description="Test Bill", - amount=Decimal('42.50') + statement=self.st, short_description="Test Bill", amount=Decimal("42.50") ) pretty = bill.pretty_amount() @@ -641,19 +883,17 @@ class BillTestCase(TestCase): def test_zero_amount(self): """Test bill with zero amount""" bill = Bill.objects.create( - statement=self.st, - short_description="Zero Bill", - amount=Decimal('0.00') + statement=self.st, short_description="Zero Bill", amount=Decimal("0.00") ) - self.assertEqual(bill.amount, Decimal('0.00')) + self.assertEqual(bill.amount, Decimal("0.00")) pretty = bill.pretty_amount() self.assertIn("0.00", pretty) class TransactionIssueTestCase(TestCase): def setUp(self): - self.issue = TransactionIssue('foo', 42, 26) + self.issue = TransactionIssue("foo", 42, 26) def test_difference(self): self.assertEqual(self.issue.difference, 26 - 42) diff --git a/jdav_web/finance/tests/rules.py b/jdav_web/finance/tests/rules.py index 5470f54..a2d593b 100644 --- a/jdav_web/finance/tests/rules.py +++ b/jdav_web/finance/tests/rules.py @@ -1,11 +1,21 @@ -from django.test import TestCase -from django.utils import timezone +from unittest.mock import Mock + from django.conf import settings from django.contrib.auth.models import User -from unittest.mock import Mock -from finance.rules import is_creator, not_submitted, leads_excursion -from finance.models import Statement, Ledger -from members.models import Member, Group, Freizeit, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE, MALE, FEMALE +from django.test import TestCase +from django.utils import timezone +from finance.models import Ledger +from finance.models import Statement +from finance.rules import is_creator +from finance.rules import leads_excursion +from finance.rules import not_submitted +from members.models import FEMALE +from members.models import Freizeit +from members.models import GEMEINSCHAFTS_TOUR +from members.models import Group +from members.models import MALE +from members.models import Member +from members.models import MUSKELKRAFT_ANREISE class FinanceRulesTestCase(TestCase): @@ -15,15 +25,23 @@ class FinanceRulesTestCase(TestCase): self.user1 = User.objects.create_user(username="alice", password="test123") self.member1 = Member.objects.create( - prename="Alice", lastname="Smith", birth_date=timezone.now().date(), - email=settings.TEST_MAIL, gender=FEMALE, user=self.user1 + prename="Alice", + lastname="Smith", + birth_date=timezone.now().date(), + email=settings.TEST_MAIL, + gender=FEMALE, + user=self.user1, ) self.member1.group.add(self.group) self.user2 = User.objects.create_user(username="bob", password="test123") self.member2 = Member.objects.create( - prename="Bob", lastname="Jones", birth_date=timezone.now().date(), - email=settings.TEST_MAIL, gender=MALE, user=self.user2 + prename="Bob", + lastname="Jones", + birth_date=timezone.now().date(), + email=settings.TEST_MAIL, + gender=MALE, + user=self.user2, ) self.member2.group.add(self.group) @@ -32,7 +50,7 @@ class FinanceRulesTestCase(TestCase): kilometers_traveled=100, tour_type=GEMEINSCHAFTS_TOUR, tour_approach=MUSKELKRAFT_ANREISE, - difficulty=2 + difficulty=2, ) self.freizeit.jugendleiter.add(self.member1) @@ -41,7 +59,7 @@ class FinanceRulesTestCase(TestCase): explanation="Test explanation", night_cost=27, created_by=self.member1, - excursion=self.freizeit + excursion=self.freizeit, ) self.statement.allowance_to.add(self.member1) @@ -68,8 +86,8 @@ class FinanceRulesTestCase(TestCase): # Create a mock Freizeit that truly doesn't have the statement attribute mock_freizeit = Mock(spec=Freizeit) # Remove the statement attribute entirely - if hasattr(mock_freizeit, 'statement'): - delattr(mock_freizeit, 'statement') + if hasattr(mock_freizeit, "statement"): + delattr(mock_freizeit, "statement") self.assertTrue(not_submitted(self.user1, mock_freizeit)) def test_leads_excursion_freizeit_user_is_leader(self): @@ -96,7 +114,7 @@ class FinanceRulesTestCase(TestCase): explanation="Test explanation", night_cost=27, created_by=self.member1, - excursion=None + excursion=None, ) result = leads_excursion(self.user1, statement_no_excursion) self.assertFalse(result) diff --git a/jdav_web/logindata/migrations/0001_initial.py b/jdav_web/logindata/migrations/0001_initial.py index 0a26b33..30cfd91 100644 --- a/jdav_web/logindata/migrations/0001_initial.py +++ b/jdav_web/logindata/migrations/0001_initial.py @@ -1,55 +1,58 @@ # Generated by Django 4.0.1 on 2024-11-23 21:15 import django.contrib.auth.models -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ("auth", "0012_alter_user_first_name_max_length"), ] operations = [ migrations.CreateModel( - name='RegistrationPassword', + name="RegistrationPassword", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=100, verbose_name='Password')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("password", models.CharField(max_length=100, verbose_name="Password")), ], ), migrations.CreateModel( - name='AuthGroup', - fields=[ - ], + name="AuthGroup", + fields=[], options={ - 'verbose_name': 'Permission group', - 'verbose_name_plural': 'Permission groups', - 'proxy': True, - 'indexes': [], - 'constraints': [], + "verbose_name": "Permission group", + "verbose_name_plural": "Permission groups", + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('auth.group',), + bases=("auth.group",), managers=[ - ('objects', django.contrib.auth.models.GroupManager()), + ("objects", django.contrib.auth.models.GroupManager()), ], ), migrations.CreateModel( - name='LoginDatum', - fields=[ - ], + name="LoginDatum", + fields=[], options={ - 'verbose_name': 'Login Datum', - 'verbose_name_plural': 'Login Data', - 'proxy': True, - 'indexes': [], - 'constraints': [], + "verbose_name": "Login Datum", + "verbose_name_plural": "Login Data", + "proxy": True, + "indexes": [], + "constraints": [], }, - bases=('auth.user',), + bases=("auth.user",), managers=[ - ('objects', django.contrib.auth.models.UserManager()), + ("objects", django.contrib.auth.models.UserManager()), ], ), ] diff --git a/jdav_web/logindata/migrations/0002_alter_registrationpassword_options.py b/jdav_web/logindata/migrations/0002_alter_registrationpassword_options.py index 8331c8e..84ab482 100644 --- a/jdav_web/logindata/migrations/0002_alter_registrationpassword_options.py +++ b/jdav_web/logindata/migrations/0002_alter_registrationpassword_options.py @@ -4,14 +4,16 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('logindata', '0001_initial'), + ("logindata", "0001_initial"), ] operations = [ migrations.AlterModelOptions( - name='registrationpassword', - options={'verbose_name': 'Active registration password', 'verbose_name_plural': 'Active registration passwords'}, + name="registrationpassword", + options={ + "verbose_name": "Active registration password", + "verbose_name_plural": "Active registration passwords", + }, ), ] diff --git a/jdav_web/logindata/tests/__init__.py b/jdav_web/logindata/tests/__init__.py index e39bc3b..9dacb05 100644 --- a/jdav_web/logindata/tests/__init__.py +++ b/jdav_web/logindata/tests/__init__.py @@ -1,2 +1,4 @@ +# ruff: noqa F403 + +from .oauth import * from .views import * -from .oauth import * \ No newline at end of file diff --git a/jdav_web/logindata/tests/oauth.py b/jdav_web/logindata/tests/oauth.py index 9a414a1..867889d 100644 --- a/jdav_web/logindata/tests/oauth.py +++ b/jdav_web/logindata/tests/oauth.py @@ -1,9 +1,11 @@ -from django.test import TestCase -from django.contrib.auth.models import User -from django.conf import settings from unittest.mock import Mock + +from django.conf import settings +from django.contrib.auth.models import User +from django.test import TestCase from logindata.oauth import CustomOAuth2Validator -from members.models import Member, MALE +from members.models import MALE +from members.models import Member class CustomOAuth2ValidatorTestCase(TestCase): @@ -13,8 +15,12 @@ class CustomOAuth2ValidatorTestCase(TestCase): # Create user with member self.user_with_member = User.objects.create_user(username="alice", password="test123") self.member = Member.objects.create( - prename="Alice", lastname="Smith", birth_date="1990-01-01", - email=settings.TEST_MAIL, gender=MALE, user=self.user_with_member + prename="Alice", + lastname="Smith", + birth_date="1990-01-01", + email=settings.TEST_MAIL, + gender=MALE, + user=self.user_with_member, ) # Create user without member @@ -27,8 +33,8 @@ class CustomOAuth2ValidatorTestCase(TestCase): result = self.validator.get_additional_claims(request) - self.assertEqual(result['email'], settings.TEST_MAIL) - self.assertEqual(result['preferred_username'], 'alice') + self.assertEqual(result["email"], settings.TEST_MAIL) + self.assertEqual(result["preferred_username"], "alice") def test_get_additional_claims_without_member(self): """Test get_additional_claims when user has no member""" diff --git a/jdav_web/logindata/tests/views.py b/jdav_web/logindata/tests/views.py index 00e22d2..e109e07 100644 --- a/jdav_web/logindata/tests/views.py +++ b/jdav_web/logindata/tests/views.py @@ -1,12 +1,16 @@ from http import HTTPStatus -from django.test import TestCase, Client + +from django.contrib.auth.models import Group +from django.contrib.auth.models import User +from django.test import Client +from django.test import TestCase from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext as _ -from django.contrib.auth.models import User, Group +from members.models import DIVERSE +from members.models import Member -from members.models import Member, DIVERSE -from ..models import RegistrationPassword, initial_user_setup +from ..models import RegistrationPassword class RegistrationPasswordTestCase(TestCase): @@ -22,133 +26,152 @@ class RegisterViewTestCase(TestCase): # Create a test member with invite key self.member = Member.objects.create( - prename='Test', - lastname='User', + prename="Test", + lastname="User", birth_date=timezone.now().date(), - email='test@example.com', + email="test@example.com", gender=DIVERSE, - invite_as_user_key='test_key_123' + invite_as_user_key="test_key_123", ) # Create a registration password - self.registration_password = RegistrationPassword.objects.create( - password='test_password' - ) + self.registration_password = RegistrationPassword.objects.create(password="test_password") # Get or create Standard group for user setup - self.standard_group, created = Group.objects.get_or_create(name='Standard') + self.standard_group, created = Group.objects.get_or_create(name="Standard") def test_register_get_without_key_redirects(self): """Test GET request without key redirects to startpage.""" - url = reverse('logindata:register') + url = reverse("logindata:register") response = self.client.get(url) self.assertEqual(response.status_code, HTTPStatus.FOUND) def test_register_post_without_key_redirects(self): """Test POST request without key redirects to startpage.""" - url = reverse('logindata:register') + url = reverse("logindata:register") response = self.client.post(url) self.assertEqual(response.status_code, HTTPStatus.FOUND) def test_register_get_with_empty_key_shows_failed(self): """Test GET request with empty key shows registration failed page.""" - url = reverse('logindata:register') - response = self.client.get(url, {'key': ''}) + url = reverse("logindata:register") + response = self.client.get(url, {"key": ""}) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Something went wrong. The registration key is invalid or has expired.')) + self.assertContains( + response, _("Something went wrong. The registration key is invalid or has expired.") + ) def test_register_get_with_invalid_key_shows_failed(self): """Test GET request with invalid key shows registration failed page.""" - url = reverse('logindata:register') - response = self.client.get(url, {'key': 'invalid_key'}) + url = reverse("logindata:register") + response = self.client.get(url, {"key": "invalid_key"}) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Something went wrong. The registration key is invalid or has expired.')) + self.assertContains( + response, _("Something went wrong. The registration key is invalid or has expired.") + ) def test_register_get_with_valid_key_shows_password_form(self): """Test GET request with valid key shows password entry form.""" - url = reverse('logindata:register') - response = self.client.get(url, {'key': self.member.invite_as_user_key}) + url = reverse("logindata:register") + response = self.client.get(url, {"key": self.member.invite_as_user_key}) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Set login data')) - self.assertContains(response, _('Welcome, ')) + self.assertContains(response, _("Set login data")) + self.assertContains(response, _("Welcome, ")) self.assertContains(response, self.member.prename) def test_register_post_without_password_shows_failed(self): """Test POST request without password shows registration failed page.""" - url = reverse('logindata:register') - response = self.client.post(url, {'key': self.member.invite_as_user_key}) + url = reverse("logindata:register") + response = self.client.post(url, {"key": self.member.invite_as_user_key}) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Something went wrong. The registration key is invalid or has expired.')) + self.assertContains( + response, _("Something went wrong. The registration key is invalid or has expired.") + ) def test_register_post_with_wrong_password_shows_error(self): """Test POST request with wrong password shows error message.""" - url = reverse('logindata:register') - response = self.client.post(url, { - 'key': self.member.invite_as_user_key, - 'password': 'wrong_password' - }) + url = reverse("logindata:register") + response = self.client.post( + url, {"key": self.member.invite_as_user_key, "password": "wrong_password"} + ) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('You entered a wrong password.')) + self.assertContains(response, _("You entered a wrong password.")) def test_register_post_with_correct_password_shows_form(self): """Test POST request with correct password shows user creation form.""" - url = reverse('logindata:register') - response = self.client.post(url, { - 'key': self.member.invite_as_user_key, - 'password': self.registration_password.password - }) + url = reverse("logindata:register") + response = self.client.post( + url, + { + "key": self.member.invite_as_user_key, + "password": self.registration_password.password, + }, + ) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Set login data')) + self.assertContains(response, _("Set login data")) self.assertContains(response, self.member.suggested_username()) def test_register_post_with_save_and_invalid_form_shows_errors(self): """Test POST request with save but invalid form shows form errors.""" - url = reverse('logindata:register') - response = self.client.post(url, { - 'key': self.member.invite_as_user_key, - 'password': self.registration_password.password, - 'save': 'true', - 'username': '', # Invalid - empty username - 'password1': 'testpass123', - 'password2': 'different_pass' # Invalid - passwords don't match - }) + url = reverse("logindata:register") + response = self.client.post( + url, + { + "key": self.member.invite_as_user_key, + "password": self.registration_password.password, + "save": "true", + "username": "", # Invalid - empty username + "password1": "testpass123", + "password2": "different_pass", # Invalid - passwords don't match + }, + ) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Set login data')) + self.assertContains(response, _("Set login data")) def test_register_post_with_save_and_valid_form_shows_success(self): """Test POST request with save and valid form shows success page.""" - url = reverse('logindata:register') - response = self.client.post(url, { - 'key': self.member.invite_as_user_key, - 'password': self.registration_password.password, - 'save': 'true', - 'username': 'testuser', - 'password1': 'testpass123', - 'password2': 'testpass123' - }) + url = reverse("logindata:register") + response = self.client.post( + url, + { + "key": self.member.invite_as_user_key, + "password": self.registration_password.password, + "save": "true", + "username": "testuser", + "password1": "testpass123", + "password2": "testpass123", + }, + ) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('You successfully set your login data. You can now proceed to')) + self.assertContains( + response, _("You successfully set your login data. You can now proceed to") + ) # Verify user was created and associated with member - user = User.objects.get(username='testuser') + user = User.objects.get(username="testuser") self.assertEqual(user.is_staff, True) self.member.refresh_from_db() self.assertEqual(self.member.user, user) - self.assertEqual(self.member.invite_as_user_key, '') + self.assertEqual(self.member.invite_as_user_key, "") def test_register_post_with_save_and_no_standard_group_shows_failed(self): """Test POST request with save but no Standard group shows failed page.""" # Delete the Standard group self.standard_group.delete() - url = reverse('logindata:register') - response = self.client.post(url, { - 'key': self.member.invite_as_user_key, - 'password': self.registration_password.password, - 'save': 'true', - 'username': 'testuser', - 'password1': 'testpass123', - 'password2': 'testpass123' - }) + url = reverse("logindata:register") + response = self.client.post( + url, + { + "key": self.member.invite_as_user_key, + "password": self.registration_password.password, + "save": "true", + "username": "testuser", + "password1": "testpass123", + "password2": "testpass123", + }, + ) self.assertEqual(response.status_code, HTTPStatus.OK) - self.assertContains(response, _('Something went wrong. The registration key is invalid or has expired.')) \ No newline at end of file + self.assertContains( + response, _("Something went wrong. The registration key is invalid or has expired.") + ) diff --git a/jdav_web/ludwigsburgalpin/migrations/0001_initial_squashed_0007_alter_termin_group.py b/jdav_web/ludwigsburgalpin/migrations/0001_initial_squashed_0007_alter_termin_group.py index c34e944..3292c16 100644 --- a/jdav_web/ludwigsburgalpin/migrations/0001_initial_squashed_0007_alter_termin_group.py +++ b/jdav_web/ludwigsburgalpin/migrations/0001_initial_squashed_0007_alter_termin_group.py @@ -1,46 +1,194 @@ # Generated by Django 4.0.1 on 2023-03-29 20:40 import django.core.validators -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - - replaces = [('ludwigsburgalpin', '0001_initial'), ('ludwigsburgalpin', '0002_auto_20190926_1432'), ('ludwigsburgalpin', '0003_auto_20190926_1749'), ('ludwigsburgalpin', '0004_alter_termin_id'), ('ludwigsburgalpin', '0005_alter_termin_id'), ('ludwigsburgalpin', '0006_termin_anforderung_dauer_termin_anforderung_hoehe_and_more'), ('ludwigsburgalpin', '0007_alter_termin_group')] - - dependencies = [ + replaces = [ + ("ludwigsburgalpin", "0001_initial"), + ("ludwigsburgalpin", "0002_auto_20190926_1432"), + ("ludwigsburgalpin", "0003_auto_20190926_1749"), + ("ludwigsburgalpin", "0004_alter_termin_id"), + ("ludwigsburgalpin", "0005_alter_termin_id"), + ("ludwigsburgalpin", "0006_termin_anforderung_dauer_termin_anforderung_hoehe_and_more"), + ("ludwigsburgalpin", "0007_alter_termin_group"), ] + dependencies = [] + operations = [ migrations.CreateModel( - name='Termin', + name="Termin", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=100, verbose_name='Titel')), - ('start_date', models.DateField(verbose_name='Von')), - ('end_date', models.DateField(verbose_name='Bis')), - ('group', models.CharField(choices=[('ASG', 'Alpinsportgruppe'), ('OGB', 'Ortsgruppe Bietigheim'), ('OGV', 'Ortsgruppe Vaihingen'), ('JUG', 'Jugend'), ('FAM', 'Familie'), ('Ü30', 'Ü30'), ('MTB', 'Mountainbike'), ('RA', 'RegioAktiv'), ('SEK', 'Sektion')], max_length=100, verbose_name='Gruppe')), - ('description', models.TextField(blank=True, verbose_name='Beschreibung')), - ('email', models.EmailField(max_length=100, verbose_name='Email')), - ('phone', models.CharField(blank=True, max_length=20, verbose_name='Telefonnumer')), - ('responsible', models.CharField(max_length=100, verbose_name='Organisator')), - ('anforderung_dauer', models.IntegerField(blank=True, default=0, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Etappendauer in Stunden')), - ('anforderung_hoehe', models.IntegerField(blank=True, default=0, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Höhenmeter in Meter')), - ('anforderung_strecke', models.IntegerField(blank=True, default=0, validators=[django.core.validators.MinValueValidator(0)], verbose_name='Strecke in Kilometer')), - ('category', models.CharField(choices=[('WAN', 'Wandern'), ('BW', 'Bergwandern'), ('KST', 'Klettersteig'), ('KL', 'Klettern'), ('SKI', 'Piste, Loipe'), ('SCH', 'Schneeschuhgehen'), ('ST', 'Skitour'), ('STH', 'Skihochtour'), ('HT', 'Hochtour'), ('MTB', 'Montainbike'), ('AUS', 'Ausbildung'), ('SON', 'Sonstiges z.B. Treffen')], default='SON', max_length=100, verbose_name='Kategorie')), - ('condition', models.CharField(choices=[('gering', 'gering'), ('mittel', 'mittel'), ('groß', 'groß'), ('sehr groß', 'sehr groß')], default='mittel', max_length=100, verbose_name='Kondition')), - ('equipment', models.TextField(blank=True, verbose_name='Ausrüstung')), - ('eventart', models.CharField(choices=[('Einzeltermin', 'Einzeltermin'), ('Mehrtagesevent', 'Mehrtagesevent'), ('Regelmäßiges Event/Training', 'Regelmäßiges Event/Training'), ('Tagesevent', 'Tagesevent'), ('Wochenendevent', 'Wochenendevent')], default='Einzeltermin', max_length=100, verbose_name='Eventart')), - ('klassifizierung', models.CharField(choices=[('Gemeinschaftstour', 'Gemeinschaftstour'), ('Ausbildung', 'Ausbildung')], default='Gemeinschaftstour', max_length=100, verbose_name='Klassifizierung')), - ('max_participants', models.IntegerField(default=10, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Max. Teilnehmerzahl')), - ('saison', models.CharField(choices=[('ganzjährig', 'ganzjährig'), ('Indoor', 'Indoor'), ('Sommer', 'Sommer'), ('Winter', 'Winter')], default='ganzjährig', max_length=100, verbose_name='Saison')), - ('subtitle', models.CharField(blank=True, max_length=100, verbose_name='Untertitel')), - ('technik', models.CharField(choices=[('leicht', 'leicht'), ('mittel', 'mittel'), ('schwer', 'schwer'), ('sehr schwer', 'sehr schwer')], default='mittel', max_length=100, verbose_name='Technik')), - ('voraussetzungen', models.TextField(blank=True, verbose_name='Voraussetzungen')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("title", models.CharField(max_length=100, verbose_name="Titel")), + ("start_date", models.DateField(verbose_name="Von")), + ("end_date", models.DateField(verbose_name="Bis")), + ( + "group", + models.CharField( + choices=[ + ("ASG", "Alpinsportgruppe"), + ("OGB", "Ortsgruppe Bietigheim"), + ("OGV", "Ortsgruppe Vaihingen"), + ("JUG", "Jugend"), + ("FAM", "Familie"), + ("Ü30", "Ü30"), + ("MTB", "Mountainbike"), + ("RA", "RegioAktiv"), + ("SEK", "Sektion"), + ], + max_length=100, + verbose_name="Gruppe", + ), + ), + ("description", models.TextField(blank=True, verbose_name="Beschreibung")), + ("email", models.EmailField(max_length=100, verbose_name="Email")), + ("phone", models.CharField(blank=True, max_length=20, verbose_name="Telefonnumer")), + ("responsible", models.CharField(max_length=100, verbose_name="Organisator")), + ( + "anforderung_dauer", + models.IntegerField( + blank=True, + default=0, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="Etappendauer in Stunden", + ), + ), + ( + "anforderung_hoehe", + models.IntegerField( + blank=True, + default=0, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="Höhenmeter in Meter", + ), + ), + ( + "anforderung_strecke", + models.IntegerField( + blank=True, + default=0, + validators=[django.core.validators.MinValueValidator(0)], + verbose_name="Strecke in Kilometer", + ), + ), + ( + "category", + models.CharField( + choices=[ + ("WAN", "Wandern"), + ("BW", "Bergwandern"), + ("KST", "Klettersteig"), + ("KL", "Klettern"), + ("SKI", "Piste, Loipe"), + ("SCH", "Schneeschuhgehen"), + ("ST", "Skitour"), + ("STH", "Skihochtour"), + ("HT", "Hochtour"), + ("MTB", "Montainbike"), + ("AUS", "Ausbildung"), + ("SON", "Sonstiges z.B. Treffen"), + ], + default="SON", + max_length=100, + verbose_name="Kategorie", + ), + ), + ( + "condition", + models.CharField( + choices=[ + ("gering", "gering"), + ("mittel", "mittel"), + ("groß", "groß"), + ("sehr groß", "sehr groß"), + ], + default="mittel", + max_length=100, + verbose_name="Kondition", + ), + ), + ("equipment", models.TextField(blank=True, verbose_name="Ausrüstung")), + ( + "eventart", + models.CharField( + choices=[ + ("Einzeltermin", "Einzeltermin"), + ("Mehrtagesevent", "Mehrtagesevent"), + ("Regelmäßiges Event/Training", "Regelmäßiges Event/Training"), + ("Tagesevent", "Tagesevent"), + ("Wochenendevent", "Wochenendevent"), + ], + default="Einzeltermin", + max_length=100, + verbose_name="Eventart", + ), + ), + ( + "klassifizierung", + models.CharField( + choices=[ + ("Gemeinschaftstour", "Gemeinschaftstour"), + ("Ausbildung", "Ausbildung"), + ], + default="Gemeinschaftstour", + max_length=100, + verbose_name="Klassifizierung", + ), + ), + ( + "max_participants", + models.IntegerField( + default=10, + validators=[django.core.validators.MinValueValidator(1)], + verbose_name="Max. Teilnehmerzahl", + ), + ), + ( + "saison", + models.CharField( + choices=[ + ("ganzjährig", "ganzjährig"), + ("Indoor", "Indoor"), + ("Sommer", "Sommer"), + ("Winter", "Winter"), + ], + default="ganzjährig", + max_length=100, + verbose_name="Saison", + ), + ), + ( + "subtitle", + models.CharField(blank=True, max_length=100, verbose_name="Untertitel"), + ), + ( + "technik", + models.CharField( + choices=[ + ("leicht", "leicht"), + ("mittel", "mittel"), + ("schwer", "schwer"), + ("sehr schwer", "sehr schwer"), + ], + default="mittel", + max_length=100, + verbose_name="Technik", + ), + ), + ("voraussetzungen", models.TextField(blank=True, verbose_name="Voraussetzungen")), ], options={ - 'verbose_name_plural': 'Termine', - 'verbose_name': 'Termin', + "verbose_name_plural": "Termine", + "verbose_name": "Termin", }, ), ] diff --git a/jdav_web/mailer/admin.py b/jdav_web/mailer/admin.py index 2fa17cc..615db82 100644 --- a/jdav_web/mailer/admin.py +++ b/jdav_web/mailer/admin.py @@ -7,14 +7,10 @@ from django.contrib import admin from django.contrib import messages from django.contrib.admin import helpers from django.shortcuts import render -from django.utils.translation import ( - gettext_lazy as _, -) +from django.utils.translation import gettext_lazy as _ from members.admin import FilteredMemberFieldMixin from members.models import Member -from rules.contrib.admin import ( - ObjectPermissionsModelAdmin, -) +from rules.contrib.admin import ObjectPermissionsModelAdmin from .mailutils import NOT_SENT from .mailutils import PARTLY_SENT @@ -23,6 +19,7 @@ from .models import EmailAddress from .models import EmailAddressForm from .models import Message from .models import MessageForm + # from easy_select2 import apply_select2 diff --git a/jdav_web/mailer/mailutils.py b/jdav_web/mailer/mailutils.py index c98dca1..6164e2e 100644 --- a/jdav_web/mailer/mailutils.py +++ b/jdav_web/mailer/mailutils.py @@ -4,7 +4,6 @@ from django.conf import settings from django.core import mail from django.core.mail import EmailMessage - logger = logging.getLogger(__name__) diff --git a/jdav_web/mailer/management/commands/notify_active.py b/jdav_web/mailer/management/commands/notify_active.py index 4a71779..665e546 100644 --- a/jdav_web/mailer/management/commands/notify_active.py +++ b/jdav_web/mailer/management/commands/notify_active.py @@ -1,20 +1,19 @@ +from django.conf import settings from django.core.management.base import BaseCommand -from mailer.models import Message -from members.models import Member, annotate_activity_score -from django.db.models import Q from django.utils.translation import gettext_lazy as _ from mailer.mailutils import send -from django.conf import settings - -import re +from members.models import annotate_activity_score +from members.models import Member class Command(BaseCommand): - help = 'Congratulates the most active members' + help = "Congratulates the most active members" requires_system_checks = False def handle(self, *args, **options): - qs = list(reversed(annotate_activity_score(Member.objects.all()).order_by('_activity_score')))[:settings.CONGRATULATE_MEMBERS_MAX] + qs = list( + reversed(annotate_activity_score(Member.objects.all()).order_by("_activity_score")) + )[: settings.CONGRATULATE_MEMBERS_MAX] for position, member in enumerate(qs): positiontext = "{}. ".format(position + 1) if position > 0 else "" score = member._activity_score @@ -28,11 +27,17 @@ class Command(BaseCommand): level = 4 else: level = 5 - content = settings.NOTIFY_MOST_ACTIVE_TEXT.format(name=member.prename, - congratulate_max=CONGRATULATE_MEMBERS_MAX, - score=score, - level=level, - position=positiontext) - send(_("Congratulation %(name)s") % { 'name': member.prename }, - content, settings.DEFAULT_SENDING_ADDRESS, [member.email], - reply_to=[settings.RESPONSIBLE_MAIL]) + content = settings.NOTIFY_MOST_ACTIVE_TEXT.format( + name=member.prename, + congratulate_max=settings.CONGRATULATE_MEMBERS_MAX, + score=score, + level=level, + position=positiontext, + ) + send( + _("Congratulation %(name)s") % {"name": member.prename}, + content, + settings.DEFAULT_SENDING_ADDRESS, + [member.email], + reply_to=[settings.RESPONSIBLE_MAIL], + ) diff --git a/jdav_web/mailer/management/commands/reply_addrs.py b/jdav_web/mailer/management/commands/reply_addrs.py index 89f373e..a256c73 100644 --- a/jdav_web/mailer/management/commands/reply_addrs.py +++ b/jdav_web/mailer/management/commands/reply_addrs.py @@ -1,30 +1,30 @@ +import re + from django.core.management.base import BaseCommand from mailer.models import Message from members.models import Member -from django.db.models import Q - -import re class Command(BaseCommand): - help = 'Shows reply-to addresses' + help = "Shows reply-to addresses" requires_system_checks = False def add_arguments(self, parser): - parser.add_argument('--message_id', default="-1") - parser.add_argument('--subject', default="") + parser.add_argument("--message_id", default="-1") + parser.add_argument("--subject", default="") def handle(self, *args, **options): replies = [] try: - message_id = int(options['message_id']) + message_id = int(options["message_id"]) message = Message.objects.get(pk=message_id) if message.reply_to: replies = list(message.reply_to.all()) replies.extend(message.reply_to_email_address.all()) except (Message.DoesNotExist, ValueError): - extracted = re.match("^([Ww][Gg]: *|[Ff][Ww]: *|[Rr][Ee]: *|[Aa][Ww]: *)* *(.*)$", - options['subject']).group(2) + extracted = re.match( + "^([Ww][Gg]: *|[Ff][Ww]: *|[Rr][Ee]: *|[Aa][Ww]: *)* *(.*)$", options["subject"] + ).group(2) try: msgs = Message.objects.filter(subject=extracted) message = msgs.all()[0] @@ -36,8 +36,7 @@ class Command(BaseCommand): if not replies: # send mail to all jugendleiters - replies = Member.objects.filter(group__name='Jugendleiter', - gets_newsletter=True) - forwards = [l.email for l in replies] + replies = Member.objects.filter(group__name="Jugendleiter", gets_newsletter=True) + forwards = [lst.email for lst in replies] self.stdout.write(" ".join(forwards)) diff --git a/jdav_web/mailer/migrations/0001_initial_squashed_0006_auto_20210924_1155.py b/jdav_web/mailer/migrations/0001_initial_squashed_0006_auto_20210924_1155.py index 9673d80..4d52ea7 100644 --- a/jdav_web/mailer/migrations/0001_initial_squashed_0006_auto_20210924_1155.py +++ b/jdav_web/mailer/migrations/0001_initial_squashed_0006_auto_20210924_1155.py @@ -1,79 +1,165 @@ # Generated by Django 4.0.1 on 2023-03-29 20:38 import django.core.validators -from django.db import migrations, models import django.db.models.deletion import utils +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - - replaces = [('mailer', '0001_initial'), ('mailer', '0002_auto_20190615_1225'), ('mailer', '0003_emailaddress'), ('mailer', '0004_auto_20200924_1744'), ('mailer', '0005_auto_20200924_2139'), ('mailer', '0006_auto_20210924_1155')] + replaces = [ + ("mailer", "0001_initial"), + ("mailer", "0002_auto_20190615_1225"), + ("mailer", "0003_emailaddress"), + ("mailer", "0004_auto_20200924_1744"), + ("mailer", "0005_auto_20200924_2139"), + ("mailer", "0006_auto_20210924_1155"), + ] dependencies = [ - ('members', '0006_auto_20190914_2341'), - ('members', '0008_auto_20210924_1155'), - ('members', '0001_initial'), - ('members', '0007_auto_20200924_1512'), - ('members', '0005_auto_20190615_1224'), + ("members", "0006_auto_20190914_2341"), + ("members", "0008_auto_20210924_1155"), + ("members", "0001_initial"), + ("members", "0007_auto_20200924_1512"), + ("members", "0005_auto_20190615_1224"), ] operations = [ migrations.CreateModel( - name='Message', + name="Message", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('subject', models.CharField(max_length=50, verbose_name='subject')), - ('content', models.TextField(verbose_name='content')), - ('sent', models.BooleanField(default=False, verbose_name='sent')), - ('to_groups', models.ManyToManyField(blank=True, to='members.Group', verbose_name='to group')), - ('to_members', models.ManyToManyField(blank=True, to='members.Member', verbose_name='to member')), - ('reply_to', models.ManyToManyField(blank=True, related_name='reply_to', to='members.Member', verbose_name='reply to participant')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("subject", models.CharField(max_length=50, verbose_name="subject")), + ("content", models.TextField(verbose_name="content")), + ("sent", models.BooleanField(default=False, verbose_name="sent")), + ( + "to_groups", + models.ManyToManyField(blank=True, to="members.Group", verbose_name="to group"), + ), + ( + "to_members", + models.ManyToManyField( + blank=True, to="members.Member", verbose_name="to member" + ), + ), + ( + "reply_to", + models.ManyToManyField( + blank=True, + related_name="reply_to", + to="members.Member", + verbose_name="reply to participant", + ), + ), ], options={ - 'verbose_name_plural': 'messages', - 'permissions': (('submit_mails', 'Can submit mails'),), - 'verbose_name': 'message', + "verbose_name_plural": "messages", + "permissions": (("submit_mails", "Can submit mails"),), + "verbose_name": "message", }, ), migrations.CreateModel( - name='Attachment', + name="Attachment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('f', utils.RestrictedFileField(blank=True, upload_to='attachments', verbose_name='file')), - ('msg', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mailer.message')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "f", + utils.RestrictedFileField( + blank=True, upload_to="attachments", verbose_name="file" + ), + ), + ( + "msg", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="mailer.message" + ), + ), ], options={ - 'verbose_name_plural': 'attachments', - 'verbose_name': 'attachment', + "verbose_name_plural": "attachments", + "verbose_name": "attachment", }, ), migrations.CreateModel( - name='EmailAddress', + name="EmailAddress", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z]*$', 'Only alphanumeric characters are allowed')], verbose_name='name')), - ('to_members', models.ManyToManyField(blank=True, to='members.Member', verbose_name='Forward to participants')), - ('to_groups', models.ManyToManyField(blank=True, to='members.Group', verbose_name='Forward to group')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "name", + models.CharField( + max_length=50, + validators=[ + django.core.validators.RegexValidator( + "^[0-9a-zA-Z]*$", "Only alphanumeric characters are allowed" + ) + ], + verbose_name="name", + ), + ), + ( + "to_members", + models.ManyToManyField( + blank=True, to="members.Member", verbose_name="Forward to participants" + ), + ), + ( + "to_groups", + models.ManyToManyField( + blank=True, to="members.Group", verbose_name="Forward to group" + ), + ), ], options={ - 'verbose_name_plural': 'email addresses', - 'verbose_name': 'email address', + "verbose_name_plural": "email addresses", + "verbose_name": "email address", }, ), migrations.AddField( - model_name='message', - name='reply_to_email_address', - field=models.ManyToManyField(blank=True, related_name='reply_to_email_addr', to='mailer.EmailAddress', verbose_name='reply to custom email address'), + model_name="message", + name="reply_to_email_address", + field=models.ManyToManyField( + blank=True, + related_name="reply_to_email_addr", + to="mailer.EmailAddress", + verbose_name="reply to custom email address", + ), ), migrations.AddField( - model_name='message', - name='to_freizeit', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='members.freizeit', verbose_name='to freizeit'), + model_name="message", + name="to_freizeit", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="members.freizeit", + verbose_name="to freizeit", + ), ), migrations.AddField( - model_name='message', - name='to_notelist', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='members.membernotelist', verbose_name='to notes list'), + model_name="message", + name="to_notelist", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="members.membernotelist", + verbose_name="to notes list", + ), ), ] diff --git a/jdav_web/mailer/migrations/0002_message_created_by.py b/jdav_web/mailer/migrations/0002_message_created_by.py index 1a2d565..ea73320 100644 --- a/jdav_web/mailer/migrations/0002_message_created_by.py +++ b/jdav_web/mailer/migrations/0002_message_created_by.py @@ -1,20 +1,27 @@ # Generated by Django 4.0.1 on 2023-04-02 12:06 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('members', '0006_rename_permissions'), - ('mailer', '0001_initial_squashed_0006_auto_20210924_1155'), + ("members", "0006_rename_permissions"), + ("mailer", "0001_initial_squashed_0006_auto_20210924_1155"), ] operations = [ migrations.AddField( - model_name='message', - name='created_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_messages', to='members.member', verbose_name='Created by'), + model_name="message", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="created_messages", + to="members.member", + verbose_name="Created by", + ), ), ] diff --git a/jdav_web/mailer/migrations/0003_alter_message_options.py b/jdav_web/mailer/migrations/0003_alter_message_options.py index 2b142bd..a765d5f 100644 --- a/jdav_web/mailer/migrations/0003_alter_message_options.py +++ b/jdav_web/mailer/migrations/0003_alter_message_options.py @@ -4,14 +4,25 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('mailer', '0002_message_created_by'), + ("mailer", "0002_message_created_by"), ] operations = [ migrations.AlterModelOptions( - name='message', - options={'default_permissions': ('add_global', 'change_global', 'view_global', 'delete_global', 'list_global', 'view'), 'permissions': (('submit_mails', 'Can submit mails'),), 'verbose_name': 'message', 'verbose_name_plural': 'messages'}, + name="message", + options={ + "default_permissions": ( + "add_global", + "change_global", + "view_global", + "delete_global", + "list_global", + "view", + ), + "permissions": (("submit_mails", "Can submit mails"),), + "verbose_name": "message", + "verbose_name_plural": "messages", + }, ), ] diff --git a/jdav_web/mailer/migrations/0004_alter_attachment_f.py b/jdav_web/mailer/migrations/0004_alter_attachment_f.py index d92419c..8607d55 100644 --- a/jdav_web/mailer/migrations/0004_alter_attachment_f.py +++ b/jdav_web/mailer/migrations/0004_alter_attachment_f.py @@ -1,19 +1,18 @@ # Generated by Django 4.0.1 on 2024-11-17 23:31 -from django.db import migrations import utils +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('mailer', '0003_alter_message_options'), + ("mailer", "0003_alter_message_options"), ] operations = [ migrations.AlterField( - model_name='attachment', - name='f', - field=utils.RestrictedFileField(upload_to='attachments', verbose_name='file'), + model_name="attachment", + name="f", + field=utils.RestrictedFileField(upload_to="attachments", verbose_name="file"), ), ] diff --git a/jdav_web/mailer/migrations/0005_alter_emailaddress_name.py b/jdav_web/mailer/migrations/0005_alter_emailaddress_name.py index 4910bc5..7a55fab 100644 --- a/jdav_web/mailer/migrations/0005_alter_emailaddress_name.py +++ b/jdav_web/mailer/migrations/0005_alter_emailaddress_name.py @@ -1,19 +1,27 @@ # Generated by Django 4.0.1 on 2024-11-23 14:03 import django.core.validators -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('mailer', '0004_alter_attachment_f'), + ("mailer", "0004_alter_attachment_f"), ] operations = [ migrations.AlterField( - model_name='emailaddress', - name='name', - field=models.CharField(max_length=50, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z._-]*$', 'Only alphanumeric characters, ., - and _ are allowed')], verbose_name='name'), + model_name="emailaddress", + name="name", + field=models.CharField( + max_length=50, + validators=[ + django.core.validators.RegexValidator( + "^[0-9a-zA-Z._-]*$", "Only alphanumeric characters, ., - and _ are allowed" + ) + ], + verbose_name="name", + ), ), ] diff --git a/jdav_web/mailer/migrations/0006_emailaddress_allowed_senders.py b/jdav_web/mailer/migrations/0006_emailaddress_allowed_senders.py index dd6de2f..1f86d24 100644 --- a/jdav_web/mailer/migrations/0006_emailaddress_allowed_senders.py +++ b/jdav_web/mailer/migrations/0006_emailaddress_allowed_senders.py @@ -1,19 +1,25 @@ # Generated by Django 4.0.1 on 2024-12-01 15:54 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('members', '0029_alter_member_gender_alter_memberwaitinglist_gender'), - ('mailer', '0005_alter_emailaddress_name'), + ("members", "0029_alter_member_gender_alter_memberwaitinglist_gender"), + ("mailer", "0005_alter_emailaddress_name"), ] operations = [ migrations.AddField( - model_name='emailaddress', - name='allowed_senders', - field=models.ManyToManyField(blank=True, help_text='Only forward e-mails of members of selected groups. Leave empty to allow all senders.', related_name='allowed_sender_on_emailaddresses', to='members.Group', verbose_name='Allowed sender'), + model_name="emailaddress", + name="allowed_senders", + field=models.ManyToManyField( + blank=True, + help_text="Only forward e-mails of members of selected groups. Leave empty to allow all senders.", + related_name="allowed_sender_on_emailaddresses", + to="members.Group", + verbose_name="Allowed sender", + ), ), ] diff --git a/jdav_web/mailer/migrations/0007_emailaddress_internal_only.py b/jdav_web/mailer/migrations/0007_emailaddress_internal_only.py index 6bc75ce..c975df7 100644 --- a/jdav_web/mailer/migrations/0007_emailaddress_internal_only.py +++ b/jdav_web/mailer/migrations/0007_emailaddress_internal_only.py @@ -1,18 +1,22 @@ # Generated by Django 4.0.1 on 2024-12-01 17:45 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('mailer', '0006_emailaddress_allowed_senders'), + ("mailer", "0006_emailaddress_allowed_senders"), ] operations = [ migrations.AddField( - model_name='emailaddress', - name='internal_only', - field=models.BooleanField(default=False, help_text='Only allow forwarding to this e-mail address from the internal domain.', verbose_name='Restrict to internal email addresses'), + model_name="emailaddress", + name="internal_only", + field=models.BooleanField( + default=False, + help_text="Only allow forwarding to this e-mail address from the internal domain.", + verbose_name="Restrict to internal email addresses", + ), ), ] diff --git a/jdav_web/mailer/migrations/0008_alter_emailaddress_name.py b/jdav_web/mailer/migrations/0008_alter_emailaddress_name.py index 952bacd..4494e8d 100644 --- a/jdav_web/mailer/migrations/0008_alter_emailaddress_name.py +++ b/jdav_web/mailer/migrations/0008_alter_emailaddress_name.py @@ -1,19 +1,28 @@ # Generated by Django 4.0.1 on 2024-12-03 23:19 import django.core.validators -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('mailer', '0007_emailaddress_internal_only'), + ("mailer", "0007_emailaddress_internal_only"), ] operations = [ migrations.AlterField( - model_name='emailaddress', - name='name', - field=models.CharField(max_length=50, unique=True, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z._-]*$', 'Only alphanumeric characters, ., - and _ are allowed')], verbose_name='name'), + model_name="emailaddress", + name="name", + field=models.CharField( + max_length=50, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^[0-9a-zA-Z._-]*$", "Only alphanumeric characters, ., - and _ are allowed" + ) + ], + verbose_name="name", + ), ), ] diff --git a/jdav_web/mailer/models.py b/jdav_web/mailer/models.py index da77291..a8ecb80 100644 --- a/jdav_web/mailer/models.py +++ b/jdav_web/mailer/models.py @@ -20,7 +20,6 @@ from .mailutils import send from .mailutils import SENT from .rules import is_creator - logger = logging.getLogger(__name__) diff --git a/jdav_web/mailer/templates/mailer/subscribe.html b/jdav_web/mailer/templates/mailer/subscribe.html index 66b4223..281a1e8 100644 --- a/jdav_web/mailer/templates/mailer/subscribe.html +++ b/jdav_web/mailer/templates/mailer/subscribe.html @@ -1,4 +1,4 @@ - + {% load i18n %} diff --git a/jdav_web/mailer/templates/mailer/unsubscribe.html b/jdav_web/mailer/templates/mailer/unsubscribe.html index 53ba56e..e85b77d 100644 --- a/jdav_web/mailer/templates/mailer/unsubscribe.html +++ b/jdav_web/mailer/templates/mailer/unsubscribe.html @@ -20,7 +20,7 @@ {% trans "Email address" %}

- +

diff --git a/jdav_web/mailer/tests/__init__.py b/jdav_web/mailer/tests/__init__.py index 7d5234a..0bad135 100644 --- a/jdav_web/mailer/tests/__init__.py +++ b/jdav_web/mailer/tests/__init__.py @@ -1,5 +1,7 @@ -from .models import * +# ruff: noqa F403 + from .admin import * -from .views import * -from .rules import * from .mailutils import * +from .models import * +from .rules import * +from .views import * diff --git a/jdav_web/mailer/tests/admin.py b/jdav_web/mailer/tests/admin.py index 52b0dab..127fb15 100644 --- a/jdav_web/mailer/tests/admin.py +++ b/jdav_web/mailer/tests/admin.py @@ -1,29 +1,31 @@ import json -import unittest from http import HTTPStatus -from django.test import TestCase, override_settings +from unittest.mock import patch + +from django.conf import settings from django.contrib.admin.sites import AdminSite -from django.test import RequestFactory, Client -from django.contrib.auth.models import User, Permission -from django.utils import timezone -from django.contrib.sessions.middleware import SessionMiddleware +from django.contrib.auth.models import User +from django.contrib.messages import get_messages from django.contrib.messages.middleware import MessageMiddleware from django.contrib.messages.storage.fallback import FallbackStorage -from django.contrib.messages import get_messages +from django.contrib.sessions.middleware import SessionMiddleware +from django.http import HttpResponseRedirect +from django.test import RequestFactory +from django.test import TestCase +from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from django.urls import reverse, reverse_lazy -from django.http import HttpResponseRedirect, HttpResponse -from unittest.mock import Mock, patch -from django.test.utils import override_settings -from django.urls import path, include -from django.contrib import admin as django_admin -from django.conf import settings - +from members.models import DIVERSE +from members.models import Group +from members.models import Member from members.tests.utils import create_custom_user -from members.models import Member, MALE, DIVERSE, Group -from ..models import Message, Attachment, EmailAddress -from ..admin import MessageAdmin, submit_message -from ..mailutils import SENT, NOT_SENT, PARTLY_SENT + +from ..admin import MessageAdmin +from ..admin import submit_message +from ..mailutils import NOT_SENT +from ..mailutils import PARTLY_SENT +from ..mailutils import SENT +from ..models import EmailAddress +from ..models import Message class AdminTestCase(TestCase): @@ -32,11 +34,9 @@ class AdminTestCase(TestCase): self.model = model if model is not None and admin is not None: self.admin = admin(model, AdminSite()) - superuser = User.objects.create_superuser( - username='superuser', password='secret' - ) - standard = create_custom_user('standard', ['Standard'], 'Paul', 'Wulter') - trainer = create_custom_user('trainer', ['Standard', 'Trainings'], 'Lise', 'Lotte') + User.objects.create_superuser(username="superuser", password="secret") + create_custom_user("standard", ["Standard"], "Paul", "Wulter") + create_custom_user("trainer", ["Standard", "Trainings"], "Lise", "Lotte") def _add_middleware(self, request): """Add required middleware to request.""" @@ -56,53 +56,56 @@ class MessageAdminTestCase(AdminTestCase): super().setUp(Message, MessageAdmin) # Create test data - self.group = Group.objects.create(name='Test Group') - self.email_address = EmailAddress.objects.create(name='testmail') + self.group = Group.objects.create(name="Test Group") + self.email_address = EmailAddress.objects.create(name="testmail") # Create test member with internal email self.internal_member = Member.objects.create( - prename='Internal', - lastname='User', + prename="Internal", + lastname="User", birth_date=timezone.now().date(), - email=f'internal@{settings.ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER[0]}', - gender=DIVERSE + email=f"internal@{settings.ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER[0]}", + gender=DIVERSE, ) # Create test member with external email self.external_member = Member.objects.create( - prename='External', - lastname='User', + prename="External", + lastname="User", birth_date=timezone.now().date(), - email='external@example.com', - gender=DIVERSE + email="external@example.com", + gender=DIVERSE, ) # Create users for testing - self.user_with_internal_member = User.objects.create_user(username='testuser', password='secret') + self.user_with_internal_member = User.objects.create_user( + username="testuser", password="secret" + ) self.user_with_internal_member.member = self.internal_member self.user_with_internal_member.save() - self.user_with_external_member = User.objects.create_user(username='external_user', password='secret') + self.user_with_external_member = User.objects.create_user( + username="external_user", password="secret" + ) self.user_with_external_member.member = self.external_member self.user_with_external_member.save() - self.user_without_member = User.objects.create_user(username='no_member_user', password='secret') + self.user_without_member = User.objects.create_user( + username="no_member_user", password="secret" + ) # Create test message - self.message = Message.objects.create( - subject='Test Message', - content='Test content' - ) + self.message = Message.objects.create(subject="Test Message", content="Test content") self.message.to_groups.add(self.group) self.message.to_members.add(self.internal_member) def test_save_model_sets_created_by(self): """Test that save_model sets created_by when creating new message.""" - request = self.factory.post('/admin/mailer/message/add/') + request = self.factory.post("/admin/mailer/message/add/") request.user = self.user_with_internal_member # Create new message - new_message = Message(subject='New Message', content='New content') + new_message = Message(subject="New Message", content="New content") # Test save_model for new object (change=False) self.admin.save_model(request, new_message, None, change=False) @@ -111,7 +114,7 @@ class MessageAdminTestCase(AdminTestCase): def test_save_model_does_not_change_created_by_on_update(self): """Test that save_model doesn't change created_by when updating.""" - request = self.factory.post('/admin/mailer/message/1/change/') + request = self.factory.post("/admin/mailer/message/1/change/") request.user = self.user_with_internal_member # Message already has created_by set @@ -122,12 +125,12 @@ class MessageAdminTestCase(AdminTestCase): self.assertEqual(self.message.created_by, self.external_member) - @patch('mailer.models.Message.submit') + @patch("mailer.models.Message.submit") def test_submit_message_success(self, mock_submit): """Test submit_message with successful send.""" mock_submit.return_value = SENT - request = self.factory.post('/admin/mailer/message/') + request = self.factory.post("/admin/mailer/message/") request.user = self.user_with_internal_member self._add_middleware(request) @@ -140,14 +143,14 @@ class MessageAdminTestCase(AdminTestCase): # Check success message messages_list = list(get_messages(request)) self.assertEqual(len(messages_list), 1) - self.assertIn(str(_('Successfully sent message')), str(messages_list[0])) + self.assertIn(str(_("Successfully sent message")), str(messages_list[0])) - @patch('mailer.models.Message.submit') + @patch("mailer.models.Message.submit") def test_submit_message_not_sent(self, mock_submit): """Test submit_message when sending fails.""" mock_submit.return_value = NOT_SENT - request = self.factory.post('/admin/mailer/message/') + request = self.factory.post("/admin/mailer/message/") request.user = self.user_with_internal_member self._add_middleware(request) @@ -157,14 +160,14 @@ class MessageAdminTestCase(AdminTestCase): # Check error message messages_list = list(get_messages(request)) self.assertEqual(len(messages_list), 1) - self.assertIn(str(_('Failed to send message')), str(messages_list[0])) + self.assertIn(str(_("Failed to send message")), str(messages_list[0])) - @patch('mailer.models.Message.submit') + @patch("mailer.models.Message.submit") def test_submit_message_partly_sent(self, mock_submit): """Test submit_message when partially sent.""" mock_submit.return_value = PARTLY_SENT - request = self.factory.post('/admin/mailer/message/') + request = self.factory.post("/admin/mailer/message/") request.user = self.user_with_internal_member self._add_middleware(request) @@ -174,11 +177,11 @@ class MessageAdminTestCase(AdminTestCase): # Check warning message messages_list = list(get_messages(request)) self.assertEqual(len(messages_list), 1) - self.assertIn(str(_('Failed to send some messages')), str(messages_list[0])) + self.assertIn(str(_("Failed to send some messages")), str(messages_list[0])) def test_submit_message_user_has_no_member(self): """Test submit_message when user has no associated member.""" - request = self.factory.post('/admin/mailer/message/') + request = self.factory.post("/admin/mailer/message/") request.user = self.user_without_member self._add_middleware(request) @@ -188,11 +191,18 @@ class MessageAdminTestCase(AdminTestCase): # Check error message messages_list = list(get_messages(request)) self.assertEqual(len(messages_list), 1) - self.assertIn(str(_('Your account is not connected to a member. Please contact your system administrator.')), str(messages_list[0])) + self.assertIn( + str( + _( + "Your account is not connected to a member. Please contact your system administrator." + ) + ), + str(messages_list[0]), + ) def test_submit_message_user_has_external_email(self): """Test submit_message when user has external email.""" - request = self.factory.post('/admin/mailer/message/') + request = self.factory.post("/admin/mailer/message/") request.user = self.user_with_external_member self._add_middleware(request) @@ -202,12 +212,20 @@ class MessageAdminTestCase(AdminTestCase): # Check error message messages_list = list(get_messages(request)) self.assertEqual(len(messages_list), 1) - self.assertIn(str(_('Your email address is not an internal email address. Please use an email address with one of the following domains: %(domains)s.') % {'domains': ", ".join(settings.ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER)}), str(messages_list[0])) + self.assertIn( + str( + _( + "Your email address is not an internal email address. Please use an email address with one of the following domains: %(domains)s." + ) + % {"domains": ", ".join(settings.ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER)} + ), + str(messages_list[0]), + ) - @patch('mailer.admin.submit_message') + @patch("mailer.admin.submit_message") def test_send_message_action_confirmed(self, mock_submit_message): """Test send_message action when confirmed.""" - request = self.factory.post('/admin/mailer/message/', {'confirmed': 'true'}) + request = self.factory.post("/admin/mailer/message/", {"confirmed": "true"}) request.user = self.user_with_internal_member self._add_middleware(request) @@ -224,7 +242,7 @@ class MessageAdminTestCase(AdminTestCase): def test_send_message_action_not_confirmed(self): """Test send_message action when not confirmed (shows confirmation page).""" - request = self.factory.post('/admin/mailer/message/') + request = self.factory.post("/admin/mailer/message/") request.user = self.user_with_internal_member self._add_middleware(request) @@ -237,17 +255,17 @@ class MessageAdminTestCase(AdminTestCase): self.assertIsNotNone(result) self.assertEqual(result.status_code, HTTPStatus.OK) - @patch('mailer.admin.submit_message') + @patch("mailer.admin.submit_message") def test_response_change_with_send(self, mock_submit_message): """Test response_change when _send is in POST.""" - request = self.factory.post('/admin/mailer/message/1/change/', {'_send': 'Send'}) + request = self.factory.post("/admin/mailer/message/1/change/", {"_send": "Send"}) request.user = self.user_with_internal_member self._add_middleware(request) # Test response_change - with patch.object(self.admin.__class__.__bases__[2], 'response_change') as mock_super: - mock_super.return_value = HttpResponseRedirect('/admin/') - result = self.admin.response_change(request, self.message) + with patch.object(self.admin.__class__.__bases__[2], "response_change") as mock_super: + mock_super.return_value = HttpResponseRedirect("/admin/") + self.admin.response_change(request, self.message) # Verify submit_message was called mock_submit_message.assert_called_once_with(self.message, request) @@ -255,17 +273,17 @@ class MessageAdminTestCase(AdminTestCase): # Verify super method was called mock_super.assert_called_once() - @patch('mailer.admin.submit_message') + @patch("mailer.admin.submit_message") def test_response_change_without_send(self, mock_submit_message): """Test response_change when _send is not in POST.""" - request = self.factory.post('/admin/mailer/message/1/change/', {'_save': 'Save'}) + request = self.factory.post("/admin/mailer/message/1/change/", {"_save": "Save"}) request.user = self.user_with_internal_member self._add_middleware(request) # Test response_change - with patch.object(self.admin.__class__.__bases__[2], 'response_change') as mock_super: - mock_super.return_value = HttpResponseRedirect('/admin/') - result = self.admin.response_change(request, self.message) + with patch.object(self.admin.__class__.__bases__[2], "response_change") as mock_super: + mock_super.return_value = HttpResponseRedirect("/admin/") + self.admin.response_change(request, self.message) # Verify submit_message was NOT called mock_submit_message.assert_not_called() @@ -273,17 +291,17 @@ class MessageAdminTestCase(AdminTestCase): # Verify super method was called mock_super.assert_called_once() - @patch('mailer.admin.submit_message') + @patch("mailer.admin.submit_message") def test_response_add_with_send(self, mock_submit_message): """Test response_add when _send is in POST.""" - request = self.factory.post('/admin/mailer/message/add/', {'_send': 'Send'}) + request = self.factory.post("/admin/mailer/message/add/", {"_send": "Send"}) request.user = self.user_with_internal_member self._add_middleware(request) # Test response_add - with patch.object(self.admin.__class__.__bases__[2], 'response_add') as mock_super: - mock_super.return_value = HttpResponseRedirect('/admin/') - result = self.admin.response_add(request, self.message) + with patch.object(self.admin.__class__.__bases__[2], "response_add") as mock_super: + mock_super.return_value = HttpResponseRedirect("/admin/") + self.admin.response_add(request, self.message) # Verify submit_message was called mock_submit_message.assert_called_once_with(self.message, request) @@ -295,7 +313,7 @@ class MessageAdminTestCase(AdminTestCase): """Test get_form when members parameter is provided.""" # Create request with members parameter members_ids = [self.internal_member.pk, self.external_member.pk] - request = self.factory.get(f'/admin/mailer/message/add/?members={json.dumps(members_ids)}') + request = self.factory.get(f"/admin/mailer/message/add/?members={json.dumps(members_ids)}") request.user = self.user_with_internal_member # Test get_form @@ -303,7 +321,9 @@ class MessageAdminTestCase(AdminTestCase): form = form_class() # Verify initial members are set - self.assertEqual(list(form.fields['to_members'].initial), [self.internal_member, self.external_member]) + self.assertEqual( + list(form.fields["to_members"].initial), [self.internal_member, self.external_member] + ) def test_get_form_with_invalid_members_param(self): """Test get_form when members parameter is not a list.""" @@ -320,7 +340,7 @@ class MessageAdminTestCase(AdminTestCase): def test_get_form_without_members_param(self): """Test get_form when no members parameter is provided.""" # Create request without members parameter - request = self.factory.get('/admin/mailer/message/add/') + request = self.factory.get("/admin/mailer/message/add/") request.user = self.user_with_internal_member # Test get_form diff --git a/jdav_web/mailer/tests/mailutils.py b/jdav_web/mailer/tests/mailutils.py index 00eed05..f626400 100644 --- a/jdav_web/mailer/tests/mailutils.py +++ b/jdav_web/mailer/tests/mailutils.py @@ -1,6 +1,10 @@ -from django.test import TestCase, override_settings -from unittest.mock import patch, Mock -from mailer.mailutils import send, SENT, NOT_SENT +from unittest.mock import Mock +from unittest.mock import patch + +from django.test import TestCase +from mailer.mailutils import NOT_SENT +from mailer.mailutils import send +from mailer.mailutils import SENT class MailUtilsTest(TestCase): @@ -11,24 +15,36 @@ class MailUtilsTest(TestCase): self.recipient = "recipient@example.com" def test_send_with_reply_to(self): - with patch('mailer.mailutils.mail.get_connection') as mock_connection: + with patch("mailer.mailutils.mail.get_connection") as mock_connection: mock_conn = Mock() mock_connection.return_value = mock_conn - result = send(self.subject, self.content, self.sender, self.recipient, reply_to=["reply@example.com"]) + result = send( + self.subject, + self.content, + self.sender, + self.recipient, + reply_to=["reply@example.com"], + ) self.assertEqual(result, SENT) def test_send_with_message_id(self): - with patch('mailer.mailutils.mail.get_connection') as mock_connection: + with patch("mailer.mailutils.mail.get_connection") as mock_connection: mock_conn = Mock() mock_connection.return_value = mock_conn - result = send(self.subject, self.content, self.sender, self.recipient, message_id="") + result = send( + self.subject, + self.content, + self.sender, + self.recipient, + message_id="", + ) self.assertEqual(result, SENT) def test_send_exception_handling(self): - with patch('mailer.mailutils.mail.get_connection') as mock_connection: + with patch("mailer.mailutils.mail.get_connection") as mock_connection: mock_conn = Mock() mock_conn.send_messages.side_effect = Exception("Test exception") mock_connection.return_value = mock_conn - with patch('builtins.print'): + with patch("builtins.print"): result = send(self.subject, self.content, self.sender, self.recipient) - self.assertEqual(result, NOT_SENT) \ No newline at end of file + self.assertEqual(result, NOT_SENT) diff --git a/jdav_web/mailer/tests/models.py b/jdav_web/mailer/tests/models.py index 51e183f..2b54db7 100644 --- a/jdav_web/mailer/tests/models.py +++ b/jdav_web/mailer/tests/models.py @@ -1,13 +1,23 @@ -from unittest import skip, mock -from django.test import TestCase +from unittest import mock + from django.conf import settings +from django.core.files.uploadedfile import SimpleUploadedFile from django.utils import timezone -from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ -from django.core.files.uploadedfile import SimpleUploadedFile -from members.models import Member, Group, DIVERSE, Freizeit, MemberNoteList, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE -from mailer.models import EmailAddress, EmailAddressForm, Message, MessageForm, Attachment -from mailer.mailutils import SENT, NOT_SENT, PARTLY_SENT +from mailer.mailutils import NOT_SENT +from mailer.mailutils import PARTLY_SENT +from mailer.mailutils import SENT +from mailer.models import Attachment +from mailer.models import EmailAddressForm +from mailer.models import Message +from mailer.models import MessageForm +from members.models import DIVERSE +from members.models import Freizeit +from members.models import GEMEINSCHAFTS_TOUR +from members.models import Member +from members.models import MemberNoteList +from members.models import MUSKELKRAFT_ANREISE + from .utils import BasicMailerTestCase @@ -19,13 +29,13 @@ class EmailAddressTestCase(BasicMailerTestCase): self.assertEqual(self.em.email, str(self.em)) def test_forwards(self): - self.assertEqual(self.em.forwards, {'fritz@foo.com', 'paul@foo.com'}) + self.assertEqual(self.em.forwards, {"fritz@foo.com", "paul@foo.com"}) class EmailAddressFormTestCase(BasicMailerTestCase): def test_clean(self): # instantiate form with only name field set - form = EmailAddressForm(data={'name': 'bar'}) + form = EmailAddressForm(data={"name": "bar"}) # validate the form - this should fail due to missing required recipients self.assertFalse(form.is_valid()) @@ -33,7 +43,7 @@ class EmailAddressFormTestCase(BasicMailerTestCase): class MessageFormTestCase(BasicMailerTestCase): def test_clean(self): # instantiate form with only subject and content fields set - form = MessageForm(data={'subject': 'Test Subject', 'content': 'Test content'}) + form = MessageForm(data={"subject": "Test Subject", "content": "Test content"}) # validate the form - this should fail due to missing required recipients self.assertFalse(form.is_valid()) @@ -42,19 +52,16 @@ class MessageTestCase(BasicMailerTestCase): def setUp(self): super().setUp() self.message = Message.objects.create( - subject='Test Message', - content='This is a test message' + subject="Test Message", content="This is a test message" ) self.freizeit = Freizeit.objects.create( - name='Test Freizeit', + name="Test Freizeit", kilometers_traveled=120, tour_type=GEMEINSCHAFTS_TOUR, tour_approach=MUSKELKRAFT_ANREISE, - difficulty=1 - ) - self.notelist = MemberNoteList.objects.create( - title='Test Note List' + difficulty=1, ) + self.notelist = MemberNoteList.objects.create(title="Test Note List") # Set up message with multiple recipient types self.message.to_groups.add(self.mygroup) @@ -65,39 +72,39 @@ class MessageTestCase(BasicMailerTestCase): # Create a sender member for submit tests self.sender = Member.objects.create( - prename='Sender', - lastname='Test', + prename="Sender", + lastname="Test", birth_date=timezone.now().date(), - email='sender@test.com', - gender=DIVERSE + email="sender@test.com", + gender=DIVERSE, ) def test_str(self): - self.assertEqual(str(self.message), 'Test Message') + self.assertEqual(str(self.message), "Test Message") def test_get_recipients(self): recipients = self.message.get_recipients() - self.assertIn('My Group', recipients) - self.assertIn('Test Freizeit', recipients) - self.assertIn('Test Note List', recipients) - self.assertIn('Fritz Wulter', recipients) + self.assertIn("My Group", recipients) + self.assertIn("Test Freizeit", recipients) + self.assertIn("Test Note List", recipients) + self.assertIn("Fritz Wulter", recipients) def test_get_recipients_with_many_members(self): # Add additional members to test the "Some other members" case for i in range(3): member = Member.objects.create( - prename=f'Member{i}', - lastname='Test', + prename=f"Member{i}", + lastname="Test", birth_date=timezone.now().date(), - email=f'member{i}@test.com', - gender=DIVERSE + email=f"member{i}@test.com", + gender=DIVERSE, ) self.message.to_members.add(member) recipients = self.message.get_recipients() - self.assertIn(_('Some other members'), recipients) + self.assertIn(_("Some other members"), recipients) - @mock.patch('mailer.models.send') + @mock.patch("mailer.models.send") def test_submit_successful(self, mock_send): # Mock successful email sending mock_send.return_value = SENT @@ -113,7 +120,7 @@ class MessageTestCase(BasicMailerTestCase): # Verify send was called self.assertTrue(mock_send.called) - @mock.patch('mailer.models.send') + @mock.patch("mailer.models.send") def test_submit_failed(self, mock_send): # Mock failed email sending mock_send.return_value = NOT_SENT @@ -127,7 +134,7 @@ class MessageTestCase(BasicMailerTestCase): # Note: The submit method always returns SENT when an exception occurs self.assertEqual(result, SENT) - @mock.patch('mailer.models.send') + @mock.patch("mailer.models.send") def test_submit_without_sender(self, mock_send): # Mock successful email sending mock_send.return_value = SENT @@ -140,26 +147,25 @@ class MessageTestCase(BasicMailerTestCase): self.assertTrue(self.message.sent) self.assertEqual(result, SENT) - @mock.patch('mailer.models.send') + @mock.patch("mailer.models.send") def test_submit_subject_cleaning(self, mock_send): # Mock successful email sending mock_send.return_value = SENT # Create message with underscores in subject message_with_underscores = Message.objects.create( - subject='Test_Message_With_Underscores', - content='Test content' + subject="Test_Message_With_Underscores", content="Test content" ) message_with_underscores.to_members.add(self.fritz) # Test submit method - result = message_with_underscores.submit() + message_with_underscores.submit() # Verify underscores were removed from subject message_with_underscores.refresh_from_db() - self.assertEqual(message_with_underscores.subject, 'Test Message With Underscores') + self.assertEqual(message_with_underscores.subject, "Test Message With Underscores") - @mock.patch('mailer.models.send') + @mock.patch("mailer.models.send") def test_submit_exception_handling(self, mock_send): # Mock an exception during email sending mock_send.side_effect = Exception("Email sending failed") @@ -173,8 +179,8 @@ class MessageTestCase(BasicMailerTestCase): # When exception occurs, it should return NOT_SENT self.assertEqual(result, NOT_SENT) - @mock.patch('mailer.models.send') - @mock.patch('django.conf.settings.SEND_FROM_ASSOCIATION_EMAIL', False) + @mock.patch("mailer.models.send") + @mock.patch("django.conf.settings.SEND_FROM_ASSOCIATION_EMAIL", False) def test_submit_with_sender_no_association_email(self, mock_send): # Mock successful email sending mock_send.return_value = PARTLY_SENT @@ -187,23 +193,23 @@ class MessageTestCase(BasicMailerTestCase): self.assertTrue(self.message.sent) self.assertEqual(result, SENT) - @mock.patch('mailer.models.send') - @mock.patch('django.conf.settings.SEND_FROM_ASSOCIATION_EMAIL', False) + @mock.patch("mailer.models.send") + @mock.patch("django.conf.settings.SEND_FROM_ASSOCIATION_EMAIL", False) def test_submit_with_reply_to_logic(self, mock_send): # Mock successful email sending mock_send.return_value = SENT # Create a sender with internal email capability sender_with_internal = Member.objects.create( - prename='Internal', - lastname='Sender', + prename="Internal", + lastname="Sender", birth_date=timezone.now().date(), - email='internal@test.com', - gender=DIVERSE + email="internal@test.com", + gender=DIVERSE, ) # Mock has_internal_email to return True - with mock.patch.object(sender_with_internal, 'has_internal_email', return_value=True): + with mock.patch.object(sender_with_internal, "has_internal_email", return_value=True): # Test submit method result = self.message.submit(sender=sender_with_internal) @@ -212,14 +218,16 @@ class MessageTestCase(BasicMailerTestCase): self.assertTrue(self.message.sent) self.assertEqual(result, SENT) - @mock.patch('mailer.models.send') - @mock.patch('os.remove') + @mock.patch("mailer.models.send") + @mock.patch("os.remove") def test_submit_with_attachments(self, mock_os_remove, mock_send): # Mock successful email sending mock_send.return_value = SENT # Create an attachment with a file - test_file = SimpleUploadedFile("test_file.pdf", b"file_content", content_type="application/pdf") + test_file = SimpleUploadedFile( + "test_file.pdf", b"file_content", content_type="application/pdf" + ) attachment = Attachment.objects.create(msg=self.message, f=test_file) # Test submit method @@ -236,14 +244,14 @@ class MessageTestCase(BasicMailerTestCase): with self.assertRaises(Attachment.DoesNotExist): attachment.refresh_from_db() - @mock.patch('mailer.models.send') + @mock.patch("mailer.models.send") def test_submit_with_association_email_enabled(self, mock_send): """Test submit method when SEND_FROM_ASSOCIATION_EMAIL is True and sender has association_email""" mock_send.return_value = SENT # Mock settings to enable association email sending - with mock.patch.object(settings, 'SEND_FROM_ASSOCIATION_EMAIL', True): - result = self.message.submit(sender=self.sender) + with mock.patch.object(settings, "SEND_FROM_ASSOCIATION_EMAIL", True): + self.message.submit(sender=self.sender) # Check that send was called with sender's association email self.assertTrue(mock_send.called) @@ -256,16 +264,13 @@ class MessageTestCase(BasicMailerTestCase): class AttachmentTestCase(BasicMailerTestCase): def setUp(self): super().setUp() - self.message = Message.objects.create( - subject='Test Message', - content='Test content' - ) + self.message = Message.objects.create(subject="Test Message", content="Test content") self.attachment = Attachment.objects.create(msg=self.message) def test_str_with_file(self): # Simulate a file name - self.attachment.f.name = 'attachments/test_document.pdf' - self.assertEqual(str(self.attachment), 'test_document.pdf') + self.attachment.f.name = "attachments/test_document.pdf" + self.assertEqual(str(self.attachment), "test_document.pdf") def test_str_without_file(self): - self.assertEqual(str(self.attachment), _('Empty')) + self.assertEqual(str(self.attachment), _("Empty")) diff --git a/jdav_web/mailer/tests/rules.py b/jdav_web/mailer/tests/rules.py index 74eb958..f5dc99d 100644 --- a/jdav_web/mailer/tests/rules.py +++ b/jdav_web/mailer/tests/rules.py @@ -1,23 +1,26 @@ -from django.test import TestCase from django.conf import settings from django.contrib.auth.models import User -from mailer.rules import is_creator +from django.test import TestCase from mailer.models import Message -from members.models import Member, MALE +from mailer.rules import is_creator +from members.models import MALE +from members.models import Member class MailerRulesTestCase(TestCase): def setUp(self): self.user1 = User.objects.create_user(username="alice", password="test123") self.member1 = Member.objects.create( - prename="Alice", lastname="Smith", birth_date="1990-01-01", - email=settings.TEST_MAIL, gender=MALE, user=self.user1 + prename="Alice", + lastname="Smith", + birth_date="1990-01-01", + email=settings.TEST_MAIL, + gender=MALE, + user=self.user1, ) self.message = Message.objects.create( - subject="Test Message", - content="Test content", - created_by=self.member1 + subject="Test Message", content="Test content", created_by=self.member1 ) def test_is_creator_returns_true_when_user_created_message(self): diff --git a/jdav_web/mailer/tests/utils.py b/jdav_web/mailer/tests/utils.py index 3ad3e50..59d21eb 100644 --- a/jdav_web/mailer/tests/utils.py +++ b/jdav_web/mailer/tests/utils.py @@ -1,27 +1,33 @@ -from unittest import skip, mock from django.test import TestCase -from django.conf import settings from django.utils import timezone -from django.core.exceptions import ValidationError -from django.utils.translation import gettext as _ -from django.core.files.uploadedfile import SimpleUploadedFile -from members.models import Member, Group, DIVERSE, Freizeit, MemberNoteList, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE -from mailer.models import EmailAddress, EmailAddressForm, Message, MessageForm, Attachment -from mailer.mailutils import SENT, NOT_SENT, PARTLY_SENT +from mailer.models import EmailAddress +from members.models import DIVERSE +from members.models import Group +from members.models import Member class BasicMailerTestCase(TestCase): def setUp(self): self.mygroup = Group.objects.create(name="My Group") - self.fritz = Member.objects.create(prename="Fritz", lastname="Wulter", birth_date=timezone.now().date(), - email='fritz@foo.com', gender=DIVERSE) + self.fritz = Member.objects.create( + prename="Fritz", + lastname="Wulter", + birth_date=timezone.now().date(), + email="fritz@foo.com", + gender=DIVERSE, + ) self.fritz.group.add(self.mygroup) self.fritz.save() self.fritz.generate_key() - self.paul = Member.objects.create(prename="Paul", lastname="Wulter", birth_date=timezone.now().date(), - email='paul@foo.com', gender=DIVERSE) + self.paul = Member.objects.create( + prename="Paul", + lastname="Wulter", + birth_date=timezone.now().date(), + email="paul@foo.com", + gender=DIVERSE, + ) - self.em = EmailAddress.objects.create(name='foobar') + self.em = EmailAddress.objects.create(name="foobar") self.em.to_groups.add(self.mygroup) self.em.to_members.add(self.paul) diff --git a/jdav_web/mailer/tests/views.py b/jdav_web/mailer/tests/views.py index fc00b54..e62d197 100644 --- a/jdav_web/mailer/tests/views.py +++ b/jdav_web/mailer/tests/views.py @@ -1,65 +1,59 @@ -from unittest import skip, mock from http import HTTPStatus + from django.urls import reverse -from django.test import TestCase -from django.conf import settings from django.utils import timezone -from django.core.exceptions import ValidationError from django.utils.translation import gettext as _ -from django.core.files.uploadedfile import SimpleUploadedFile -from members.models import Member, Group, DIVERSE, Freizeit, MemberNoteList, GEMEINSCHAFTS_TOUR, MUSKELKRAFT_ANREISE -from mailer.models import EmailAddress, EmailAddressForm, Message, MessageForm, Attachment -from mailer.mailutils import SENT, NOT_SENT, PARTLY_SENT + from .utils import BasicMailerTestCase class IndexTestCase(BasicMailerTestCase): def test_index(self): - url = reverse('mailer:index') + url = reverse("mailer:index") response = self.client.get(url) self.assertEqual(response.status_code, HTTPStatus.FOUND) class UnsubscribeTestCase(BasicMailerTestCase): def test_unsubscribe(self): - url = reverse('mailer:unsubscribe') + url = reverse("mailer:unsubscribe") response = self.client.get(url) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, _("Here you can unsubscribe from the newsletter")) def test_unsubscribe_key_invalid(self): - url = reverse('mailer:unsubscribe') + url = reverse("mailer:unsubscribe") # invalid key - response = self.client.get(url, data={'key': 'invalid'}) + response = self.client.get(url, data={"key": "invalid"}) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, _("Can't verify this link. Try again!")) # expired key self.fritz.unsubscribe_expire = timezone.now() self.fritz.save() - response = self.client.get(url, data={'key': self.fritz.unsubscribe_key}) + response = self.client.get(url, data={"key": self.fritz.unsubscribe_key}) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, _("Can't verify this link. Try again!")) def test_unsubscribe_key(self): - url = reverse('mailer:unsubscribe') - response = self.client.get(url, data={'key': self.fritz.unsubscribe_key}) + url = reverse("mailer:unsubscribe") + response = self.client.get(url, data={"key": self.fritz.unsubscribe_key}) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, _("Successfully unsubscribed from the newsletter for ")) def test_unsubscribe_post_incomplete(self): - url = reverse('mailer:unsubscribe') - response = self.client.post(url, data={'post': True}) + url = reverse("mailer:unsubscribe") + response = self.client.post(url, data={"post": True}) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, _("Please fill in every field")) - response = self.client.post(url, data={'post': True, 'email': 'foobar@notexisting.com'}) + response = self.client.post(url, data={"post": True, "email": "foobar@notexisting.com"}) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, _("Please fill in every field")) def test_unsubscribe_post(self): - url = reverse('mailer:unsubscribe') - response = self.client.post(url, data={'post': True, 'email': self.fritz.email}) + url = reverse("mailer:unsubscribe") + response = self.client.post(url, data={"post": True, "email": self.fritz.email}) self.assertEqual(response.status_code, HTTPStatus.OK) self.assertContains(response, _("Sent confirmation mail to")) diff --git a/jdav_web/material/admin.py b/jdav_web/material/admin.py index 803ec21..6ea0428 100644 --- a/jdav_web/material/admin.py +++ b/jdav_web/material/admin.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ from .models import MaterialCategory from .models import MaterialPart from .models import Ownership + # from easy_select2 import apply_select2 diff --git a/jdav_web/material/migrations/0001_initial_squashed_0002_auto_20171011_2045.py b/jdav_web/material/migrations/0001_initial_squashed_0002_auto_20171011_2045.py index eb18291..0c708de 100644 --- a/jdav_web/material/migrations/0001_initial_squashed_0002_auto_20171011_2045.py +++ b/jdav_web/material/migrations/0001_initial_squashed_0002_auto_20171011_2045.py @@ -1,61 +1,98 @@ # Generated by Django 4.0.1 on 2023-03-29 20:39 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - - replaces = [('material', '0001_initial'), ('material', '0002_auto_20171011_2045')] + replaces = [("material", "0001_initial"), ("material", "0002_auto_20171011_2045")] dependencies = [ - ('members', '0001_initial'), + ("members", "0001_initial"), ] operations = [ migrations.CreateModel( - name='MaterialPart', + name="MaterialPart", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=30, verbose_name='name')), - ('description', models.CharField(default='', max_length=140, verbose_name='description')), - ('quantity', models.IntegerField(default=0, verbose_name='quantity')), - ('buy_date', models.DateField(verbose_name='purchase date')), - ('lifetime', models.DecimalField(decimal_places=0, max_digits=3, verbose_name='lifetime (years)')), - ('photo', models.ImageField(blank=True, upload_to='images', verbose_name='photo')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=30, verbose_name="name")), + ( + "description", + models.CharField(default="", max_length=140, verbose_name="description"), + ), + ("quantity", models.IntegerField(default=0, verbose_name="quantity")), + ("buy_date", models.DateField(verbose_name="purchase date")), + ( + "lifetime", + models.DecimalField( + decimal_places=0, max_digits=3, verbose_name="lifetime (years)" + ), + ), + ("photo", models.ImageField(blank=True, upload_to="images", verbose_name="photo")), ], options={ - 'verbose_name': 'material part', - 'verbose_name_plural': 'material parts', + "verbose_name": "material part", + "verbose_name_plural": "material parts", }, ), migrations.CreateModel( - name='Ownership', + name="Ownership", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('count', models.IntegerField(default=1, verbose_name='count')), - ('material', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='material.materialpart')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='members.member', verbose_name='owner')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("count", models.IntegerField(default=1, verbose_name="count")), + ( + "material", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="material.materialpart" + ), + ), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="members.member", + verbose_name="owner", + ), + ), ], options={ - 'verbose_name': 'ownership', - 'verbose_name_plural': 'ownerships', + "verbose_name": "ownership", + "verbose_name_plural": "ownerships", }, ), migrations.CreateModel( - name='MaterialCategory', + name="MaterialCategory", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=40, verbose_name='Name')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=40, verbose_name="Name")), ], options={ - 'verbose_name': 'Material category', - 'verbose_name_plural': 'Material categories', + "verbose_name": "Material category", + "verbose_name_plural": "Material categories", }, ), migrations.AddField( - model_name='materialpart', - name='material_cat', - field=models.ManyToManyField(default=None, to='material.MaterialCategory', verbose_name='Material category'), + model_name="materialpart", + name="material_cat", + field=models.ManyToManyField( + default=None, to="material.MaterialCategory", verbose_name="Material category" + ), ), ] diff --git a/jdav_web/startpage/migrations/0001_initial.py b/jdav_web/startpage/migrations/0001_initial.py index a867c0f..1f87ca9 100644 --- a/jdav_web/startpage/migrations/0001_initial.py +++ b/jdav_web/startpage/migrations/0001_initial.py @@ -1,77 +1,152 @@ # Generated by Django 4.0.1 on 2023-04-09 12:11 -from django.db import migrations, models import django.db.models.deletion import django.utils.timezone import markdownx.models import utils +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - initial = True dependencies = [ - ('members', '0012_member_image_group_description'), + ("members", "0012_member_image_group_description"), ] operations = [ migrations.CreateModel( - name='Section', + name="Section", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=50, verbose_name='Title')), - ('urlname', models.CharField(max_length=25, verbose_name='URL')), - ('website_text', markdownx.models.MarkdownxField(blank=True, default='', verbose_name='website text')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("title", models.CharField(max_length=50, verbose_name="Title")), + ("urlname", models.CharField(max_length=25, verbose_name="URL")), + ( + "website_text", + markdownx.models.MarkdownxField( + blank=True, default="", verbose_name="website text" + ), + ), ], options={ - 'verbose_name': 'Section', - 'verbose_name_plural': 'Sections', - 'unique_together': {('urlname',)}, + "verbose_name": "Section", + "verbose_name_plural": "Sections", + "unique_together": {("urlname",)}, }, ), migrations.CreateModel( - name='Post', + name="Post", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(default='', max_length=50, verbose_name='Title')), - ('urlname', models.CharField(default='', max_length=50, verbose_name='URL')), - ('date', models.DateField(blank=True, default=django.utils.timezone.localdate, null=True, verbose_name='Date')), - ('website_text', markdownx.models.MarkdownxField(blank=True, default='', verbose_name='website text')), - ('detailed', models.BooleanField(default=False, verbose_name='detailed')), - ('groups', models.ManyToManyField(blank=True, to='members.Group', verbose_name='Groups')), - ('section', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='startpage.section', verbose_name='section')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("title", models.CharField(default="", max_length=50, verbose_name="Title")), + ("urlname", models.CharField(default="", max_length=50, verbose_name="URL")), + ( + "date", + models.DateField( + blank=True, + default=django.utils.timezone.localdate, + null=True, + verbose_name="Date", + ), + ), + ( + "website_text", + markdownx.models.MarkdownxField( + blank=True, default="", verbose_name="website text" + ), + ), + ("detailed", models.BooleanField(default=False, verbose_name="detailed")), + ( + "groups", + models.ManyToManyField(blank=True, to="members.Group", verbose_name="Groups"), + ), + ( + "section", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="startpage.section", + verbose_name="section", + ), + ), ], options={ - 'verbose_name': 'Post', - 'verbose_name_plural': 'Posts', - 'unique_together': {('section', 'urlname')}, + "verbose_name": "Post", + "verbose_name_plural": "Posts", + "unique_together": {("section", "urlname")}, }, ), migrations.CreateModel( - name='MemberOnPost', + name="MemberOnPost", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.TextField(blank=True, default='', verbose_name='Description')), - ('tag', models.CharField(blank=True, default='', max_length=20, verbose_name='Tag')), - ('members', models.ManyToManyField(blank=True, to='members.Member', verbose_name='Member')), - ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='people', to='startpage.post', verbose_name='Member')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "description", + models.TextField(blank=True, default="", verbose_name="Description"), + ), + ( + "tag", + models.CharField(blank=True, default="", max_length=20, verbose_name="Tag"), + ), + ( + "members", + models.ManyToManyField(blank=True, to="members.Member", verbose_name="Member"), + ), + ( + "post", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="people", + to="startpage.post", + verbose_name="Member", + ), + ), ], options={ - 'verbose_name': 'Person', - 'verbose_name_plural': 'Persons', + "verbose_name": "Person", + "verbose_name_plural": "Persons", }, ), migrations.CreateModel( - name='Image', + name="Image", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('f', utils.RestrictedFileField(blank=True, upload_to='images', verbose_name='file')), - ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='startpage.post')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "f", + utils.RestrictedFileField(blank=True, upload_to="images", verbose_name="file"), + ), + ( + "post", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="startpage.post" + ), + ), ], options={ - 'verbose_name': 'image', - 'verbose_name_plural': 'images', + "verbose_name": "image", + "verbose_name_plural": "images", }, ), ] diff --git a/jdav_web/startpage/migrations/0002_section_show_in_navigation.py b/jdav_web/startpage/migrations/0002_section_show_in_navigation.py index 246f3d4..68915e5 100644 --- a/jdav_web/startpage/migrations/0002_section_show_in_navigation.py +++ b/jdav_web/startpage/migrations/0002_section_show_in_navigation.py @@ -1,18 +1,18 @@ # Generated by Django 4.0.1 on 2024-11-23 17:00 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('startpage', '0001_initial'), + ("startpage", "0001_initial"), ] operations = [ migrations.AddField( - model_name='section', - name='show_in_navigation', - field=models.BooleanField(default=True, verbose_name='Show in navigation'), + model_name="section", + name="show_in_navigation", + field=models.BooleanField(default=True, verbose_name="Show in navigation"), ), ] diff --git a/jdav_web/startpage/migrations/0003_alter_post_section.py b/jdav_web/startpage/migrations/0003_alter_post_section.py index 730e375..5b32d3b 100644 --- a/jdav_web/startpage/migrations/0003_alter_post_section.py +++ b/jdav_web/startpage/migrations/0003_alter_post_section.py @@ -1,19 +1,24 @@ # Generated by Django 4.0.1 on 2024-11-23 17:41 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('startpage', '0002_section_show_in_navigation'), + ("startpage", "0002_section_show_in_navigation"), ] operations = [ migrations.AlterField( - model_name='post', - name='section', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='startpage.section', verbose_name='section'), + model_name="post", + name="section", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="startpage.section", + verbose_name="section", + ), ), ] diff --git a/jdav_web/startpage/migrations/0004_internal_startpage_links.py b/jdav_web/startpage/migrations/0004_internal_startpage_links.py index 6d3850a..00d7205 100644 --- a/jdav_web/startpage/migrations/0004_internal_startpage_links.py +++ b/jdav_web/startpage/migrations/0004_internal_startpage_links.py @@ -1,29 +1,45 @@ # Generated by Django 4.0.1 on 2025-02-25 12:20 -from django.db import migrations, models import utils +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('startpage', '0003_alter_post_section'), + ("startpage", "0003_alter_post_section"), ] operations = [ migrations.CreateModel( - name='Link', + name="Link", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('icon', utils.RestrictedFileField(blank=True, upload_to='icons', verbose_name='Link Icon')), - ('title', models.CharField(blank=True, default='', max_length=100, verbose_name='Title')), - ('description', models.TextField(blank=True, default='', verbose_name='Description')), - ('url', models.URLField(max_length=250)), - ('visible', models.BooleanField(default=True, verbose_name='Visible')), + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "icon", + utils.RestrictedFileField( + blank=True, upload_to="icons", verbose_name="Link Icon" + ), + ), + ( + "title", + models.CharField(blank=True, default="", max_length=100, verbose_name="Title"), + ), + ( + "description", + models.TextField(blank=True, default="", verbose_name="Description"), + ), + ("url", models.URLField(max_length=250)), + ("visible", models.BooleanField(default=True, verbose_name="Visible")), ], options={ - 'verbose_name': 'Link', - 'verbose_name_plural': 'Links', + "verbose_name": "Link", + "verbose_name_plural": "Links", }, ), ] diff --git a/jdav_web/startpage/templates/startpage/contact.html b/jdav_web/startpage/templates/startpage/contact.html index 096c902..8743f1e 100644 --- a/jdav_web/startpage/templates/startpage/contact.html +++ b/jdav_web/startpage/templates/startpage/contact.html @@ -17,4 +17,4 @@ weiterweißt oder sonst der Schuh drückt, schreibe eine E-Mail an eine der folg
- \ No newline at end of file + diff --git a/jdav_web/startpage/templatetags/markdown_extras.py b/jdav_web/startpage/templatetags/markdown_extras.py index 00ae99e..7bd1b25 100644 --- a/jdav_web/startpage/templatetags/markdown_extras.py +++ b/jdav_web/startpage/templatetags/markdown_extras.py @@ -1,10 +1,9 @@ -from django import template -from django.urls import reverse -from django.utils.safestring import mark_safe -from django.template import Template, Variable, TemplateSyntaxError - import re +from django import template +from django.template import Template +from django.template import Variable + register = template.Library() @@ -13,6 +12,7 @@ class RenderAsTemplateNode(template.Node): Renders passed content as template. This is probably dangerous and should only be exposed to admins! """ + def __init__(self, item_to_be_rendered, var_name): self.item_to_be_rendered = Variable(item_to_be_rendered) self.var_name = var_name @@ -23,7 +23,7 @@ class RenderAsTemplateNode(template.Node): context[self.var_name] = Template(actual_item).render(context) return "" except template.VariableDoesNotExist: - return '' + return "" def render_as_template(parser, token): @@ -32,17 +32,13 @@ def render_as_template(parser, token): # Splitting by None == splitting by spaces. tag_name, arg = token.contents.split(None, 1) except ValueError: - raise template.TemplateSyntaxError( - "%r tag requires arguments" % token.contents.split()[0] - ) + raise template.TemplateSyntaxError("%r tag requires arguments" % token.contents.split()[0]) m = re.search(r"(.*?) as (\w+)", arg) if not m: raise template.TemplateSyntaxError("%r tag had invalid arguments" % tag_name) format_string, var_name = m.groups() if not (format_string[0] == format_string[-1] and format_string[0] in ('"', "'")): - raise template.TemplateSyntaxError( - "%r tag's argument should be in quotes" % tag_name - ) + raise template.TemplateSyntaxError("%r tag's argument should be in quotes" % tag_name) return RenderAsTemplateNode(format_string[1:-1], var_name)