[PYTHON] Divide your data into project-like units with Django

Preface

Article content

--I implemented in Django to create multiple "projects" like those found in task management tools and manage items within each project. ――The users who can access each project also change.

What you are making

――It is a tool to assist the theater training plan. ――The "project unit" mentioned in the title is "performance" here. ――For each performance, manage the training schedule, actors, characters, scenes, etc. ---> GitHub repository (Python 3.8.1, Django 3.0.3)

Article concept

――I couldn't find a sample of what I wanted to do on the net, so I thought about it myself and implemented it. Share it ――If you know a better way, I would be grateful if you could tell me.

Basic parts

First, create "performances" and "performance users", and list the performances that the logged-in user is involved in.

Performance (project unit) model

The only field is name.

production/models.py


from django.db import models

class Production(models.Model):
    name = models.CharField('Performance name', max_length=50)

    def __str__(self):
        return self.name

Performance user model

It is a user for each performance. I've set up fields for permissions by referring to performances and users (users managed by Django) in ForeignKey. See here for ʻAUTH_USER_MODEL`. In this example, when a performance or user is deleted, the performance user is also deleted.

production/models.py


from django.conf import settings

class ProdUser(models.Model):
    '''Users and permissions for each performance
    '''
    production = models.ForeignKey(Production, verbose_name='Performance',
        on_delete=models.CASCADE)
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
        verbose_name='User', on_delete=models.CASCADE)
    is_owner = models.BooleanField('Ownership', default=False)
    is_editor = models.BooleanField('Editing rights', default=False)

View of performance list

This is a list of performances that the logged-in user is involved in. ʻGet the "performance user" such that the userfield is the logged-in user. You may also get it by back-referencing fromself.request.user`.

Since the performance name and "performance ID" can be used indirectly, we will display the list of performances. In other words, if you have the performance user data, you do not need to get the performance itself.

production/views.py


from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import ProdUser

class ProdList(LoginRequiredMixin, ListView):
    '''Production list view
    
The model is ProdUser because only the performances of the logged-in user are displayed.
    '''
    model = ProdUser
    template_name = 'production/production_list.html'
    
    def get_queryset(self):
        '''Filter the records displayed in the list
        '''
        #Get your own ProdUser
        prod_users = ProdUser.objects.filter(user=self.request.user)
        return prod_users

If you write an appropriate URL pattern and template, add "Performance" and "Performance User" on the Admin screen, and log in, a list of performances for which that person is the user will be displayed.

Performance list.png

-> Template looks like this

Make a new performance

The task management tool is like creating a new project.

View to add performances

If you look at the "Performance List" template above, you will see a link called "New". I press this to bring up the following view. No parameters required.

production/views.py


from django.views.generic.edit import CreateView
from .models import Production

class ProdCreate(LoginRequiredMixin, CreateView):
    '''Additional view of Production
    '''
    model = Production
    fields = ('name',) #The only edit item is name
    template_name_suffix = '_create'
    success_url = reverse_lazy('production:prod_list') #After adding, go to the list
    
    def form_valid(self, form):
        '''When passed validation
        '''
        #Get the saved record
        new_prod = form.save(commit=True)
        
        #Add yourself as the owner to the performance user
        prod_user = ProdUser(production=new_prod, user=self.request.user,
            is_owner=True)
        prod_user.save()
        
        messages.success(self.request, str(new_prod) + "is created.")
        return super().form_valid(form)
    
    def form_invalid(self, form):
        '''When the addition fails
        '''
        messages.warning(self.request, "Could not create.")
        return super().form_invalid(form)

The page is simple because the field is just name (performance name), but the logged-in user becomes the" performance user "and is also given ownership.

New performance.png

-> Template looks like this

Now the logged-in user can create their own "performance".

Top view of each performance

Click each performance in the performance list to move to the top view of that performance. From here, each performance will be a different world.

Looking at the performance list template above, the performance name part is the link that opens the top view of that "performance".

production/templates/production/production_list.html


    <tr>
        <td>
            <a href="{% url 'rehearsal:rhsl_top' prod_id=item.production.id %}">
                {{ item.production }}</a>
        </td>
        <td>{{ item.is_owner }}</td>
        <td>{{ item.is_editor }}</td>
    </tr>

I created the top view in an application called rehearsal, so it looks like this. This link requires a "performance ID" as a parameter (the parameter name in the code is prod_id). Since ʻitem being displayed is a "performance user", pass ʻitem.production.id to the "performance ID".

The view is a simple menu screen.

rehearsal/views/views.py


from django.views.generic import TemplateView
from django.core.exceptions import PermissionDenied
from .view_func import *

class RhslTop(LoginRequiredMixin, TemplateView):
    '''Rehearsal top page
    '''
    template_name = 'rehearsal/top.html'
    
    def get(self, request, *args, **kwargs):
        '''Handler that received the request at the time of display
        '''
        #Obtain performance users from access information and inspect access rights
        prod_user = accessing_prod_user(self)
        if not prod_user:
            raise PermissionDenied
        
        #Have production as an attribute of view
        self.production = prod_user.production
        
        return super().get(request, *args, **kwargs)

The reason we have the production ("performance" object) as an attribute of view is to reference it from the template.

ʻAccessing_prod_user ()` is a function defined in another module called view_func.py and you are logged in. If the user of is the user of the performance, then the "performance user" object is returned.

rehearsal/views/view_func.py


from production.models import ProdUser

def accessing_prod_user(view, prod_id=None):
    '''Get the corresponding ProdUser from the access information
    
    Parameters
    ----------
    view : View
View from which access information is obtained
    prod_id : int
Prod in URLconf_Specify if there is no id
    '''
    if not prod_id:
        prod_id=view.kwargs['prod_id']
    prod_users = ProdUser.objects.filter(
        production__pk=prod_id, user=view.request.user)
    if len(prod_users) < 1:
        return None
    return prod_users[0]

If the logged-in user is not a performance user, it returns None, which allows access control.

Rehearsal top.png

-> Templates look like this

It's just a linked list, but every link has a prod_id parameter.

Data belonging to the performance

Now that we have the groundwork to divide the data in units called "performances", we will be able to handle the data belonging to each performance.

Actor model

It is the data of the actors involved in the performance. The data belonging to the performance should have a ForeignKey called production to decide which performance it belongs to.

rehearsal/models.py


class Actor(models.Model):
    '''actor
    '''
    production = models.ForeignKey(Production, verbose_name='Performance',
        on_delete=models.CASCADE)
    name = models.CharField('name', max_length=50)
    short_name = models.CharField('Abbreviated name', max_length=5, blank=True)
    
    class Meta:
        verbose_name = verbose_name_plural = 'actor'
    
    def __str__(self):
        return self.name
    
    def get_short_name(self):
        return self.short_name or self.name[:3]

short_name is used when displaying in tabular format.

View of actor list

It is a view that transitions from the top view of the performance by clicking "Actor List". Of course, only the actors of the performance are displayed, and it cannot be seen by anyone other than the "performance user" of the performance.

Before looking at the actor list, let's take a look at the link to the actor list in the top view of the performance.

rehearsal/templates/rehearsal/top.html


<li><a href="{% url 'rehearsal:actr_list' prod_id=view.production.id %}">
List of actors</a></li>

Since we had a production ("performance" object) as an attribute of view, we can get the "performance ID" in the form view.production.id. By passing it to the prod_id parameter, you will be taken to the list of actors for that performance.

Now, let's take a look at the list of actors.

rehearsal/views/views.py


class ActrList(ProdBaseListView):
    '''List view of Actor
    '''
    model = Actor
    
    def get_queryset(self):
        '''Filter the records displayed in the list
        '''
        prod_id=self.kwargs['prod_id']
        return Actor.objects.filter(production__pk=prod_id)\
            .order_by('name')

The inherited ProdBaseListView is an abstract class for displaying a list of data belonging to a performance. Since we will create other views like this, we have put together an abstract class for access control and so on. Let's see this too.

rehearsal/views/views.py


class ProdBaseListView(LoginRequiredMixin, ListView):
    '''ListView Base class to check permissions
    '''
    def get(self, request, *args, **kwargs):
        '''Handler to receive the request at the time of display
        '''
        #Obtain performance users from access information and inspect access rights
        prod_user = accessing_prod_user(self)
        if not prod_user:
            raise PermissionDenied
        
        #Have the accessing ProdUser as an attribute of view
        #To decide if there is an add button in the template
        self.prod_user = prod_user
        
        return super().get(request, *args, **kwargs)
    
    def get_context_data(self, **kwargs):
        '''Modify the parameters passed to the template
        '''
        context = super().get_context_data(**kwargs)
        
        #back button,Prod for add button_set id
        context['prod_id'] = self.kwargs['prod_id']
        
        return context

