[PYTHON] Divide data into project-like units with Django (2)

Preface

This is a continuation of this article. A "project-like unit" is a "project" in a task management tool, etc., and here it is a "performance" because it manages plays.

---> GitHub repository (Python 3.8.1, Django 3.0.3)

Article content

Last time, we created "performances" and "performance users" and controlled access. We also made it possible for "users of a performance" to add data (actors) belonging to that performance.

This time, I would like to think about the interface for adding and editing data belonging to the performance.

Specific goals

For example, if you create a ForeignKey field called" Casting "in the data" Characters "and refer to" Actors ", you want to be able to select only the actors for that performance. However, just by creating Model and View as shown below, all actors (including actors belonging to other performances) can be selected as cast.

models.py


from django.db import models

class Character(models.Model):
    '''Character
    '''
    production = models.ForeignKey(Production, verbose_name='Performance',
        on_delete=models.CASCADE)
    name = models.CharField('Role name', max_length=50)
    cast = models.ForeignKey(Actor, verbose_name='Casting',
        on_delete=models.SET_NULL, blank=True, null=True)

views.py


from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from .models import Character

class ChrCreate(LoginRequiredMixin, CreateView):
    '''Additional view of Character
    '''
    model = Character
    fields = ('name', 'cast')
Add characters.png

▲ You can choose the actors of other performances.

Organize what you want to do

Consider adding a "character" to a "performance". "Characters" has a field called "Casting", and "refer to" actor data "belonging to the performance".

UI for selecting a cast

The above illustration is "an example where the actors of other performances can choose", but except for that problem, the UI is fine.

Validation

Considering that the request sent from the web browser may have been tampered with, not only the UI choices but also the "accepted value" should be limited to "only the actor of the performance".

When editing later

Think about when you want to edit the added "characters" later. It would be the original Mokuami if the affiliation (performance) of the character who cast the "actor of the performance" could be changed. It seems good to make it impossible to change the affiliation of the "character" once created.

UI changes

This app uses a class-based view (CreateView), so the form is automatically generated and you can write the template as follows:

modelname_form.html


<form method="post">
    {% csrf_token %}
    <table>
        {{ form }}
    </table>
    {% if object %}
    <input type="submit" value="update">
    {% else %}
    <input type="submit" value="add to">
    {% endif %}
</form>

Since the input UI of each field of the object is expanded in this part called {{form}}, you can also rewrite it. In that case, you need to pass the cast choice as context and also separate the form writing style in the template for each field.

Here, we will stop it and modify the contents of the form expanded in the {{form}} part in advance.

Modify the contents of the form

Another way to modify the contents of a form is to create a subclass of the form, but you can modify it without doing so. Here, I will modify it in the method of the view. We'll talk about subclassing forms later.

Since a form is a type of context passed from a view to a template, you can overrideget_context_data ()and modify it as follows: ProdBaseCreateView, which the view inherits in the code below, is an abstract class that receives the performance ID and controls access (see Previous article for details). Please see. You don't have to worry too much now).

rehearsal/views/views.py


class ChrCreate(ProdBaseCreateView):
    '''Additional view of Character
    '''
    model = Character
    fields = ('name', 'cast')
    
    def get_context_data(self, **kwargs):
        '''Modify the parameters passed to the template
        '''
        context = super().get_context_data(**kwargs)
        
        #Show only the actors of the performance
        actors = Actor.objects.filter(production=self.production)
        #Create a choice
        choices = [('', '---------')]
        choices.extend([(a.id, str(a)) for a in actors])
        #Set in Form
        context['form'].fields['cast'].choices = choices
        
        return context

Detailed explanation

--context ['form'] allows you to see the form the view is trying to pass to the template. --If you set choices in a field that the form has, it will be displayed as a choice. --Each element of choices is a tuple of" value "and" display string ". --This app allows you to set cast to empty, so the value of the first option is empty.

Addition of characters 2.png

▲ Only the actors of the performance to which the characters belong were displayed.

Validation

I see information that setting choices in a field on a form automatically repels other values, but when I actually tried it, it didn't. It may be because the field with choices was ForeignKey.

Tamper.png

▲ Try tampering with the options in your browser.

I was able to add.png

▲ The falsified value has been added.

Modify validation results

You can override the view's methods to modify the validation results. In the end, it's cleaner to create a subclass of the form, but first let's do it with just the view.

rehearsal/views/views.py


class ChrCreate(ProdBaseCreateView):

    # (Omission)
    
    def form_valid(self, form):
        '''When passed validation
        '''
        #Validation failed if cast is not an actor in the same performance
        instance = form.save(commit=False)
        if instance.cast.production != self.production:
            return self.form_invalid(form)
        
        return super().form_valid(form)

form_valid () is a view-side processing method that is called when the form passes validation. If you forcibly call the method (form_invalid ()) that is called when the validation is not passed, you can prevent the save.

Detailed explanation

