Class-based views

Class-based views are highly practical for the add/edit view of physical processes because they keep code duplication at a minimum. In some cases, you get away with only a few lines of code. Mixin classes reduce the redundancy further. Although it is still possible to have ordinary view functions for physical processes, we do not recommend this. If you follow the convention of calling your view class “EditView” and place it in a module called class_name.py, the PatternGenerator will detect it and create the URL dispatch for it.

The API

The API of JuliaBase’s class-based view classes is best described by discussing the attributes and methods of the common base class ProcessWithoutSamplesView. Not only if you derive your views, but also if you need to define your own abstract view class, you should derive it from one of the concrete classes presented in the next section, though, because you probably want to re-use part of their functionality.

This class is found in the module samples.utils.views.class_views.

class ProcessWithoutSamplesView(**kwargs)

Abstract base class for the classed-based views. It deals only with the process per se, and in partuclar, with no samples associated with this process. This is done in the concrete derived classes ProcessView (one sample) and ProcessMultipleSamplesView (multiple samples). So, you should never instantiate this one.

The methods that you most likely want to redefine in you own concrete class are, with decreasing probability:

  • is_referentially_valid()
  • save_to_database()
  • get_next_id()
  • build_forms()
  • get_title()
  • get_context_data()

Note that for is_referentially_valid(), save_to_database(), build_forms(), and get_context_data(), it is necessary to call the inherited method.

Since you connect forms with the view class, the view class expects certain constructor signatures of the forms. As for the process model form, it must accept the logged-in user as the first argument. This is the case for ProcessForm and DepositionForm, so this should not be a problem. The derived class (see below) may impose constrains on their external forms either.

Variables:
  • form_class – The model form class of the process of this view.
  • model – The model class of the view. If not given, it is derived from the process form class.
  • class_name – The name of the model class, e.g. "clustertooldeposition".
  • process – The process instance this view is about. If we are about to add a new process, this is None until the respective form is saved.
  • forms – A dictionary mapping template context names to forms, or lists of forms. Mandatory keys in this dictionary are "process" and "edit_description". (Derived classes add "sample", "samples", "remove_from_my_samples", "layers", etc.)
  • data – The POST data if we have a POST request, or None otherwise.
  • id – The ID of the process to edit, or None if we are about to add a new one. This is the recommended way to distinguish between editing and adding.
  • preset_sample – The sample with which the process should be connected by default. May be None.
  • request – The current request object. This is inherited from Django’s view classes.
  • template_name – The file name of the rendering template, with the same path syntax as in the render() function.
  • identifying_field – The name of the field in the process which is the poor man’s primary key for this process, e.g. the deposition number. It is taken from the model class.
build_forms()

Fills the forms dictionary with the forms, or lists of them. In this base class, we only add "process" itself and "edit_description". Note that the dictionary key is later used in the template as context variable.

This method has no parameters and no return values, self is modified in-situ. It is good habit to check for a key before setting it, allowing derived methods to set it themselves without doing double work.

get_context_data(**kwargs)

Generates the template context. In particular, we inject the forms and the title here into the context. This method is part of the official Django API.

Return:the context dict
Return type:dict
get_next_id()

Gets the next identifying value for the process class. In its default implementation, it just takes the maximal existing value and adds 1. This needs to be overridden if the identifying field is non-numeric.

Return:The next untaken of the identifying field, e.g. the next free deposition number.
Return type:object
get_title()

Creates the title of the response. This is used in the <title> tag and the <h1> tag at the top of the page.

Return:the title of the response
Return type:str
is_all_valid()

Checks whether all forms are valid. Moreover, this method guarantees that the is_valid() method of every form is called in order to collect all error messages.

You may mark any unbound form as valid for this method by setting its attribute dont_check_validity to True. If it is not present, it is assumed to be False. This is helpful for marking unbound forms that should not let the request fail during a POST request. An example is a form in which the user enters the number of to-be-added sub-processes. It is reset (emptied) after each POST request by setting it to a pristine unbound form. However, this must not prevent the view from succeeding.