The performance ID (prod_id) is included in the context so that it can be easily referenced from the template. You can refer to it by doing view.prod_user.production.id without putting it in context, but it is long.

-> Templates look like this

Let's add some "actors" on the Admin screen to display the list of actors.

List of actors.png

View to add actors

If you look at the "Actor List" template above, you'll see a link called "Add". And it is only visible to users who own or edit the performance. Of course, users who can't see the link can open the URL directly in the browser, so access control is also required for the view to which the actor is added.

Again, we are creating an abstract class similar to the ProdBaseListView above. Let's look at the abstract class first.

rehearsal/views/views.py


class ProdBaseCreateView(LoginRequiredMixin, CreateView):
    '''CreateView Base class to check permissions
    '''
    def get(self, request, *args, **kwargs):
        '''Handler to receive the request at the time of display
        '''
        #Inspect edit rights to get access users
        prod_user = test_edit_permission(self)
        
        #Have production as an attribute of view
        #To display as a fixed element in the template
        self.production = prod_user.production
        
        return super().get(request, *args, **kwargs)
    
    def post(self, request, *args, **kwargs):
        '''Handler to receive request at save
        '''
        #Inspect edit rights to get access users
        prod_user = test_edit_permission(self)
        
        #Have production as an attribute of view
        #To set in the instance when saving
        self.production = prod_user.production
        
        return super().post(request, *args, **kwargs)
    
    def form_valid(self, form):
        '''When passed validation
        '''
        #Set the production of the record you want to add
        instance = form.save(commit=False)
        instance.production = self.production
        
        messages.success(self.request, str(instance) + "Added.")
        return super().form_valid(form)
    
    def form_invalid(self, form):
        '''When the addition fails
        '''
        messages.warning(self.request, "It could not be added.")
        return super().form_invalid(form)

test_edit_permission () is a function defined in view_func.py by the logged-in user. If you own or edit the performance, it returns the "performance user".

def test_edit_permission(view, prod_id=None):
    '''Check editing rights
    
    Returns
    -------
    prod_user : ProdUser
Accessing ProdUser who has edit rights
    '''
    #Get performance users from access information
    prod_user = accessing_prod_user(view, prod_id=prod_id)
    if not prod_user:
        raise PermissionDenied
    
    #Deny access if you do not have ownership or editing rights
    if not (prod_user.is_owner or prod_user.is_editor):
        raise PermissionDenied
    
    return prod_user

The permissions are checked both when displaying the view (get method) and when pressing the" Add "button for additional processing (post method).

In addition, this abstract class also implements the common process of "when adding data belonging to a performance, set the performance in the production field ".

Now for the view to add an actor.

rehearsal/views/views.py


class ActrCreate(ProdBaseCreateView):
    '''Additional view of Actor
    '''
    model = Actor
    fields = ('name', 'short_name')
    
    def get_success_url(self):
        '''Dynamically give the transition destination when the addition is successful
        '''
        prod_id = self.production.id
        url = reverse_lazy('rehearsal:actr_list', kwargs={'prod_id': prod_id})
        return url

After adding it, you will be returned to the list of actors "for that performance".

Add actor.png

-> Templates look like this

Summary

――It is a tool that assists the rehearsal plan of the play, and I made multiple "performances" and separated the data for each performance. ――We created a model called "Performance User" so that access can be controlled for each user (user managed by Django). --The data belonging to the performance (actors, etc.) has a ForeignKey field called production, and when accessed from the performance menu, it is filtered by that. --When accessing the view to write data such as additions and changes, the edit right is checked both when viewing and writing. --When adding new data belonging to a performance, the performance is automatically set in the production field.

I wrote the continuation

-Django divides data into project units (2)

Recommended Posts

Divide your data into project-like units with Django (3)
Divide your data into project-like units with Django
Divide data into project-like units with Django (2)
Save tweet data with Django
Manage your data with AWS RDS
Your own Twitter client made with Django
Divide Japanese (katakana) into syllable units [Python]
Create your first app with Django startproject
Generate and post dummy image data with Django
Make holiday data into a data frame with pandas
Train Stanford NER Tagger with your own data
Internationalization with django
CRUD with Django
Try to divide twitter data into SPAM and HAM
Single sign-on to your Django application with AWS SSO
Divide the dataset (ndarray) into arbitrary proportions with NumPy