--form.save (commit = False) gets the object that the form is trying to save. If you set commit = True, it will be saved and then retrieved. --You can also get objects with self.object and form.instance, but I don't really understand the difference. I would be grateful if anyone who knows this could let me know. --self.production in the ʻifstatement is an attribute of the view's superclassProdBaseCreateView`, which has the" performance "of the object you are trying to save (see [For more information]. See the previous article](https://qiita.com/satamame/items/959e21ade18c48e1b4d6).

The unpleasant part of this

You may find it unpleasant to call form_invalid () from form_valid () as above. If you think of the meaning of form_valid () as "the place where it is called when it passes the validation and performs the subsequent processing", it is because there is a sense of incongruity like an act of over-righting. As a result, form_invalid () is called, and I feel that such a chabudai return is allowed.

In the worst case scenario, you could allow form_invalid () to call form_valid () as well, resulting in a circular call. Of course, if you decide on rules such as making it one-way, there is no problem with the above method.

Another way (form subclassing)

If you're subclassing your form, you don't even need to override the get_context_data () you did in "Modifying the UI". Instead, the view overrides a method called get_form_kwargs (). It is an image that customizes the generated form itself, instead of modifying the "form to be passed to the template".

Pass data to the form

The return value that can be obtained with get_form_kwargs () is a dictionary of keyword arguments passed to the form constructor. So if you add an entry to this, you can use it on the form side.

The following is an example of overriding get_form_kwargs () assuming a class called ChrForm is defined. If you set form_class in the view, don't set fields (I think it would have resulted in an error).

rehearsal/views/views.py


from rehearsal.forms import ChrForm

class ChrCreate(ProdBaseCreateView):
    '''Additional view of Character
    '''
    model = Character
    form_class = ChrForm
    
    def get_form_kwargs(self):
        '''Modify the information passed to the form
        '''
        kwargs = super().get_form_kwargs()
        
        #Pass production because it is used for validation on the form side
        kwargs['production'] = self.production
        
        return kwargs

Form side implementation

rehearsal/forms.py


from django import forms
from production.models import Production
from .models Character

class ChrForm(forms.ModelForm):
    '''Character addition / update form
    '''
    class Meta:
        model = Character
        fields = ('name', 'cast')
    
    def __init__(self, *args, **kwargs):
        #Extract the parameters added in view
        production = kwargs.pop('production')
        
        super().__init__(*args, **kwargs)
        
        #Casting can only be selected by actors of the same performance
        queryset = Actor.objects.filter(production=production)
        self.fields['cast'].queryset = queryset

You are setting queryset instead of choices in the cast field. This will allow you to generate and validate choices based on the query set you have set.

If you create this form, you can use it not only for the additional view but also for the update view.

Now, let's tamper with the options we did in "Validation".

Repelled.png

When editing later

Prohibit changes to production in these add / update views so that you cannot change the" performance "of the" characters "or" actors ".

Change editable fields

If you normally create a form with model in Django, all the fields will be editable, so you need to specify the editable fields.

--If you want to specify form_class in the view, as you did in Form Subclassing, specify fields in the form's Meta. --If you do not specify form_class, specify fields in the view.

I don't edit it, but I want to refer to it

You may also want to reference fields that you do not edit from the form or template. In either case, as we've mentioned, you can use the views get_context_data () and get_form_kwargs ().

Summary

--I figured out how to make production the same when associating two types of data ("characters" and "actors"), both of which have a ForeignKey field called production. --Although it was possible to modify both the UI and validation on the view side, subclassing the form resulted in a cleaner implementation. --Overridden the get_context_data () method when passing data from the view to the template. --Overridden the get_form_kwargs () method when passing data from the view to the form. --To specify input value choices on the form side, set the field queryset and both the UI and validation followed suit.

I wrote the continuation

-Django divides data into project units (3)

Recommended Posts

Divide data into project-like units with Django (2)
Divide your data into project-like units with Django (3)
Divide your data into project-like units with Django
Save tweet data with Django
Divide Japanese (katakana) into syllable units [Python]
Generate and post dummy image data with Django
Internationalization with django
CRUD with Django
Make holiday data into a data frame with pandas
Try to divide twitter data into SPAM and HAM
Divide the dataset (ndarray) into arbitrary proportions with NumPy
Authenticate Google with Django
Data analysis with python 2
Upload files with Django
Development digest with Django
Output PDF with Django
Markdown output with Django
Twitter OAuth with Django
Reading data with TensorFlow
Getting Started with Django 1
Send email with Django
File upload with django
Data visualization with pandas
Data manipulation with Pandas!
Use LESS with Django
Shuffle data with pandas
Pooling mechanize with Django
Data Augmentation with openCV
Use MySQL with Django
Normarize data with Scipy
Data analysis with Python
Start today with Django
LOAD DATA with PyMysql
Getting Started with Django 2
Stylish technique for pasting CSV data into Excel with Python
I tried to divide the file into folders with Python