Return:whether all forms are valid
Return type:bool
is_referentially_valid()

Checks whether the data of all forms is consistent with each other and with the database. This is the partner of is_all_valid() but checks the inter-relations of data.

This method is frequently overriden in concrete view classes.

Note that a True here does not imply a True from is_all_valid(). Both methods are independent of each other. In particular, you must check the validity of froms that you use here.

Return:whether the data submitted to the view is valid
Return type:bool
save_to_database()

Saves the data to the database.

Return:the saved process instance
Return type:samples.models.PhysicalProcess
startup()

Fetch the process to-be-edited from the database and check permissions. This method has no parameters and no return values, self is modified in-situ.

Main classes

The following names are found in the module samples.utils.views.

class ProcessView(**kwargs)

View class for physical processes with one sample each. The HTML form for the sample is called sample in the template. Typical usage can be very short:

from samples.utils.views import ProcessForm, ProcessView

class LayerThicknessForm(ProcessForm):
    class Meta:
        model = LayerThicknessMeasurement
        fields = "__all__"

class EditView(ProcessView):
    form_class = LayerThicknessForm
class ProcessMultipleSamplesView(**kwargs)

View class for physical processes with one or more samples each. The HTML form for the sample list is called samples in the template. The usage is analogous to ProcessView.

class DepositionView(**kwargs)

View class for views for depositions with layers. The layers of the process must always be of the same type. If they are not, you must use DepositionMultipleTypeView instead. Additionally to form_class, you must set the step_form_class class variable to the form class to be used for the layers.

The layer form should be a subclass of SubprocessForm.

class DepositionMultipleTypeView(**kwargs)

View class for depositions the layers of which are of different types (i.e., different models). You can see it in action in the module institute.views.samples.cluster_tool_deposition. Additionally to the class variable form_class, you must set:

Variables:
  • step_form_classes – This is a tuple of the form classes for the layers
  • short_labels(optional) This is a dict mapping a layer form class to a concise name of that layer type. It is used in the selection widget of the add-step form.
class SubprocessForm(view, *args, **kwargs)

Model form class for subprocesses. Its only purpose is to eat up the view parameter to the constructor so that you need not redefine the constructor every time.

class SubprocessMultipleTypesForm(view, data=None, **kwargs)

Abstract model form for all step types in a process. It is to be used in conjunction with MultipleStepTypesMixin. See the views of th cluster-tool deposition in the INM “institute” app for an example for how to use this class.

Mixins

class RemoveFromMySamplesMixin(**kwargs)

Mixin for views that like to offer a “Remove from my samples” button. In the template, they may add the following code:

{{ remove_from_my_samples.as_p }}

This mixin must come before the main view class in the list of parents.

class SamplePositionsMixin(**kwargs)

Mixin for views that need to store the positions the samples used to have during the processing. The respective process class must inherit from ProcessWithSamplePositions. In the edit template, you must add the following code:

{% include "samples/edit_sample_positions.html" %}

This mixin must come before the main view class in the list of parents.

class SubprocessesMixin(**kwargs)

Mixing for views that represent processes with subprocesses. Have a look at institute.views.samples.solarsimulator_measurement for an example. In a way, it is a light-weight variant of the MultipleStepsMixin below. In contrast to that, this mixin doesn’t order its subprocesses (you still may enforce ordering in the show template).

For this to work, you must define the following additional class variables:

  • subform_class: the model form class for the subprocesses
  • process_field: the name of the field of the parent process in the subprocess model
  • subprocess_field: the related_name parameter in the field of the parent process in the subprocess model

You should derive the model form class of the subprocess from SubprocessForm. This is not mandatory but convenient, see there.

In the template, the forms of the subprocesses are available in a list called subprocesses. Furthermore, you should include

{{ number.as_p }}

in the template so that the user can set the number of subprocesses.

