Skip to content

Commit 7598b85

Browse files
committed
Merge branch 'sevdog-master'
2 parents e4a15b3 + 4d6563b commit 7598b85

File tree

3 files changed

+87
-29
lines changed

3 files changed

+87
-29
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
.DS_Store
22
*.pyc
33
.vscode
4+
.python-version
45
/dist
56
/*.egg-info
67
/build

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ admin.site.register(MailingList, MailingListAdmin)
8080

8181
With just a few lines of code you get a fully functional `ModelAdmin`, that will automatically pull in just the relevant related objects, based on `ForeignKey` relation between the two models, it will also auto set `ForeignKey` fields for nested relations and exclude them from change form when adding and editing objects on subadmin.
8282

83+
84+
### Caveats
85+
86+
In order to properly support unique field validation (see #7), `SubAdmin` will inject a small mixin into the form. This is done in the `get_form` method and if you override this method in your own classes, make sure to call `super()` or `perp_subadmin_form()` directly. See `subadmin` source code for more details.
87+
88+
Also, the injected mixin `SubAdminFormMixin` overrides `validate_unique` on the form. If your custom form overrides this method as well, have a look at `subadmin` source code for ways in which it differs from stock `ModelForm` implementation.
89+
90+
8391
### Screenshots
8492

8593
![alt text](https://github.com/inueni/django-subadmin-example/raw/master/screenshots/subadmin_screenshot_1.png?raw=true)
@@ -100,7 +108,6 @@ When adding or editing objects with `SubAdmin`, `ForeignKey` fields to parent in
100108

101109
> If you want to see it in action, or get a more in-depth look at how to set everything up, check out <https://github.com/inueni/django-subadmin-example>.
102110
103-
104111
## Stability
105112

106113
`django-subadmin` has evolved from code that has been running on production servers since early 2014 without any issues. The code is provided **as-is** and the developers bear no responsibility for any issues stemming from it's use.

subadmin/__init__.py

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from urllib.parse import parse_qsl, urlparse, urlunparse
55

66
from django.urls import path, re_path, include
7+
from django.core.exceptions import ValidationError
78
from django.contrib.admin.options import IS_POPUP_VAR, TO_FIELD_VAR
89
from django.contrib.admin.utils import unquote, quote
910
from django.contrib import admin
@@ -14,7 +15,7 @@
1415
from django.forms.models import _get_foreign_key
1516
from django.http import HttpResponseRedirect
1617
from django.shortcuts import get_object_or_404
17-
from django.template.response import SimpleTemplateResponse
18+
from django.template.response import TemplateResponse
1819
from django.utils.decorators import method_decorator
1920
from django.utils.functional import cached_property
2021
from django.utils.html import format_html
@@ -25,7 +26,7 @@
2526

2627
csrf_protect_m = method_decorator(csrf_protect)
2728

28-
__all__ = ('SubAdmin', 'RootSubAdmin', 'SubAdminMixin', 'RootSubAdminMixin', 'SubAdminChangeList', 'SubAdminHelper')
29+
__all__ = ('SubAdmin', 'RootSubAdmin', 'SubAdminMixin', 'RootSubAdminMixin', 'SubAdminChangeList', 'SubAdminHelper', 'SubAdminFormMixin')
2930

3031

3132
class SubAdminHelper(object):
@@ -37,15 +38,14 @@ def __init__(self, sub_admin, view_args, object_id=None):
3738
self.view_args = view_args
3839
self.base_viewname = sub_admin.get_base_viewname()
3940
self.load_tree(sub_admin)
40-
41-
41+
4242
def load_tree(self, sub_admin):
4343
parent_admin = sub_admin.parent_admin
4444
fk_lookup = sub_admin.fk_name
4545

4646
i = 2 if self.object_id else 1
4747
while parent_admin:
48-
obj = sub_admin.get_parent_instance(self.view_args[i * -1])
48+
obj = sub_admin.get_parent_instance(self.view_args[-i])
4949
self.parents.append({
5050
'admin': parent_admin,
5151
'object': obj,
@@ -57,13 +57,13 @@ def load_tree(self, sub_admin):
5757
parent_admin = getattr(sub_admin, 'parent_admin', None)
5858
if parent_admin:
5959
fk_lookup = '%s__%s' % (fk_lookup, sub_admin.fk_name)
60-
60+
6161
i += 1
62-
62+
6363
@cached_property
6464
def parent(self):
6565
return self.parents[0]
66-
66+
6767
@cached_property
6868
def root(self):
6969
return self.parents[-1]
@@ -87,6 +87,38 @@ def url_for_result(self, result):
8787
return self.model_admin.reverse_url('change', *self.model_admin.get_base_url_args(self.request) + [pk])
8888

8989

90+
class SubAdminFormMixin(object):
91+
def _post_clean(self):
92+
validate_unique = self._validate_unique
93+
self._validate_unique = False
94+
super()._post_clean()
95+
96+
for fk_field, fk_instance in self._related_instances_fields.items():
97+
setattr(self.instance, fk_field, fk_instance)
98+
99+
self._validate_unique = validate_unique
100+
if self._validate_unique:
101+
self.validate_unique()
102+
103+
104+
def validate_unique(self):
105+
exclude = self._get_subadmin_validation_exclusions()
106+
107+
try:
108+
self.instance.validate_unique(exclude=exclude)
109+
except ValidationError as e:
110+
self._update_errors(e)
111+
112+
def _get_subadmin_validation_exclusions(self):
113+
return [f for f in self._get_validation_exclusions() if f not in self._related_instances_fields.keys()]
114+
115+
@cached_property
116+
def _related_instances_fields(self):
117+
return {
118+
key: self._related_instances[key] for key in self._related_instances.keys() if key in self._meta.model._meta._forward_fields_map.keys()
119+
}
120+
121+
90122
class SubAdminBase(object):
91123
subadmins = None
92124

@@ -104,7 +136,7 @@ def get_subadmin_urls(self):
104136
]
105137

106138
urlpatterns += urls
107-
139+
108140
return urlpatterns
109141

110142
def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None):
@@ -117,7 +149,7 @@ def render_change_form(self, request, context, add=False, change=False, form_url
117149
'name': modeladmin.model._meta.verbose_name_plural,
118150
'url': modeladmin.reverse_url('changelist', *url_args),
119151
})
120-
152+
121153
context.update({'subadmin_links': subadmin_links})
122154
return super().render_change_form(request, context, add=add, change=change,
123155
form_url=form_url, obj=obj)
@@ -142,7 +174,7 @@ def __init__(self, parent_model, parent_admin):
142174
self.fk_name = _get_foreign_key(parent_model, self.model).name
143175

144176
super().__init__(self.model, parent_admin.admin_site)
145-
177+
146178
self.subadmin_instances = self.get_subadmin_instances()
147179

148180
def get_subadmin_helper(self, view_args, object_id=None):
@@ -198,11 +230,17 @@ def get_exclude(self, request, obj=None):
198230
exclude.extend(request.subadmin.related_instances.keys())
199231
return list(set(exclude))
200232

201-
def save_model(self, request, obj, form, change):
202-
for fk_field, instance in request.subadmin.related_instances.items():
203-
if fk_field in self.model._meta._forward_fields_map.keys():
204-
setattr(obj, fk_field, instance)
205-
super().save_model(request, obj, form, change)
233+
def prep_subadmin_form(self, request, form):
234+
attrs = {'_related_instances': request.subadmin.related_instances}
235+
return type(form)(form.__name__, (SubAdminFormMixin, form), attrs)
236+
237+
def get_form(self, request, obj=None, **kwargs):
238+
form = super().get_form(request, obj, **kwargs)
239+
return self.prep_subadmin_form(request, form)
240+
241+
def get_changelist_form(self, request, **kwargs):
242+
form = super().get_changelist_form(request, **kwargs)
243+
return self.prep_subadmin_form(request, form)
206244

207245
def get_base_viewname(self):
208246
if hasattr(self.parent_admin, 'get_base_viewname'):
@@ -220,7 +258,7 @@ def get_base_url_args(self, request):
220258
if hasattr(request, 'subadmin'):
221259
return request.subadmin.base_url_args
222260
return []
223-
261+
224262
def context_add_parent_data(self, request, context=None):
225263
context = context or {}
226264
parent_instance = request.subadmin.parent_instance
@@ -229,7 +267,7 @@ def context_add_parent_data(self, request, context=None):
229267
'parent_opts': parent_instance._meta,
230268
})
231269
return context
232-
270+
233271
def get_parent_instance(self, parent_id):
234272
return get_object_or_404(self.parent_model, pk=unquote(parent_id))
235273

@@ -327,16 +365,16 @@ def response_add(self, request, obj, post_url_continue=None):
327365

328366
if "_saveasnew" in request.POST:
329367
url_args = url_args[:-1]
330-
368+
331369
obj_url = self.reverse_url('change', *url_args + [quote(pk_value)])
332-
370+
333371
if self.has_change_permission(request, obj):
334372
obj_repr = format_html('<a href="{}">{}</a>', urlquote(obj_url), obj)
335373
else:
336374
obj_repr = str(obj)
337-
375+
338376
msg_dict = {
339-
'name': str(opts.verbose_name),
377+
'name': opts.verbose_name,
340378
'obj': obj_repr,
341379
}
342380

@@ -351,7 +389,11 @@ def response_add(self, request, obj, post_url_continue=None):
351389
'value': str(value),
352390
'obj': str(obj),
353391
})
354-
return SimpleTemplateResponse('admin/popup_response.html', {
392+
return TemplateResponse(request, self.popup_response_template or [
393+
'admin/%s/%s/popup_response.html' % (opts.app_label, opts.model_name),
394+
'admin/%s/popup_response.html' % opts.app_label,
395+
'admin/popup_response.html',
396+
], {
355397
'popup_response_data': popup_response_data,
356398
})
357399

@@ -389,7 +431,7 @@ def response_add(self, request, obj, post_url_continue=None):
389431
)
390432
self.message_user(request, msg, messages.SUCCESS)
391433
return self.response_post_save_add(request, obj)
392-
434+
393435
def response_change(self, request, obj):
394436
if IS_POPUP_VAR in request.POST:
395437
to_field = request.POST.get(TO_FIELD_VAR)
@@ -402,7 +444,11 @@ def response_change(self, request, obj):
402444
'obj': str(obj),
403445
'new_value': str(new_value),
404446
})
405-
return SimpleTemplateResponse('admin/popup_response.html', {
447+
return TemplateResponse(request, self.popup_response_template or [
448+
'admin/%s/%s/popup_response.html' % (opts.app_label, opts.model_name),
449+
'admin/%s/popup_response.html' % opts.app_label,
450+
'admin/popup_response.html',
451+
], {
406452
'popup_response_data': popup_response_data,
407453
})
408454

@@ -472,7 +518,7 @@ def response_post_save_change(self, request, obj):
472518
else:
473519
post_url = reverse('admin:index', current_app=self.admin_site.name)
474520
return HttpResponseRedirect(post_url)
475-
521+
476522
def response_delete(self, request, obj_display, obj_id):
477523
opts = self.model._meta
478524

@@ -481,7 +527,11 @@ def response_delete(self, request, obj_display, obj_id):
481527
'action': 'delete',
482528
'value': str(obj_id),
483529
})
484-
return SimpleTemplateResponse('admin/popup_response.html', {
530+
return TemplateResponse(request, self.popup_response_template or [
531+
'admin/%s/%s/popup_response.html' % (opts.app_label, opts.model_name),
532+
'admin/%s/popup_response.html' % opts.app_label,
533+
'admin/popup_response.html',
534+
], {
485535
'popup_response_data': popup_response_data,
486536
})
487537

@@ -510,7 +560,7 @@ class RootSubAdminMixin(SubAdminBase):
510560

511561
def __init__(self, *args, **kwargs):
512562
super().__init__(*args, **kwargs)
513-
self.subadmin_instances = self.get_subadmin_instances()
563+
self.subadmin_instances = self.get_subadmin_instances()
514564

515565
def get_urls(self):
516566
return self.get_subadmin_urls() + super().get_urls()

0 commit comments

Comments
 (0)