4

I need to have a dynamic URL prefix for all URLs in my app.

I'm familiar with doing a static prefix, such as url(r'^myprefix/app', include('app.urls')).

Instead, I need the myprefixto be dynamic, such as url(r'^(?P<prefix>\w+)/app', include('app.urls')).

That works, but here's the kicker. I don't want that prefix to be sent as a keyword argument to all of the views. I want to be able to capture it and use it in Middleware or something similar.


To give my specific use case, we have software (this Django project) used to manage various testing labs. The app needs knowledge of which lab it is operating on.

Currently I'm doing this with the following:

class LabMiddleware(object):
    def process_request(self, request):
        request.current_lab = 'lab1'  # Note that this is currently hard coded

The requirement states that the users be able to go to a URL such as http://host/<lab_name>/app where the lab_name would then get used in my LabMiddleware. Because of this, I obviously don't want to have to accept the lab_name in every single one of my views as it's cumbersome and overkill.


UPDATE:

Building on what Sohan gave in his answer, I ended up using a custom middleware class:

urls.py

url(r'^(?P<lab_name>\w+)/', include('apps.urls')),

apps/urls.py

url(r'^app1/', include('apps.app1.urls', namespace='app1')),

middleware.py

class LabMiddleware(object):
    def process_view(self, request, view_func, view_args, view_kwargs):
        lab_name = view_kwargs.get('lab_name')
        if lab_name:
            request.current_lab = Lab.objects.get(name=lab_name)
            # Remove the lab name from the kwargs before calling the view
            view_kwargs.pop('lab_name')
            return view_func(request, *view_args, **view_kwargs)

settings.py

MIDDLEWARE_CLASSES = (
    # Default Django middleware classes here...
    'raamp.middleware.LabMiddleware',
)

This allowed me to have the lab name in the URL and to add it to the request. Then by removing it from view_kwargs, it doesn't get passed on to the view function and everything works as I intended it.

Also note that the code I have above isn't the most optimized (e.g. I'm querying the database for every request). I stripped out the code I have for caching this as it's not important to showing how this problem was solved, but is worth mentioning that some improvements should be made to this code if you are using it in a production system.

Jared
  • 4,567
  • 2
  • 26
  • 30
  • IMO accepting another argument into all of your view functions is not overkill and only minimally cumbersome. I think your app will be easier to maintain and understand if the view arguments directly match the structure in routes.py – Sohan Jain Dec 04 '14 at 21:29
  • The problem is that for every single view function, a `lab_name` parameter would need to be included, then a function would need to be called in every single one to do something with that information. For a large application (which this currently is), this is cumbersome and error prone, especially with multiple developers. In the end, if I can't get it to work any other way, then this will be the implementation I will take but I'm trying to avoid it. – Jared Dec 04 '14 at 21:34
  • Ah I see -- actually a good solution could be to use a decorator instead. Your decorator can process and "swallow" the `lab_name` arg. Let me write up an example... – Sohan Jain Dec 04 '14 at 21:44

1 Answers1

2

You can create a decorator that wraps each view function. The decorator can take care of any processing work you have on the lab name parameter, and every view won't need to see the lab_name param.

def process_lab_name(request, lab_name):
    request.current_lab = lab_name

def lab(view_func):
    def _decorator(request, lab_name, *args, **kwargs):
        # process the lab_name argument
        process_lab_name(request, lab_name)
        # when calling the view function, exclude the lab_name argument
        response = view_func(request, *args, **kwargs)
        return response
    return wraps(view_func)(_decorator)

@lab
def some_view(request):
    return render(...)

And your route will look like url(r'^(?P<lab_name>\w+)/app'

Sohan Jain
  • 2,318
  • 1
  • 16
  • 17
  • Thanks for this. It actually lead me in a similar direction, but instead using a middleware class that defines process_view() to effectively do what you've shown here. I then remove `lab_name` from from `kwargs` before calling off to the view function. I'll update my post later with the particular solution that I went with, but I'm marking your answer as accepted since it does solve the problem. Thanks! – Jared Dec 05 '14 at 04:26