Excel-styled class-based view in Django

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