Patch Detail
get:
Show a patch.
patch:
Update a patch.
put:
Update a patch.
GET /api/patches/1516045/?format=api
{ "id": 1516045, "url": "http://patchwork.ozlabs.org/api/patches/1516045/?format=api", "web_url": "http://patchwork.ozlabs.org/project/patchwork/patch/20210811213705.36293-12-stephen@that.guru/", "project": { "id": 16, "url": "http://patchwork.ozlabs.org/api/projects/16/?format=api", "name": "Patchwork", "link_name": "patchwork", "list_id": "patchwork.lists.ozlabs.org", "list_email": "patchwork@lists.ozlabs.org", "web_url": "http://jk.ozlabs.org/projects/patchwork/", "scm_url": "git://github.com/getpatchwork/patchwork", "webscm_url": "https://github.com/getpatchwork/patchwork", "list_archive_url": "", "list_archive_url_format": "", "commit_url_format": "" }, "msgid": "<20210811213705.36293-12-stephen@that.guru>", "list_archive_url": null, "date": "2021-08-11T21:36:57", "name": "[RFC,11/19] templates: Enhance profile view further", "commit_ref": null, "pull_url": null, "state": "rfc", "archived": false, "hash": "533aadbe9ee4c62be9e5419d81bff48c01c1370c", "submitter": { "id": 69991, "url": "http://patchwork.ozlabs.org/api/people/69991/?format=api", "name": "Stephen Finucane", "email": "stephen@that.guru" }, "delegate": null, "mbox": "http://patchwork.ozlabs.org/project/patchwork/patch/20210811213705.36293-12-stephen@that.guru/mbox/", "series": [ { "id": 257699, "url": "http://patchwork.ozlabs.org/api/series/257699/?format=api", "web_url": "http://patchwork.ozlabs.org/project/patchwork/list/?series=257699", "date": "2021-08-11T21:36:49", "name": "Integrate Bulma", "version": 1, "mbox": "http://patchwork.ozlabs.org/series/257699/mbox/" } ], "comments": "http://patchwork.ozlabs.org/api/patches/1516045/comments/", "check": "pending", "checks": "http://patchwork.ozlabs.org/api/patches/1516045/checks/", "tags": {}, "related": [ { "id": 1523348, "url": "http://patchwork.ozlabs.org/api/patches/1523348/?format=api", "web_url": "http://patchwork.ozlabs.org/project/patchwork/patch/20210901165756.181192-12-stephen@that.guru/", "msgid": "<20210901165756.181192-12-stephen@that.guru>", "list_archive_url": null, "date": "2021-09-01T16:57:48", "name": "[RFC,v2,11/19] templates: Enhance profile view further", "mbox": "http://patchwork.ozlabs.org/project/patchwork/patch/20210901165756.181192-12-stephen@that.guru/mbox/" } ], "headers": { "Return-Path": "\n <patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org>", "X-Original-To": [ "incoming@patchwork.ozlabs.org", "patchwork@lists.ozlabs.org" ], "Delivered-To": [ "patchwork-incoming@bilbo.ozlabs.org", "patchwork@lists.ozlabs.org" ], "Authentication-Results": [ "ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=lists.ozlabs.org\n (client-ip=2404:9400:2:0:216:3eff:fee1:b9f1; helo=lists.ozlabs.org;\n envelope-from=patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org;\n receiver=<UNKNOWN>)", "ozlabs.org;\n\tdkim=fail reason=\"key not found in DNS\" header.d=that.guru\n header.i=@that.guru header.a=rsa-sha256 header.s=x header.b=uTn+pxL6;\n\tdkim-atps=neutral", "lists.ozlabs.org;\n\tdkim=fail reason=\"key not found in DNS\" header.d=that.guru\n header.i=@that.guru header.a=rsa-sha256 header.s=x header.b=uTn+pxL6;\n\tdkim-atps=neutral", "lists.ozlabs.org;\n spf=none (no SPF record) smtp.mailfrom=that.guru\n (client-ip=136.175.108.159; helo=mail-108-mta159.mxroute.com;\n envelope-from=stephen@that.guru; receiver=<UNKNOWN>)", "lists.ozlabs.org;\n dkim=fail reason=\"key not found in DNS\" header.d=that.guru\n header.i=@that.guru\n header.a=rsa-sha256 header.s=x header.b=uTn+pxL6;\n dkim-atps=neutral" ], "Received": [ "from lists.ozlabs.org (lists.ozlabs.org\n [IPv6:2404:9400:2:0:216:3eff:fee1:b9f1])\n\t(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n\t key-exchange X25519 server-signature RSA-PSS (4096 bits))\n\t(No client certificate requested)\n\tby ozlabs.org (Postfix) with ESMTPS id 4GlPBt2QYfz9sSs\n\tfor <incoming@patchwork.ozlabs.org>; Thu, 12 Aug 2021 08:10:14 +1000 (AEST)", "from boromir.ozlabs.org (localhost [IPv6:::1])\n\tby lists.ozlabs.org (Postfix) with ESMTP id 4GlPBt0yfbz3bhr\n\tfor <incoming@patchwork.ozlabs.org>; Thu, 12 Aug 2021 08:10:14 +1000 (AEST)", "from mail-108-mta159.mxroute.com (mail-108-mta159.mxroute.com\n [136.175.108.159])\n (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))\n (No client certificate requested)\n by lists.ozlabs.org (Postfix) with ESMTPS id 4GlPBb54xSz301J\n for <patchwork@lists.ozlabs.org>; Thu, 12 Aug 2021 08:09:59 +1000 (AEST)", "from filter004.mxroute.com ([149.28.56.236] filter004.mxroute.com)\n (Authenticated sender: mN4UYu2MZsgR)\n by mail-108-mta159.mxroute.com (ZoneMTA) with ESMTPSA id\n 17b3743c45000074ba.002 for <patchwork@lists.ozlabs.org>\n (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256);\n Wed, 11 Aug 2021 22:09:53 +0000" ], "X-Zone-Loop": "a1a49eb0959f069105b67a40b60f3e0d3142be96f555", "X-Originating-IP": "[149.28.56.236]", "DKIM-Signature": "v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=that.guru;\n s=x;\n h=Content-Transfer-Encoding:MIME-Version:References:In-Reply-To:\n Message-Id:Date:Subject:Cc:To:From:Sender:Reply-To:Content-Type:Content-ID:\n Content-Description:Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc\n :Resent-Message-ID:List-Id:List-Help:List-Unsubscribe:List-Subscribe:\n List-Post:List-Owner:List-Archive;\n bh=wtMEnZuVLZ0Y8ATxIL/DpSxC/omkr+91hcpgu7G8U9Y=; b=uTn+pxL6vMgBjR5KLcsPmJL3Jg\n iAT3Hf2EYHQJIo3+yG5uncoeMb2bLkXWm/0NdIIK2IFMThqI7F3vGRKeiC5Ewc+hKtrjHVKv4QjcR\n xq+YS+wF+h93K8E1kultKXNvNyTo3+d9bhFOK2WhkHNblUinCGsLzbSCCLqTnrZuEsZwCEmgwcxzI\n JCbE2KloP82ZusxhlGVl4pxUu7v5PC7flO5aDnmGakpmySsiF+KAMFBlergr5sRM1xTuP7A7xJBup\n hA33AD/HcS6rhnFK5/Cvo4J26BWmNBZhHR2YRkmCGMYfnfLOWnIEYNlFC3fushUYE/VsKBci8CWbr\n Gh6sflFg==;", "From": "Stephen Finucane <stephen@that.guru>", "To": "patchwork@lists.ozlabs.org", "Subject": "[RFC PATCH 11/19] templates: Enhance profile view further", "Date": "Wed, 11 Aug 2021 22:36:57 +0100", "Message-Id": "<20210811213705.36293-12-stephen@that.guru>", "X-Mailer": "git-send-email 2.31.1", "In-Reply-To": "<20210811213705.36293-1-stephen@that.guru>", "References": "<20210811213705.36293-1-stephen@that.guru>", "MIME-Version": "1.0", "X-AuthUser": "stephen@that.guru", "X-Spam": "Yes", "X-Zone-Spam-Resolution": "add header", "X-Zone-Spam-Status": "Yes, score=10, required=15, tests=[ARC_NA=0,\n SPAM_FLAG=5, MID_CONTAINS_FROM=1, FROM_HAS_DN=0, RCPT_COUNT_THREE=0,\n TO_DN_SOME=0, R_MISSING_CHARSET=2.5, RCVD_COUNT_ZERO=0, FROM_EQ_ENVFROM=0,\n MIME_TRACE=0, BROKEN_CONTENT_TYPE=1.5, NEURAL_SPAM=0]", "X-BeenThere": "patchwork@lists.ozlabs.org", "X-Mailman-Version": "2.1.29", "Precedence": "list", "List-Id": "Patchwork development <patchwork.lists.ozlabs.org>", "List-Unsubscribe": "<https://lists.ozlabs.org/options/patchwork>,\n <mailto:patchwork-request@lists.ozlabs.org?subject=unsubscribe>", "List-Archive": "<http://lists.ozlabs.org/pipermail/patchwork/>", "List-Post": "<mailto:patchwork@lists.ozlabs.org>", "List-Help": "<mailto:patchwork-request@lists.ozlabs.org?subject=help>", "List-Subscribe": "<https://lists.ozlabs.org/listinfo/patchwork>,\n <mailto:patchwork-request@lists.ozlabs.org?subject=subscribe>", "Content-Type": "text/plain; charset=\"us-ascii\"", "Content-Transfer-Encoding": "7bit", "Errors-To": "patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org", "Sender": "\"Patchwork\"\n <patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org>" }, "content": "Fill in the gaps intentionally missed previously by amalgamating most\nuser-specific views into the user profile view.\n\nSigned-off-by: Stephen Finucane <stephen@that.guru>\n---\n patchwork/forms.py | 252 ++++++++++++++---\n patchwork/templates/patchwork/profile.html | 86 +++++-\n .../patchwork/user-link-confirm.html | 17 --\n patchwork/templates/patchwork/user-link.html | 32 ---\n patchwork/tests/views/test_user.py | 52 ++--\n patchwork/urls.py | 14 +-\n patchwork/views/mail.py | 6 +-\n patchwork/views/user.py | 263 +++++++++++++-----\n 8 files changed, 513 insertions(+), 209 deletions(-)\n delete mode 100644 patchwork/templates/patchwork/user-link-confirm.html\n delete mode 100644 patchwork/templates/patchwork/user-link.html", "diff": "diff --git patchwork/forms.py patchwork/forms.py\nindex 24322c78..5f8dff96 100644\n--- patchwork/forms.py\n+++ patchwork/forms.py\n@@ -4,10 +4,12 @@\n # SPDX-License-Identifier: GPL-2.0-or-later\n \n from django.contrib.auth.models import User\n+from django.core import exceptions\n from django import forms\n from django.db.models import Q\n from django.db.utils import ProgrammingError\n \n+from patchwork import models\n from patchwork.models import Bundle\n from patchwork.models import Patch\n from patchwork.models import State\n@@ -15,13 +17,14 @@ from patchwork.models import UserProfile\n \n \n class RegistrationForm(forms.Form):\n+\n first_name = forms.CharField(max_length=30, required=False)\n last_name = forms.CharField(max_length=30, required=False)\n- username = forms.RegexField(regex=r'^\\w+$', max_length=30,\n- label=u'Username')\n- email = forms.EmailField(max_length=100, label=u'Email address')\n- password = forms.CharField(widget=forms.PasswordInput(),\n- label='Password')\n+ username = forms.RegexField(\n+ regex=r'^\\w+$', max_length=30, label='Username'\n+ )\n+ email = forms.EmailField(max_length=100, label='Email address')\n+ password = forms.CharField(widget=forms.PasswordInput(), label='Password')\n \n def clean_username(self):\n value = self.cleaned_data['username']\n@@ -29,8 +32,9 @@ class RegistrationForm(forms.Form):\n User.objects.get(username__iexact=value)\n except User.DoesNotExist:\n return self.cleaned_data['username']\n- raise forms.ValidationError('This username is already taken. '\n- 'Please choose another.')\n+ raise forms.ValidationError(\n+ 'This username is already taken. Please choose another.'\n+ )\n \n def clean_email(self):\n value = self.cleaned_data['email']\n@@ -38,21 +42,24 @@ class RegistrationForm(forms.Form):\n user = User.objects.get(email__iexact=value)\n except User.DoesNotExist:\n return self.cleaned_data['email']\n- raise forms.ValidationError('This email address is already in use '\n- 'for the account \"%s\".\\n' % user.username)\n+ raise forms.ValidationError(\n+ 'This email address is already in use '\n+ 'for the account \"%s\".\\n' % user.username\n+ )\n \n def clean(self):\n return self.cleaned_data\n \n \n-class EmailForm(forms.Form):\n- email = forms.EmailField(max_length=200)\n-\n-\n class BundleForm(forms.ModelForm):\n+\n name = forms.RegexField(\n- regex=r'^[^/]+$', min_length=1, max_length=50, label=u'Name',\n- error_messages={'invalid': 'Bundle names can\\'t contain slashes'})\n+ regex=r'^[^/]+$',\n+ min_length=1,\n+ max_length=50,\n+ label='Name',\n+ error_messages={'invalid': 'Bundle names can\\'t contain slashes'},\n+ )\n \n class Meta:\n model = Bundle\n@@ -61,37 +68,180 @@ class BundleForm(forms.ModelForm):\n \n class CreateBundleForm(BundleForm):\n \n- def __init__(self, *args, **kwargs):\n- super(CreateBundleForm, self).__init__(*args, **kwargs)\n-\n- class Meta:\n- model = Bundle\n- fields = ['name']\n-\n def clean_name(self):\n name = self.cleaned_data['name']\n- count = Bundle.objects.filter(owner=self.instance.owner,\n- name=name).count()\n+ count = Bundle.objects.filter(\n+ owner=self.instance.owner, name=name\n+ ).count()\n if count > 0:\n- raise forms.ValidationError('A bundle called %s already exists'\n- % name)\n+ raise forms.ValidationError(\n+ 'A bundle called %s already exists' % name\n+ )\n return name\n \n+ class Meta:\n+ model = Bundle\n+ fields = ['name']\n+\n \n class DeleteBundleForm(forms.Form):\n+\n name = 'deletebundleform'\n form_name = forms.CharField(initial=name, widget=forms.HiddenInput)\n bundle_id = forms.IntegerField(widget=forms.HiddenInput)\n \n \n+class UserForm(forms.ModelForm):\n+\n+ name = 'user-form'\n+\n+ class Meta:\n+ model = User\n+ fields = ['first_name', 'last_name']\n+\n+\n+class EmailForm(forms.Form):\n+\n+ email = forms.EmailField(max_length=200)\n+\n+\n+class UserLinkEmailForm(forms.Form):\n+\n+ name = 'user-link-email-form'\n+\n+ email = forms.EmailField(max_length=200)\n+\n+ def __init__(self, user, *args, **kwargs):\n+ self.user = user\n+ super().__init__(*args, **kwargs)\n+\n+ def clean_email(self):\n+ email = self.cleaned_data['email']\n+\n+ # ensure this email is not already linked to our account\n+ try:\n+ models.Person.objects.get(email=email, user=self.user)\n+ except models.Person.DoesNotExist:\n+ pass\n+ else:\n+ raise exceptions.ValidationError(\n+ \"That email is already linked to your account.\"\n+ )\n+\n+ return email\n+\n+\n+class UserUnlinkEmailForm(forms.Form):\n+\n+ name = 'user-unlink-email-form'\n+\n+ email = forms.EmailField(max_length=200)\n+\n+ def __init__(self, user, *args, **kwargs):\n+ self.user = user\n+ super().__init__(*args, **kwargs)\n+\n+ def clean_email(self):\n+ email = self.cleaned_data['email']\n+\n+ # ensure we're not unlinking the final email\n+ if email == self.user.email:\n+ raise exceptions.ValidationError(\n+ \"You can't unlink your primary email.\"\n+ )\n+\n+ # and that this email is in fact our email to unlink\n+ try:\n+ models.Person.objects.get(email=email, user=self.user)\n+ except models.Person.DoesNotExist:\n+ raise exceptions.ValidationError(\n+ \"That email is not linked to your account.\"\n+ )\n+\n+ return email\n+\n+\n+class UserPrimaryEmailForm(forms.ModelForm):\n+\n+ name = 'user-primary-email-form'\n+\n+ class Meta:\n+ model = User\n+ fields = ['email']\n+\n+\n+class UserEmailOptinForm(forms.Form):\n+\n+ name = 'user-email-optin-form'\n+\n+ email = forms.EmailField(max_length=200)\n+\n+ def __init__(self, user, *args, **kwargs):\n+ self.user = user\n+ super().__init__(*args, **kwargs)\n+\n+ def clean_email(self):\n+ email = self.cleaned_data['email']\n+\n+ # ensure this email is linked to our account\n+ try:\n+ models.Person.objects.get(email=email, user=self.user)\n+ except models.Person.DoesNotExist:\n+ raise exceptions.ValidationError(\n+ \"You can't configure mail preferences for an email that is \"\n+ \"not associated with your account.\"\n+ )\n+\n+ return email\n+\n+\n+class UserEmailOptoutForm(forms.Form):\n+\n+ name = 'user-email-optout-form'\n+\n+ email = forms.EmailField(max_length=200)\n+\n+ def __init__(self, user, *args, **kwargs):\n+ self.user = user\n+ super().__init__(*args, **kwargs)\n+\n+ def clean_email(self):\n+ email = self.cleaned_data['email']\n+\n+ # ensure this email is linked to our account\n+ try:\n+ models.Person.objects.get(email=email, user=self.user)\n+ except models.Person.DoesNotExist:\n+ raise exceptions.ValidationError(\n+ \"You can't configure mail preferences for an email that is \"\n+ \"not associated with your account\"\n+ )\n+\n+ try:\n+ models.EmailOptout.objects.get(email=email)\n+ except models.EmailOptout.DoesNotExist:\n+ pass\n+ else:\n+ raise exceptions.ValidationError(\n+ \"You have already opted out of emails to this address.\"\n+ )\n+\n+ return email\n+\n+\n class UserProfileForm(forms.ModelForm):\n \n+ name = 'user-profile-form'\n+ show_ids = forms.TypedChoiceField(\n+ coerce=lambda x: x == 'yes',\n+ choices=(('yes', 'Yes'), ('no', 'No')),\n+ widget=forms.RadioSelect,\n+ )\n+\n class Meta:\n model = UserProfile\n fields = ['items_per_page', 'show_ids']\n- labels = {\n- 'show_ids': 'Show Patch IDs:'\n- }\n+ labels = {'show_ids': 'Show Patch IDs:'}\n \n \n def _get_delegate_qs(project, instance=None):\n@@ -101,20 +251,23 @@ def _get_delegate_qs(project, instance=None):\n if not project:\n raise ValueError('Expected a project')\n \n- q = Q(profile__in=UserProfile.objects\n- .filter(maintainer_projects=project)\n- .values('pk').query)\n+ q = Q(\n+ profile__in=UserProfile.objects.filter(maintainer_projects=project)\n+ .values('pk')\n+ .query\n+ )\n if instance and instance.delegate:\n q = q | Q(username=instance.delegate)\n+\n return User.objects.complex_filter(q)\n \n \n class PatchForm(forms.ModelForm):\n-\n def __init__(self, instance=None, project=None, *args, **kwargs):\n super(PatchForm, self).__init__(instance=instance, *args, **kwargs)\n self.fields['delegate'] = forms.ModelChoiceField(\n- queryset=_get_delegate_qs(project, instance), required=False)\n+ queryset=_get_delegate_qs(project, instance), required=False\n+ )\n \n class Meta:\n model = Patch\n@@ -122,12 +275,14 @@ class PatchForm(forms.ModelForm):\n \n \n class OptionalModelChoiceField(forms.ModelChoiceField):\n+\n no_change_choice = ('*', 'no change')\n to_field_name = None\n \n def __init__(self, *args, **kwargs):\n super(OptionalModelChoiceField, self).__init__(\n- initial=self.no_change_choice[0], *args, **kwargs)\n+ initial=self.no_change_choice[0], *args, **kwargs\n+ )\n \n def _get_choices(self):\n # _get_choices queries the database, which can fail if the db\n@@ -135,7 +290,8 @@ class OptionalModelChoiceField(forms.ModelChoiceField):\n # set of choices for now.\n try:\n choices = list(\n- super(OptionalModelChoiceField, self)._get_choices())\n+ super(OptionalModelChoiceField, self)._get_choices()\n+ )\n except ProgrammingError:\n choices = []\n choices.append(self.no_change_choice)\n@@ -153,31 +309,39 @@ class OptionalModelChoiceField(forms.ModelChoiceField):\n \n \n class OptionalBooleanField(forms.TypedChoiceField):\n-\n def is_no_change(self, value):\n return value == self.empty_value\n \n \n class MultiplePatchForm(forms.Form):\n+\n action = 'update'\n archived = OptionalBooleanField(\n- choices=[('*', 'no change'), ('True', 'Archived'),\n- ('False', 'Unarchived')],\n+ choices=[\n+ ('*', 'no change'),\n+ ('True', 'Archived'),\n+ ('False', 'Unarchived'),\n+ ],\n coerce=lambda x: x == 'True',\n- empty_value='*')\n+ empty_value='*',\n+ )\n \n def __init__(self, project, *args, **kwargs):\n super(MultiplePatchForm, self).__init__(*args, **kwargs)\n self.fields['delegate'] = OptionalModelChoiceField(\n- queryset=_get_delegate_qs(project=project), required=False)\n+ queryset=_get_delegate_qs(project=project), required=False\n+ )\n self.fields['state'] = OptionalModelChoiceField(\n- queryset=State.objects.all())\n+ queryset=State.objects.all()\n+ )\n \n def save(self, instance, commit=True):\n opts = instance.__class__._meta\n if self.errors:\n- raise ValueError(\"The %s could not be changed because the data \"\n- \"didn't validate.\" % opts.object_name)\n+ raise ValueError(\n+ \"The %s could not be changed because the data \"\n+ \"didn't validate.\" % opts.object_name\n+ )\n data = self.cleaned_data\n # Update the instance\n for f in opts.fields:\ndiff --git patchwork/templates/patchwork/profile.html patchwork/templates/patchwork/profile.html\nindex 7a0b54fe..a5a57150 100644\n--- patchwork/templates/patchwork/profile.html\n+++ patchwork/templates/patchwork/profile.html\n@@ -3,6 +3,20 @@\n {% block title %}{{ user.username }}{% endblock %}\n \n {% block body %}\n+{% for message in messages %}\n+{% if message.tags == 'success' %}\n+<div class=\"notification is-success\">\n+{% elif message.tags == 'warning' %}\n+<div class=\"notification is-warning\">\n+{% elif message.tags == 'error' %}\n+<div class=\"notification is-danger\">\n+{% else %}\n+<div class=\"notification\">\n+{% endif %}\n+ {{ message }}\n+ <button class=\"delete\" onclick=\"dismiss(this);\"></button>\n+</div>\n+{% endfor %}\n <div class=\"container\" style=\"margin-top: 1rem;\">\n <div class=\"columns\">\n <div class=\"column is-3\">\n@@ -100,7 +114,6 @@\n Settings\n </h1>\n \n-{# TODO: Add view to enable this #}\n <section class=\"block\">\n <h2 id=\"profile\" class=\"title is-4\">\n <a href=\"#profile\" title=\"Permalink to this section\">#</a>\n@@ -108,6 +121,7 @@\n </h2>\n <form method=\"post\">\n {% csrf_token %}\n+ <input type=\"hidden\" name=\"form_name\" value=\"user-form\">\n <div class=\"field\">\n <label for=\"id_username\" class=\"label\">\n Username\n@@ -143,6 +157,12 @@\n <a href=\"#linked-emails\" title=\"Permalink to this section\">#</a>\n Linked emails\n </h2>\n+{% if user_link_email_form.non_field_errors %}\n+ <div class=\"notification is-danger is-light\">\n+ <button class=\"delete\" onclick=\"dismiss(this);\"></button>\n+ {{ user_link_email_form.non_field_errors }}\n+ </div>\n+{% endif %}\n {% for email in linked_emails %}\n <div class=\"card\">\n <div class=\"card-content\">\n@@ -155,14 +175,17 @@\n </div>\n {% if user.email != email.email %}\n <div class=\"column is-narrow\">\n- <form method=\"post\" action=\"{% url 'user-unlink' person_id=email.id %}\">\n+ <form method=\"post\">\n {% csrf_token %}\n+ <input type=\"hidden\" name=\"form_name\" value=\"user-unlink-email-form\">\n+ <input type=\"hidden\" name=\"email\" value=\"{{ email.email }}\">\n <button class=\"button is-danger\">Unlink</button>\n </form>\n </div>\n-{# TODO: Add view to enable this #}\n <div class=\"column is-narrow\">\n <form method=\"post\">\n+ <input type=\"hidden\" name=\"form_name\" value=\"user-primary-email-form\">\n+ <input type=\"hidden\" name=\"email\" value=\"{{ email.email }}\">\n {% csrf_token %}\n <button class=\"button is-info\">Make primary</button>\n </form>\n@@ -170,14 +193,16 @@\n {% endif %}\n <div class=\"column is-narrow\">\n {% if email.is_optout %}\n- <form method=\"post\" action=\"{% url 'mail-optin' %}\">\n+ <form method=\"post\">\n {% csrf_token %}\n+ <input type=\"hidden\" name=\"form_name\" value=\"user-email-optin-form\">\n <input type=\"hidden\" name=\"email\" value=\"{{ email.email }}\"/>\n <button class=\"button is-info is-right\">Opt-in</button>\n </form>\n {% else %}\n- <form method=\"post\" action=\"{% url 'mail-optout' %}\">\n+ <form method=\"post\">\n {% csrf_token %}\n+ <input type=\"hidden\" name=\"form_name\" value=\"user-email-optout-form\">\n <input type=\"hidden\" name=\"email\" value=\"{{ email.email }}\"/>\n <button class=\"button is-info\">Opt-out</button>\n </form>\n@@ -189,14 +214,18 @@\n {% endfor %}\n <div class=\"block\"></div>\n <div class=\"block\">\n- <form class=\"block\" method=\"post\" action=\"{% url 'user-link' %}\">\n+ <form class=\"block\" method=\"post\">\n {% csrf_token %}\n+ <input type=\"hidden\" name=\"form_name\" value=\"user-link-email-form\">\n <label for=\"id_email\" class=\"label\">\n Add email address\n </label>\n <div class=\"field is-grouped\">\n <div class=\"control\">\n- <input id=\"id_email\" type=\"email\" name=\"email\" placeholder=\"e.g. bobsmith@example.com\" class=\"input\" required>\n+ <input id=\"id_email\" type=\"email\" name=\"email\" placeholder=\"e.g. bobsmith@example.com\" class=\"input\" value=\"{{ user_link_email_form.email.value|default:'' }}\" required>\n+{% for error in user_link_email_form.email.errors %}\n+ <p class=\"help is-danger\">{{ error }}</p>\n+{% endfor %}\n </div>\n <div class=\"control\">\n <button class=\"button is-info\">\n@@ -213,8 +242,15 @@\n <a href=\"#profile-settings\" title=\"Permalink to this section\">#</a>\n Profile settings\n </h2>\n+{% if user_profile_form.non_field_errors %}\n+ <div class=\"notification is-danger is-light\">\n+ <button class=\"delete\" onclick=\"dismiss(this);\"></button>\n+ {{ user_profile_form.non_field_errors }}\n+ </div>\n+{% endif %}\n <form class=\"block\" method=\"post\">\n {% csrf_token %}\n+ <input type=\"hidden\" name=\"form_name\" value=\"user-profile-form\">\n <div class=\"field\">\n <label for=\"id_items_per_page\" class=\"label\">\n Items per page\n@@ -222,6 +258,9 @@\n <div class=\"control\">\n <input id=\"id_items_per_page\" type=\"number\" name=\"items_per_page\" class=\"input\" value=\"{{ user.profile.items_per_page }}\" required>\n <p class=\"help\">Number of items to display per page</p>\n+{% for error in user_profile_form.items_per_page.errors %}\n+ <p class=\"help is-danger\">{{ error }}</p>\n+{% endfor %}\n </div>\n </div>\n <div class=\"field\">\n@@ -230,14 +269,17 @@\n </p>\n <div class=\"control\">\n <label class=\"radio\">\n- <input type=\"radio\" name=\"show_ids\">\n+ <input type=\"radio\" name=\"show_ids\" value=\"yes\" {% if user.profile.show_ids %}checked{% endif %}>\n Yes\n </label>\n <label class=\"radio\">\n- <input type=\"radio\" name=\"show_ids\">\n+ <input type=\"radio\" name=\"show_ids\" value=\"no\" {% if not user.profile.show_ids %}checked{% endif %}>\n No\n </label>\n <p class=\"help\">Show click-to-copy patch IDs in the list view</p>\n+{% for error in user_profile_form.show_ids.errors %}\n+ <p class=\"help is-danger\">{{ error }}</p>\n+{% endfor %}\n </div>\n </div>\n <div class=\"control\">\n@@ -251,8 +293,15 @@\n <a href=\"#security\" title=\"Permalink to this section\">#</a>\n Security\n </h2>\n- <form class=\"block\" method=\"post\" action=\"{% url 'password_change' %}\">\n+{% if user_password_form.non_field_errors %}\n+ <div class=\"notification is-danger is-light\">\n+ <button class=\"delete\" onclick=\"dismiss(this);\"></button>\n+ {{ user_password_form.non_field_errors }}\n+ </div>\n+{% endif %}\n+ <form class=\"block\" method=\"post\">\n {% csrf_token %}\n+ <input type=\"hidden\" name=\"form_name\" value=\"user-password-form\">\n <div class=\"field\">\n <label for=\"id_old_password\" class=\"label\">\n Current password\n@@ -260,22 +309,31 @@\n <div class=\"control\">\n <input id=\"id_old_password\" type=\"password\" name=\"old_password\" class=\"input\" required>\n </div>\n+{% for error in user_password_form.old_password.errors %}\n+ <p class=\"help is-danger\">{{ error }}</p>\n+{% endfor %}\n </div>\n <div class=\"field\">\n <label for=\"id_new_password1\" class=\"label\">\n New password\n </label>\n <div class=\"control\">\n- <input id=\"id_new_password1\" type=\"password\" name=\"new_password1\" class=\"input\" required>\n+ <input id=\"id_new_password1\" type=\"password\" name=\"new_password1\" class=\"input\" autocomplete=\"new-password\" required>\n </div>\n+{% for error in user_password_form.new_password1.errors %}\n+ <p class=\"help is-danger\">{{ error }}</p>\n+{% endfor %}\n </div>\n <div class=\"field\">\n <label for=\"id_new_password2\" class=\"label\">\n Confirm password\n </label>\n <div class=\"control\">\n- <input id=\"id_new_password2\" type=\"password\" name=\"new_password2\" class=\"input\" required>\n+ <input id=\"id_new_password2\" type=\"password\" name=\"new_password2\" class=\"input\" autocomplete=\"new-password\" required>\n </div>\n+{% for error in user_password_form.new_password2.errors %}\n+ <p class=\"help is-danger\">{{ error }}</p>\n+{% endfor %}\n </div>\n <div class=\"control\">\n <button class=\"button is-primary is-disabled\">Update password</button>\n@@ -333,5 +391,9 @@ document.addEventListener('DOMContentLoaded', () => {\n });\n }\n });\n+\n+function dismiss(el){\n+ el.parentNode.style.display = 'none';\n+};\n </script>\n {% endblock %}\ndiff --git patchwork/templates/patchwork/user-link-confirm.html patchwork/templates/patchwork/user-link-confirm.html\ndeleted file mode 100644\nindex aa91fcbd..00000000\n--- patchwork/templates/patchwork/user-link-confirm.html\n+++ /dev/null\n@@ -1,17 +0,0 @@\n-{% extends \"base.html\" %}\n-\n-{% block title %}Link accounts{% endblock %}\n-{% block heading %}Link accounts for {{ user.username }}{% endblock %}\n-\n-{% block body %}\n-\n-{% if errors %}\n-<p>{{ errors }}</p>\n-{% else %}\n-<p>\n- You have successfully linked the email address {{ person.email }} to your\n- Patchwork account\n-</p>\n-{% endif %}\n-<p>Back to <a href=\"{% url 'user-profile' %}\">your profile</a>.</p>\n-{% endblock %}\ndiff --git patchwork/templates/patchwork/user-link.html patchwork/templates/patchwork/user-link.html\ndeleted file mode 100644\nindex 8b3fe8f6..00000000\n--- patchwork/templates/patchwork/user-link.html\n+++ /dev/null\n@@ -1,32 +0,0 @@\n-{% extends \"base.html\" %}\n-\n-{% block title %}Link accounts{% endblock %}\n-{% block heading %}Link accounts for {{ user.username }}{% endblock %}\n-\n-{% block body %}\n-{% if confirmation and not error %}\n-<p>\n- A confirmation email has been sent to {{ confirmation.email }}.\n- Click on the link provided in the email to confirm that this address\n- belongs to you.\n-</p>\n-{% else %}\n-{% if form.errors %}\n-<p>\n- There was an error submitting your link request.\n-</p>\n-{{ form.non_field_errors }}\n-{% endif %}\n-{% if error %}\n-<ul class=\"errorlist\">\n- <li>{{ error }}</li>\n-</ul>\n-{% endif %}\n-\n-<form action=\"{% url 'user-link' %}\" method=\"post\">\n- {% csrf_token %}\n- {{ linkform.email.errors }}\n- Link an email address: {{ linkform.email }}\n-</form>\n-{% endif %}\n-{% endblock %}\ndiff --git patchwork/tests/views/test_user.py patchwork/tests/views/test_user.py\nindex 22bb9839..abd9e583 100644\n--- patchwork/tests/views/test_user.py\n+++ patchwork/tests/views/test_user.py\n@@ -243,29 +243,40 @@ class UserLinkTest(_UserTestCase):\n self.secondary_email = _generate_secondary_email(self.user)\n \n def test_user_person_request_form(self):\n- response = self.client.get(reverse('user-link'))\n- self.assertEqual(response.status_code, 200)\n- self.assertTrue(response.context['linkform'])\n-\n- def test_user_person_request_empty(self):\n- response = self.client.post(reverse('user-link'), {'email': ''})\n+ response = self.client.get(reverse('user-profile'))\n self.assertEqual(response.status_code, 200)\n- self.assertTrue(response.context['linkform'])\n- self.assertFormError(response, 'linkform', 'email',\n- 'This field is required.')\n+ self.assertTrue(response.context['user_link_email_form'])\n \n- def test_user_person_request_invalid(self):\n- response = self.client.post(reverse('user-link'), {'email': 'foo'})\n+ def _test_user_link_error(self, email, error):\n+ response = self.client.post(\n+ reverse('user-profile'),\n+ {'form_name': 'user-link-email-form', 'email': email},\n+ )\n self.assertEqual(response.status_code, 200)\n- self.assertTrue(response.context['linkform'])\n- self.assertFormError(response, 'linkform', 'email',\n- error_strings['email'])\n-\n- def test_user_person_request_valid(self):\n- response = self.client.post(reverse('user-link'),\n- {'email': self.secondary_email})\n+ self.assertTrue(response.context['user_link_email_form'])\n+ self.assertFormError(\n+ response, 'user_link_email_form', 'email', error)\n+\n+ def test_user_link_empty_request(self):\n+ self._test_user_link_error('', 'This field is required.')\n+\n+ def test_user_link_invalid_email(self):\n+ self._test_user_link_error('foo', error_strings['email'])\n+\n+ def test_user_link_email_already_linked(self):\n+ self._test_user_link_error(\n+ self.user.email, 'That email is already linked to your account.')\n+\n+ def test_user_link_success(self):\n+ response = self.client.post(\n+ reverse('user-profile'),\n+ {\n+ 'form_name': 'user-link-email-form',\n+ 'email': self.secondary_email,\n+ },\n+ )\n self.assertEqual(response.status_code, 200)\n- self.assertTrue(response.context['confirmation'])\n+ self.assertTrue(response.context['user_link_email_form'])\n \n # check that we have a confirmation saved\n self.assertEqual(EmailConfirmation.objects.count(), 1)\n@@ -283,8 +294,7 @@ class UserLinkTest(_UserTestCase):\n \n # ...and that the URL is valid\n response = self.client.get(_confirmation_url(conf))\n- self.assertEqual(response.status_code, 200)\n- self.assertTemplateUsed(response, 'patchwork/user-link-confirm.html')\n+ self.assertRedirects(response, reverse('user-profile'))\n \n \n class ConfirmationTest(TestCase):\ndiff --git patchwork/urls.py patchwork/urls.py\nindex 0c557f78..fa14f40c 100644\n--- patchwork/urls.py\n+++ patchwork/urls.py\n@@ -128,19 +128,7 @@ urlpatterns = [\n path('user/todo/', user_views.todo_lists, name='user-todos'),\n path('user/todo/<project_id>/', user_views.todo_list, name='user-todo'),\n path('user/bundles/', bundle_views.bundle_list, name='user-bundles'),\n- path('user/link/', user_views.link, name='user-link'),\n- path('user/unlink/<person_id>/', user_views.unlink, name='user-unlink'),\n- # password change\n- path(\n- 'user/password-change/',\n- auth_views.PasswordChangeView.as_view(),\n- name='password_change',\n- ),\n- path(\n- 'user/password-change/done/',\n- auth_views.PasswordChangeDoneView.as_view(),\n- name='password_change_done',\n- ),\n+ # password reset\n path(\n 'user/password-reset/',\n auth_views.PasswordResetView.as_view(),\ndiff --git patchwork/views/mail.py patchwork/views/mail.py\nindex 8b31fc9e..1a2019eb 100644\n--- patchwork/views/mail.py\n+++ patchwork/views/mail.py\n@@ -86,8 +86,8 @@ def _optinout(request, action):\n \n email = form.cleaned_data['email']\n if (\n- action == 'optin'\n- and EmailOptout.objects.filter(email=email).count() == 0\n+ action == 'optin' and\n+ EmailOptout.objects.filter(email=email).count() == 0\n ):\n context['error'] = (\n \"The email address %s is not on the patchwork \"\n@@ -109,7 +109,7 @@ def _optinout(request, action):\n except smtplib.SMTPException:\n context['confirmation'] = None\n context['error'] = (\n- 'An error occurred during confirmation . '\n+ 'An error occurred during confirmation. '\n 'Please try again later.'\n )\n context['admins'] = conf_settings.ADMINS\ndiff --git patchwork/views/user.py patchwork/views/user.py\nindex 7bf6377e..d1a1180e 100644\n--- patchwork/views/user.py\n+++ patchwork/views/user.py\n@@ -6,7 +6,10 @@\n import smtplib\n \n from django.contrib import auth\n+from django.contrib.auth import forms as auth_forms\n from django.contrib.auth.decorators import login_required\n+from django.contrib.auth import update_session_auth_hash\n+from django.contrib import messages\n from django.contrib.sites.models import Site\n from django.conf import settings\n from django.core.mail import send_mail\n@@ -17,8 +20,12 @@ from django.template.loader import render_to_string\n from django.urls import reverse\n \n from patchwork.filters import DelegateFilter\n-from patchwork.forms import EmailForm\n+from patchwork import forms\n from patchwork.forms import RegistrationForm\n+from patchwork.forms import UserLinkEmailForm\n+from patchwork.forms import UserUnlinkEmailForm\n+from patchwork.forms import UserPrimaryEmailForm\n+from patchwork.forms import UserForm\n from patchwork.forms import UserProfileForm\n from patchwork.models import EmailConfirmation\n from patchwork.models import EmailOptout\n@@ -96,20 +103,199 @@ def register_confirm(request, conf):\n return render(request, 'patchwork/registration-confirm.html')\n \n \n+def _opt_in(request, email):\n+ conf = EmailConfirmation(type='optin', email=email)\n+ conf.save()\n+\n+ context = {'confirmation': conf}\n+ subject = render_to_string('patchwork/mails/optin-request-subject.txt')\n+ message = render_to_string(\n+ 'patchwork/mails/optin-request.txt', context, request=request)\n+\n+ try:\n+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email])\n+ except smtplib.SMTPException:\n+ messages.error(\n+ request,\n+ 'An error occurred while submitting this request. '\n+ 'Please contact an administrator.'\n+ )\n+ return False\n+\n+ return True\n+\n+\n+def _opt_out(request, email):\n+ conf = EmailConfirmation(type='optout', email=email)\n+ conf.save()\n+\n+ context = {'confirmation': conf}\n+ subject = render_to_string('patchwork/mails/optout-request-subject.txt')\n+ message = render_to_string(\n+ 'patchwork/mails/optout-request.txt', context, request=request)\n+\n+ try:\n+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email])\n+ except smtplib.SMTPException:\n+ messages.error(\n+ request,\n+ 'An error occurred while submitting this request. '\n+ 'Please contact an administrator.'\n+ )\n+ return False\n+\n+ return True\n+\n+\n+def _send_confirmation_email(request, email):\n+ conf = EmailConfirmation(type='userperson', user=request.user, email=email)\n+ conf.save()\n+\n+ context = {'confirmation': conf}\n+ subject = render_to_string('patchwork/mails/user-link-subject.txt')\n+ message = render_to_string(\n+ 'patchwork/mails/user-link.txt',\n+ context,\n+ request=request,\n+ )\n+\n+ try:\n+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email])\n+ except smtplib.SMTPException:\n+ messages.error(\n+ request,\n+ 'An error occurred while submitting this request. '\n+ 'Please contact an administrator.'\n+ )\n+ return False\n+\n+ return True\n+\n+\n @login_required\n def profile(request):\n+ user_link_email_form = UserLinkEmailForm(user=request.user)\n+ user_unlink_email_form = UserUnlinkEmailForm(user=request.user)\n+ user_primary_email_form = UserPrimaryEmailForm(instance=request.user)\n+ user_email_optin_form = forms.UserEmailOptinForm(user=request.user)\n+ user_email_optout_form = forms.UserEmailOptoutForm(user=request.user)\n+ user_form = UserForm(instance=request.user)\n+ user_password_form = auth_forms.PasswordChangeForm(user=request.user)\n+ user_profile_form = UserProfileForm(instance=request.user.profile)\n+\n if request.method == 'POST':\n- form = UserProfileForm(\n- instance=request.user.profile, data=request.POST\n- )\n- if form.is_valid():\n- form.save()\n- else:\n- form = UserProfileForm(instance=request.user.profile)\n+ form_name = request.POST.get('form_name', '')\n+ if form_name == UserLinkEmailForm.name:\n+ user_link_email_form = UserLinkEmailForm(\n+ user=request.user, data=request.POST\n+ )\n+ if user_link_email_form.is_valid():\n+ if _send_confirmation_email(\n+ request, user_link_email_form.cleaned_data['email'],\n+ ):\n+ messages.success(\n+ request,\n+ 'Added new email. Check your email for confirmation.',\n+ )\n+ return HttpResponseRedirect(reverse('user-profile'))\n+\n+ messages.error(request, 'Error linking new email.')\n+ elif form_name == UserUnlinkEmailForm.name:\n+ user_unlink_email_form = UserUnlinkEmailForm(\n+ user=request.user, data=request.POST\n+ )\n+ if user_unlink_email_form.is_valid():\n+ person = get_object_or_404(\n+ Person, email=user_unlink_email_form.cleaned_data['email']\n+ )\n+ person.user = None\n+ person.save()\n+ messages.success(request, 'Unlinked email.')\n+ return HttpResponseRedirect(reverse('user-profile'))\n+\n+ messages.error(request, 'Error unlinking email.')\n+ elif form_name == UserPrimaryEmailForm.name:\n+ user_primary_email_form = UserPrimaryEmailForm(\n+ instance=request.user, data=request.POST\n+ )\n+ if user_primary_email_form.is_valid():\n+ user_primary_email_form.save()\n+ messages.success(request, 'Primary email updated.')\n+ return HttpResponseRedirect(reverse('user-profile'))\n+\n+ messages.error(request, 'Error updating primary email.')\n+ elif form_name == forms.UserEmailOptinForm.name:\n+ user_email_optin_form = forms.UserEmailOptinForm(\n+ user=request.user, data=request.POST)\n+ if user_email_optin_form.is_valid():\n+ if _opt_in(\n+ request, user_email_optin_form.cleaned_data['email'],\n+ ):\n+ messages.success(\n+ request,\n+ 'Requested opt-in to email from Patchwork. '\n+ 'Check your email for confirmation.',\n+ )\n+ return HttpResponseRedirect(reverse('user-profile'))\n+\n+ messages.error(request, 'Error opting into email.')\n+ elif form_name == forms.UserEmailOptoutForm.name:\n+ user_email_optout_form = forms.UserEmailOptoutForm(\n+ user=request.user, data=request.POST)\n+ if user_email_optout_form.is_valid():\n+ if _opt_out(\n+ request, user_email_optout_form.cleaned_data['email'],\n+ ):\n+ messages.success(\n+ request,\n+ 'Requested opt-out from email from Patchwork. '\n+ 'Check your email for confirmation.',\n+ )\n+ return HttpResponseRedirect(reverse('user-profile'))\n+\n+ messages.error(request, 'Error opting out of email.')\n+ elif form_name == UserForm.name:\n+ user_form = UserForm(instance=request.user, data=request.POST)\n+ if user_form.is_valid():\n+ user_form.save()\n+ messages.success(request, 'Name updated.')\n+ return HttpResponseRedirect(reverse('user-profile'))\n+\n+ messages.error(request, 'Error updating name.')\n+ elif form_name == 'user-password-form':\n+ user_password_form = auth_forms.PasswordChangeForm(\n+ user=request.user, data=request.POST\n+ )\n+ if user_password_form.is_valid():\n+ user_password_form.save()\n+ update_session_auth_hash(request, request.user)\n+ messages.success(request, 'Password updated.')\n+ return HttpResponseRedirect(reverse('user-profile'))\n+\n+ messages.error(request, 'Error updating password.')\n+ elif form_name == UserProfileForm.name:\n+ user_profile_form = UserProfileForm(\n+ instance=request.user.profile, data=request.POST\n+ )\n+ if user_profile_form.is_valid():\n+ user_profile_form.save()\n+ messages.success(request, 'Preferences updated.')\n+ return HttpResponseRedirect(reverse('user-profile'))\n+\n+ messages.error(request, 'Error updating preferences.')\n+ else:\n+ messages.error(request, 'Unrecognized request')\n \n context = {\n 'bundles': request.user.bundles.all(),\n- 'profileform': form,\n+ 'user_link_email_form': user_link_email_form,\n+ 'user_unlink_email_form': user_unlink_email_form,\n+ 'user_primary_email_form': user_primary_email_form,\n+ 'user_email_optin_form': user_email_optin_form,\n+ 'user_email_optout_form': user_email_optout_form,\n+ 'user_form': user_form,\n+ 'user_password_form': user_password_form,\n+ 'user_profile_form': user_profile_form,\n }\n \n # This looks unsafe but is actually fine: it just gets the names\n@@ -127,55 +313,11 @@ def profile(request):\n select={'is_optout': optout_query}\n )\n context['linked_emails'] = people\n- context['linkform'] = EmailForm()\n context['api_token'] = request.user.profile.token\n- if settings.ENABLE_REST_API:\n- context['rest_api_enabled'] = True\n \n return render(request, 'patchwork/profile.html', context)\n \n \n-@login_required\n-def link(request):\n- context = {}\n-\n- if request.method == 'POST':\n- form = EmailForm(request.POST)\n- if form.is_valid():\n- conf = EmailConfirmation(\n- type='userperson',\n- user=request.user,\n- email=form.cleaned_data['email'],\n- )\n- conf.save()\n-\n- context['confirmation'] = conf\n-\n- subject = render_to_string('patchwork/mails/user-link-subject.txt')\n- message = render_to_string(\n- 'patchwork/mails/user-link.txt', context, request=request\n- )\n- try:\n- send_mail(\n- subject,\n- message,\n- settings.DEFAULT_FROM_EMAIL,\n- [form.cleaned_data['email']],\n- )\n- except smtplib.SMTPException:\n- context['confirmation'] = None\n- context['error'] = (\n- 'An error occurred during confirmation. '\n- 'Please try again later'\n- )\n- else:\n- form = EmailForm()\n-\n- context['linkform'] = form\n-\n- return render(request, 'patchwork/user-link.html', context)\n-\n-\n @login_required\n def link_confirm(request, conf):\n try:\n@@ -187,20 +329,7 @@ def link_confirm(request, conf):\n person.save()\n conf.deactivate()\n \n- context = {\n- 'person': person,\n- }\n-\n- return render(request, 'patchwork/user-link-confirm.html', context)\n-\n-\n-@login_required\n-def unlink(request, person_id):\n- person = get_object_or_404(Person, id=person_id)\n-\n- if request.method == 'POST' and person.email != request.user.email:\n- person.user = None\n- person.save()\n+ messages.success(request, 'Successfully linked email to account.')\n \n return HttpResponseRedirect(reverse('user-profile'))\n \n", "prefixes": [ "RFC", "11/19" ] }