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