Last active
September 29, 2019 03:11
-
-
Save vdboor/7f7865df748ed8949ba5 to your computer and use it in GitHub Desktop.
A view mixin that allows fetching a parent object, and limiting the children for that. A typical example would be "users belonging to a customer" or "entries in a category".
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Parent object mixin, also available at: https://gist.github.com/vdboor/7f7865df748ed8949ba5 | |
""" | |
from functools import lru_cache | |
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist | |
from django.db import models | |
from django.http import Http404 | |
from django.urls import reverse | |
class ParentObjectMixin(object): | |
""" | |
A variation of Django's :class:`~django.views.generic.detail.SingleObjectMixin` that | |
provides a "parent" context for a listing of subobjects. Example usage: | |
.. code-block:: python | |
# views.py: | |
class UserListView(ParentObjectMixin, ListView): | |
model = User | |
parent_model = Customer | |
parent_related_name = 'customer' | |
class UserDetailView(ParentObjectMixin, DetailView): | |
model = User | |
parent_model = Customer | |
parent_related_name = 'customer' | |
# urls.py: | |
url(r'customers/(?P<parent_pk>\d+)/users/', UserListView.as_view()), | |
url(r'customers/(?P<parent_pk>\d+)/users/(?P<pk>\d)/', UserDetailView.as_view()), | |
You can also override :meth:`get_parent_object` and choose a different implementation. | |
For example, the "parent object" could be retrieved from the session. | |
By applying this mixin everywhere, the chance of exposing too many objects is reduced. | |
Thus, it might add in learning curve complexity (more inheritance levels), | |
but reduces security risks. Applying a base class everywhere is easier to test | |
then updating ``get_queryset()`` everywhere to reduce exposed objects. | |
""" | |
#: Whether the *pk* and *slug* fields can be omitted to work without filtering. | |
parent_optional = False | |
#: The parent model to filter on. | |
parent_model = None | |
#: The *slug* field that the parent model uses. | |
parent_slug_field = 'slug' | |
#: The context variable to add to the template | |
parent_context_object_name = None | |
#: The name of the field in the URL pattern that represents the *slug*. | |
parent_slug_url_kwarg = 'parent_slug' | |
#: The name of the field in the URL pattern that represents the *pk*. | |
parent_pk_url_kwarg = 'parent_pk' | |
#: The related field name, used by the queryset of the child model to filter on the parent. | |
parent_related_name = None | |
def dispatch(self, request, *args, **kwargs): | |
""" | |
Make self.parent_object available for all calls. | |
Please note that this might be too early if this mixin is included in applications. | |
For example, a base view might want to perform a permission check before fetching objects. | |
""" | |
self.parent_object = self.get_parent_object() | |
return super(ParentObjectMixin, self).dispatch(request, *args, **kwargs) | |
def get_parent_queryset(self): | |
""" | |
The processing function to get the parent queryset | |
similar so Django's :meth:`~django.views.generic.detail.SingleObjectMixin.get_queryset`. | |
""" | |
return self.parent_model.objects.all() | |
def get_parent_object(self): | |
""" | |
The processing function to get the parent object, | |
similar so Django's :meth:`~django.views.generic.detail.SingleObjectMixin.get_object`. | |
""" | |
pk = self.kwargs.get(self.parent_pk_url_kwarg, None) | |
slug = self.kwargs.get(self.parent_slug_url_kwarg, None) | |
if pk is None and slug is None: | |
if self.parent_optional: | |
# If not provided in url patters, the view can ignore the parent | |
return None | |
else: | |
raise ImproperlyConfigured("No '{0}' or kwarg provided in URL pattern!".format( | |
self.parent_pk_url_kwarg | |
)) | |
queryset = self.get_parent_queryset() | |
if pk is not None: | |
queryset = queryset.filter(pk=pk) | |
# Next, try looking up by slug. | |
elif slug is not None: | |
slug_field = self.get_parent_slug_field() | |
queryset = queryset.filter(**{slug_field: slug}) | |
try: | |
return queryset.get() | |
except ObjectDoesNotExist as e: | |
raise Http404(e) | |
def get_queryset(self): | |
""" | |
Auto filter the child queryset on parent object. | |
This also avoids hacking via /parent/OTHER_ID/child/ID/ mixups | |
""" | |
queryset = super(ParentObjectMixin, self).get_queryset() | |
rel_name = self.get_parent_related_name() | |
if not rel_name or (self.parent_optional and self.parent_object is None): | |
return queryset | |
else: | |
return self.limit_queryset_to_parent_object(queryset) | |
def limit_queryset_to_parent_object(self, queryset): | |
""" | |
Filter the queryset to the parent model. | |
""" | |
filter = {self.get_parent_related_name(): self.parent_object} | |
return queryset.filter(**filter) | |
def get_parent_slug_field(self): | |
""" | |
Get the name of a slug field to be used to look up by slug. | |
""" | |
return self.parent_slug_field | |
def get_parent_related_name(self): | |
""" | |
Return the related name that connects the child and parent model. | |
""" | |
if self.parent_related_name: | |
return self.parent_related_name | |
else: | |
# Auto detect the possible relation! | |
return _get_foreign_key(self.parent_model, self.model).name | |
def get_parent_context_object_name(self, obj): | |
""" | |
Get the name to use for the parent object in the template. | |
""" | |
if self.parent_context_object_name: | |
return self.parent_context_object_name | |
elif isinstance(obj, models.Model): | |
return obj._meta.model_name | |
else: | |
return None | |
def get_context_data(self, **kwargs): | |
""" | |
Include ``parent_object`` in the template context. | |
""" | |
context = super(ParentObjectMixin, self).get_context_data(**kwargs) | |
if self.parent_object is not None: | |
context['parent_object'] = self.parent_object | |
context_object_name = self.get_parent_context_object_name(self.parent_object) | |
if context_object_name: | |
context[context_object_name] = self.parent_object | |
return context | |
def get_parent_url_kwargs(self): | |
""" | |
Return the extra optional parent kwargs, if they were part of this URL. | |
""" | |
kwargs = {} | |
# Only include the kwargs that are part of the current URL | |
if self.parent_pk_url_kwarg in self.kwargs: | |
kwargs[self.parent_pk_url_kwarg] = self.kwargs[self.parent_pk_url_kwarg] | |
elif self.parent_slug_url_kwarg in self.kwargs: | |
kwargs[self.parent_slug_url_kwarg] = self.kwargs[self.parent_slug_url_kwarg] | |
return kwargs | |
def reverse(self, view_name, args=None, kwargs=None): | |
""" | |
Resolve an URL, and preserve the optional parent object kwargs if they were provided. | |
""" | |
kwargs = kwargs or {} | |
kwargs.update(self.get_parent_url_kwargs()) | |
try: | |
current_app = self.request.current_app | |
except AttributeError: | |
try: | |
current_app = self.request.resolver_match.namespace | |
except AttributeError: | |
current_app = None | |
return reverse(view_name, args=args, kwargs=kwargs, current_app=current_app) | |
@lru_cache() | |
def _get_foreign_key(parent_model, model): | |
""" | |
Copied and simplified from ``django.forms.models._get_foreign_key()`` | |
""" | |
# Try to discover what the ForeignKey from model to parent_model is | |
fks_to_parent = [ | |
f for f in model._meta.fields | |
if isinstance(f, models.ForeignKey) and ( | |
f.remote_field.model == parent_model or | |
f.remote_field.model in parent_model._meta.get_parent_list() | |
) | |
] | |
if len(fks_to_parent) == 1: | |
return fks_to_parent[0] | |
elif len(fks_to_parent) == 0: | |
raise ValueError( | |
"'{0}.{1}' has no ForeignKey to '{2}.{3}'.".format( | |
model._meta.app_label, model._meta.object_name, | |
parent_model._meta.app_label, parent_model._meta.object_name | |
) | |
) | |
else: | |
raise ValueError( | |
"'{0}.{1}' has more than one ForeignKey to '{2}.{3}'.".format( | |
model._meta.app_label, model._meta.object_name, | |
parent_model._meta.app_label, parent_model._meta.object_name | |
) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment