chore(*): reformat using ruff (#22)

This is the remainder except for `jdav_web/members/`.
mk-personal-profile
Christian Merten 2 weeks ago committed by GitHub
parent aaa1324da9
commit 78c117f300
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

2
Jenkinsfile vendored

@ -1,5 +1,5 @@
node {
checkout scm
checkout scm
}
pipeline {

@ -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.")
)

@ -3,6 +3,7 @@ from django.conf import settings
register = template.Library()
# settings value
@register.simple_tag
def settings_value(name):

@ -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",),
),
]

@ -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",
},
),
]

@ -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",
},
),
]

@ -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"
),
),
]

@ -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"
),
),
]

@ -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",
),
),
]

@ -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",
),
),
]

@ -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",
),
),
]

@ -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",
),
),
]

@ -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),
]

@ -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",
),
]

@ -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",),
),
]

@ -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" %}';
});
});
</script>

@ -124,9 +124,9 @@
{% if statement.ljp_to %}
<h3>{% trans "LJP contributions" %}</h3>
<p>
{% 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 %}
<table>
<th>

@ -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 %}

@ -1,4 +1,6 @@
# ruff: noqa F403
from .admin import *
from .migrations import *
from .models import *
from .rules import *
from .migrations import *

@ -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))

@ -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",
)

File diff suppressed because it is too large Load Diff

@ -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)

@ -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()),
],
),
]

@ -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",
},
),
]

@ -1,2 +1,4 @@
# ruff: noqa F403
from .oauth import *
from .views import *
from .oauth import *

@ -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"""

@ -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.'))
self.assertContains(
response, _("Something went wrong. The registration key is invalid or has expired.")
)

@ -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",
},
),
]

@ -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

@ -4,7 +4,6 @@ from django.conf import settings
from django.core import mail
from django.core.mail import EmailMessage
logger = logging.getLogger(__name__)

@ -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],
)

@ -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))

@ -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",
),
),
]

@ -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",
),
),
]

@ -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",
},
),
]

@ -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"),
),
]

@ -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",
),
),
]

@ -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",
),
),
]

@ -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",
),
),
]

@ -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",
),
),
]

@ -20,7 +20,6 @@ from .mailutils import send
from .mailutils import SENT
from .rules import is_creator
logger = logging.getLogger(__name__)

@ -1,4 +1,4 @@
<link rel="stylesheet" href="http://code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css">
<link rel="stylesheet" href="http://code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css">
<script src="http://code.jquery.com/jquery-1.9.1.js"></script>
<script src="http://code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
{% load i18n %}

@ -20,7 +20,7 @@
{% trans "Email address" %}
<input type="email" name="email">
</p>
<p>
<input type="submit" value="{% trans "Unsubscribe" %}">
</p>

@ -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 *

@ -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

@ -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="<test@example.com>")
result = send(
self.subject,
self.content,
self.sender,
self.recipient,
message_id="<test@example.com>",
)
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)
self.assertEqual(result, NOT_SENT)

@ -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"))

@ -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):

@ -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)

@ -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"))

@ -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

@ -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"
),
),
]

@ -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",
},
),
]

@ -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"),
),
]

@ -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",
),
),
]

@ -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",
},
),
]

@ -17,4 +17,4 @@ weiterweißt oder sonst der Schuh drückt, schreibe eine E-Mail an eine der folg
</td>
</tr>
</table>
</div>
</div>

@ -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)

Loading…
Cancel
Save