Fill in the gaps intentionally missed previously by amalgamating most user-specific views into the user profile view.
Signed-off-by: Stephen Finucane <step...@that.guru> --- patchwork/forms.py | 252 ++++++++++++++--- patchwork/templates/patchwork/profile.html | 86 +++++- .../patchwork/user-link-confirm.html | 15 - patchwork/templates/patchwork/user-link.html | 28 -- patchwork/tests/views/test_user.py | 52 ++-- patchwork/urls.py | 14 +- patchwork/views/mail.py | 6 +- patchwork/views/user.py | 263 +++++++++++++----- 8 files changed, 513 insertions(+), 203 deletions(-) delete mode 100644 patchwork/templates/patchwork/user-link-confirm.html delete mode 100644 patchwork/templates/patchwork/user-link.html diff --git patchwork/forms.py patchwork/forms.py index 24322c78..5f8dff96 100644 --- patchwork/forms.py +++ patchwork/forms.py @@ -4,10 +4,12 @@ # SPDX-License-Identifier: GPL-2.0-or-later from django.contrib.auth.models import User +from django.core import exceptions from django import forms from django.db.models import Q from django.db.utils import ProgrammingError +from patchwork import models from patchwork.models import Bundle from patchwork.models import Patch from patchwork.models import State @@ -15,13 +17,14 @@ from patchwork.models import UserProfile class RegistrationForm(forms.Form): + first_name = forms.CharField(max_length=30, required=False) last_name = forms.CharField(max_length=30, required=False) - username = forms.RegexField(regex=r'^\w+$', max_length=30, - label=u'Username') - email = forms.EmailField(max_length=100, label=u'Email address') - password = forms.CharField(widget=forms.PasswordInput(), - label='Password') + username = forms.RegexField( + regex=r'^\w+$', max_length=30, label='Username' + ) + email = forms.EmailField(max_length=100, label='Email address') + password = forms.CharField(widget=forms.PasswordInput(), label='Password') def clean_username(self): value = self.cleaned_data['username'] @@ -29,8 +32,9 @@ class RegistrationForm(forms.Form): User.objects.get(username__iexact=value) except User.DoesNotExist: return self.cleaned_data['username'] - raise forms.ValidationError('This username is already taken. ' - 'Please choose another.') + raise forms.ValidationError( + 'This username is already taken. Please choose another.' + ) def clean_email(self): value = self.cleaned_data['email'] @@ -38,21 +42,24 @@ class RegistrationForm(forms.Form): user = User.objects.get(email__iexact=value) except User.DoesNotExist: return self.cleaned_data['email'] - raise forms.ValidationError('This email address is already in use ' - 'for the account "%s".\n' % user.username) + raise forms.ValidationError( + 'This email address is already in use ' + 'for the account "%s".\n' % user.username + ) def clean(self): return self.cleaned_data -class EmailForm(forms.Form): - email = forms.EmailField(max_length=200) - - class BundleForm(forms.ModelForm): + name = forms.RegexField( - regex=r'^[^/]+$', min_length=1, max_length=50, label=u'Name', - error_messages={'invalid': 'Bundle names can\'t contain slashes'}) + regex=r'^[^/]+$', + min_length=1, + max_length=50, + label='Name', + error_messages={'invalid': 'Bundle names can\'t contain slashes'}, + ) class Meta: model = Bundle @@ -61,37 +68,180 @@ class BundleForm(forms.ModelForm): class CreateBundleForm(BundleForm): - def __init__(self, *args, **kwargs): - super(CreateBundleForm, self).__init__(*args, **kwargs) - - class Meta: - model = Bundle - fields = ['name'] - def clean_name(self): name = self.cleaned_data['name'] - count = Bundle.objects.filter(owner=self.instance.owner, - name=name).count() + count = Bundle.objects.filter( + owner=self.instance.owner, name=name + ).count() if count > 0: - raise forms.ValidationError('A bundle called %s already exists' - % name) + raise forms.ValidationError( + 'A bundle called %s already exists' % name + ) return name + class Meta: + model = Bundle + fields = ['name'] + class DeleteBundleForm(forms.Form): + name = 'deletebundleform' form_name = forms.CharField(initial=name, widget=forms.HiddenInput) bundle_id = forms.IntegerField(widget=forms.HiddenInput) +class UserForm(forms.ModelForm): + + name = 'user-form' + + class Meta: + model = User + fields = ['first_name', 'last_name'] + + +class EmailForm(forms.Form): + + email = forms.EmailField(max_length=200) + + +class UserLinkEmailForm(forms.Form): + + name = 'user-link-email-form' + + email = forms.EmailField(max_length=200) + + def __init__(self, user, *args, **kwargs): + self.user = user + super().__init__(*args, **kwargs) + + def clean_email(self): + email = self.cleaned_data['email'] + + # ensure this email is not already linked to our account + try: + models.Person.objects.get(email=email, user=self.user) + except models.Person.DoesNotExist: + pass + else: + raise exceptions.ValidationError( + "That email is already linked to your account." + ) + + return email + + +class UserUnlinkEmailForm(forms.Form): + + name = 'user-unlink-email-form' + + email = forms.EmailField(max_length=200) + + def __init__(self, user, *args, **kwargs): + self.user = user + super().__init__(*args, **kwargs) + + def clean_email(self): + email = self.cleaned_data['email'] + + # ensure we're not unlinking the final email + if email == self.user.email: + raise exceptions.ValidationError( + "You can't unlink your primary email." + ) + + # and that this email is in fact our email to unlink + try: + models.Person.objects.get(email=email, user=self.user) + except models.Person.DoesNotExist: + raise exceptions.ValidationError( + "That email is not linked to your account." + ) + + return email + + +class UserPrimaryEmailForm(forms.ModelForm): + + name = 'user-primary-email-form' + + class Meta: + model = User + fields = ['email'] + + +class UserEmailOptinForm(forms.Form): + + name = 'user-email-optin-form' + + email = forms.EmailField(max_length=200) + + def __init__(self, user, *args, **kwargs): + self.user = user + super().__init__(*args, **kwargs) + + def clean_email(self): + email = self.cleaned_data['email'] + + # ensure this email is linked to our account + try: + models.Person.objects.get(email=email, user=self.user) + except models.Person.DoesNotExist: + raise exceptions.ValidationError( + "You can't configure mail preferences for an email that is " + "not associated with your account." + ) + + return email + + +class UserEmailOptoutForm(forms.Form): + + name = 'user-email-optout-form' + + email = forms.EmailField(max_length=200) + + def __init__(self, user, *args, **kwargs): + self.user = user + super().__init__(*args, **kwargs) + + def clean_email(self): + email = self.cleaned_data['email'] + + # ensure this email is linked to our account + try: + models.Person.objects.get(email=email, user=self.user) + except models.Person.DoesNotExist: + raise exceptions.ValidationError( + "You can't configure mail preferences for an email that is " + "not associated with your account" + ) + + try: + models.EmailOptout.objects.get(email=email) + except models.EmailOptout.DoesNotExist: + pass + else: + raise exceptions.ValidationError( + "You have already opted out of emails to this address." + ) + + return email + + class UserProfileForm(forms.ModelForm): + name = 'user-profile-form' + show_ids = forms.TypedChoiceField( + coerce=lambda x: x == 'yes', + choices=(('yes', 'Yes'), ('no', 'No')), + widget=forms.RadioSelect, + ) + class Meta: model = UserProfile fields = ['items_per_page', 'show_ids'] - labels = { - 'show_ids': 'Show Patch IDs:' - } + labels = {'show_ids': 'Show Patch IDs:'} def _get_delegate_qs(project, instance=None): @@ -101,20 +251,23 @@ def _get_delegate_qs(project, instance=None): if not project: raise ValueError('Expected a project') - q = Q(profile__in=UserProfile.objects - .filter(maintainer_projects=project) - .values('pk').query) + q = Q( + profile__in=UserProfile.objects.filter(maintainer_projects=project) + .values('pk') + .query + ) if instance and instance.delegate: q = q | Q(username=instance.delegate) + return User.objects.complex_filter(q) class PatchForm(forms.ModelForm): - def __init__(self, instance=None, project=None, *args, **kwargs): super(PatchForm, self).__init__(instance=instance, *args, **kwargs) self.fields['delegate'] = forms.ModelChoiceField( - queryset=_get_delegate_qs(project, instance), required=False) + queryset=_get_delegate_qs(project, instance), required=False + ) class Meta: model = Patch @@ -122,12 +275,14 @@ class PatchForm(forms.ModelForm): class OptionalModelChoiceField(forms.ModelChoiceField): + no_change_choice = ('*', 'no change') to_field_name = None def __init__(self, *args, **kwargs): super(OptionalModelChoiceField, self).__init__( - initial=self.no_change_choice[0], *args, **kwargs) + initial=self.no_change_choice[0], *args, **kwargs + ) def _get_choices(self): # _get_choices queries the database, which can fail if the db @@ -135,7 +290,8 @@ class OptionalModelChoiceField(forms.ModelChoiceField): # set of choices for now. try: choices = list( - super(OptionalModelChoiceField, self)._get_choices()) + super(OptionalModelChoiceField, self)._get_choices() + ) except ProgrammingError: choices = [] choices.append(self.no_change_choice) @@ -153,31 +309,39 @@ class OptionalModelChoiceField(forms.ModelChoiceField): class OptionalBooleanField(forms.TypedChoiceField): - def is_no_change(self, value): return value == self.empty_value class MultiplePatchForm(forms.Form): + action = 'update' archived = OptionalBooleanField( - choices=[('*', 'no change'), ('True', 'Archived'), - ('False', 'Unarchived')], + choices=[ + ('*', 'no change'), + ('True', 'Archived'), + ('False', 'Unarchived'), + ], coerce=lambda x: x == 'True', - empty_value='*') + empty_value='*', + ) def __init__(self, project, *args, **kwargs): super(MultiplePatchForm, self).__init__(*args, **kwargs) self.fields['delegate'] = OptionalModelChoiceField( - queryset=_get_delegate_qs(project=project), required=False) + queryset=_get_delegate_qs(project=project), required=False + ) self.fields['state'] = OptionalModelChoiceField( - queryset=State.objects.all()) + queryset=State.objects.all() + ) def save(self, instance, commit=True): opts = instance.__class__._meta if self.errors: - raise ValueError("The %s could not be changed because the data " - "didn't validate." % opts.object_name) + raise ValueError( + "The %s could not be changed because the data " + "didn't validate." % opts.object_name + ) data = self.cleaned_data # Update the instance for f in opts.fields: diff --git patchwork/templates/patchwork/profile.html patchwork/templates/patchwork/profile.html index 7a0b54fe..a5a57150 100644 --- patchwork/templates/patchwork/profile.html +++ patchwork/templates/patchwork/profile.html @@ -3,6 +3,20 @@ {% block title %}{{ user.username }}{% endblock %} {% block body %} +{% for message in messages %} +{% if message.tags == 'success' %} +<div class="notification is-success"> +{% elif message.tags == 'warning' %} +<div class="notification is-warning"> +{% elif message.tags == 'error' %} +<div class="notification is-danger"> +{% else %} +<div class="notification"> +{% endif %} + {{ message }} + <button class="delete" onclick="dismiss(this);"></button> +</div> +{% endfor %} <div class="container" style="margin-top: 1rem;"> <div class="columns"> <div class="column is-3"> @@ -100,7 +114,6 @@ Settings </h1> -{# TODO: Add view to enable this #} <section class="block"> <h2 id="profile" class="title is-4"> <a href="#profile" title="Permalink to this section">#</a> @@ -108,6 +121,7 @@ </h2> <form method="post"> {% csrf_token %} + <input type="hidden" name="form_name" value="user-form"> <div class="field"> <label for="id_username" class="label"> Username @@ -143,6 +157,12 @@ <a href="#linked-emails" title="Permalink to this section">#</a> Linked emails </h2> +{% if user_link_email_form.non_field_errors %} + <div class="notification is-danger is-light"> + <button class="delete" onclick="dismiss(this);"></button> + {{ user_link_email_form.non_field_errors }} + </div> +{% endif %} {% for email in linked_emails %} <div class="card"> <div class="card-content"> @@ -155,14 +175,17 @@ </div> {% if user.email != email.email %} <div class="column is-narrow"> - <form method="post" action="{% url 'user-unlink' person_id=email.id %}"> + <form method="post"> {% csrf_token %} + <input type="hidden" name="form_name" value="user-unlink-email-form"> + <input type="hidden" name="email" value="{{ email.email }}"> <button class="button is-danger">Unlink</button> </form> </div> -{# TODO: Add view to enable this #} <div class="column is-narrow"> <form method="post"> + <input type="hidden" name="form_name" value="user-primary-email-form"> + <input type="hidden" name="email" value="{{ email.email }}"> {% csrf_token %} <button class="button is-info">Make primary</button> </form> @@ -170,14 +193,16 @@ {% endif %} <div class="column is-narrow"> {% if email.is_optout %} - <form method="post" action="{% url 'mail-optin' %}"> + <form method="post"> {% csrf_token %} + <input type="hidden" name="form_name" value="user-email-optin-form"> <input type="hidden" name="email" value="{{ email.email }}"/> <button class="button is-info is-right">Opt-in</button> </form> {% else %} - <form method="post" action="{% url 'mail-optout' %}"> + <form method="post"> {% csrf_token %} + <input type="hidden" name="form_name" value="user-email-optout-form"> <input type="hidden" name="email" value="{{ email.email }}"/> <button class="button is-info">Opt-out</button> </form> @@ -189,14 +214,18 @@ {% endfor %} <div class="block"></div> <div class="block"> - <form class="block" method="post" action="{% url 'user-link' %}"> + <form class="block" method="post"> {% csrf_token %} + <input type="hidden" name="form_name" value="user-link-email-form"> <label for="id_email" class="label"> Add email address </label> <div class="field is-grouped"> <div class="control"> - <input id="id_email" type="email" name="email" placeholder="e.g. bobsm...@example.com" class="input" required> + <input id="id_email" type="email" name="email" placeholder="e.g. bobsm...@example.com" class="input" value="{{ user_link_email_form.email.value|default:'' }}" required> +{% for error in user_link_email_form.email.errors %} + <p class="help is-danger">{{ error }}</p> +{% endfor %} </div> <div class="control"> <button class="button is-info"> @@ -213,8 +242,15 @@ <a href="#profile-settings" title="Permalink to this section">#</a> Profile settings </h2> +{% if user_profile_form.non_field_errors %} + <div class="notification is-danger is-light"> + <button class="delete" onclick="dismiss(this);"></button> + {{ user_profile_form.non_field_errors }} + </div> +{% endif %} <form class="block" method="post"> {% csrf_token %} + <input type="hidden" name="form_name" value="user-profile-form"> <div class="field"> <label for="id_items_per_page" class="label"> Items per page @@ -222,6 +258,9 @@ <div class="control"> <input id="id_items_per_page" type="number" name="items_per_page" class="input" value="{{ user.profile.items_per_page }}" required> <p class="help">Number of items to display per page</p> +{% for error in user_profile_form.items_per_page.errors %} + <p class="help is-danger">{{ error }}</p> +{% endfor %} </div> </div> <div class="field"> @@ -230,14 +269,17 @@ </p> <div class="control"> <label class="radio"> - <input type="radio" name="show_ids"> + <input type="radio" name="show_ids" value="yes" {% if user.profile.show_ids %}checked{% endif %}> Yes </label> <label class="radio"> - <input type="radio" name="show_ids"> + <input type="radio" name="show_ids" value="no" {% if not user.profile.show_ids %}checked{% endif %}> No </label> <p class="help">Show click-to-copy patch IDs in the list view</p> +{% for error in user_profile_form.show_ids.errors %} + <p class="help is-danger">{{ error }}</p> +{% endfor %} </div> </div> <div class="control"> @@ -251,8 +293,15 @@ <a href="#security" title="Permalink to this section">#</a> Security </h2> - <form class="block" method="post" action="{% url 'password_change' %}"> +{% if user_password_form.non_field_errors %} + <div class="notification is-danger is-light"> + <button class="delete" onclick="dismiss(this);"></button> + {{ user_password_form.non_field_errors }} + </div> +{% endif %} + <form class="block" method="post"> {% csrf_token %} + <input type="hidden" name="form_name" value="user-password-form"> <div class="field"> <label for="id_old_password" class="label"> Current password @@ -260,22 +309,31 @@ <div class="control"> <input id="id_old_password" type="password" name="old_password" class="input" required> </div> +{% for error in user_password_form.old_password.errors %} + <p class="help is-danger">{{ error }}</p> +{% endfor %} </div> <div class="field"> <label for="id_new_password1" class="label"> New password </label> <div class="control"> - <input id="id_new_password1" type="password" name="new_password1" class="input" required> + <input id="id_new_password1" type="password" name="new_password1" class="input" autocomplete="new-password" required> </div> +{% for error in user_password_form.new_password1.errors %} + <p class="help is-danger">{{ error }}</p> +{% endfor %} </div> <div class="field"> <label for="id_new_password2" class="label"> Confirm password </label> <div class="control"> - <input id="id_new_password2" type="password" name="new_password2" class="input" required> + <input id="id_new_password2" type="password" name="new_password2" class="input" autocomplete="new-password" required> </div> +{% for error in user_password_form.new_password2.errors %} + <p class="help is-danger">{{ error }}</p> +{% endfor %} </div> <div class="control"> <button class="button is-primary is-disabled">Update password</button> @@ -333,5 +391,9 @@ document.addEventListener('DOMContentLoaded', () => { }); } }); + +function dismiss(el){ + el.parentNode.style.display = 'none'; +}; </script> {% endblock %} diff --git patchwork/templates/patchwork/user-link-confirm.html patchwork/templates/patchwork/user-link-confirm.html deleted file mode 100644 index 411fcf72..00000000 --- patchwork/templates/patchwork/user-link-confirm.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Link accounts{% endblock %} -{% block heading %}Link accounts for {{ user.username }}{% endblock %} - -{% block body %} - -{% if not errors %} -<p> - You have successfully linked the email address {{ person.email }} to - your Patchwork account -</p> -{% endif %} -<p>Back to <a href="{% url 'user-profile' %}">your profile</a>.</p> -{% endblock %} diff --git patchwork/templates/patchwork/user-link.html patchwork/templates/patchwork/user-link.html deleted file mode 100644 index a005782b..00000000 --- patchwork/templates/patchwork/user-link.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Link accounts{% endblock %} -{% block heading %}Link accounts for {{ user.username }}{% endblock %} - -{% block body %} -{% if confirmation and not error %} -<p> - A confirmation email has been sent to {{ confirmation.email }}. - Click on the link provided in the email to confirm that this address - belongs to you. -</p> -{% else %} -<p>There was an error submitting your link request:</p> -{% if form.errors %} -{{ form.non_field_errors }} -{% endif %} -{% if error %} -<ul class="error-list"><li>{{error}}</li></ul> -{% endif %} - -<form action="{% url 'user-link' %}" method="post"> - {% csrf_token %} - {{ linkform.email.errors }} - Link an email address: {{ linkform.email }} -</form> -{% endif %} -{% endblock %} diff --git patchwork/tests/views/test_user.py patchwork/tests/views/test_user.py index 22bb9839..abd9e583 100644 --- patchwork/tests/views/test_user.py +++ patchwork/tests/views/test_user.py @@ -243,29 +243,40 @@ class UserLinkTest(_UserTestCase): self.secondary_email = _generate_secondary_email(self.user) def test_user_person_request_form(self): - response = self.client.get(reverse('user-link')) - self.assertEqual(response.status_code, 200) - self.assertTrue(response.context['linkform']) - - def test_user_person_request_empty(self): - response = self.client.post(reverse('user-link'), {'email': ''}) + response = self.client.get(reverse('user-profile')) self.assertEqual(response.status_code, 200) - self.assertTrue(response.context['linkform']) - self.assertFormError(response, 'linkform', 'email', - 'This field is required.') + self.assertTrue(response.context['user_link_email_form']) - def test_user_person_request_invalid(self): - response = self.client.post(reverse('user-link'), {'email': 'foo'}) + def _test_user_link_error(self, email, error): + response = self.client.post( + reverse('user-profile'), + {'form_name': 'user-link-email-form', 'email': email}, + ) self.assertEqual(response.status_code, 200) - self.assertTrue(response.context['linkform']) - self.assertFormError(response, 'linkform', 'email', - error_strings['email']) - - def test_user_person_request_valid(self): - response = self.client.post(reverse('user-link'), - {'email': self.secondary_email}) + self.assertTrue(response.context['user_link_email_form']) + self.assertFormError( + response, 'user_link_email_form', 'email', error) + + def test_user_link_empty_request(self): + self._test_user_link_error('', 'This field is required.') + + def test_user_link_invalid_email(self): + self._test_user_link_error('foo', error_strings['email']) + + def test_user_link_email_already_linked(self): + self._test_user_link_error( + self.user.email, 'That email is already linked to your account.') + + def test_user_link_success(self): + response = self.client.post( + reverse('user-profile'), + { + 'form_name': 'user-link-email-form', + 'email': self.secondary_email, + }, + ) self.assertEqual(response.status_code, 200) - self.assertTrue(response.context['confirmation']) + self.assertTrue(response.context['user_link_email_form']) # check that we have a confirmation saved self.assertEqual(EmailConfirmation.objects.count(), 1) @@ -283,8 +294,7 @@ class UserLinkTest(_UserTestCase): # ...and that the URL is valid response = self.client.get(_confirmation_url(conf)) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'patchwork/user-link-confirm.html') + self.assertRedirects(response, reverse('user-profile')) class ConfirmationTest(TestCase): diff --git patchwork/urls.py patchwork/urls.py index a7dfc3d3..30c070a9 100644 --- patchwork/urls.py +++ patchwork/urls.py @@ -128,19 +128,7 @@ urlpatterns = [ path('user/todo/', user_views.todo_lists, name='user-todos'), path('user/todo/<project_id>/', user_views.todo_list, name='user-todo'), path('user/bundles/', bundle_views.bundle_list, name='user-bundles'), - path('user/link/', user_views.link, name='user-link'), - path('user/unlink/<person_id>/', user_views.unlink, name='user-unlink'), - # password change - path( - 'user/password-change/', - auth_views.PasswordChangeView.as_view(), - name='password_change', - ), - path( - 'user/password-change/done/', - auth_views.PasswordChangeDoneView.as_view(), - name='password_change_done', - ), + # password reset path( 'user/password-reset/', auth_views.PasswordResetView.as_view(), diff --git patchwork/views/mail.py patchwork/views/mail.py index 8b31fc9e..1a2019eb 100644 --- patchwork/views/mail.py +++ patchwork/views/mail.py @@ -86,8 +86,8 @@ def _optinout(request, action): email = form.cleaned_data['email'] if ( - action == 'optin' - and EmailOptout.objects.filter(email=email).count() == 0 + action == 'optin' and + EmailOptout.objects.filter(email=email).count() == 0 ): context['error'] = ( "The email address %s is not on the patchwork " @@ -109,7 +109,7 @@ def _optinout(request, action): except smtplib.SMTPException: context['confirmation'] = None context['error'] = ( - 'An error occurred during confirmation . ' + 'An error occurred during confirmation. ' 'Please try again later.' ) context['admins'] = conf_settings.ADMINS diff --git patchwork/views/user.py patchwork/views/user.py index 7bf6377e..d1a1180e 100644 --- patchwork/views/user.py +++ patchwork/views/user.py @@ -6,7 +6,10 @@ import smtplib from django.contrib import auth +from django.contrib.auth import forms as auth_forms from django.contrib.auth.decorators import login_required +from django.contrib.auth import update_session_auth_hash +from django.contrib import messages from django.contrib.sites.models import Site from django.conf import settings from django.core.mail import send_mail @@ -17,8 +20,12 @@ from django.template.loader import render_to_string from django.urls import reverse from patchwork.filters import DelegateFilter -from patchwork.forms import EmailForm +from patchwork import forms from patchwork.forms import RegistrationForm +from patchwork.forms import UserLinkEmailForm +from patchwork.forms import UserUnlinkEmailForm +from patchwork.forms import UserPrimaryEmailForm +from patchwork.forms import UserForm from patchwork.forms import UserProfileForm from patchwork.models import EmailConfirmation from patchwork.models import EmailOptout @@ -96,20 +103,199 @@ def register_confirm(request, conf): return render(request, 'patchwork/registration-confirm.html') +def _opt_in(request, email): + conf = EmailConfirmation(type='optin', email=email) + conf.save() + + context = {'confirmation': conf} + subject = render_to_string('patchwork/mails/optin-request-subject.txt') + message = render_to_string( + 'patchwork/mails/optin-request.txt', context, request=request) + + try: + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email]) + except smtplib.SMTPException: + messages.error( + request, + 'An error occurred while submitting this request. ' + 'Please contact an administrator.' + ) + return False + + return True + + +def _opt_out(request, email): + conf = EmailConfirmation(type='optout', email=email) + conf.save() + + context = {'confirmation': conf} + subject = render_to_string('patchwork/mails/optout-request-subject.txt') + message = render_to_string( + 'patchwork/mails/optout-request.txt', context, request=request) + + try: + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email]) + except smtplib.SMTPException: + messages.error( + request, + 'An error occurred while submitting this request. ' + 'Please contact an administrator.' + ) + return False + + return True + + +def _send_confirmation_email(request, email): + conf = EmailConfirmation(type='userperson', user=request.user, email=email) + conf.save() + + context = {'confirmation': conf} + subject = render_to_string('patchwork/mails/user-link-subject.txt') + message = render_to_string( + 'patchwork/mails/user-link.txt', + context, + request=request, + ) + + try: + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email]) + except smtplib.SMTPException: + messages.error( + request, + 'An error occurred while submitting this request. ' + 'Please contact an administrator.' + ) + return False + + return True + + @login_required def profile(request): + user_link_email_form = UserLinkEmailForm(user=request.user) + user_unlink_email_form = UserUnlinkEmailForm(user=request.user) + user_primary_email_form = UserPrimaryEmailForm(instance=request.user) + user_email_optin_form = forms.UserEmailOptinForm(user=request.user) + user_email_optout_form = forms.UserEmailOptoutForm(user=request.user) + user_form = UserForm(instance=request.user) + user_password_form = auth_forms.PasswordChangeForm(user=request.user) + user_profile_form = UserProfileForm(instance=request.user.profile) + if request.method == 'POST': - form = UserProfileForm( - instance=request.user.profile, data=request.POST - ) - if form.is_valid(): - form.save() - else: - form = UserProfileForm(instance=request.user.profile) + form_name = request.POST.get('form_name', '') + if form_name == UserLinkEmailForm.name: + user_link_email_form = UserLinkEmailForm( + user=request.user, data=request.POST + ) + if user_link_email_form.is_valid(): + if _send_confirmation_email( + request, user_link_email_form.cleaned_data['email'], + ): + messages.success( + request, + 'Added new email. Check your email for confirmation.', + ) + return HttpResponseRedirect(reverse('user-profile')) + + messages.error(request, 'Error linking new email.') + elif form_name == UserUnlinkEmailForm.name: + user_unlink_email_form = UserUnlinkEmailForm( + user=request.user, data=request.POST + ) + if user_unlink_email_form.is_valid(): + person = get_object_or_404( + Person, email=user_unlink_email_form.cleaned_data['email'] + ) + person.user = None + person.save() + messages.success(request, 'Unlinked email.') + return HttpResponseRedirect(reverse('user-profile')) + + messages.error(request, 'Error unlinking email.') + elif form_name == UserPrimaryEmailForm.name: + user_primary_email_form = UserPrimaryEmailForm( + instance=request.user, data=request.POST + ) + if user_primary_email_form.is_valid(): + user_primary_email_form.save() + messages.success(request, 'Primary email updated.') + return HttpResponseRedirect(reverse('user-profile')) + + messages.error(request, 'Error updating primary email.') + elif form_name == forms.UserEmailOptinForm.name: + user_email_optin_form = forms.UserEmailOptinForm( + user=request.user, data=request.POST) + if user_email_optin_form.is_valid(): + if _opt_in( + request, user_email_optin_form.cleaned_data['email'], + ): + messages.success( + request, + 'Requested opt-in to email from Patchwork. ' + 'Check your email for confirmation.', + ) + return HttpResponseRedirect(reverse('user-profile')) + + messages.error(request, 'Error opting into email.') + elif form_name == forms.UserEmailOptoutForm.name: + user_email_optout_form = forms.UserEmailOptoutForm( + user=request.user, data=request.POST) + if user_email_optout_form.is_valid(): + if _opt_out( + request, user_email_optout_form.cleaned_data['email'], + ): + messages.success( + request, + 'Requested opt-out from email from Patchwork. ' + 'Check your email for confirmation.', + ) + return HttpResponseRedirect(reverse('user-profile')) + + messages.error(request, 'Error opting out of email.') + elif form_name == UserForm.name: + user_form = UserForm(instance=request.user, data=request.POST) + if user_form.is_valid(): + user_form.save() + messages.success(request, 'Name updated.') + return HttpResponseRedirect(reverse('user-profile')) + + messages.error(request, 'Error updating name.') + elif form_name == 'user-password-form': + user_password_form = auth_forms.PasswordChangeForm( + user=request.user, data=request.POST + ) + if user_password_form.is_valid(): + user_password_form.save() + update_session_auth_hash(request, request.user) + messages.success(request, 'Password updated.') + return HttpResponseRedirect(reverse('user-profile')) + + messages.error(request, 'Error updating password.') + elif form_name == UserProfileForm.name: + user_profile_form = UserProfileForm( + instance=request.user.profile, data=request.POST + ) + if user_profile_form.is_valid(): + user_profile_form.save() + messages.success(request, 'Preferences updated.') + return HttpResponseRedirect(reverse('user-profile')) + + messages.error(request, 'Error updating preferences.') + else: + messages.error(request, 'Unrecognized request') context = { 'bundles': request.user.bundles.all(), - 'profileform': form, + 'user_link_email_form': user_link_email_form, + 'user_unlink_email_form': user_unlink_email_form, + 'user_primary_email_form': user_primary_email_form, + 'user_email_optin_form': user_email_optin_form, + 'user_email_optout_form': user_email_optout_form, + 'user_form': user_form, + 'user_password_form': user_password_form, + 'user_profile_form': user_profile_form, } # This looks unsafe but is actually fine: it just gets the names @@ -127,55 +313,11 @@ def profile(request): select={'is_optout': optout_query} ) context['linked_emails'] = people - context['linkform'] = EmailForm() context['api_token'] = request.user.profile.token - if settings.ENABLE_REST_API: - context['rest_api_enabled'] = True return render(request, 'patchwork/profile.html', context) -@login_required -def link(request): - context = {} - - if request.method == 'POST': - form = EmailForm(request.POST) - if form.is_valid(): - conf = EmailConfirmation( - type='userperson', - user=request.user, - email=form.cleaned_data['email'], - ) - conf.save() - - context['confirmation'] = conf - - subject = render_to_string('patchwork/mails/user-link-subject.txt') - message = render_to_string( - 'patchwork/mails/user-link.txt', context, request=request - ) - try: - send_mail( - subject, - message, - settings.DEFAULT_FROM_EMAIL, - [form.cleaned_data['email']], - ) - except smtplib.SMTPException: - context['confirmation'] = None - context['error'] = ( - 'An error occurred during confirmation. ' - 'Please try again later' - ) - else: - form = EmailForm() - - context['linkform'] = form - - return render(request, 'patchwork/user-link.html', context) - - @login_required def link_confirm(request, conf): try: @@ -187,20 +329,7 @@ def link_confirm(request, conf): person.save() conf.deactivate() - context = { - 'person': person, - } - - return render(request, 'patchwork/user-link-confirm.html', context) - - -@login_required -def unlink(request, person_id): - person = get_object_or_404(Person, id=person_id) - - if request.method == 'POST' and person.email != request.user.email: - person.user = None - person.save() + messages.success(request, 'Successfully linked email to account.') return HttpResponseRedirect(reverse('user-profile')) -- 2.31.1 _______________________________________________ Patchwork mailing list Patchwork@lists.ozlabs.org https://lists.ozlabs.org/listinfo/patchwork