diff --git a/projectroles/models.py b/projectroles/models.py
index 11e8afc4..82286ac1 100644
--- a/projectroles/models.py
+++ b/projectroles/models.py
@@ -62,7 +62,7 @@
'Invalid project type "{project_type}" for ' 'role "{role_name}"'
)
CAT_PUBLIC_ACCESS_MSG = 'Public guest access is not allowed for categories'
-ADD_EMAIL_ALREADY_SET_MSG = 'Email already set as primary email for user'
+ADD_EMAIL_ALREADY_SET_MSG = 'Email already set as {email_type} email for user'
# Project ----------------------------------------------------------------------
@@ -1453,7 +1453,9 @@ def _validate_email_unique(self):
Assert the same email has not yet been set for the user.
"""
if self.email == self.user.email:
- raise ValidationError(ADD_EMAIL_ALREADY_SET_MSG)
+ raise ValidationError(
+ ADD_EMAIL_ALREADY_SET_MSG.format(email_type='primary')
+ )
def save(self, *args, **kwargs):
self._validate_email_unique()
diff --git a/userprofile/forms.py b/userprofile/forms.py
index 955c8644..41ec6013 100644
--- a/userprofile/forms.py
+++ b/userprofile/forms.py
@@ -7,12 +7,19 @@
from projectroles.app_settings import AppSettingAPI
from projectroles.forms import (
SODARForm,
+ SODARModelForm,
SETTING_CUSTOM_VALIDATE_MSG,
SETTING_DISABLE_LABEL,
SETTING_SOURCE_ONLY_MSG,
)
-from projectroles.models import APP_SETTING_VAL_MAXLENGTH, SODAR_CONSTANTS
+from projectroles.models import (
+ SODARUserAdditionalEmail,
+ SODAR_CONSTANTS,
+ APP_SETTING_VAL_MAXLENGTH,
+ ADD_EMAIL_ALREADY_SET_MSG,
+)
from projectroles.plugins import get_active_plugins
+from projectroles.utils import build_secret
app_settings = AppSettingAPI()
@@ -24,11 +31,8 @@
APP_SETTING_SCOPE_USER = SODAR_CONSTANTS['APP_SETTING_SCOPE_USER']
-# User Settings Form -----------------------------------------------------------
-
-
class UserSettingsForm(SODARForm):
- """The form for configuring user settings."""
+ """Form for configuring user settings"""
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('current_user')
@@ -203,3 +207,38 @@ def clean(self):
),
)
return self.cleaned_data
+
+
+class UserEmailForm(SODARModelForm):
+ """Form for creating additional email addresses for user"""
+
+ class Meta:
+ model = SODARUserAdditionalEmail
+ fields = ['email', 'user', 'secret']
+
+ def __init__(self, current_user=None, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ if current_user:
+ self.current_user = current_user
+ self.fields['user'].widget = forms.HiddenInput()
+ self.initial['user'] = current_user
+ self.fields['secret'].widget = forms.HiddenInput()
+ self.initial['secret'] = build_secret(32)
+
+ def clean(self):
+ if self.cleaned_data['email'] == self.current_user.email:
+ self.add_error(
+ 'email', ADD_EMAIL_ALREADY_SET_MSG.format(email_type='primary')
+ )
+ return self.cleaned_data
+ if (
+ SODARUserAdditionalEmail.objects.filter(
+ user=self.current_user, email=self.cleaned_data['email']
+ ).count()
+ > 0
+ ):
+ self.add_error(
+ 'email',
+ ADD_EMAIL_ALREADY_SET_MSG.format(email_type='additional'),
+ )
+ return self.cleaned_data
diff --git a/userprofile/rules.py b/userprofile/rules.py
index 6ae2f366..e4d47477 100644
--- a/userprofile/rules.py
+++ b/userprofile/rules.py
@@ -18,3 +18,6 @@
# Allow viewing user detail
rules.add_perm('userprofile.view_detail', rules.is_authenticated)
+
+# Allow adding additional email
+rules.add_perm('userprofile.add_email', rules.is_authenticated)
diff --git a/userprofile/templates/userprofile/detail.html b/userprofile/templates/userprofile/detail.html
index 214b99fc..1fecf1be 100644
--- a/userprofile/templates/userprofile/detail.html
+++ b/userprofile/templates/userprofile/detail.html
@@ -6,7 +6,27 @@
{% block css %}
{{ block.super }}
- {# TODO: Add styles for email table #}
+
{% endblock %}
{% block projectroles %}
@@ -97,74 +117,77 @@
-
-
-
-
-
-
- Address |
- Status |
- Created |
- Modified |
- |
-
-
-
- {% if add_emails.count > 0 %}
- {% for email in add_emails %}
-
- {{ email.email }} |
-
- {% if email.verified %}Verified{% else %}Unverified{% endif %}
- |
- {{ email.date_created | date:'Y-m-d H:i'}} |
- {{ email.date_modified | date:'Y-m-d H:i'}} |
-
-
-
+ {% get_django_setting 'PROJECTROLES_SEND_EMAIL' as send_email %}
+ {% if send_email %}
+
+
+
+
+
+
+ Address |
+ Status |
+ Created |
+ Modified |
+ |
+
+
+
+ {% if add_emails.count > 0 %}
+ {% for email in add_emails %}
+
+ {{ email.email }} |
+
+ {% if email.verified %}Verified{% else %}Unverified{% endif %}
+ |
+ {{ email.date_created | date:'Y-m-d H:i'}} |
+ {{ email.date_modified | date:'Y-m-d H:i'}} |
+
+
+
+ |
+
+ {% endfor %}
+ {% else %}
+
+
+ No additional email addresses set.
|
- {% endfor %}
- {% else %}
-
-
- No additional email addresses set.
- |
-
- {% endif %}
-
-
+ {% endif %}
+ |
+
+
-
+ {% endif %}
{% endblock projectroles %}
diff --git a/userprofile/templates/userprofile/email_form.html b/userprofile/templates/userprofile/email_form.html
new file mode 100644
index 00000000..715333d6
--- /dev/null
+++ b/userprofile/templates/userprofile/email_form.html
@@ -0,0 +1,45 @@
+{% extends 'projectroles/base.html' %}
+
+{% load rules %}
+{% load crispy_forms_filters %}
+{% load projectroles_tags %}
+{% load projectroles_common_tags %}
+
+{% block title %}
+ Add Email Address
+{% endblock title %}
+
+{% block head_extend %}
+ {{ form.media }}
+{% endblock head_extend %}
+
+{% block projectroles %}
+
+
+
{{ request.user.get_full_name }}
+
User Profile
+
+
+
+
Add Email Address
+
+
+
+
+{% endblock projectroles %}
diff --git a/userprofile/tests/test_permissions.py b/userprofile/tests/test_permissions.py
index a34dd99d..fe3bd67d 100644
--- a/userprofile/tests/test_permissions.py
+++ b/userprofile/tests/test_permissions.py
@@ -35,3 +35,16 @@ def test_get_settings_update_anon(self):
url = reverse('userprofile:settings_update')
self.assert_response(url, [self.superuser, self.regular_user], 200)
self.assert_response(url, self.anonymous, 302)
+
+ def test_get_email_create(self):
+ """Test UserEmailCreateView GET"""
+ url = reverse('userprofile:email_create')
+ self.assert_response(url, [self.superuser, self.regular_user], 200)
+ self.assert_response(url, self.anonymous, 302)
+
+ @override_settings(PROJECTROLES_ALLOW_ANONYMOUS=True)
+ def test_get_email_create_anon(self):
+ """Test UserEmailCreateView GET with anonymous access"""
+ url = reverse('userprofile:email_create')
+ self.assert_response(url, [self.superuser, self.regular_user], 200)
+ self.assert_response(url, self.anonymous, 302)
diff --git a/userprofile/tests/test_ui.py b/userprofile/tests/test_ui.py
index 03cdf702..f053adb1 100644
--- a/userprofile/tests/test_ui.py
+++ b/userprofile/tests/test_ui.py
@@ -33,7 +33,7 @@ def test_update_button(self):
expected = [(self.local_user, 1), (self.ldap_user, 0)]
self.assert_element_count(expected, self.url, 'sodar-user-btn-update')
- def test_add_email_unset(self):
+ def test_additional_email_unset(self):
"""Test existence of additional email elements without email"""
self.assert_element_count(
[(self.local_user, 0)],
@@ -48,7 +48,7 @@ def test_add_email_unset(self):
True,
)
- def test_add_email_set(self):
+ def test_additional_email_set(self):
"""Test existence of additional email elements with email"""
self.make_email(self.local_user, 'add1@example.com')
self.make_email(self.local_user, 'add2@example.com', verified=False)
@@ -67,6 +67,16 @@ def test_add_email_set(self):
False,
)
+ @override_settings(PROJECTROLES_SEND_EMAIL=False)
+ def test_additional_email_disabled(self):
+ """Test existence of email card with PROJECTROLES_SEND_EMAIL=False"""
+ self.assert_element_exists(
+ [self.local_user],
+ self.url,
+ 'sodar-user-email-card',
+ False,
+ )
+
class TestUserSettings(UITestBase):
"""Tests for user settings page"""
diff --git a/userprofile/tests/test_views.py b/userprofile/tests/test_views.py
index 3ee035a6..bfa10179 100644
--- a/userprofile/tests/test_views.py
+++ b/userprofile/tests/test_views.py
@@ -2,18 +2,23 @@
from django.contrib import auth
from django.contrib.messages import get_messages
+from django.core import mail
+from django.forms.models import model_to_dict
+from django.test import override_settings
from django.urls import reverse
from test_plus.test import TestCase
# Projectroles dependency
from projectroles.app_settings import AppSettingAPI
+from projectroles.models import SODARUserAdditionalEmail
from projectroles.tests.test_models import (
EXAMPLE_APP_NAME,
AppSettingMixin,
SODARUserAdditionalEmailMixin,
)
from projectroles.views import MSG_FORM_INVALID
+from projectroles.utils import build_secret
from userprofile.views import SETTING_UPDATE_MSG
@@ -24,6 +29,9 @@
# Local constants
INVALID_VALUE = 'INVALID VALUE'
+ADD_EMAIL = 'add1@example.com'
+ADD_EMAIL2 = 'add2@example.com'
+ADD_EMAIL_SECRET = build_secret(32)
class UserViewTestBase(TestCase):
@@ -237,3 +245,95 @@ def test_post_custom_validation(self):
MSG_FORM_INVALID,
)
self.assertEqual(self._get_setting('user_str_setting'), 'test')
+
+
+class TestUserEmailCreateView(SODARUserAdditionalEmailMixin, UserViewTestBase):
+ """Tests for UserEmailCreateView"""
+
+ def setUp(self):
+ super().setUp()
+ self.url = reverse('userprofile:email_create')
+ self.url_redirect = reverse('userprofile:detail')
+
+ def test_get(self):
+ """Test UserEmailCreateView GET"""
+ with self.login(self.user):
+ response = self.client.get(self.url)
+ self.assertEqual(response.status_code, 200)
+ self.assertIsNotNone(response.context['form'])
+
+ @override_settings(PROJECTROLES_SEND_EMAIL=False)
+ def test_get_email_disabled(self):
+ with self.login(self.user):
+ response = self.client.get(self.url)
+ self.assertRedirects(response, self.url_redirect)
+
+ def test_post(self):
+ """Test POST"""
+ self.assertEqual(SODARUserAdditionalEmail.objects.count(), 0)
+ self.assertEqual(len(mail.outbox), 0)
+ with self.login(self.user):
+ data = {
+ 'user': self.user.pk,
+ 'email': ADD_EMAIL,
+ 'secret': ADD_EMAIL_SECRET,
+ }
+ response = self.client.post(self.url, data)
+ self.assertRedirects(response, self.url_redirect)
+ self.assertEqual(SODARUserAdditionalEmail.objects.count(), 1)
+ email = SODARUserAdditionalEmail.objects.first()
+ expected = {
+ 'id': email.pk,
+ 'user': self.user.pk,
+ 'email': ADD_EMAIL,
+ 'secret': ADD_EMAIL_SECRET,
+ 'verified': False,
+ 'sodar_uuid': email.sodar_uuid,
+ }
+ self.assertEqual(model_to_dict(email), expected)
+ self.assertEqual(len(mail.outbox), 1)
+ self.assertEqual(mail.outbox[0].recipients(), [ADD_EMAIL])
+ # TODO: Assert sent URL
+
+ def test_post_existing_primary(self):
+ """Test POST with email used as primary email for user"""
+ self.assertEqual(SODARUserAdditionalEmail.objects.count(), 0)
+ self.assertEqual(len(mail.outbox), 0)
+ with self.login(self.user):
+ data = {
+ 'user': self.user.pk,
+ 'email': self.user.email,
+ 'secret': ADD_EMAIL_SECRET,
+ }
+ response = self.client.post(self.url, data)
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(SODARUserAdditionalEmail.objects.count(), 0)
+ self.assertEqual(len(mail.outbox), 0)
+
+ def test_post_existing_additional(self):
+ """Test POST with email used as additional email for user"""
+ self.make_email(self.user, ADD_EMAIL)
+ with self.login(self.user):
+ data = {
+ 'user': self.user.pk,
+ 'email': ADD_EMAIL,
+ 'secret': ADD_EMAIL_SECRET,
+ }
+ response = self.client.post(self.url, data)
+ self.assertEqual(response.status_code, 200)
+
+ def test_post_multiple(self):
+ """Test POST with different existing additional email"""
+ self.make_email(self.user, ADD_EMAIL)
+ self.assertEqual(SODARUserAdditionalEmail.objects.count(), 1)
+ self.assertEqual(len(mail.outbox), 0)
+ with self.login(self.user):
+ data = {
+ 'user': self.user.pk,
+ 'email': ADD_EMAIL2,
+ 'secret': ADD_EMAIL_SECRET,
+ }
+ response = self.client.post(self.url, data)
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(SODARUserAdditionalEmail.objects.count(), 2)
+ self.assertEqual(len(mail.outbox), 1)
diff --git a/userprofile/urls.py b/userprofile/urls.py
index a7bd9492..0462b9c3 100644
--- a/userprofile/urls.py
+++ b/userprofile/urls.py
@@ -15,4 +15,9 @@
view=views.UserSettingsView.as_view(),
name='settings_update',
),
+ path(
+ route='profile/email/create',
+ view=views.UserEmailCreateView.as_view(),
+ name='email_create',
+ ),
]
diff --git a/userprofile/views.py b/userprofile/views.py
index ece23d1b..0b1f37f6 100644
--- a/userprofile/views.py
+++ b/userprofile/views.py
@@ -1,21 +1,25 @@
"""UI views for the userprofile app"""
+from django.conf import settings
from django.contrib import auth, messages
from django.contrib.auth.mixins import LoginRequiredMixin
-from django.urls import reverse_lazy
-from django.views.generic import TemplateView, FormView
+from django.shortcuts import redirect
+from django.urls import reverse, reverse_lazy
+from django.views.generic import TemplateView, FormView, CreateView
# Projectroles dependency
from projectroles.app_settings import AppSettingAPI
+from projectroles.email import send_generic_mail, get_email_user
from projectroles.models import SODARUserAdditionalEmail, SODAR_CONSTANTS
from projectroles.plugins import get_active_plugins
from projectroles.views import (
LoggedInPermissionMixin,
HTTPRefererMixin,
InvalidFormMixin,
+ CurrentUserFormMixin,
)
-from userprofile.forms import UserSettingsForm
+from userprofile.forms import UserSettingsForm, UserEmailForm
User = auth.get_user_model()
@@ -28,6 +32,19 @@
# Local Constants
SETTING_UPDATE_MSG = 'User settings updated.'
+VERIFY_EMAIL_SUBJECT = 'Verify your additional email address for {site}'
+VERIFY_EMAIL_BODY = r'''
+{user} has added this address to their
+additional emails on {site}.
+
+Once verified, the site may use this email to send automated email for
+notifications as configured in user settings.
+
+Please verify this email address by following this URL:
+{url}
+
+If this was not requested by you, this message can be ignored.
+'''.lstrip()
class UserDetailView(LoginRequiredMixin, LoggedInPermissionMixin, TemplateView):
@@ -100,3 +117,47 @@ def form_valid(self, form):
)
messages.success(self.request, SETTING_UPDATE_MSG)
return result
+
+
+class UserEmailCreateView(
+ LoginRequiredMixin,
+ LoggedInPermissionMixin,
+ CurrentUserFormMixin,
+ CreateView,
+):
+ """User additional email creation view"""
+
+ form_class = UserEmailForm
+ permission_required = 'userprofile.add_email'
+ template_name = 'userprofile/email_form.html'
+
+ def get_success_url(self):
+ subject = VERIFY_EMAIL_SUBJECT.format(site=settings.SITE_INSTANCE_TITLE)
+ body = VERIFY_EMAIL_BODY.format(
+ user=get_email_user(self.object.user),
+ site=settings.SITE_INSTANCE_TITLE,
+ url='#', # TODO
+ )
+ try:
+ send_generic_mail(subject, body, [self.object.email], self.request)
+ messages.success(
+ self.request,
+ 'Email added. A verification message has been sent to the '
+ 'address. Follow the received verification link to activate '
+ 'the address.',
+ )
+ except Exception as ex:
+ messages.error(
+ self.request, 'Failed to send verification mail: {}'.format(ex)
+ )
+ return reverse('userprofile:detail')
+
+ def get(self, *args, **kwargs):
+ if not settings.PROJECTROLES_SEND_EMAIL:
+ messages.warning(
+ self.request,
+ 'Email sending disabled, adding email addresses is not '
+ 'allowed.',
+ )
+ return redirect(reverse('userprofile:detail'))
+ return super().get(*args, **kwargs)