get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/patches/1523348/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "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/",
    "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": "<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",
    "commit_ref": null,
    "pull_url": null,
    "state": "rfc",
    "archived": false,
    "hash": "28353f9792e81289c53f125a459611da112e41d9",
    "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/20210901165756.181192-12-stephen@that.guru/mbox/",
    "series": [
        {
            "id": 260605,
            "url": "http://patchwork.ozlabs.org/api/series/260605/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/patchwork/list/?series=260605",
            "date": "2021-09-01T16:57:37",
            "name": "Integrate Bulma",
            "version": 2,
            "mbox": "http://patchwork.ozlabs.org/series/260605/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/1523348/comments/",
    "check": "pending",
    "checks": "http://patchwork.ozlabs.org/api/patches/1523348/checks/",
    "tags": {},
    "related": [
        {
            "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/",
            "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",
            "mbox": "http://patchwork.ozlabs.org/project/patchwork/patch/20210811213705.36293-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\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=Gi7GSejh;\n\tdkim-atps=neutral",
            "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>)",
            "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=Gi7GSejh;\n\tdkim-atps=neutral",
            "lists.ozlabs.org;\n spf=none (no SPF record) smtp.mailfrom=that.guru\n (client-ip=136.175.108.57; helo=mail-108-mta57.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=Gi7GSejh;\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 4H09jH3TVvz9sXN\n\tfor <incoming@patchwork.ozlabs.org>; Thu,  2 Sep 2021 03:17:23 +1000 (AEST)",
            "from boromir.ozlabs.org (localhost [IPv6:::1])\n\tby lists.ozlabs.org (Postfix) with ESMTP id 4H09jH2JPMz30Hr\n\tfor <incoming@patchwork.ozlabs.org>; Thu,  2 Sep 2021 03:17:23 +1000 (AEST)",
            "from mail-108-mta57.mxroute.com (mail-108-mta57.mxroute.com\n [136.175.108.57])\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 4H09hm4G4bz2yKT\n for <patchwork@lists.ozlabs.org>; Thu,  2 Sep 2021 03:16:55 +1000 (AEST)",
            "from filter004.mxroute.com ([149.28.56.236] filter004.mxroute.com)\n (Authenticated sender: mN4UYu2MZsgR)\n by mail-108-mta57.mxroute.com (ZoneMTA) with ESMTPSA id\n 17ba25d091c00074ba.001\n for <patchwork@lists.ozlabs.org>\n (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256);\n Wed, 01 Sep 2021 17:16:51 +0000"
        ],
        "X-Zone-Loop": "96e29c6fdc920aeeeeb4a796ee9f1455d1f059a6bcf3",
        "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=7jZcUEBWnp0gOugu1Rhdm8wRNlmYBhoLtJ/mGrY2Ais=; b=Gi7GSejhdSmdLB1YxJbu6xSana\n 7XmbALILNe4tLIZL7z5EysZf/rpRmLXyPmu/A2GIpQL8QpGk0HhMisSfWR3GWAndHr3FsCelEHypI\n vsrxetBOgOcxO8QE+t3aLINOs32VsxCwii6+66w1QjMJkuButZVxOLymlfEIS9cW+HMknCX6waVzV\n 2gDKF2aQrMJXY8PBLek2IKCWlqVuXq3hwhKAQqXYz8FHPGVnsXWec5o32Ud9gJsiy1LjZ5f5iocTP\n t1ZECxYegzDMnboFKGtLo7WFNZ1/5/xUSEETAGgSf3sl+dlKqFokkcDNiD+JThWHPSlm8OFSpsJ67\n MUqRQz5Q==;",
        "From": "Stephen Finucane <stephen@that.guru>",
        "To": "patchwork@lists.ozlabs.org",
        "Subject": "[RFC PATCH v2 11/19] templates: Enhance profile view further",
        "Date": "Wed,  1 Sep 2021 17:57:48 +0100",
        "Message-Id": "<20210901165756.181192-12-stephen@that.guru>",
        "X-Mailer": "git-send-email 2.31.1",
        "In-Reply-To": "<20210901165756.181192-1-stephen@that.guru>",
        "References": "<20210901165756.181192-1-stephen@that.guru>",
        "MIME-Version": "1.0",
        "X-AuthUser": "stephen@that.guru",
        "X-Spam": "Yes",
        "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          |  15 -\n patchwork/templates/patchwork/user-link.html  |  28 --\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(+), 203 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 411fcf72..00000000\n--- patchwork/templates/patchwork/user-link-confirm.html\n+++ /dev/null\n@@ -1,15 +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 not errors %}\n-<p>\n-  You have successfully linked the email address {{ person.email }} to\n-  your 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 a005782b..00000000\n--- patchwork/templates/patchwork/user-link.html\n+++ /dev/null\n@@ -1,28 +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-<p>There was an error submitting your link request:</p>\n-{% if form.errors %}\n-{{ form.non_field_errors }}\n-{% endif %}\n-{% if error %}\n-<ul class=\"error-list\"><li>{{error}}</li></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 a7dfc3d3..30c070a9 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",
        "v2",
        "11/19"
    ]
}