Skip to content

Commit 4d6563b

Browse files
committed
Properly validate unique_together. (fixes inueni#7).
1 parent aebe374 commit 4d6563b

File tree

4 files changed

+63
-39
lines changed

4 files changed

+63
-39
lines changed

.gitignore

+1
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

+8-1
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

+54-10
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,19 +15,18 @@
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, TemplateResponse
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
2122
from django.utils.http import urlencode, urlquote
2223
from django.utils.translation import ugettext as _
2324
from django.views.decorators.csrf import csrf_protect
2425
from django.urls import Resolver404, get_script_prefix, resolve, reverse
25-
from .forms import get_form
2626

2727
csrf_protect_m = method_decorator(csrf_protect)
2828

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

3131

3232
class SubAdminHelper(object):
@@ -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

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

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+
201241
def get_changelist_form(self, request, **kwargs):
202242
form = super().get_changelist_form(request, **kwargs)
203-
return get_form(form, self.model, request.subadmin.related_instances)
204-
205-
def get_form(self, request, obj=None, change=False, **kwargs):
206-
form = super().get_form(request, obj, change, **kwargs)
207-
return get_form(form, self.model, request.subadmin.related_instances)
243+
return self.prep_subadmin_form(request, form)
208244

209245
def get_base_viewname(self):
210246
if hasattr(self.parent_admin, 'get_base_viewname'):
@@ -408,7 +444,11 @@ def response_change(self, request, obj):
408444
'obj': str(obj),
409445
'new_value': str(new_value),
410446
})
411-
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+
], {
412452
'popup_response_data': popup_response_data,
413453
})
414454

@@ -487,7 +527,11 @@ def response_delete(self, request, obj_display, obj_id):
487527
'action': 'delete',
488528
'value': str(obj_id),
489529
})
490-
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+
], {
491535
'popup_response_data': popup_response_data,
492536
})
493537

subadmin/forms.py

-28
This file was deleted.

0 commit comments

Comments
 (0)