This mixin must come before the main view class in the list of parents.

class MultipleStepsMixin(**kwargs)

Mixin class for views for processes with steps. The steps of the process must always be of the same type. If they are not, you must use MultipleTypeStepsMixin instead. The step model must include a field called “number”, which should be ordered. This mixin must come before the main view class in the list of parents.

You can see it in action in the module institute.views.samples.five_chamber_deposition. In the associated edit template, you can also see the usage of the three additional template variables steps, change_steps (well, at least their combination steps_and_change_steps), and add_steps.

Additionally to form_class, you must set the following class variables:

Variables:
  • step_form_class – the form class to be used for the steps.
  • process_field – to the name of the field of the parent process in the step model.
class MultipleStepTypesMixin(**kwargs)

Mixin class for processes the steps of which are of different types (i.e., different models). The step model must include a field called “number”, which should be ordered. This mixin must come before the main view class in the list of parents.

You can see it in action in the module institute.views.samples.cluster_tool_deposition. In the associated edit template, you can also see the usage of the three additional template variables steps, change_steps (well, at least their combination steps_and_change_steps), and add_steps. Moreover, note the use of the step_type and type fields of each layer (= step).

Additionally to the class variable form_class, you must set:

Variables:
  • step_form_classes – This is a tuple of the form classes for the steps
  • process_field – to the name of the field of the parent process in the step model.
  • short_labels(optional) This is a dict mapping a step form class to a concise name of that step type. It is used in the selection widget of the add-step form.

Sub-processes

Quite often, there is the need to divide a process further into sub-processes. JuliaBase realises this by special mixin classes. In this section, I discuss the bigger picture of it. Look at the reference above for the details and further information.

There are three mixins that deal with sub-processes:

SubprocessesMixin

This is a lightweight solution if you just want to have the forms for sub-processes auto-generated. It lets you create an edit/add view which allows the user to enter the number of sub-processes, and to enter the sub-processes’ data. This mixin does not enforce any ordering of the sub-processes – you may, howvever, enforce an ordering in the show view yourself, possibly by a model setting.

Because of the lack of user convenience, this mixin is useful particularly for edit/add views which are primarily used by programs (e.g. crawlers) rather than by human beings. The solar simulator of the INM institute app demonstrates how to use this mixin.

MultipleStepsMixin

This mixin realises the JuliaBase concept of a step. Steps are ordered sub-processes. On the model layer, they are models not derived from Process that contain an interger field number. This field is used to define the ordering, and helps JuliaBase to provide some convenience functionality: Re-ordering steps, duplicating them, deleting them. The parent model, for example the deposition process class, must define a method steps() that returns a query set of all steps, as in:

def get_steps(self):
    return self.layers

The MultipleStepsMixin is the main ingredient for the class DepositionView. You can see the latter in action in the 5-chamber deposition views of the INM institute app.

“My steps”

Moreover with steps, your users can use something called “my steps”. It is a list of favourite steps that occur frequently. Every step in this list has a nickname, chosen by the user. When composing a new process, the user can select from this list instead of entering the step data manually. JuliaBase stores the “my steps” list for each user, however, you must add a view that lets the user set this list for each process class(es) that should be supported. In JuliaBase’s example app “institute”, a “my layers” view is included which realises this functionality for deposition layers.

MultipleStepTypesMixin

This mixin is the same as above, but each step may be of a different model class. Using this mixin is slightly more complicated but also more powerful for obvious reasons. The steps must be of a common base model class, of which the concrete model classes are derived. Consequently, the base model class must inherit from jb_common.models.PolymorphicModel. The steps() method may return instances of the base model class, because the class-based view already takes care of finding the actual instance.

In the view model, you should derive the forms classes for your step types from SubprocessMultipleTypesForm. This takes care of an extra field step_type that helps the view class to identify the step type from he HTTP POST data.

This forms the basis of DepositionMultipleTypeView. You can see the latter in action in the cluster-tool deposition views of the INM institute app.