When I was working on an ERP project for a company, that had all their production managed through spreadsheets, they asked me if I can make the interface resemble that of MS Excel, to make the transition less painful for their employees. Here's how I ended up emplementing it. First, we create a class-based view that takes a model as an attribute and renders a formset with 1 extra form to create new inputs.
from django.shortcuts import render, redirect
from django.forms import modelformset_factory, BaseModelFormSet
from django.http import HttpResponseNotAllowed
class ExcelView():
model = None
form_class = None
formset = BaseModelFormSet
template_name = 'excelview.html'
redirect_url = '.'
edit_url = ''
extra = 1
@classmethod
def as_view(cls):
def view(request, *args, **kwargs):
self = cls(request, *args, **kwargs)
return self.dispatch(request, *args, **kwargs)
return view
def dispatch(self, request, *args, **kwargs):
allowed_methods = ["GET", "POST"]
if request.method not in allowed_methods:
return HttpResponseNotAllowed(allowed_methods)
method = getattr(self, self.request.method.lower())
return method(request, *args, **kwargs)
def __init__(self, request, **kwargs):
self.request = request
for key, value in kwargs.items():
setattr(self, key, value)
def get_fields(self):
qs = Column.objects.filter(
profile=self.request.user.profile,
model_name=str(self.model._meta),
show=True,
)
field_names = []
verbose_field_names = []
for q in qs:
field = self.model._meta.get_field(q.field_name)
field_names.append(q.field_name)
verbose_field_names.append(
gen_field_ver_name(field.verbose_name))
context = {
'field_names': field_names,
'verbose_field_names': verbose_field_names,
}
return context
def get_queryset(self):
return self.model.objects.all()
def get_form_kwargs(self):
return {}
def get_modelformset(self, data=None):
modelformset = modelformset_factory(
self.model,
form=self.form_class,
fields=self.get_fields()['field_names'],
formset=self.formset,
extra=self.extra,
)
return modelformset(data, queryset=self.get_queryset(),
form_kwargs=self.get_form_kwargs())
def get_context_data(self, *args, **kwargs):
context = {}
fields = self.get_fields()
context['fields'] = fields.get('verbose_field_names')
if 'formset' in kwargs:
context['formset'] = kwargs['formset']
else:
context['formset'] = self.get_modelformset()
return context
def render_to_response(self, context):
return render(
request=self.request,
template_name=self.template_name,
context=context,
)
def get(self, request, *args, **kwargs):
return self.render_to_response(self.get_context_data())
def post(self, request, *args, **kwargs):
formset = self.get_modelformset(request.POST)
if formset.is_valid() and write_check(request.user):
formset.save()
return redirect(self.redirect_url)
else:
return self.render_to_response(
self.get_context_data(formset=formset))
def gen_field_ver_name(str):
new_str = str[0].capitalize() + str[1:]
return new_str.replace("_", " ")
def write_check(user):
return user.profile.level in ('A', 'W')
Method get_fields() let's us customize the columns of our table, re-arranging order and choosing which fields to show. I decided to make it customizable for each user by creating a separate model, that is generated on creating a new profile.
class Column(models.Model):
profile = models.ForeignKey(Profile, on_delete=models.CASCADE)
model_name = models.CharField(max_length=24)
field_name = models.CharField(max_length=24)
show = models.BooleanField(default=True)
order = models.PositiveSmallIntegerField(null=True)
class Meta:
ordering = ['profile', 'model_name', 'order']
def generate_columns(profile, model):
fields = model._meta.get_fields()
last_column = model.objects.all().last()
i = last_column.order + 1
exclude = ('id',)
for f in fields:
if f.name not in exclude:
new_column, created = Column.objects.get_or_create(
profile=profile,
model_name=str(model._meta),
field_name=f.name,
order=i,
)
if created:
i += 1
Now all we have to do is create a form and and an inherent view for each model. We can assign a class form_field to each form field so we can later configure the CSS to make it look more excel-ey.
class MyModelForm(forms.Form):
class Meta:
model = MyModel
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for f in self.fields:
self.fields[f].widget.attrs.update({'class': 'form_field'})
class MyModelExcelView(ExcelView):
model = MyModel
form_class = MyModelForm
edit_url = 'mymodel_edit'
The template would look something like this:
<form method='post' id="data_form">
{% csrf_token %}
{{ formset.management_form }}
<table class="formset-table {{ font_class }}">
<thead>
<tr>
{% for field in fields %}
<th> {{ field }} </th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for form in formset %}
<tr>
{% for field in form %}
{% if '-id' in field.auto_id %}
{{ field }}
{% if field.value and edit_url %}
<td class="formset_field">
<a href="{% url edit_url field.value %}">
<img src="{% static 'img/edit.png' %}">
{{ field }}
</a>
</td>
{% endif %}
{% else %}
<td> {{ field }}{{ field.errors }}</td>
{% endif %}
{% endfor %}
</tr>
{{ form.non_field_errors }}
{% endfor %}
</tbody>
</table>
{{ formset.non_form_errors }}
</form>
I also added a button at the end of each row that links to the instance's detail/edit view.
Log in to leave comments