From c7d1a16cb313f9e8f9f1e55d01a9f0794f82fa2e Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:04:17 +0200 Subject: [PATCH 1/7] feat(config): implement field customization in settings.toml --- jdav_web/contrib/admin.py | 58 +++++++++++++++++++++++++++++ jdav_web/jdav_web/settings/local.py | 5 +++ jdav_web/utils.py | 26 +++++++++++++ 3 files changed, 89 insertions(+) diff --git a/jdav_web/contrib/admin.py b/jdav_web/contrib/admin.py index 4346029..fa762d7 100644 --- a/jdav_web/contrib/admin.py +++ b/jdav_web/contrib/admin.py @@ -8,8 +8,10 @@ from django.http import HttpResponse, HttpResponseRedirect from django.urls import path, reverse from django.db import models from django.contrib.admin import helpers, widgets +from django.conf import settings import rules.contrib.admin from rules.permissions import perm_exists +from utils import OrderedSet class FieldPermissionsAdminMixin: @@ -174,6 +176,62 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere # For any other type of field, just call its formfield() method. return db_field.formfield(**kwargs) + + @property + def field_key(self): + return f"{self.model._meta.app_label}_{self.model.__name__}".lower() + + def get_excluded_fields(self): + return OrderedSet(settings.CUSTOM_MODEL_FIELDS.get(self.field_key, {}).get('exclude', [])) + + def get_included_fields(self): + return OrderedSet(settings.CUSTOM_MODEL_FIELDS.get(self.field_key, {}).get('fields', [])) + + + def get_fieldsets(self, request, obj=None): + included = self.get_included_fields() + excluded = self.get_excluded_fields() + original_fieldsets = super().get_fieldsets(request, obj) + if original_fieldsets: + print(f"get_fieldsets called for {self.field_key}") + print(f"Original fieldsets: {original_fieldsets}") + new_fieldsets = [] + + for title, attrs in original_fieldsets: + fields = attrs.get("fields", []) + + # Flatten groupings like tuples if needed + filtered_fields = [ + f for f in fields + if ( + (not included or f in included) + and f not in excluded + ) + ] + + if filtered_fields: + new_fieldsets.append((title, dict(attrs, **{"fields": filtered_fields}))) + + if new_fieldsets: + print(f"Filtered fieldsets: {new_fieldsets}") + return new_fieldsets + + + def get_fields(self, request, obj=None): + fields = OrderedSet(super().get_fields(request, obj) or []) + custom_fields = self.get_included_fields() - self.get_excluded_fields() + if custom_fields: + print(f"get_fields called for {self.field_key}, fields: {fields}, custom_fields: {custom_fields}") + return list(custom_fields) + return list(fields) + + def get_exclude(self, request, obj=None): + excluded = OrderedSet(super().get_exclude(request, obj) or []) + custom_excluded = self.get_excluded_fields() - self.get_included_fields() + if custom_excluded: + print(f"get_exclude called for {self.field_key}, excluded: {excluded}, custom_excluded: {custom_excluded}") + return list(excluded | custom_excluded) + return list(excluded) class CommonAdminInlineMixin(CommonAdminMixin): diff --git a/jdav_web/jdav_web/settings/local.py b/jdav_web/jdav_web/settings/local.py index 71e260c..6da4a84 100644 --- a/jdav_web/jdav_web/settings/local.py +++ b/jdav_web/jdav_web/settings/local.py @@ -76,3 +76,8 @@ REPORTS_SECTION = get_var('startpage', 'reports_section', default='reports') # testing TEST_MAIL = get_var('testing', 'mail', default='test@localhost') + + +# excluded and included model fields in admin and admin forms +CUSTOM_MODELS = list(get_var('custom_model_fields', default={}).keys()) +CUSTOM_MODEL_FIELDS = {model.lower(): get_var('model_fields', model, default=[]) for model in CUSTOM_MODELS} \ No newline at end of file diff --git a/jdav_web/utils.py b/jdav_web/utils.py index 129e495..439b0e9 100644 --- a/jdav_web/utils.py +++ b/jdav_web/utils.py @@ -5,6 +5,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from decimal import Decimal, ROUND_HALF_DOWN import unicodedata +from collections import OrderedDict def file_size_validator(max_upload_size): @@ -88,3 +89,28 @@ def coming_midnight(): return timezone.datetime(year=base.year, month=base.month, day=base.day, hour=0, minute=0, second=0, microsecond=0, tzinfo=base.tzinfo) + + +class OrderedSet(OrderedDict): + def __init__(self, iterable=None): + super().__init__() + if iterable: + for item in iterable: + self[item] = None + + def __sub__(self, other): + if not isinstance(other, OrderedSet): + return NotImplemented + return OrderedSet(k for k in self if k not in other) + + def add(self, item): + self[item] = None + + def discard(self, item): + self.pop(item, None) + + def __contains__(self, item): + return item in self.keys() + + def __iter__(self): + return iter(self.keys()) \ No newline at end of file -- 2.38.4 From 2ef737f751a0e5f723d38a60bcd9d1b28a5e2024 Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sun, 24 Aug 2025 13:37:04 +0200 Subject: [PATCH 2/7] documentation, fixed variable name --- jdav_web/contrib/admin.py | 10 +++++++++- jdav_web/jdav_web/settings/local.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/jdav_web/contrib/admin.py b/jdav_web/contrib/admin.py index fa762d7..d37b358 100644 --- a/jdav_web/contrib/admin.py +++ b/jdav_web/contrib/admin.py @@ -179,19 +179,25 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere @property def field_key(self): + """returns the key to look if model has custom fields in settings""" return f"{self.model._meta.app_label}_{self.model.__name__}".lower() def get_excluded_fields(self): + """if model has custom excluded fields in settings, return them as a set""" return OrderedSet(settings.CUSTOM_MODEL_FIELDS.get(self.field_key, {}).get('exclude', [])) def get_included_fields(self): + """if model has an entire custom fieldset in settings, return it as a set""" return OrderedSet(settings.CUSTOM_MODEL_FIELDS.get(self.field_key, {}).get('fields', [])) def get_fieldsets(self, request, obj=None): + """filter fieldsets according to included and excluded fields in settings""" + + # get original fields and user-defined included and excluded fields + original_fieldsets = super().get_fieldsets(request, obj) included = self.get_included_fields() excluded = self.get_excluded_fields() - original_fieldsets = super().get_fieldsets(request, obj) if original_fieldsets: print(f"get_fieldsets called for {self.field_key}") print(f"Original fieldsets: {original_fieldsets}") @@ -218,6 +224,7 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere def get_fields(self, request, obj=None): + """filter fields according to included and excluded fields in settings""" fields = OrderedSet(super().get_fields(request, obj) or []) custom_fields = self.get_included_fields() - self.get_excluded_fields() if custom_fields: @@ -226,6 +233,7 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere return list(fields) def get_exclude(self, request, obj=None): + """filter excluded fields according to included and excluded fields in settings""" excluded = OrderedSet(super().get_exclude(request, obj) or []) custom_excluded = self.get_excluded_fields() - self.get_included_fields() if custom_excluded: diff --git a/jdav_web/jdav_web/settings/local.py b/jdav_web/jdav_web/settings/local.py index 6da4a84..38f7683 100644 --- a/jdav_web/jdav_web/settings/local.py +++ b/jdav_web/jdav_web/settings/local.py @@ -80,4 +80,4 @@ TEST_MAIL = get_var('testing', 'mail', default='test@localhost') # excluded and included model fields in admin and admin forms CUSTOM_MODELS = list(get_var('custom_model_fields', default={}).keys()) -CUSTOM_MODEL_FIELDS = {model.lower(): get_var('model_fields', model, default=[]) for model in CUSTOM_MODELS} \ No newline at end of file +CUSTOM_MODEL_FIELDS = {model.lower(): get_var('custom_model_fields', model, default={}) for model in CUSTOM_MODELS} \ No newline at end of file -- 2.38.4 From 817e3a516c1bb28c7ab91769f129cd1449e74353 Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sun, 24 Aug 2025 22:57:38 +0200 Subject: [PATCH 3/7] fix logic --- jdav_web/contrib/admin.py | 50 ++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/jdav_web/contrib/admin.py b/jdav_web/contrib/admin.py index d37b358..af3a401 100644 --- a/jdav_web/contrib/admin.py +++ b/jdav_web/contrib/admin.py @@ -183,14 +183,13 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere return f"{self.model._meta.app_label}_{self.model.__name__}".lower() def get_excluded_fields(self): - """if model has custom excluded fields in settings, return them as a set""" - return OrderedSet(settings.CUSTOM_MODEL_FIELDS.get(self.field_key, {}).get('exclude', [])) + """if model has custom excluded fields in settings, return them as list""" + return settings.CUSTOM_MODEL_FIELDS.get(self.field_key, {}).get('exclude', []) def get_included_fields(self): - """if model has an entire custom fieldset in settings, return it as a set""" - return OrderedSet(settings.CUSTOM_MODEL_FIELDS.get(self.field_key, {}).get('fields', [])) + """if model has an entire fieldset in settings, return them as list""" + return settings.CUSTOM_MODEL_FIELDS.get(self.field_key, {}).get('fields', []) - def get_fieldsets(self, request, obj=None): """filter fieldsets according to included and excluded fields in settings""" @@ -198,48 +197,51 @@ class CommonAdminMixin(FieldPermissionsAdminMixin, ChangeViewAdminMixin, Filtere original_fieldsets = super().get_fieldsets(request, obj) included = self.get_included_fields() excluded = self.get_excluded_fields() - if original_fieldsets: - print(f"get_fieldsets called for {self.field_key}") - print(f"Original fieldsets: {original_fieldsets}") + new_fieldsets = [] for title, attrs in original_fieldsets: fields = attrs.get("fields", []) - # Flatten groupings like tuples if needed + # custom fields take precedence over exclude filtered_fields = [ f for f in fields if ( (not included or f in included) - and f not in excluded + and (included or f not in excluded) ) ] if filtered_fields: + # only add fieldset if it has any fields left new_fieldsets.append((title, dict(attrs, **{"fields": filtered_fields}))) - if new_fieldsets: - print(f"Filtered fieldsets: {new_fieldsets}") return new_fieldsets def get_fields(self, request, obj=None): """filter fields according to included and excluded fields in settings""" - fields = OrderedSet(super().get_fields(request, obj) or []) - custom_fields = self.get_included_fields() - self.get_excluded_fields() - if custom_fields: - print(f"get_fields called for {self.field_key}, fields: {fields}, custom_fields: {custom_fields}") - return list(custom_fields) - return list(fields) + fields = super().get_fields(request, obj) or [] + excluded = super().get_exclude(request, obj) or [] + custom_included = self.get_included_fields() + custom_excluded = self.get_excluded_fields() + + if custom_included: + # custom included fields take precedence over exclude + return custom_included + return [f for f in fields if f not in custom_excluded and f not in excluded] def get_exclude(self, request, obj=None): """filter excluded fields according to included and excluded fields in settings""" - excluded = OrderedSet(super().get_exclude(request, obj) or []) - custom_excluded = self.get_excluded_fields() - self.get_included_fields() - if custom_excluded: - print(f"get_exclude called for {self.field_key}, excluded: {excluded}, custom_excluded: {custom_excluded}") - return list(excluded | custom_excluded) - return list(excluded) + excluded = super().get_exclude(request, obj) or [] + custom_included = self.get_included_fields() + custom_excluded = self.get_excluded_fields() + + if custom_included: + # custom included fields take precedence over exclude + return custom_included + return list(set(excluded) | set(custom_excluded)) + class CommonAdminInlineMixin(CommonAdminMixin): -- 2.38.4 From 97b33fc513f060343abc2f9bde44fe1fc6b191ef Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sun, 24 Aug 2025 22:57:50 +0200 Subject: [PATCH 4/7] add logic tests --- jdav_web/contrib/tests.py | 236 +++++++++++++++++++++++++++++++++++++- 1 file changed, 231 insertions(+), 5 deletions(-) diff --git a/jdav_web/contrib/tests.py b/jdav_web/contrib/tests.py index 99493e1..b485bb2 100644 --- a/jdav_web/contrib/tests.py +++ b/jdav_web/contrib/tests.py @@ -1,13 +1,38 @@ -from django.test import TestCase +from django.test import TestCase, RequestFactory +from django.test.utils import override_settings from django.contrib.auth import get_user_model from django.contrib import admin +from django.contrib.admin.sites import AdminSite from django.db import models -from django.test import RequestFactory from unittest.mock import Mock -from rules.contrib.models import RulesModelMixin, RulesModelBase + +from contrib.admin import CommonAdminMixin from contrib.models import CommonModel from contrib.rules import has_global_perm -from contrib.admin import CommonAdminMixin +from rules.contrib.models import RulesModelMixin, RulesModelBase + +# Test model for admin customization +class DummyModel(models.Model): + field1 = models.CharField(max_length=100) + field2 = models.IntegerField() + field3 = models.BooleanField() + field4 = models.DateField() + field5 = models.TextField() + + class Meta: + app_label = 'contrib' + +class DummyAdmin(CommonAdminMixin, admin.ModelAdmin): + model = DummyModel + fieldsets = ( + ('Group1', {'fields': ('field1', 'field2')}), + ('Group2', {'fields': ('field3', 'field4', 'field5')}), + ) + fields = ['field1', 'field2', 'field3', 'field4', 'field5'] + + def __init__(self): + self.opts = self.model._meta + self.admin_site = AdminSite() User = get_user_model() @@ -61,8 +86,15 @@ class GlobalPermissionRulesTestCase(TestCase): class CommonAdminMixinTestCase(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_superuser('admin', 'admin@test.com', 'password') + def setUp(self): - self.user = User.objects.create_user(username='testuser', password='testpass') + self.request = RequestFactory().get('/') + self.request.user = self.__class__.user + self.admin = DummyAdmin() + self.admin.admin_site = AdminSite() def test_formfield_for_dbfield_with_formfield_overrides(self): """Test formfield_for_dbfield when db_field class is in formfield_overrides""" @@ -91,3 +123,197 @@ class CommonAdminMixinTestCase(TestCase): # Verify that the formfield_overrides were used self.assertIsNotNone(result) + + def test_default_behavior(self): + """Test with no customization settings""" + fields = self.admin.get_fields(self.request) + self.assertEqual(fields, ['field1', 'field2', 'field3', 'field4', 'field5']) + + fieldsets = self.admin.get_fieldsets(self.request) + self.assertEqual(len(fieldsets), 2) + self.assertEqual(fieldsets[0][1]['fields'], ['field1', 'field2']) + self.assertEqual(fieldsets[1][1]['fields'], ['field3', 'field4', 'field5']) + + @override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'fields': ['field1', 'field3', 'field5'] + } + }) + def test_included_fields_only(self): + """Test with only included fields specified""" + fields = self.admin.get_fields(self.request) + self.assertEqual(fields, ['field1', 'field3', 'field5']) + + fieldsets = self.admin.get_fieldsets(self.request) + self.assertEqual(len(fieldsets), 2) + self.assertEqual(fieldsets[0][1]['fields'], ['field1']) + self.assertEqual(fieldsets[1][1]['fields'], ['field3', 'field5']) + + @override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'exclude': ['field2', 'field4'] + } + }) + def test_excluded_fields_only(self): + """Test with only excluded fields specified""" + fields = self.admin.get_fields(self.request) + self.assertEqual(fields, ['field1', 'field3', 'field5']) + + fieldsets = self.admin.get_fieldsets(self.request) + self.assertEqual(len(fieldsets), 2) + self.assertEqual(fieldsets[0][1]['fields'], ['field1']) + self.assertEqual(fieldsets[1][1]['fields'], ['field3', 'field5']) + + @override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'fields': ['field1', 'field3', 'field5'], + 'exclude': ['field3'] + } + }) + def test_included_and_excluded_fields(self): + """Test with both included and excluded fields""" + fields = self.admin.get_fields(self.request) + # custom fields should take precedence over exclude + self.assertEqual(fields, ['field1', 'field3', 'field5']) + + fieldsets = self.admin.get_fieldsets(self.request) + self.assertEqual(len(fieldsets), 2) + self.assertEqual(fieldsets[0][1]['fields'], ['field1']) + self.assertEqual(fieldsets[1][1]['fields'], ['field3', 'field5']) + + @override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'fields': ['field5', 'field3', 'field1'] + } + }) + def test_field_order_preservation(self): + """Test that field order from settings is preserved""" + fields = self.admin.get_fields(self.request) + self.assertEqual(fields, ['field5', 'field3', 'field1']) + + + def test_nonexistent_fields(self): + """Test behavior with nonexistent fields in settings""" + with override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'fields': ['field1', 'nonexistent_field'] + } + }): + fields = self.admin.get_fields(self.request) + self.assertEqual(fields, ['field1', 'nonexistent_field']) + + def test_nonexistent_exclude(self): + """Test behavior with nonexistent fields in settings""" + with override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'exclude': ['field1', 'nonexistent', 'field2'] + } + }): + fields = self.admin.get_fields(self.request) + self.assertEqual(fields, ['field3', 'field4', 'field5']) + + exclude = self.admin.get_exclude(self.request) + self.assertEqual(set(exclude), {'nonexistent', 'field1', 'field2'}) + + @override_settings(CUSTOM_MODEL_FIELDS={}) + def test_empty_settings(self): + """Test behavior with empty settings""" + fields = self.admin.get_fields(self.request) + self.assertEqual(fields, ['field1', 'field2', 'field3', 'field4', 'field5']) + + fieldsets = self.admin.get_fieldsets(self.request) + self.assertEqual(len(fieldsets), 2) + self.assertEqual(fieldsets[0][1]['fields'], ['field1', 'field2']) + self.assertEqual(fieldsets[1][1]['fields'], ['field3', 'field4', 'field5']) + + @override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'fields': [] + } + }) + def test_empty_included_fields(self): + """Test behavior with empty included fields list""" + fields = self.admin.get_fields(self.request) + # empty custom fields is perceived as no restriction + self.assertEqual(fields, ['field1', 'field2', 'field3', 'field4', 'field5']) + + @override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'exclude': ['field1', 'field2', 'field3', 'field4', 'field5'] + } + }) + def test_exclude_all_fields(self): + """Test behavior when all fields are excluded""" + fields = self.admin.get_fields(self.request) + self.assertEqual(fields, []) + + fieldsets = self.admin.get_fieldsets(self.request) + # as all fields from group2 are excluded, only group1 remains + self.assertEqual(len(fieldsets), 0) + + + @override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'exclude': ['field5'] + } + }) + def test_custom_fields_exclude_exclude(self): + """Test that custom excluded fields are respected""" + class OrderedAdmin(DummyAdmin): + exclude = ['field2', 'field4'] + + admin_instance = OrderedAdmin() + exclude = admin_instance.get_exclude(self.request) + # app and custom excludes should be additive + self.assertEqual(set(exclude), {'field2', 'field4', 'field5'}) + + fields = admin_instance.get_fields(self.request) + self.assertEqual(fields, ['field1', 'field3']) + + fieldsets = admin_instance.get_fieldsets(self.request) + # for fieldsets, the app exclude is irrelevant, thus only field5 is excluded + self.assertEqual(len(fieldsets), 2) + self.assertEqual(fieldsets[0][1]['fields'], ['field1', 'field2']) + self.assertEqual(fieldsets[1][1]['fields'], ['field3', 'field4']) + + @override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'exclude': ['field3', 'field4', 'field5'] + } + }) + def test_custom_fields_fields_exclude(self): + """Test that custom excluded fields are respected""" + class OrderedAdmin(DummyAdmin): + fields = ['field1', 'field2', 'field4'] + + admin_instance = OrderedAdmin() + exclude = admin_instance.get_exclude(self.request) + self.assertEqual(set(exclude), {'field3', 'field4', 'field5'}) + + fields = admin_instance.get_fields(self.request) + self.assertEqual(fields, ['field1', 'field2']) + + fieldsets = admin_instance.get_fieldsets(self.request) + # as all fields from group2 are excluded, only group1 remains + self.assertEqual(len(fieldsets), 1) + self.assertEqual(fieldsets[0][1]['fields'], ['field1', 'field2']) + + + @override_settings(CUSTOM_MODEL_FIELDS={ + 'contrib_dummymodel': { + 'exclude': ['field2', 'field4'] + } + }) + def test_combined_admin_and_settings_exclude(self): + """Test that both admin and settings excludes are applied while maintaining order""" + class CombinedAdmin(DummyAdmin): + fields = ['field5', 'field4', 'field3', 'field2', 'field1'] + exclude = ['field1'] + + admin_instance = CombinedAdmin() + + fields = admin_instance.get_fields(self.request) + self.assertEqual(fields, ['field5', 'field3']) + + exclude = admin_instance.get_exclude(self.request) + self.assertEqual(set(exclude), {'field1', 'field2', 'field4'}) -- 2.38.4 From 0e0496e10418f289725073bbe955fac54d38b9ae Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sun, 24 Aug 2025 23:07:52 +0200 Subject: [PATCH 5/7] add documentation in the development manual --- .../development_manual/customization.rst | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 docs/source/development_manual/customization.rst diff --git a/docs/source/development_manual/customization.rst b/docs/source/development_manual/customization.rst new file mode 100644 index 0000000..fe41db1 --- /dev/null +++ b/docs/source/development_manual/customization.rst @@ -0,0 +1,119 @@ +Customization Guide +================= + +This guide explains how to customize the Kompass application using configuration files and templates. + +Configuration Files +----------------- + +The application uses two main configuration files: + +* ``settings.toml``: Contains core application settings +* ``text.toml``: Contains customizable text content + +settings.toml +~~~~~~~~~~~~ + +The ``settings.toml`` file contains all core configuration settings organized in sections: + +.. code-block:: toml + + [section] + name = "Your Section Name" + street = "Street Address" + town = "12345 Town" + # ... other section details + + [LJP] + contribution_per_day = 25 + tax = 0.1 + + [finance] + allowance_per_day = 22 + max_night_cost = 11 + +Key sections include: + +* ``[section]``: Organization details +* ``[LJP]``: Youth leadership program settings +* ``[finance]``: Financial configurations +* ``[misc]``: Miscellaneous application settings +* ``[mail]``: Email configuration +* ``[database]``: Database connection details + +Customizing Model Fields +~~~~~~~~~~~~~~~~~~~~~~~ + +The ``[custom_model_fields]`` section in ``settings.toml`` allows you to customize which fields are visible in the admin interface: + +.. code-block:: toml + + [custom_model_fields] + # Format: applabel_modelname.fields = ['field1', 'field2'] + # applabel_modelname.exclude = ['field3', 'field4'] + + # Example: Show only specific fields + members_emergencycontact.fields = ['prename', 'lastname', 'phone_number'] + + # Example: Exclude specific fields + members_member.exclude = ['ticket_no', 'dav_badge_no'] + +There are two ways to customize fields: + +1. Using ``fields``: Explicitly specify which fields should be shown + - Only listed fields will be visible + - Overrides any existing field configuration + - Order of fields is preserved as specified + +2. Using ``exclude``: Specify which fields should be hidden + - All fields except the listed ones will be visible + - Adds to any existing exclusions + - Original field order is maintained + +Field customization applies to: + - Django admin views + - Admin forms + - Model admin fieldsets + +.. note:: + Custom forms must be modified manually as they are not affected by this configuration. + +Text Content +----------- + +The ``text.toml`` file allows customization of application text content: + +.. code-block:: toml + + [emails] + welcome_subject = "Welcome to {section_name}" + welcome_body = """ + Dear {name}, + Welcome to our organization... + """ + + [messages] + success_registration = "Registration successful!" + +Templates +--------- + +Template Customization +~~~~~~~~~~~~~~~~~~~~ + +You can override any template by placing a custom version in your project's templates directory: + +1. Create a directory structure matching the original template path +2. Place your custom template file with the same name +3. Django will use your custom template instead of the default + +Example directory structure:: + + templates/ + └── members/ + └── registration_form.tex + └── startpage/ + └── contact.html + └── impressum_content.html + + -- 2.38.4 From 5b22435182ab7dddc153efc8ef7eed9c4cdfd33a Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sun, 24 Aug 2025 23:20:18 +0200 Subject: [PATCH 6/7] remove unnecessary class --- jdav_web/contrib/admin.py | 2 -- jdav_web/utils.py | 24 ------------------------ 2 files changed, 26 deletions(-) diff --git a/jdav_web/contrib/admin.py b/jdav_web/contrib/admin.py index af3a401..2141d9a 100644 --- a/jdav_web/contrib/admin.py +++ b/jdav_web/contrib/admin.py @@ -11,8 +11,6 @@ from django.contrib.admin import helpers, widgets from django.conf import settings import rules.contrib.admin from rules.permissions import perm_exists -from utils import OrderedSet - class FieldPermissionsAdminMixin: field_change_permissions = {} diff --git a/jdav_web/utils.py b/jdav_web/utils.py index 439b0e9..5697ef7 100644 --- a/jdav_web/utils.py +++ b/jdav_web/utils.py @@ -90,27 +90,3 @@ def coming_midnight(): hour=0, minute=0, second=0, microsecond=0, tzinfo=base.tzinfo) - -class OrderedSet(OrderedDict): - def __init__(self, iterable=None): - super().__init__() - if iterable: - for item in iterable: - self[item] = None - - def __sub__(self, other): - if not isinstance(other, OrderedSet): - return NotImplemented - return OrderedSet(k for k in self if k not in other) - - def add(self, item): - self[item] = None - - def discard(self, item): - self.pop(item, None) - - def __contains__(self, item): - return item in self.keys() - - def __iter__(self): - return iter(self.keys()) \ No newline at end of file -- 2.38.4 From 61ae3a6f79236a0a46a7c44b37d679b64e1d1a89 Mon Sep 17 00:00:00 2001 From: mariusrklein <47218379+mariusrklein@users.noreply.github.com> Date: Sun, 24 Aug 2025 23:21:15 +0200 Subject: [PATCH 7/7] clean up utils --- jdav_web/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/jdav_web/utils.py b/jdav_web/utils.py index 5697ef7..129e495 100644 --- a/jdav_web/utils.py +++ b/jdav_web/utils.py @@ -5,7 +5,6 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from decimal import Decimal, ROUND_HALF_DOWN import unicodedata -from collections import OrderedDict def file_size_validator(max_upload_size): @@ -89,4 +88,3 @@ def coming_midnight(): return timezone.datetime(year=base.year, month=base.month, day=base.day, hour=0, minute=0, second=0, microsecond=0, tzinfo=base.tzinfo) - -- 2.38.4