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)
Last time, I thought about the data input interface that belongs to "performance". This time, I would like to think about a mechanism to invite users other than myself and add them to the members of the "performance".
--The techniques used are summarized in "Summary". --There is a working demo on Heroku. See GitHub Pages for more information.
--Allows "performance" owners to create "invitations" for specific accounts. ――You can check or delete the created "invitation card". --The "invitation" will be displayed when you log in with the invited account. --You will be able to participate in "performances" from the "invitations" you received, and delete the "invitations" you received. ――We will make it a mechanism that is completed within the web application without using e-mail etc.
Those who invite
Invited
In this web app, ["Performance User"](https://qiita.com/satamame/items/959e21ade18c48e1b4d6#%E5%85%AC%E6%BC%94%E3%83%A6%E3%83%BC% E3% 82% B6% E3% 81% AE% E3% 83% A2% E3% 83% 87% E3% 83% AB) to Django-managed accounts and each "performance" The participation status is associated. Therefore, the process when the invited person selects "Join" is the process of creating a new "Performance User".
It has the following fields.
field | Contents |
---|---|
production | Invited "performance" |
inviter | Inviting person(Accounts managed by Django) |
invitee | Invited person(Accounts managed by Django) |
exp_dt | Invitation expiration date |
--There is a screen called "Member List", so create a button called "Invite" there. --When you press this button, the screen for creating an "invitation" will be displayed. ――On the screen to create an "invitation", you need to be able to specify the "invited person" (the contents of other fields are decided by the app).
--When the owner of "Performance" displays the "Member List" screen, the "Invitation Card" that he created will be displayed. --You can also delete it here. --When the invited person displays the "Participating Performances" screen (the screen that is displayed immediately after logging in), the "Invitation Card" addressed to you will be displayed. --However, those that have passed the expiration date will not be displayed. ――Here, you can participate in the "performance" and delete the "invitation card".
--The process when the invited person selects "Join" is the process of creating a new "Performance User".
from datetime import datetime, timezone
from django.conf import settings
from django.db import models
class Invitation(models.Model):
'''Invitation to the troupe
'''
production = models.ForeignKey(Production, verbose_name='Performance',
on_delete=models.CASCADE)
inviter = models.ForeignKey(settings.AUTH_USER_MODEL,
verbose_name='Inviting person',
related_name='inviter', on_delete=models.CASCADE)
invitee = models.ForeignKey(settings.AUTH_USER_MODEL,
verbose_name='Invited person',
related_name='invitee', on_delete=models.CASCADE)
exp_dt = models.DateTimeField(verbose_name='Deadline')
class Meta:
verbose_name = verbose_name_plural = 'Invitation to the troupe'
ordering = ['exp_dt']
def __str__(self):
return f'{self.invitee}To{self.production}Invitation to'
def expired(self):
'''Returns whether this invitation has expired
'''
now = datetime.now(timezone.utc)
return now > self.exp_dt
--Production
is defined in this web app (https://qiita.com/satamame/items/959e21ade18c48e1b4d6#%E5%85%AC%E6%BC%94-%E3%83%97%E3% 83% AD% E3% 82% B8% E3% 82% A7% E3% 82% AF% E3% 83% 88% E7% 9A% 84% E5% 8D% 98% E4% BD% 8D-% E3% 81 % AE% E3% 83% A2% E3% 83% 87% E3% 83% AB) This is a model of "performance".
――Since both ʻinviter and ʻinvitee
are accounts managed by Django, they are references to the object in settings.AUTH_USER_MODEL
.
--Please see here for ʻAUTH_USER_MODEL`.
The following is a description of View and Template.
Since it is created in an application called production
, the URL is specified as'production: usr_list'.
See Source for production / urls.py
for how to write ʻurls.py` for the call. please.
Create a View that inherits CreateView
to create an invitation.
views.py
from django.views.generic.edit import CreateView
from django.contrib import messages
from django.contrib.auth import get_user_model
from .view_func import *
from .models import Production, ProdUser, Invitation
class InvtCreate(LoginRequiredMixin, CreateView):
'''Additional view of Invitation
'''
model = Invitation
fields = ()
--The reason for emptying fields
, which may be a bit tricky, is to set all the values of the fields (except ʻid) in code instead of
Form`.
views.py
def get(self, request, *args, **kwargs):
'''Handler to receive the request at the time of display
'''
#Inspect ownership to get accessing performance users
prod_user = test_owner_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)
--We call a method called test_owner_permission ()
to allow access only to users who own the performance.
--See Source for production / view_func.py
for what this method does. ..
views.py
def post(self, request, *args, **kwargs):
'''Handler to receive request at save
'''
#Inspect ownership to get accessing performance users
prod_user = test_owner_permission(self)
#"Invited person ID" entered in the form
invitee_value = request.POST['invitee_id']
#Have a matching user as an attribute of view
user_model = get_user_model()
invitees = user_model.objects.filter(username=invitee_value)
if len(invitees) > 0:
self.invitee = invitees[0]
# prod_user,Have production as an attribute of view
#For use during validation and storage
self.prod_user = prod_user
self.production = prod_user.production
return super().post(request, *args, **kwargs)
--On the screen for creating invitations, "invited person" is written as "invited person" in the context of the UI.
--Since the "invited person" is of type ForeignKey
, normally Form
is used to display the selectable UI, but I'm wondering if all users can be selected, so the account ID (text The actual field name is ʻusername). --The process of finding the user's object based on the entered ID is done by the
post () method. --ʻUser
Use get_user_model ()
to reference the model (> Documents -model)).
This is because settings.AUTH_USER_MODEL
is a string.
--I try to set the View's ʻinvitee` attribute only when it is found, and check for its existence at the time of validation.
views.py
def form_valid(self, form):
'''When passed validation
'''
#Add failed if the POST handler did not get the user to invite
if not hasattr(self, 'invitee'):
return self.form_invalid(form)
#User ID list of performance users
prod_users = ProdUser.objects.filter(production=self.production)
prod_user_user_ids = [prod_user.user.id for prod_user in prod_users]
#ID list of invited users
invitations = Invitation.objects.filter(production=self.production)
current_invitee_ids = [invt.invitee.id for invt in invitations]
#It is not possible to invite performance users or invited users.
if self.invitee.id in prod_user_user_ids\
or self.invitee.id in current_invitee_ids:
return self.form_invalid(form)
#Set each field of the record you want to add
instance = form.save(commit=False)
instance.production = self.production
instance.inviter = self.prod_user.user
instance.invitee = self.invitee
#Deadline is 7 days
#Saved in UTC by default, but specify UTC just in case
instance.exp_dt = datetime.now(timezone.utc) + timedelta(days=7)
messages.success(self.request, str(instance.invitee) + "Was invited.")
return super().form_valid(form)
--Since fields
is empty (because Form
has nothing to validate), any user trying to create an invitation will go through this method.
--Checks for the existence of a user with the specified ID and whether the user has already joined or been invited.
--If you set all the fields and call the super method, the created invitation will be saved.
views.py
def get_success_url(self):
'''Dynamically give the transition destination when the addition is successful
'''
prod_id = self.prod_user.production.id
url = reverse_lazy('production:usr_list', kwargs={'prod_id': prod_id})
return url
def form_invalid(self, form):
'''When the addition fails
'''
messages.warning(self.request, "I couldn't invite you.")
return super().form_invalid(form)
-- get_success_url ()
and form_invalid ()
when it fails.
--The return destination (production: user_list
) is the View ("Member List" screen) where you are trying to place the "Invite" button.
invitation_form.html
{% extends 'base.html' %}
{% block content %}
<h1 style="margin: 0px;">
<a href="{% url 'production:usr_list' prod_id=view.production.id %}">◀</a>
Invitation to the troupe
</h1>
<div> </div>
<form method="post">
{% csrf_token %}
<table>
<tr><th>Performance</th><td>{{ view.production }}</td></tr>
<tr><th>ID of the person to invite</th><td><input type="text" name="invitee_id" /></td></tr>
</table>
<input type="submit" value="Invitation">
</form>
{% endblock %}
--Just enter ʻinvitee_id` and submit with the "Invite" button.
The screen that displays the invitations you created is the "Member List" screen. We will also set up an "invite" button here. The user scenario is as follows.
views.py
from django.views.generic import ListView
from django.core.exceptions import PermissionDenied
class UsrList(LoginRequiredMixin, ListView):
'''ProdUser list view
'''
model = ProdUser
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, kwargs['prod_id'])
if not prod_user:
raise PermissionDenied
#Make it a view attribute so that it can be referenced from the template
self.prod_user = prod_user
#Make it an attribute of the view to show the invited users
self.invitations = Invitation.objects.filter(production=prod_user.production)
return super().get(request, *args, **kwargs)
#The following is omitted
--Originally, it is a View that displays a list of "performance users", but in order to display a list of invitations on the same screen, the invitations are set to View attributes (ʻinvitations`) so that they can be referenced from the Template. ――In the previous section, I wrote "invitation card I made", but strictly speaking, I have obtained "invitation card inviting to this performance". The reason is that it will be displayed even if the owner of the "performance" changes.
Only the part displaying the invitation is excerpted. To see the whole thing, go to Code on GitHub.
produser_list.html
{% if view.prod_user.is_owner and view.invitations %}
<div style="border:solid thin lightgray; border-radius:10px; padding: 10px; margin:5px 0 20px;">
<p><strong>Inviting user</strong></p>
<table>
<tr>
<th>User ID</th>
<th>Surname</th>
<th>Name</th>
<th>Invitation deadline</th>
</tr>
{% for item in view.invitations %}
<tr>
<td>{{ item.invitee.username }}</td>
<td>{{ item.invitee.last_name }}</td>
<td>{{ item.invitee.first_name }}</td>
{% if item.expired %}
<td style="color:red;">{{ item.exp_dt }}</td>
{% else %}
<td>{{ item.exp_dt }}</td>
{% endif %}
<td><a href="{% url 'production:invt_delete' pk=item.id from='usr_list' %}" class="deletelink">Delete</a></td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
--In the first ʻif statement, display the list of invitations provided that the accessing user is the owner of the performance and the View has the ʻinvitations
attribute (and is not empty). I am doing it.
--I want to display it in red when the invitation has expired, so I'm using the ʻexpired method of the ʻInvitation
class created above.
--The point is that you can call the method from Template.
--Each invitation will have a "Delete" button to jump to the View you're about to delete.
――Invited people can also delete using the same View, so I added the parameter from
to the URL so that I can remember the return destination.
The screen that displays the invitations you receive is the "Participating Performances" screen. Originally, it is a screen that displays a list of participating performances, but just like when displaying the created invitation, the area for the invitation is displayed only when the conditions are met.
views.py
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(self, request, *args, **kwargs):
'''Handler to receive the request at the time of display
'''
#Make it an attribute of the view to display invitations to the troupe
now = datetime.now(timezone.utc)
self.invitations = Invitation.objects.filter(invitee=self.request.user,
exp_dt__gt=now)
return super().get(request, *args, **kwargs)
#The following is omitted
--The invitation is set as an attribute of View, just like when displaying the created invitation.
――The difference from displaying the created invitation is that only the invitations that have not expired are taken out from the beginning.
--ʻExpired () method is not used, but it is extracted by
filter () of QuerySet. --See [here](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#field-lookups) for how to write conditions such as
xx__gt = xx` as you are doing here. Please give me.
Only the part displaying the invitation is excerpted. To see the whole thing, go to Code on GitHub.
production_list.html
{% if view.invitations %}
<div style="border:solid thin lightgray; border-radius:10px; padding: 10px; margin:5px 0 20px;">
<p><strong>Invited group</strong></p>
<table>
<tr>
<th>Performance name</th>
<th>Inviter ID</th>
<th>Invitation deadline</th>
</tr>
{% for item in view.invitations %}
<tr>
<td>{{ item.production }}</td>
<td>{{ item.inviter.username }}</td>
{% if item.expired %}
<td style="color:red;">{{ item.exp_dt }}</td>
{% else %}
<td>{{ item.exp_dt }}</td>
{% endif %}
<td>
<a href="{% url 'production:prod_join' invt_id=item.id %}" class="addlink">Participation</a>
<a href="{% url 'production:invt_delete' pk=item.id from='prod_list' %}" class="deletelink">Delete</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
--In addition to the "Delete" button, the "Join" button will be displayed on the received invitation. --When you press the "Join" button, you will jump to the View that confirms and processes your participation.
When displaying the invitations I made and the invitations I received, I placed a "Delete" button in the Template.
Create a View that you can access from that link (production: invoke_delete
).
views.py
from django.views.generic.edit import DeleteView
class InvtDelete(LoginRequiredMixin, DeleteView):
'''Invitation delete view
'''
model = Invitation
template_name_suffix = '_delete'
--template_name_suffix
seems to default to'_confirm_delete'.
This area is your choice.
views.py
def get(self, request, *args, **kwargs):
'''Handler to receive the request at the time of display
'''
#Check that you are the owner of the performance or the invitee of the invitation
invt = self.get_object()
prod_id = invt.production.id
prod_user = accessing_prod_user(self, prod_id)
is_owner = prod_user and prod_user.is_owner
is_invitee = self.request.user == invt.invitee
if not (is_owner or is_invitee):
raise PermissionDenied
return super().get(request, *args, **kwargs)
--The ID of the "performance" is taken from the object (invitation) to be deleted, and it is checked that the accessing user is the owner of the performance or the invited person.
--ʻAccessing_prod_user ()For what the method does, see [Source for
production / view_func.py`](https://github.com/satamame/pscweb2/blob/master/production/view_func.py) Please see.
views.py
def post(self, request, *args, **kwargs):
'''Handler to receive request at save
'''
#Check that you are the owner of the performance or the invitee of the invitation
invt = self.get_object()
prod_id = invt.production.id
prod_user = accessing_prod_user(self, prod_id)
is_owner = prod_user and prod_user.is_owner
is_invitee = self.request.user == invt.invitee
if not (is_owner or is_invitee):
raise PermissionDenied
return super().post(request, *args, **kwargs)
--The same thing that the POST
handler does is the same as GET
.
If you feel uncomfortable with duplication, you can make it a method.
views.py
def get_success_url(self):
'''Dynamically give the transition destination when the deletion is successful
'''
if self.kwargs['from'] == 'usr_list':
prod_id = self.object.production.id
url = reverse_lazy('production:usr_list', kwargs={'prod_id': prod_id})
else:
url = reverse_lazy('production:prod_list')
return url
def delete(self, request, *args, **kwargs):
'''Message when deleted
'''
result = super().delete(request, *args, **kwargs)
messages.success(
self.request, str(self.object) + "Has been deleted.")
return result
--Since the from
parameter is added to the link URL of the" Delete "button, the return destination is sorted using it.
--You can access ʻURLConf with
self.kwargseven if you are not in the
get ()or
post () methods. --ʻURLConf
is specified in production / urls.py
as follows (> Source ).
```path('invt_delete/<int:pk>/<str:from>/', views.InvtDelete.as_view(), name='invt_delete'),```
It is a simple page with only a "Delete" button and a "Cancel" button.
invitation_delete.html
{% extends 'base.html' %}
{% block content %}
<h1 style="margin: 0px;">
{% if view.kwargs.from == 'usr_list' %}
<a href="{% url 'production:usr_list' prod_id=object.production.id %}">◀</a>
{% else %}
<a href="{% url 'production:prod_list' %}">◀</a>
{% endif %}
Remove invitation to troupe
</h1>
<form method="post">{% csrf_token %}
<p>Do you want to delete this invitation?</p>
<input type="submit" value="delete">
<input type="button"
{% if view.kwargs.from == 'usr_list' %}
onclick="location.href='{% url 'production:usr_list' prod_id=object.production.id %}'"
{% else %}
onclick="location.href='{% url 'production:prod_list' %}'"
{% endif %}
value="Cancel">
</form>
<table>
<tr><th>Performance</th><td>{{ object.production }}</td></tr>
<tr><th>ID of the invited person</th><td>{{ object.invitee.username }}</td></tr>
<tr><th>Surname</th><td>{{ object.invitee.last_name }}</td></tr>
<tr><th>Name</th><td>{{ object.invitee.first_name }}</td></tr>
<tr><th>Deadline</th>
{% if object.expired %}
<td style="color:red;">{{ object.exp_dt }}</td>
{% else %}
<td>{{ object.exp_dt }}</td>
{% endif %}
</tr>
</table>
{% endblock %}
--You may want to hide the "ID of the invited person" if from
is'prod_list' (if the invited person opens it).
--To access ʻURLConfin the Template, use something like
view.kwargs.from` (if'from'is the parameter name).
The process when "Join" is selected is the process of creating a new "Performance User", so create the following CreateView
.
However, the name of the Template is'production_join.html' for clarity.
views.py
from django.http import Http404
class ProdJoin(LoginRequiredMixin, CreateView):
'''View to participate in the performance
'''
model = ProdUser
fields = ('production', 'user')
template_name = 'production/production_join.html'
success_url = reverse_lazy('production:prod_list')
--The production
and ʻuser specified in
fieldsare not input by the user, but they are hidden and embedded in the Template so that they can be received from
Form`.
――Of course, you can set it with the code as you did in ["Mechanism for creating invitations"](#Mechanism for creating invitations).
views.py
def get(self, request, *args, **kwargs):
'''Handler to receive the request at the time of display
'''
#Inspect if you are invited and get a performance that you can participate in
self.production = self.production_to_join()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
'''Handler to receive request at save
'''
#Inspect if you are invited and get a performance that you can participate in
self.production = self.production_to_join()
return super().post(request, *args, **kwargs)
--In the GET
and POST
handlers, based on the information of ʻURLConf, which performance to participate in is saved as an attribute. --The
production_to_join () method to get the object of the performance (
Production`) of the participation destination is as follows.
views.py
def production_to_join(self):
'''Inspect if you are invited and return a performance that you can attend
'''
#Throw a 404 error if not invited
invt_id = self.kwargs['invt_id'];
invts = Invitation.objects.filter(id=invt_id)
if len(invts) < 1:
raise Http404
invt = invts[0]
#Throw a 404 error if the invitation has expired
now = datetime.now(timezone.utc)
if now > invt.exp_dt:
raise Http404
#PermissionDenied if the accessing user is not invitee
user = self.request.user
if user != invt.invitee:
raise PermissionDenied
return invt.production
-At ["Mechanism to display received invitations"](#Mechanism to display received invitations), the parameter ʻinvt_id is embedded in the link of the "Join" button, so invite using this. I'm getting an object of state (ʻInvitation
).
--The path to get here (view to participate in the performance) is ʻurls.py` as follows. (> Source).
```path('prod_join/<int:invt_id>/', views.ProdJoin.as_view(), name='prod_join'),```
――It's better to display a message about the expiration date, but here it just returns 404.
views.py
def form_valid(self, form):
'''When passed validation
'''
#Inspect invitation
invt_id = self.kwargs['invt_id'];
invts = Invitation.objects.filter(id=invt_id)
if len(invts) < 1:
return self.form_invalid(form)
invt = invts[0]
#Get the record to save
new_prod_user = form.save(commit=False)
#Is the correct performance set?
if new_prod_user.production != invt.production:
return self.form_invalid(form)
#Is the correct user set?
if new_prod_user.user != invt.invitee:
return self.form_invalid(form)
#Delete invitation
invt.delete()
messages.success(self.request, str(invt.production) + "I participated in.")
return super().form_valid(form)
def form_invalid(self, form):
'''When you fail to participate
'''
messages.warning(self.request, "I couldn't participate.")
return super().form_invalid(form)
--After receiving the correct field values (production
and ʻuser in this case) from
Form, View's
form_valid () method will be called, so match it with the contents of the "invitation" again. --The reason this check is needed is that the field value has once left the ʻInvitation
object and is exposed to HTML.
--If you call the super method after passing the check, a "performance user" will be created, but before that, delete the "invitation".
It's a simple page with only a "Join" button and a "Later" button.
production_join.html
{% extends 'base.html' %}
{% block content %}
<h1 style="margin: 0px;">
<a href="{% url 'production:prod_list' %}">◀</a>
Participate in the performance
</h1>
<div> </div>
<p>
{{ view.production }}Have been invited to. Are you sure that you want to participate?
</p>
<form method="post">
{% csrf_token %}
<input type="hidden" name="production" value="{{ view.production.id }}">
<input type="hidden" name="user" value="{{ view.request.user.id }}">
<input type="submit" value="Participation">
<input type="button"
onclick="location.href='{% url 'production:prod_list' %}'"
value="later">
</form>
{% endblock %}
--The field value you want to pass to the processing after the POST
handler is hidden and written directly.
--If you want to write a field value of type ForeignKey
directly, use the object's ʻid`.
We introduced the implementation of the function to invite users to a "project unit" (group) called "performance" and to let the invited users participate in it. The techniques used are summarized below.
--I used settings.AUTH_USER_MODEL
to specify the ʻUser type as a model field, and
get_user_model () to refer to the ʻUser
model in the View method.
--Actually, it seems that get_user_model ()
can be used even when specifying it as a model field.
--Even if you set all field values (except ʻid) in code in the View that creates the object, you could use
CreateView by emptying
fields. --If you don't want the Template to use a selective UI, you can now enter the value of a field (such as ʻusername
) to find the object and find it in the code (post ()
method). ..
--In Python, you can add an object's attributes at any time, so keep them as View attributes only when a valid value is found, and later check for the existence of the attributes (using the hasattr ()
method). I was able to validate.
--If you set the data you want to display to the View attribute, you can check the existence of the attribute (whether it is empty or not) on the Tamplate side and display it as needed.
--From Template, you could also call the View method and use the return value.
--If you want to jump to the same view from multiple pages and decide the return destination dynamically, you could do it by adding a parameter such as from
to the URL to that view.
--The kwargs
received by theget ()
andpost ()
methods could later be referenced as self.kwargs
.
--Since it is an attribute of View, it can be referred as view.kwargs.Parameter name
from Template.
--If you know the field value of the object you are trying to create at the time of GET
, you could also write it as hidden in the Template.
--If you wrote a field value of type ForeignKey
directly into the Template (as hidden, etc.), you could use ʻid for that object and it would be handled correctly by
Form`.
Recommended Posts