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).
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.
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.
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),
... )
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,) )
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With