[RFC PATCH 11/19] templates: Enhance profile view further
Stephen Finucane
stephen at that.guru
Thu Aug 12 07:36:57 AEST 2021
Fill in the gaps intentionally missed previously by amalgamating most
user-specific views into the user profile view.
Signed-off-by: Stephen Finucane <stephen at that.guru>
---
patchwork/forms.py | 252 ++++++++++++++---
patchwork/templates/patchwork/profile.html | 86 +++++-
.../patchwork/user-link-confirm.html | 17 --
patchwork/templates/patchwork/user-link.html | 32 ---
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(+), 209 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. bobsmith at example.com" class="input" required>
+ <input id="id_email" type="email" name="email" placeholder="e.g. bobsmith at 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 aa91fcbd..00000000
--- patchwork/templates/patchwork/user-link-confirm.html
+++ /dev/null
@@ -1,17 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Link accounts{% endblock %}
-{% block heading %}Link accounts for {{ user.username }}{% endblock %}
-
-{% block body %}
-
-{% if errors %}
-<p>{{ errors }}</p>
-{% else %}
-<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 8b3fe8f6..00000000
--- patchwork/templates/patchwork/user-link.html
+++ /dev/null
@@ -1,32 +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 %}
-{% if form.errors %}
-<p>
- There was an error submitting your link request.
-</p>
-{{ form.non_field_errors }}
-{% endif %}
-{% if error %}
-<ul class="errorlist">
- <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 0c557f78..fa14f40c 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)
- at 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)
-
-
- at 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
More information about the Patchwork
mailing list