Search code examples
pythonpython-3.xdjangodjango-viewsdecorator

Django - how to reuse code in function-based view with decorators


I've got some code that I have to re-use for several views, so I would like to create a decorator so that I don't have to copy and paste many lines of code.

So the code I need to re-use in different views is:

@login_required
def add_custom_word_song(request, target_word, source_word, pk):
    """
    Add new word
    """
    if request.method == 'POST':
        form = WordForm(request.POST)
        if form.is_valid():
            deck_name = form.cleaned_data['deck_name']
            source_word = form.cleaned_data['source_word']
            target_word = form.cleaned_data['target_word']
            fluency = form.cleaned_data['fluency']
            user = request.user
            Word.objects.create(user=user, target_word=target_word,
                source_word=source_word, deck_name=deck_name, fluency=fluency)
            return HttpResponseRedirect(reverse('vocab:list'))
    if request.method =="GET":
        user = request.user
        request.session['pk'] = pk
        form = CustomWordForm(initial={'user': user, 'target_word': target_word, 'source_word': source_word, 'deck_name': 'My Words', 'fluency': 0})

    return render(request, 'vocab/add_custom_initial_song.html', {'form': form, 'pk': pk})

And the only part of the code that will change for the other views is the template part in the last line. So I tried putting everything except the last line in the decorator, but then I get:

TypeError: add_custom_word() missing 3 required positional arguments: 'target_word', 'source_word', and 'pk'

I tried different variations of this, but still get the same error.


Solution

  • If I understood correct, you would like to write some code like:

    def add_custom_word(request, target_word, second_word, pk):
        ...
    
    @add_custom_word
    def add_custom_word_song(request, target_word, second_word, pk):
        return render(request, 'vocab/add_custom_initial_song.html', {'form': form, 'pk': pk})
    

    In this case, if you called add_custom_word_song it means:

    add_custom_word_song = add_custom_word(add_custom_word_song)
    add_custom_word_song()
    

    Python while first pass add_custom_word_song as the first argument to add_custom_word when module initialing, they call the new add_custom_word_song.

    So you will get the error as you said: add_custom_word() missing 3 required positional arguments: 'target_word', 'source_word', and 'pk'

    If you really want to use decorator, you need wrap it again:

    def add_custom_word(func):
        def wrapper(request, target_word, second_word, pk):
            ...
            return func(request, target_word, second_word, pk)
        return wrapper
    
    @decorator
    def add_custom_word_song(request, target_word, second_word, pk):
        return render(request, 'vocab/add_custom_initial_song.html', {'form': form, 'pk': pk})
    

    But if you only want to change the template file, consider use a registry to manage the template content!

    Edit:

    A simplest registry could like a dict:

    registry = {
        "song": "vocab/add_custom_initial_song.html",
        "image": "vocab/add_custom_initial_image.html",
        ...
    }
    

    You could define some types as keys, then define the template files as values. So you can return it like:

    def add_custom_word(...):
        ...
        return render(request, registry[return_type], {'form': form, 'pk': pk})
    

    If you have some complex conditions, you could have a custom registry class, and it would always have register and match methods.

    class Registry(object):
        def __init__(self):
            self.registry_list = []
    
        def register(self, conditions, template_file):
            self.registry_list.append([conditions, template_file])
    
        def match(self, condition):
            for conditions, template_file in self.registry_list:
                if match(condition, conditions):
                    return template_file
    
    registry = Registry()
    

    Then you could use this registry to get template files:

        def add_custom_word(...):
        ...
        return render(request, registry.match(condition), {'form': form, 'pk': pk})