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``.
|