Skip to content

Commit

Permalink
FieldSetAdminFormMixin
Browse files Browse the repository at this point in the history
Added a MOdelAdminFormViewMixin that allows using fieldsets on forms in the admin panel. This allows selecting different fields/fieldsets than the one used for the ModelAdmin's add/change form.

Fixed a display issue where the options in OtherRadioSelect widgets were not being properly vertically centered.

Should still clean up the code.
  • Loading branch information
EricTRL committed Sep 3, 2023
1 parent 8784623 commit 6a49821
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 69 deletions.
13 changes: 9 additions & 4 deletions membership_file/admin.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down
86 changes: 63 additions & 23 deletions membership_file/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 _

Expand Down Expand Up @@ -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
Expand All @@ -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 = (
Expand All @@ -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)"),
Expand All @@ -252,9 +292,7 @@ class Meta:
)



def __init__(self, *args, **kwargs):
print(args)
super().__init__(*args, **kwargs)

# Make more fields required
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
{% block extrahead %}{{ block.super }}
<script src="{% url 'admin:jsi18n' %}"></script>
<!-- media normally form.media + inline.media -->
{{ media }}
<!-- TODO -->
{{ form.media }}
<!-- ENDTODO -->
{{ adminform.media }}
{% endblock %}

{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}">{% endblock %}
Expand Down
99 changes: 61 additions & 38 deletions membership_file/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
6 changes: 6 additions & 0 deletions utils/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 6a49821

Please sign in to comment.