Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining an API for complex View generator function (with many configurables)

Tags:

python

django

I'm writing a view generator for my Django project. I have a large number of models from a legacy application (~150 models), that all need the same basic CRUD operations (providing Admin access isn't enough apparently).

So I'm writing a generator that returns 5 Views for each model, and of course each view can potentially take a large number of options, and I'm trying to define sane API/default parameter format for my generator.

My current generator:

def generate_views(model_class, **kwargs):
    """
    For a given model, returns a dict of generic class-based views
    """
    ###
    # Forms
    #   Optionally generate form classes if not already provided
    ###

    # Append these fields with either "create_" or "update_" to have them only
    # apply to that specific type of form
    form_override_args = ['fields', 'exclude', 'form_method', 'form_class',
                          'form_layout', 'widgets', 'media_css', 'media_js']

    if 'form_class' not in kwargs and 'create_form_class' not in kwargs:
        create_form_kwargs = kwargs.copy()
        for arg in form_override_args:
            if f'create_{arg}' in kwargs:
                create_form_kwargs[arg] = kwargs[f'create_{arg}']
        kwargs['create_form_class'] = forms.FormFactory(model_class, **create_form_kwargs).form()

    if 'form_class' not in kwargs and 'update_form_class' not in kwargs:
        update_form_kwargs = kwargs.copy()
        for arg in form_override_args:
            if f'update_{arg}' in kwargs:
                update_form_kwargs[arg] = kwargs[f'update_{arg}']
        kwargs['update_form_class'] = forms.FormFactory(model_class, **update_form_kwargs).form()

    if 'form_class' not in kwargs:
        kwargs['form_class'] = forms.FormFactory(model_class, **kwargs).form()

    ###
    # Tables
    #   Optionally generate table classes if not already provided
    ###

    # Append these fields with "table_" to have them only
    # apply to the table view
    table_override_args = ['fields', 'exclude']

    if 'table_class' not in kwargs:
        update_table_kwargs = kwargs.copy()
        for arg in table_override_args:
            if f'table_{arg}' in kwargs:
                update_table_kwargs[arg] = kwargs[f'table_{arg}']
        kwargs['table_class'] = tables.TableFactory(model_class, **update_table_kwargs).table()

    ###
    # Views
    #   Generate 5 generic views based on the provided model
    ###
    view_factory = views.ViewFactory(model_class, **kwargs)

    return {
        'list_view': view_factory.list_view(),
        'detail_view': view_factory.detail_view(),
        'create_view': view_factory.create_view(),
        'update_view': view_factory.update_view(),
        'delete_view': view_factory.delete_view()
    }

I'm currently relying on kwargs, and I wanted to define what a fully filled-out kwargs dict should look like. Something like

{
    'forms': {
        'all': {

        },
        'create': {

        },
        'update': {

        }
    },
    'tables': {
        'all': {

        },
        'list': {

        }
    },
    'views': {
        'all': {

        },
        'list': {

        },
        'detail': {

        },
        'create': {

        },
        'update': {

        },
        'delete': {

        }
    }
}

And it's just seeming a bit overworked. I'm mostly looking for recommendations on a potentially better design (because I'm going cross eyed from just working on it).

like image 764
Will Gordon Avatar asked Mar 21 '19 19:03

Will Gordon


1 Answers

It seems that you are fighting the way how Django structures discrete functionalities/configurations in class-based views.

Django’s generic class-based views are built out of mixins providing discrete functionality.

So, my suggestion is: using mixins to incoporate the table and form classes into your views for the CRUD operation. In the generator, all configurable parameters should be passed only to the views.

Backgrounds knowledge

Let's look at how django.views.generic.edit.CreateView is designed. It inherits methods and attributes from: SingleObjectTemplateResponseMixin, BaseCreateView and ModelFormMixin. It can be bound to a model simply with a few lines of codes:

from myapp.models import Author
class AuthorCreateView(CreateView):
    model  = Author
    fields = ['FirstName','FamilyName','BirthDay']
    def form_valid(self, form):
        # Saves the form instance, sets the current object for the view, and redirects to get_success_url().

Here the model attribute is shared by all the mixins to do their jobs, while fields and form_valid are specific to ModelFormMixin. Although all configurable parameters/methods are put together under the View class, each mixin just picks up those it needs.

Redesign the API

Keeping this in mind, let's begin to simplify your view generator/factory. For this example, let's say you have the following base classes that include common (default) settings:

from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic import ListView, DetailView
from django_tables2 as SingleTableMixin

class TableListView(SingleTableMixin, ListView):
    table_pagination = { 'per_page': 10 }
    # add common configurable parameters here

class MyOwnCreateView(CreateView):
    success_url = "/yeah"
    # Introduce a configurable method `form_valid_hook`
    def form_valid(self, form):
        if hasattr(self,'form_valid_hook'):
            self.form_valid_hook(form)
        return super().form_valid(form)

Below is the simplified generator function for all 5 views.

BaseViews= {'create': MyOwnCreateView,
            'delete': DeleteView,
            'update': UpdateView,
            'list'  : TableListView,
            'detail': DetailView }

def generate_views(model_class, **kwargs):
    """
    Generate views for `model_class`

    Keyword parameters:
        {action}=dict(...)
        {action}_mixins=tuple(...)
        where `action` can be 'list', 'detail', 'create', 'update', 'delete'.
    """
    NewViews = {}
    for action, baseView in BaseViews.items():
        viewName = model_class.__name__ + baseView.__name__
        viewAttributes = kwargs.get(action,{})
        viewBaseCls = (baseView,) + kwargs.get(f"{action}_mixins",tuple())
        v = type(viewName, viewBaseCls, viewAttributes) # create a subclass of baseView
        v.model = model_class # bind the view to the model
        NewViews[f'{action}_view'] = v
    return NewViews

You see, the generator function is simplified to only 10 lines of code. Moreover, the API will become much cleaner:

def validate_author(self, form):
    send_email(form)

AuthorViews = generate_views(Author, 
                             create=dict(
                                 success_url='/thanks/',
                                 form_valid_hook=validate_author), 
                             ... )

How to use mixins in this API

In the above example, I use a hook/callback function form_valid_hook to inject an email-sending procedure before the form data are saved. This is ugly because the configurables for the email will be in the module scope. It's better to refactor it into a mixin class.

from django.core.mail import send_mail

class FormEmailMixin:
    from_email = '[email protected]'
    subject_template = 'We hear you'
    message_template = 'Hi {username}, ...'

    def form_valid(self, form):
        user_info = dict( username = self.request.user.username
                          to_email = ... )
        send_mail(subject_template.format(**user_info),
                  message_template.format(**user_info)
                  self.from_email , [user_info['to_email'],] )
        return super().form_valid(form)

Then you can use this mixin class in the API call.

AuthorViews = generate_views( Author, 
                  create={ 'message_template': 'Dear Author {username}, ...' }, 
                  create_mixins = (FormEmailMixin,) )
like image 177
gdlmx Avatar answered Oct 16 '22 15:10

gdlmx