155 lines
6.9 KiB
ReStructuredText
155 lines
6.9 KiB
ReStructuredText
@ParamConverter à la Django
|
||
###########################
|
||
|
||
:date: 2015-06-07T18:14:32Z
|
||
:category: blog
|
||
:tags: python,django
|
||
:url: 2015/06/07/paramconverter-a-la-django/
|
||
:save_as: 2015/06/07/paramconverter-a-la-django/index.html
|
||
:status: published
|
||
:author: Gergely Polonkai
|
||
|
||
One thing I really miss from `Django <https://www.djangoproject.com/>`_ is `Symfony
|
||
<http://symfony.com/>`_’s `@ParamConverter
|
||
<http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html>`_. It
|
||
made my life so much easier while developing with Symfony. In Django, of course, there is
|
||
`get_object_or_404
|
||
<https://docs.djangoproject.com/en/dev/topics/http/shortcuts/#get-object-or-404>`_ but, for
|
||
example, in one of my projects I had a view that had to resolve 6(!) objects from the URL, and
|
||
writing ``get_object_or_404`` six times is not what a programmer likes to do (yes, this view had a
|
||
refactor later on). A quick Google search gave me one `usable result
|
||
<http://openclassrooms.com/forum/sujet/middleware-django-genre-paramconverter-doctrine>`_ (in
|
||
French), but it was very generalized that I cannot always use. Also, it was using a middleware,
|
||
which may introduce performance issues sometimes :sup:`[citation needed]`. So I decided to go
|
||
with decorators, and at the end, I came up with this:
|
||
|
||
.. code-block:: python
|
||
|
||
import re
|
||
|
||
from django.shortcuts import get_object_or_404
|
||
from django.db import models
|
||
|
||
def convert_params(*params_to_convert, **options):
|
||
"""
|
||
Convert parameters to objects. Each parameter to this decorator
|
||
must be a model instance (subclass of django.db.models.Model) or a
|
||
tuple with the following members:
|
||
* model: a Model subclass
|
||
* param_name: the name of the parameter that holds the value to be
|
||
matched. If not exists, or is None, the model’s class name will
|
||
be converted from ModelName to model_name form, suffixed with
|
||
"_id". E.g. for MyModel, the default will be my_model_id
|
||
* the field name against which the value in param_name will be
|
||
matched. If not exists or is None, the default will be "id"
|
||
* obj_param_name: the name of the parameter that will hold the
|
||
resolved object. If not exists or None, the default value will
|
||
be the model’s class name converted from ModelName to model_name
|
||
form, e.g. for MyModel, the default value will be my_model.
|
||
The values are resolved with get_object_or_404, so if the given
|
||
object doesn’t exist, it will redirect to a 404 page. If you want
|
||
to allow non-existing models, pass prevent_404=True as a keyword
|
||
argument.
|
||
"""
|
||
|
||
prevent_404 = options.pop('prevent_404', False)
|
||
|
||
def is_model(m):
|
||
return issubclass(type(m), models.base.ModelBase)
|
||
|
||
if len(params_to_convert) == 0:
|
||
raise ValueError("Must pass at least one parameter spec!")
|
||
|
||
if (
|
||
len(params_to_convert) == 1 and \
|
||
hasattr(params_to_convert[0], '__call__') and \
|
||
not is_model(params_to_convert[0])):
|
||
raise ValueError("This decorator must have arguments!")
|
||
|
||
def convert_params_decorator(func):
|
||
def wrapper(*args, **kwargs):
|
||
converted_params = ()
|
||
for pspec in params_to_convert:
|
||
# If the current pspec is not a tuple, let’s assume
|
||
# it’s a model class
|
||
if not isinstance(pspec, tuple):
|
||
pspec = (pspec,)
|
||
|
||
# First, and the only required element in the
|
||
# parameters is the model name which this object
|
||
# belongs to
|
||
model = pspec[0]
|
||
|
||
if not is_model(model):
|
||
raise ValueError(
|
||
"First value in pspec must be a Model subclass!")
|
||
|
||
# We will calculate these soon…
|
||
param_name = None
|
||
calc_obj_name = re.sub(
|
||
'([a-z0-9])([A-Z])',
|
||
r'\1_\2',
|
||
re.sub(
|
||
'(.)([A-Z][a-z]+)',
|
||
r'\1_\2',
|
||
model.__name__)).lower()
|
||
obj_field_name = None
|
||
|
||
# The second element, if not None, is the keyword
|
||
# parameter name that holds the value to convert
|
||
if len(pspec) < 2 or pspec[1] is None:
|
||
param_name = calc_obj_name + '_id'
|
||
else:
|
||
param_name = pspec[1]
|
||
|
||
if param_name in converted_params:
|
||
raise ValueError('%s is already converted' % param_name)
|
||
|
||
converted_params += (param_name,)
|
||
field_value = kwargs.pop(param_name)
|
||
|
||
# The third element is the field name which must be
|
||
# equal to the specified value. If it doesn’t exist or
|
||
# None, it defaults to 'id'
|
||
if (len(pspec) < 3) or pspec[2] is None:
|
||
obj_field_name = 'id'
|
||
else:
|
||
obj_field_name = pspec[2]
|
||
|
||
# The fourth element is the parameter name for the
|
||
# object. If the parameter already exists, we consider
|
||
# it an error
|
||
if (len(pspec) < 4) or pspec[3] is None:
|
||
obj_param_name = calc_obj_name
|
||
else:
|
||
obj_param_name = pspec[3]
|
||
|
||
if obj_param_name in kwargs:
|
||
raise KeyError(
|
||
"'%s' already exists as a parameter" % obj_param_name)
|
||
|
||
filter_kwargs = {obj_field_name: field_value}
|
||
|
||
if (prevent_404):
|
||
kwargs[obj_param_name] = model.objects.filter(
|
||
**filter_kwargs).first()
|
||
else:
|
||
kwargs[obj_param_name] = get_object_or_404(
|
||
model,
|
||
**filter_kwargs)
|
||
|
||
return func(*args, **kwargs)
|
||
|
||
return wrapper
|
||
|
||
return convert_params_decorator
|
||
|
||
Now I can decorate my views, either class or function based, with ``@convert_params(User,
|
||
(Article, 'aid'), (Paragraph, None, 'pid'), (AnotherObject, None, None, 'obj'))`` and all the
|
||
magic happens in the background. The ``user_id`` parameter passed to my function will be popped
|
||
off, and be resolved against the ``User`` model by using the ``id`` field; the result is put in
|
||
the new ``user`` parameter. For Article, the ``aid`` parameter will be matched against the ``id``
|
||
field of the ``Article`` model putting the result into ``article``, and finally, the
|
||
``another_object_id`` will be matched against the ``id`` field of the ``AnotherObject`` model, but
|
||
in this case, the result is passed to the original function as ``obj``.
|