6.9 KiB
@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 is Symfony’s @ParamConverter. It made my life so much easier while developing with Symfony. In Django, of course, there is 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 (in French), but it was very generalized that I cannot always use. Also, it was using a middleware, which may introduce performance issues sometimes [citation needed]. So I decided to go with decorators, and at the end, I came up with this:
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.
"""
= options.pop('prevent_404', False)
prevent_404
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
= pspec[0]
model
if not is_model(model):
raise ValueError(
"First value in pspec must be a Model subclass!")
# We will calculate these soon…
= None
param_name = re.sub(
calc_obj_name '([a-z0-9])([A-Z])',
r'\1_\2',
re.sub('(.)([A-Z][a-z]+)',
r'\1_\2',
__name__)).lower()
model.= None
obj_field_name
# 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:
= calc_obj_name + '_id'
param_name else:
= pspec[1]
param_name
if param_name in converted_params:
raise ValueError('%s is already converted' % param_name)
+= (param_name,)
converted_params = kwargs.pop(param_name)
field_value
# 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:
= 'id'
obj_field_name else:
= pspec[2]
obj_field_name
# 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:
= calc_obj_name
obj_param_name else:
= pspec[3]
obj_param_name
if obj_param_name in kwargs:
raise KeyError(
"'%s' already exists as a parameter" % obj_param_name)
= {obj_field_name: field_value}
filter_kwargs
if (prevent_404):
= model.objects.filter(
kwargs[obj_param_name] **filter_kwargs).first()
else:
= get_object_or_404(
kwargs[obj_param_name]
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
.