diff --git a/membership_file/admin.py b/membership_file/admin.py index 72a40145..610ea117 100644 --- a/membership_file/admin.py +++ b/membership_file/admin.py @@ -1,10 +1,12 @@ from datetime import datetime +from typing import List from django.contrib import admin, messages from django.contrib.contenttypes.models import ContentType from django.http import HttpResponseRedirect -from django.urls import reverse from django_object_actions import DjangoObjectActions, action as object_action +from django.urls import reverse, path +from django.urls.resolvers import URLPattern from import_export.admin import ExportActionMixin from import_export.formats.base_formats import CSV, TSV, ODS, XLSX @@ -58,7 +60,9 @@ class MemberLogReadOnlyInline(DisableModificationsAdminMixin, URLLinkInlineAdmin @admin.register(Member) -class MemberWithLog(RequestUserToFormModelAdminMixin, DjangoObjectActions, ExportActionMixin, HideRelatedNameAdmin): +class MemberWithLog( + # RequestUserToFormModelAdminMixin, + DjangoObjectActions, ExportActionMixin, HideRelatedNameAdmin): ############################## # Export functionality resource_class = MemberResource @@ -71,9 +75,10 @@ class MemberWithLog(RequestUserToFormModelAdminMixin, DjangoObjectActions, Expor @object_action(attrs={"class": "addlink"}) def register_new_member(modeladmin, request, queryset): - view = modeladmin.admin_site.admin_view(RegisterNewMemberAdminView.as_view()) + view = modeladmin.admin_site.admin_view(RegisterNewMemberAdminView.as_view(model_admin=modeladmin)) return view(request) + # Note: get_urls is extended by changelist_actions = ("register_new_member",) def get_changelist_actions(self, request): @@ -227,7 +232,7 @@ def has_delete_permission(self, request, obj=None): return True -# Prevents MemberLogField creation, edting, or deletion in the Django Admin Panel +# Prevents MemberLogField creation, editing, or deletion in the Django Admin Panel class MemberLogFieldReadOnlyInline(DisableModificationsAdminMixin, admin.TabularInline): model = MemberLogField extra = 0 diff --git a/membership_file/forms.py b/membership_file/forms.py index 8f4d4484..21c0e1e0 100644 --- a/membership_file/forms.py +++ b/membership_file/forms.py @@ -5,6 +5,7 @@ from django.contrib.admin.widgets import FilteredSelectMultiple from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.mail import EmailMultiAlternatives, send_mail +from django.forms.models import ModelFormMetaclass from django.template import loader from django.utils.translation import gettext_lazy as _ @@ -162,31 +163,31 @@ def formfield_for_dbfield(db_field, **kwargs): # # passed to formfield_for_dbfield override the defaults. for klass in db_field.__class__.mro(): if klass in FORMFIELD_FOR_DBFIELD_DEFAULTS: - print(klass) + # print(klass) kwargs = {**copy.deepcopy(FORMFIELD_FOR_DBFIELD_DEFAULTS[klass]), **kwargs} - print("", kwargs) + # print("", kwargs) return db_field.formfield(**kwargs) # For any other type of field, just call its formfield() method. return db_field.formfield(**kwargs) class Foo: + # Add this as a superclass to RegisterMemberForm to use the admin form overrides class Meta: formfield_callback = formfield_for_dbfield - -class RegisterMemberForm(UpdatingUserFormMixin, Foo, forms.ModelForm): - """ - Registers a member in the membership file, and optionally sends them an email to link or register a Squire account. - Also contains some useful presets. - """ - # Required for ModelAdmin.formfield_overrides functionality - # See BaseModelAdmin.formfield_for_dbfield for other uses (e.g. foreign key/m2m/radio) - # This class variable is used by ModelFormMetaclass - # formfield_callback = "foooo" # partial(self.formfield_for_dbfield, request=request) - +class FieldsetModelFormMetaclass(ModelFormMetaclass): + def __new__(mcs, name, bases, attrs): + new_class = super().__new__(mcs, name, bases, attrs) + new_class._meta.fieldsets = None + meta_class = getattr(new_class, 'Meta', None) + if meta_class is not None: + new_class._meta.fieldsets = getattr(meta_class, "fieldsets", None) + return new_class + +class FieldsetAdminFormMixin(metaclass=FieldsetModelFormMetaclass): + """ TODO """ required_css_class = "required" - # field_order = () # ModelAdmin media @property @@ -204,6 +205,29 @@ def media(self): ] return forms.Media(js=['admin/js/%s' % url for url in js]) + super().media + def get_fieldsets(self, request, obj=None): + """ + Hook for specifying fieldsets. + """ + print(self._meta.__dict__) + if self._meta.fieldsets: + return self._meta.fieldsets + return [(None, {'fields': self.fields})] + +class RegisterMemberForm(UpdatingUserFormMixin, FieldsetAdminFormMixin, forms.ModelForm): + """ + Registers a member in the membership file, and optionally sends them an email to link or register a Squire account. + Also contains some useful presets. + """ + # Required for ModelAdmin.formfield_overrides functionality + # See BaseModelAdmin.formfield_for_dbfield for other uses (e.g. foreign key/m2m/radio) + # This class variable is used by ModelFormMetaclass + # formfield_callback = "foooo" # partial(self.formfield_for_dbfield, request=request) + + + # field_order = () + + class Meta: model = Member fields = ( @@ -226,10 +250,26 @@ class Meta: "notes", ) + fieldsets = [ + (None, {'fields': + [('first_name', 'tussenvoegsel', 'last_name'), + 'legal_name', 'date_of_birth', + ('educational_institution', 'student_number'), + 'tue_card_number', + ]}), + + ('Contact Details', {'fields': + [('email', "send_registration_email"), 'phone_number', + ('street', 'house_number', 'house_number_addition'), ('postal_code', 'city'), 'country']}), + ('Notes', {'fields': + ['notes']}), + ] + widgets = { "educational_institution": OtherRadioSelect( choices=[ (Member.EDUCATIONAL_INSTITUTION_TUE, "Eindhoven University of Technology"), + (Member.EDUCATIONAL_INSTITUTION_TUE + "PhD", "TU/e (PhD)"), ("Fontys Eindhoven", "Fontys Eindhoven"), ("Summa College", "Summa College"), ("", "None (not a student)"), @@ -252,9 +292,7 @@ class Meta: ) - def __init__(self, *args, **kwargs): - print(args) super().__init__(*args, **kwargs) # Make more fields required @@ -306,13 +344,15 @@ def clean(self) -> Dict[str, Any]: ) if self.cleaned_data["educational_institution"] and not self.cleaned_data["student_number"]: - self.add_error( - "student_number", - ValidationError( - "A student number is required when an educational institution is set.", - code="student_number_required", - ), - ) + # PhD'ers do not have student numbers + if "(PhD)" not in self.cleaned_data["educational_institution"]: + self.add_error( + "student_number", + ValidationError( + "A student number is required when an educational institution is set.", + code="student_number_required", + ), + ) return res diff --git a/membership_file/templates/membership_file/register_member.html b/membership_file/templates/membership_file/register_member.html index b2e66083..2bc1c637 100644 --- a/membership_file/templates/membership_file/register_member.html +++ b/membership_file/templates/membership_file/register_member.html @@ -4,10 +4,7 @@ {% block extrahead %}{{ block.super }} -{{ media }} - -{{ form.media }} - +{{ adminform.media }} {% endblock %} {% block extrastyle %}{{ block.super }}{% endblock %} diff --git a/membership_file/views.py b/membership_file/views.py index b0b8d2d4..5dce7b29 100644 --- a/membership_file/views.py +++ b/membership_file/views.py @@ -1,6 +1,8 @@ -from typing import Any, Dict +from functools import partial +from typing import Any, Dict, Optional, Type from django.contrib import messages -from django.contrib.admin import helpers +from django.contrib.admin import helpers, ModelAdmin +from django.contrib.admin.utils import flatten_fieldsets from django.core.exceptions import PermissionDenied from django.forms.models import BaseModelForm from django.http import HttpResponse @@ -84,7 +86,63 @@ class ExtendMembershipSuccessView(MemberMixin, UpdateMemberYearMixin, TemplateVi template_name = "membership_file/extend_membership_successpage.html" -class RegisterNewMemberAdminView(CreateView): +class ModelAdminFormViewMixin: + """ TODO """ + model_admin: ModelAdmin = None + + def __init__(self, *args, model_admin: ModelAdmin=None, **kwargs) -> None: + assert model_admin is not None + self.model_admin = model_admin + super().__init__(*args, **kwargs) + + def get_form(self, form_class: Optional[type[BaseModelForm]]=None) -> BaseModelForm: + # This should return a form instance + # NB: More defaults can be passed into the **kwargs of ModelAdmin.get_form + if form_class is None: + form_class = self.get_form_class() + + # Use this form_class's excludes instead of those from the ModelAdmin's form_class + exclude = form_class._meta.exclude or () + + # fields = flatten_fieldsets(self.get_fieldsets(request, obj)) + + # print(form_class) + + # This constructs a form class + form_class = self.model_admin.get_form( + self.request, None, change=False, + # Fields are defined in the form + fields=None, + # Override standard ModelAdmin form and ignore its exclude list + form=form_class, + exclude=exclude, + ) + + + return super().get_form(form_class) + + def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: + context = super().get_context_data(**kwargs) + form: RegisterMemberForm = context.pop("form") + adminForm = helpers.AdminForm(form, list(form.get_fieldsets(self.request, self.object)), {}, model_admin=self.model_admin) + # FORMFIELD_FOR_DBFIELD_DEFAULTS + + context.update( + { + "adminform": adminForm, + # 'form_url': form_url, + "is_nav_sidebar_enabled": True, + "opts": Member._meta, + "title": "Register new member", + # 'content_type_id': get_content_type_for_model(self.model).pk, + # 'app_label': app_label, + } + ) + + return context + + +class RegisterNewMemberAdminView(ModelAdminFormViewMixin, CreateView): """placeholder""" form_class = RegisterMemberForm @@ -111,39 +169,4 @@ def get_success_url(self) -> str: def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: context = super().get_context_data(**kwargs) - fields = ["first_name"] - fields = context["form"].fields - - fieldsets = [(None, {"fields": fields})] - - fieldsets = [ - (None, {'fields': - [('first_name', 'tussenvoegsel', 'last_name'), - 'legal_name', 'date_of_birth', - ('educational_institution', 'student_number'), - 'tue_card_number', - ]}), - - ('Contact Details', {'fields': - ['email', 'phone_number', - ('street', 'house_number', 'house_number_addition'), ('postal_code', 'city'), 'country']}), - ('Notes', {'fields': - ['notes']}), - ] - # fieldsets = [(None, {"fields": ["date_of_birth"]})] - - adminForm = helpers.AdminForm(context["form"], list(fieldsets), {}) - # FORMFIELD_FOR_DBFIELD_DEFAULTS - - context.update( - { - "adminform": adminForm, - # 'form_url': form_url, - "is_nav_sidebar_enabled": True, - "opts": Member._meta, - "title": "Register new member", - # 'content_type_id': get_content_type_for_model(self.model).pk, - # 'app_label': app_label, - } - ) return context diff --git a/utils/widgets.py b/utils/widgets.py index a4e433c7..44afb7ec 100644 --- a/utils/widgets.py +++ b/utils/widgets.py @@ -24,6 +24,12 @@ class OtherRadioSelect(RadioSelect): class Media: js = ("js/other_option_widget.js",) + def __init__(self, attrs=None, choices=None) -> None: + if attrs is None or attrs.get("class", None): + attrs = attrs or {} + attrs["class"] = "radiolist" + super().__init__(attrs, choices) + def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) # If an initial value was provided that does not occur in the list,