Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • beat/beat.web
1 result
Show changes
Commits on Source (6)
Showing
with 324 additions and 54 deletions
...@@ -31,14 +31,20 @@ from datetime import timedelta ...@@ -31,14 +31,20 @@ from datetime import timedelta
import simplejson as json import simplejson as json
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core import mail
from django.test import Client
from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from ...utils.tests.helpers import reload_urlconf
from ..models import Profile from ..models import Profile
from ..models import SupervisionTrack from ..models import SupervisionTrack
from ..models import TemporaryUrl from ..models import TemporaryUrl
from ..models import generate_url_hash
from .core import AccountTestMixin from .core import AccountTestMixin
from .test_views import ViewTestCase
# ---------------------------------------------------------- # ----------------------------------------------------------
...@@ -1402,3 +1408,77 @@ class TemporaryUrlTestCase(AccountTestCase): ...@@ -1402,3 +1408,77 @@ class TemporaryUrlTestCase(AccountTestCase):
.count() .count()
) )
self.assertEqual(count, 0) self.assertEqual(count, 0)
# ----------------------------------------------------------
class EmailSendingTestCase(ViewTestCase, AccountTestMixin):
def run_email_check_yearly_revalidation_possible_rejection(self, prefix):
client = self.yearrevalidationuser
logged_in = self.client.login(username="yearrevalidationuser", password="1234")
self.assertTrue(logged_in)
self.assertEqual(client.username, "yearrevalidationuser")
revalidation_url = reverse("api_accounts:revalidate_account")
response = self.client.put(revalidation_url, format="json")
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.client.logout()
reference_url = response.wsgi_request.build_absolute_uri(
reverse(
"accounts:temp_url_rejection", kwargs={"hash_url": generate_url_hash()},
)
)
self.assertEqual(len(mail.outbox), 1)
text_lines = mail.outbox[0].body.split("\n")
generated_url = text_lines[8]
self.assertTrue(prefix in generated_url)
self.assertEqual(
generated_url.rsplit("/", 2)[0], reference_url.rsplit("/", 2)[0]
)
client = Client()
response = client.get(generated_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 2)
def test_email_for_yearly_revalidation_possible_rejection(self):
for prefix in ["", "/platform"]:
with self.subTest(url_prefix=prefix):
mail.outbox = []
# YearRevalidation user
# Create and assign key
now = datetime.datetime.now()
expiration_date_delta = datetime.timedelta(
days=settings.ACCOUNT_BLOCKAGE_AFTER_FIRST_REJECTION_DAYS
)
self.yearrevalidationuser.profile.supervision_key = (
self.yearrevalidationuser.profile._generate_current_supervision_key()
)
supervisiontrack = SupervisionTrack.objects.create(
supervisee=self.yearrevalidationuser,
supervisor=self.firstsupervisor,
is_valid=True,
start_date=now,
expiration_date=now + expiration_date_delta,
last_validation_date=now,
)
# Assign key to supervision track
supervisiontrack.supervision_key = (
self.yearrevalidationuser.profile.supervision_key
)
supervisiontrack.save()
self.yearrevalidationuser.profile.supervision.add(supervisiontrack)
self.yearrevalidationuser.profile.save()
self.yearrevalidationuser.save()
with override_settings(URL_PREFIX=prefix):
reload_urlconf()
self.run_email_check_yearly_revalidation_possible_rejection(prefix)
...@@ -26,7 +26,6 @@ ...@@ -26,7 +26,6 @@
############################################################################### ###############################################################################
import datetime import datetime
from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
...@@ -36,6 +35,7 @@ from django.contrib.auth.models import User ...@@ -36,6 +35,7 @@ from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from ..utils import mail from ..utils import mail
...@@ -245,18 +245,22 @@ def perform_revalidation(request, supervisiontrack, supervisee, now): ...@@ -245,18 +245,22 @@ def perform_revalidation(request, supervisiontrack, supervisee, now):
# Inform supervisor about supervisee revalidation # Inform supervisor about supervisee revalidation
# Possible supervisor rejection available # Possible supervisor rejection available
parsed_url = urlparse(settings.URL_PREFIX)
server_address = "%s://%s" % (parsed_url.scheme, parsed_url.hostname,)
temp_url_rejection = TemporaryUrl.objects.create_temporary_url( temp_url_rejection = TemporaryUrl.objects.create_temporary_url(
TemporaryUrl.REJECTION, supervisiontrack TemporaryUrl.REJECTION, supervisiontrack
) )
supervisor_rejection_url = request.build_absolute_uri(
reverse(
"accounts:temp_url_rejection",
kwargs={"hash_url": temp_url_rejection.url_hash},
)
)
context = { context = {
"supervisor": supervisiontrack.supervisor, "supervisor": supervisiontrack.supervisor,
"supervisee": supervisee, "supervisee": supervisee,
"prefix": server_address, "supervisor_rejection_url": supervisor_rejection_url,
"temp_url": temp_url_rejection.url_hash,
} }
mail.send_email( mail.send_email(
...@@ -293,13 +297,9 @@ def accept_supervisee(supervisiontrack, supervisee, now): ...@@ -293,13 +297,9 @@ def accept_supervisee(supervisiontrack, supervisee, now):
supervisee.is_active = True supervisee.is_active = True
supervisee.save() supervisee.save()
parsed_url = urlparse(settings.URL_PREFIX)
server_address = "%s://%s" % (parsed_url.scheme, parsed_url.hostname)
context = { context = {
"supervisor": supervisiontrack.supervisor, "supervisor": supervisiontrack.supervisor,
"supervisee": supervisee, "supervisee": supervisee,
"prefix": server_address,
} }
mail.send_email( mail.send_email(
...@@ -384,13 +384,9 @@ def load_temporary_url_rejection(request, hash_url): ...@@ -384,13 +384,9 @@ def load_temporary_url_rejection(request, hash_url):
supervisiontrack = temp_url.supervision_track supervisiontrack = temp_url.supervision_track
supervisee = supervisiontrack.supervisee supervisee = supervisiontrack.supervisee
parsed_url = urlparse(settings.URL_PREFIX)
server_address = "%s://%s" % (parsed_url.scheme, parsed_url.hostname)
context = { context = {
"supervisor": supervisiontrack.supervisor, "supervisor": supervisiontrack.supervisor,
"supervisee": supervisee, "supervisee": supervisee,
"prefix": server_address,
} }
now = datetime.datetime.now() now = datetime.datetime.now()
......
...@@ -152,7 +152,7 @@ class RegistrationForm(forms.Form): ...@@ -152,7 +152,7 @@ class RegistrationForm(forms.Form):
return self.cleaned_data return self.cleaned_data
def save(self): def save(self, request):
""" """
Create the new ``User`` and ``RegistrationProfile``, and Create the new ``User`` and ``RegistrationProfile``, and
returns the ``User`` (by calling returns the ``User`` (by calling
...@@ -161,6 +161,7 @@ class RegistrationForm(forms.Form): ...@@ -161,6 +161,7 @@ class RegistrationForm(forms.Form):
""" """
new_user = RegistrationProfile.objects.create_inactive_user( new_user = RegistrationProfile.objects.create_inactive_user(
request,
username=self.cleaned_data["username"], username=self.cleaned_data["username"],
first_name=self.cleaned_data["first_name"], first_name=self.cleaned_data["first_name"],
last_name=self.cleaned_data["last_name"], last_name=self.cleaned_data["last_name"],
...@@ -457,7 +458,7 @@ class RegistrationSupervisorForm(forms.Form): ...@@ -457,7 +458,7 @@ class RegistrationSupervisorForm(forms.Form):
return self.cleaned_data return self.cleaned_data
def save(self): def save(self, request):
""" """
Create the new ``User`` and ``RegistrationProfile``, and Create the new ``User`` and ``RegistrationProfile``, and
returns the ``User`` (by calling returns the ``User`` (by calling
...@@ -466,6 +467,7 @@ class RegistrationSupervisorForm(forms.Form): ...@@ -466,6 +467,7 @@ class RegistrationSupervisorForm(forms.Form):
""" """
new_user = RegistrationProfile.objects.create_inactive_user( new_user = RegistrationProfile.objects.create_inactive_user(
request,
username=self.cleaned_data["username"], username=self.cleaned_data["username"],
first_name=self.cleaned_data["first_name"], first_name=self.cleaned_data["first_name"],
last_name=self.cleaned_data["last_name"], last_name=self.cleaned_data["last_name"],
......
...@@ -29,12 +29,12 @@ import datetime ...@@ -29,12 +29,12 @@ import datetime
import hashlib import hashlib
import random import random
import re import re
from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from django.db import transaction from django.db import transaction
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from ...accounts.models import Profile from ...accounts.models import Profile
...@@ -56,7 +56,7 @@ class RegistrationManager(models.Manager): ...@@ -56,7 +56,7 @@ class RegistrationManager(models.Manager):
""" """
def activate_user(self, activation_key): def activate_user(self, activation_key, request):
""" """
Validate an activation key and activate the corresponding Validate an activation key and activate the corresponding
``User`` if valid. ``User`` if valid.
...@@ -129,21 +129,23 @@ class RegistrationManager(models.Manager): ...@@ -129,21 +129,23 @@ class RegistrationManager(models.Manager):
) )
supervisor_user = supervisiontrack.supervisor supervisor_user = supervisiontrack.supervisor
parsed_url = urlparse(settings.URL_PREFIX)
server_address = "%s://%s" % (
parsed_url.scheme,
parsed_url.hostname,
)
temp_url = TemporaryUrl.objects.create_temporary_url( temp_url = TemporaryUrl.objects.create_temporary_url(
TemporaryUrl.VALIDATION, supervisiontrack TemporaryUrl.VALIDATION, supervisiontrack
) )
supervisor_validation_url = request.build_absolute_uri(
reverse(
"accounts:temp_url_validation",
kwargs={"hash_url": temp_url.url_hash},
)
)
contact_url = request.build_absolute_uri(reverse("contact"))
context = { context = {
"supervisor": supervisor_user, "supervisor": supervisor_user,
"supervisee": user, "supervisee": user,
"prefix": server_address, "supervisor_validation_url": supervisor_validation_url,
"temp_url": temp_url.url_hash, "contact_url": contact_url,
} }
mail.send_email( mail.send_email(
...@@ -159,7 +161,7 @@ class RegistrationManager(models.Manager): ...@@ -159,7 +161,7 @@ class RegistrationManager(models.Manager):
return False return False
def create_inactive_user( def create_inactive_user(
self, username, first_name, last_name, password, email, send_email=True self, request, username, first_name, last_name, password, email, send_email=True
): ):
""" """
Create a new, inactive ``User``, generate a Create a new, inactive ``User``, generate a
...@@ -209,14 +211,18 @@ class RegistrationManager(models.Manager): ...@@ -209,14 +211,18 @@ class RegistrationManager(models.Manager):
registration_profile = self.create_profile(new_user) registration_profile = self.create_profile(new_user)
if send_email: if send_email:
parsed_url = urlparse(settings.URL_PREFIX) registration_activation_url = request.build_absolute_uri(
server_address = "%s://%s" % (parsed_url.scheme, parsed_url.hostname) reverse(
"registration-activation",
kwargs={"activation_key": registration_profile.activation_key},
)
)
contact_url = request.build_absolute_uri(reverse("contact"))
context = { context = {
"activation_key": registration_profile.activation_key,
"expiration_days": settings.ACCOUNT_ACTIVATION_DAYS, "expiration_days": settings.ACCOUNT_ACTIVATION_DAYS,
"user": new_user, "user": new_user,
"prefix": server_address, "registration_activation_url": registration_activation_url,
"contact_url": contact_url,
} }
mail.send_email( mail.send_email(
......
...@@ -4,12 +4,12 @@ Thank you for registering at the Idiap Research Institute's Biometric ...@@ -4,12 +4,12 @@ Thank you for registering at the Idiap Research Institute's Biometric
Evaluation and Testing (BEAT) platform. Before we can activate your account Evaluation and Testing (BEAT) platform. Before we can activate your account
you must visit the following URL: you must visit the following URL:
{{ prefix }}{% url 'registration-activation' activation_key %} {{ registration_activation_url }}
If you don't do this in the next {{ expiration_days }} days, your registration If you don't do this in the next {{ expiration_days }} days, your registration
will be automatically cancelled and your data removed from our servers. will be automatically cancelled and your data removed from our servers.
If you are having problems to activate your account, contact a member of our If you are having problems to activate your account, contact a member of our
support staff at {{ prefix }}{% url 'contact' %}. support staff at {{ contact_url }}.
BEAT Administrators at the Idiap Research Institute BEAT Administrators at the Idiap Research Institute
...@@ -9,6 +9,6 @@ You will be informed if your supervisor has accepted or rejected your request. ...@@ -9,6 +9,6 @@ You will be informed if your supervisor has accepted or rejected your request.
If no information is given by your supervisor, you will be allowed to make a new supervision request after a week. If no information is given by your supervisor, you will be allowed to make a new supervision request after a week.
If you are having problems to activate your account, contact a member of our If you are having problems to activate your account, contact a member of our
support staff at {{ prefix }}{% url 'contact' %}. support staff at {{ contact_url }}.
BEAT Administrators at the Idiap Research Institute BEAT Administrators at the Idiap Research Institute
...@@ -6,6 +6,6 @@ If he's still your current supervisee, you can drop this message. ...@@ -6,6 +6,6 @@ If he's still your current supervisee, you can drop this message.
However if he's not your supervisee anymore, you can immediately revoke your supervision by clicking on the following link: However if he's not your supervisee anymore, you can immediately revoke your supervision by clicking on the following link:
{{ prefix }}/accounts/rejection/{{ temp_url }} {{ supervisor_rejection_url }}
BEAT Administrators at the Idiap Research Institute BEAT Administrators at the Idiap Research Institute
...@@ -12,9 +12,11 @@ Username: {{ supervisee.username }} ...@@ -12,9 +12,11 @@ Username: {{ supervisee.username }}
If you don't do this the supervisee will not be able to use his account and If you don't do this the supervisee will not be able to use his account and
will be deleted after 7 days. will be deleted after 7 days.
You can also click on the following direct link to accomplish this action and accept this new supervisee at {{ prefix }}/accounts/validation/{{ temp_url }} You can also click on the following direct link to accomplish this action and accept this new supervisee at:
{{ supervisor_validation_url }}
If you are having problems to activate your supervisee account, contact a member of our If you are having problems to activate your supervisee account, contact a member of our
support staff at {{ prefix }}{% url 'contact' %}. support staff at {{ contact_url }}.
BEAT Administrators at the Idiap Research Institute BEAT Administrators at the Idiap Research Institute
...@@ -12,10 +12,11 @@ Username: {{ supervisee.username }} ...@@ -12,10 +12,11 @@ Username: {{ supervisee.username }}
If you don't do this the supervisee will not be able to use his account in If you don't do this the supervisee will not be able to use his account in
future and will not be recognized as your supervisee. future and will not be recognized as your supervisee.
You can also click on the following direct link to accomplish this action and accept this new supervisee You can also click on the following direct link to accomplish this action and accept this new supervisee at:
at {{ prefix }}/accounts/validation/{{ temp_url }}
{{ supervisor_validation_url }}
If you are having problems to activate your supervisee account, contact a member of our If you are having problems to activate your supervisee account, contact a member of our
support staff at {{ prefix }}{% url 'contact' %}. support staff at {{ contact_url }}.
BEAT Administrators at the Idiap Research Institute BEAT Administrators at the Idiap Research Institute
# vim: set fileencoding=utf-8 :
###############################################################################
# #
# Copyright (c) 2020 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.web module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
# or FITNESS FOR A PARTICULAR PURPOSE. #
# #
# You should have received a copy of the GNU Affero Public License along #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# #
###############################################################################
from django.contrib.auth.models import User
from django.core import mail
from django.test import Client
from django.test import override_settings
from django.urls import reverse
from ...accounts.models import generate_url_hash
from ...accounts.tests.core import AccountTestMixin
from ...accounts.tests.test_views import ViewTestCase
from ...utils.tests.helpers import reload_urlconf
# ----------------------------------------------------------
class EmailSendingTestCase(ViewTestCase, AccountTestMixin):
def run_email_check_signup(self, prefix):
client = Client()
response = client.post(
reverse("registration"),
dict(
username="test",
first_name="test",
last_name="test",
email="test_email@idiap.ch",
password1="123",
password2="123",
supervisor="firstsupervisor",
tos="on",
),
)
reference_url = response.wsgi_request.build_absolute_uri(
reverse("registration")
)
self.assertEqual(response.status_code, 302)
self.assertEqual(len(mail.outbox), 1)
text_lines = mail.outbox[0].body.split("\n")
generated_url = text_lines[6]
self.assertTrue(prefix in generated_url)
self.assertEqual(
generated_url.rsplit("/", 3)[0], reference_url.rsplit("/", 1)[0]
)
def test_email_for_signup(self):
for prefix in ["", "/platform"]:
with self.subTest(url_prefix=prefix):
mail.outbox = []
User.objects.filter(username="test").delete()
with override_settings(URL_PREFIX=prefix):
reload_urlconf()
self.run_email_check_signup(prefix)
def run_email_check_activation(self, prefix):
client = Client()
response = client.post(
reverse("registration"),
dict(
username="test",
first_name="test",
last_name="test",
email="test_email@idiap.ch",
password1="123",
password2="123",
supervisor="firstsupervisor",
tos="on",
),
)
self.assertEqual(response.status_code, 302)
reference_url = response.wsgi_request.build_absolute_uri(
reverse("registration")
)
self.assertEqual(len(mail.outbox), 1)
text_lines = mail.outbox[0].body.split("\n")
generated_url = text_lines[6]
self.assertTrue(prefix in generated_url)
self.assertEqual(
generated_url.rsplit("/", 3)[0], reference_url.rsplit("/", 1)[0]
)
response = client.get(generated_url)
self.assertEqual(len(mail.outbox), 2)
text_lines = mail.outbox[1].body.split("\n")
reference_url = response.wsgi_request.build_absolute_uri(
reverse(
"accounts:temp_url_validation",
kwargs={"hash_url": generate_url_hash()},
)
)
generated_url = text_lines[16]
self.assertTrue(prefix in generated_url)
self.assertEqual(
generated_url.rsplit("/", 2)[0], reference_url.rsplit("/", 2)[0]
)
def test_email_for_activation(self):
for prefix in ["", "/platform"]:
with self.subTest(url_prefix=prefix):
mail.outbox = []
User.objects.filter(username="test").delete()
with override_settings(URL_PREFIX=prefix):
reload_urlconf()
self.run_email_check_activation(prefix)
def run_email_check_reactivation(self, prefix):
client = Client()
response = client.post(
reverse("blocked_user_reactivation"),
dict( # nosec
username="blockeduser", password="1234", supervisor="firstsupervisor",
),
)
self.assertEqual(response.status_code, 200)
reference_url = response.wsgi_request.build_absolute_uri(
reverse(
"accounts:temp_url_validation",
kwargs={"hash_url": generate_url_hash()},
)
)
self.assertEqual(len(mail.outbox), 2)
text_lines = mail.outbox[0].body.split("\n")
generated_url = text_lines[16]
self.assertTrue(prefix in generated_url)
self.assertEqual(
generated_url.rsplit("/", 2)[0], reference_url.rsplit("/", 2)[0]
)
response = client.get(generated_url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(mail.outbox), 3)
def test_email_for_reactivation(self):
for prefix in ["", "/platform"]:
with self.subTest(url_prefix=prefix):
mail.outbox = []
# Blocked user
self.blockeduser.profile.supervision_key = None
self.blockeduser.profile.rejection_date = None
self.blockeduser.is_active = False
self.blockeduser.profile.save()
self.blockeduser.save()
with override_settings(URL_PREFIX=prefix):
reload_urlconf()
self.run_email_check_reactivation(prefix)
...@@ -91,7 +91,7 @@ def activate( ...@@ -91,7 +91,7 @@ def activate(
""" """
account = RegistrationProfile.objects.activate_user(activation_key) account = RegistrationProfile.objects.activate_user(activation_key, request)
context = {"account": account, "expiration_days": settings.ACCOUNT_ACTIVATION_DAYS} context = {"account": account, "expiration_days": settings.ACCOUNT_ACTIVATION_DAYS}
...@@ -174,12 +174,12 @@ def register( ...@@ -174,12 +174,12 @@ def register(
# Check the form # Check the form
if "supervisor" not in request.POST: if "supervisor" not in request.POST:
supervisor_form_active = True supervisor_form_active = True
form_supervisor = RegistrationSupervisorForm( form = form_class()
form_supervisor = RegistrationFormTermsOfServiceSupervisor(
data=request.POST, files=request.FILES data=request.POST, files=request.FILES
) )
form = form_class()
if form_supervisor.is_valid(): if form_supervisor.is_valid():
form_supervisor.save() form_supervisor.save(request)
# success_url needs to be dynamically generated here; setting a # success_url needs to be dynamically generated here; setting a
# a default value using reverse() will cause circular-import # a default value using reverse() will cause circular-import
# problems with the default URLConf for this application, which # problems with the default URLConf for this application, which
...@@ -190,7 +190,7 @@ def register( ...@@ -190,7 +190,7 @@ def register(
form = form_class(data=request.POST, files=request.FILES) form = form_class(data=request.POST, files=request.FILES)
form_supervisor = RegistrationSupervisorForm() form_supervisor = RegistrationSupervisorForm()
if form.is_valid(): if form.is_valid():
form.save() form.save(request)
# success_url needs to be dynamically generated here; setting a # success_url needs to be dynamically generated here; setting a
# a default value using reverse() will cause circular-import # a default value using reverse() will cause circular-import
# problems with the default URLConf for this application, which # problems with the default URLConf for this application, which
......
...@@ -28,7 +28,6 @@ ...@@ -28,7 +28,6 @@
import datetime import datetime
import logging import logging
from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
...@@ -151,20 +150,24 @@ def blocked_user_reactivation(request): ...@@ -151,20 +150,24 @@ def blocked_user_reactivation(request):
supervisee.save() supervisee.save()
# Inform by email the supervisor that he has a new supervisee request # Inform by email the supervisor that he has a new supervisee request
parsed_url = urlparse(settings.URL_PREFIX)
server_address = "%s://%s" % (
parsed_url.scheme,
parsed_url.hostname,
)
temp_url = TemporaryUrl.objects.create_temporary_url( temp_url = TemporaryUrl.objects.create_temporary_url(
TemporaryUrl.VALIDATION, supervisiontrack TemporaryUrl.VALIDATION, supervisiontrack
) )
supervisor_validation_url = request.build_absolute_uri(
reverse(
"accounts:temp_url_validation",
kwargs={"hash_url": temp_url.url_hash},
)
)
contact_url = request.build_absolute_uri(
reverse("contact")
)
context = { context = {
"supervisor": supervisor, "supervisor": supervisor,
"supervisee": supervisee, "supervisee": supervisee,
"prefix": server_address, "supervisor_validation_url": supervisor_validation_url,
"temp_url": temp_url.url_hash, "contact_url": contact_url,
} }
mail.send_email( mail.send_email(
......