get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

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

{
    "id": 1523347,
    "url": "http://patchwork.ozlabs.org/api/patches/1523347/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/patchwork/patch/20210901165756.181192-19-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-19-stephen@that.guru>",
    "list_archive_url": null,
    "date": "2021-09-01T16:57:55",
    "name": "[RFC,v2,18/19] templates: Convert mail settings pages",
    "commit_ref": null,
    "pull_url": null,
    "state": "rfc",
    "archived": false,
    "hash": "98ec6437bf94c409f3f6fed9d45cf5d57c8cbd20",
    "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-19-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/1523347/comments/",
    "check": "pending",
    "checks": "http://patchwork.ozlabs.org/api/patches/1523347/checks/",
    "tags": {},
    "related": [
        {
            "id": 1516046,
            "url": "http://patchwork.ozlabs.org/api/patches/1516046/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/patchwork/patch/20210811213705.36293-19-stephen@that.guru/",
            "msgid": "<20210811213705.36293-19-stephen@that.guru>",
            "list_archive_url": null,
            "date": "2021-08-11T21:37:04",
            "name": "[RFC,18/19] templates: Convert mail settings pages",
            "mbox": "http://patchwork.ozlabs.org/project/patchwork/patch/20210811213705.36293-19-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=nSSY5YIc;\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=nSSY5YIc;\n\tdkim-atps=neutral",
            "lists.ozlabs.org;\n spf=none (no SPF record) smtp.mailfrom=that.guru\n (client-ip=136.175.108.45; helo=mail-108-mta45.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=nSSY5YIc;\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 4H09j84pbCz9sW8\n\tfor <incoming@patchwork.ozlabs.org>; Thu,  2 Sep 2021 03:17:16 +1000 (AEST)",
            "from boromir.ozlabs.org (localhost [IPv6:::1])\n\tby lists.ozlabs.org (Postfix) with ESMTP id 4H09j83HlZz307T\n\tfor <incoming@patchwork.ozlabs.org>; Thu,  2 Sep 2021 03:17:16 +1000 (AEST)",
            "from mail-108-mta45.mxroute.com (mail-108-mta45.mxroute.com\n [136.175.108.45])\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 4H09hm07ygz301j\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-mta45.mxroute.com (ZoneMTA) with ESMTPSA id\n 17ba25cfed700074ba.001\n for <patchwork@lists.ozlabs.org>\n (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES128-GCM-SHA256);\n Wed, 01 Sep 2021 17:16:48 +0000"
        ],
        "X-Zone-Loop": "f4d8b5511ac6259a7d7f37174aa8cd19e2df13775832",
        "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=TnDAee2tAMxj0TdMWyMaJyXYPKHhELrUyv/+CxTyKeY=; b=nSSY5YIcwx+E/RZ+vwaCbV16rS\n ToIIIrw30a4k7ipXls+pHQPPjJDQ67SU0So35d1vVs4HeNuegiDtgW1z/c+0khBjKV52+f0vu3yzC\n RUw4bAWgbV1NxXjv+G+jKnW5MKei9WX5m3pR2KCAoWyWW3ziM8L6XN1nwa3dnUcwJLoCb7Z6i0VCg\n pNMFQZJkvIy8mZMP+PxEkqkoFkaE6+ip7rBL/CiBjqfuHvpc4pcOrL5/tnaQojA1Q5OYvnk5pD9+1\n 22gTjFlIz8JEWVMZPhG4TizVVaCiOlPSFPETamry7fbwKFrxLTvSbN+VSB74S4KFhNDHybcRSYvHf\n jXB6K2cQ==;",
        "From": "Stephen Finucane <stephen@that.guru>",
        "To": "patchwork@lists.ozlabs.org",
        "Subject": "[RFC PATCH v2 18/19] templates: Convert mail settings pages",
        "Date": "Wed,  1 Sep 2021 17:57:55 +0100",
        "Message-Id": "<20210901165756.181192-19-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-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": "This one is rather tricky. We do a major overhaul of URLs and general\nflow of confirmations, relying heavily on the messages framework to\navoid the need to have separate pages. Previously, configuring opt-in or\nopt-out of email involved the following:\n\n  GET /mail/\n    POST /mail/\n      [some validation]\n      POST /mail/optout/  # or optin\n\nIf the last step threw an error, we'd stay on '/mail/optout/' or\n'/mail/optin/' with a displayed error and tell the user to correct the\nmistake. Now, we simply do this:\n\n  GET /mail/\n    POST /mail/\n      [some validation]\n      GET /mail/<email>/\n      POST /mail/<email>/\n\nError messages are propagated via the messages framework with all\nnon-field validation errors resulting in a redirect to the homepage.\n\nSigned-off-by: Stephen Finucane <stephen@that.guru>\n---\n .../templates/patchwork/confirm-error.html    |  23 ---\n .../templates/patchwork/mail-configure.html   |  70 ++++++++\n .../templates/patchwork/mail-settings.html    | 106 +++++++----\n patchwork/templates/patchwork/mail.html       |  37 ----\n .../templates/patchwork/optin-request.html    |  53 ------\n patchwork/templates/patchwork/optin.html      |  21 ---\n .../templates/patchwork/optout-request.html   |  56 ------\n patchwork/templates/patchwork/optout.html     |  25 ---\n .../patchwork/registration-confirm.html       |  14 --\n patchwork/urls.py                             |   3 +-\n patchwork/views/mail.py                       | 168 +++++++++++-------\n patchwork/views/notification.py               |  35 ++--\n patchwork/views/user.py                       |  56 +++---\n 13 files changed, 304 insertions(+), 363 deletions(-)\n delete mode 100644 patchwork/templates/patchwork/confirm-error.html\n create mode 100644 patchwork/templates/patchwork/mail-configure.html\n delete mode 100644 patchwork/templates/patchwork/mail.html\n delete mode 100644 patchwork/templates/patchwork/optin-request.html\n delete mode 100644 patchwork/templates/patchwork/optin.html\n delete mode 100644 patchwork/templates/patchwork/optout-request.html\n delete mode 100644 patchwork/templates/patchwork/optout.html\n delete mode 100644 patchwork/templates/patchwork/registration-confirm.html",
    "diff": "diff --git patchwork/templates/patchwork/confirm-error.html patchwork/templates/patchwork/confirm-error.html\ndeleted file mode 100644\nindex b1ce42ee..00000000\n--- patchwork/templates/patchwork/confirm-error.html\n+++ /dev/null\n@@ -1,23 +0,0 @@\n-{% extends \"base.html\" %}\n-\n-{% block title %}Confirmation{% endblock %}\n-{% block heading %}Confirmation{% endblock %}\n-\n-\n-{% block body %}\n-\n-{% if error == 'inactive' %}\n-<p>\n-  This confirmation has already been processed; you've probably visited this\n-  page before.\n-</p>\n-{% endif %}\n-\n-{% if error == 'expired' %}\n-<p>\n-  The confirmation has expired. If you'd still like to perform the\n-  {{ conf.get_type_display }} process, you'll need to resubmit the request.\n-</p>\n-{% endif %}\n-\n-{% endblock %}\ndiff --git patchwork/templates/patchwork/mail-configure.html patchwork/templates/patchwork/mail-configure.html\nnew file mode 100644\nindex 00000000..c0e07154\n--- /dev/null\n+++ patchwork/templates/patchwork/mail-configure.html\n@@ -0,0 +1,70 @@\n+{% extends \"base2.html\" %}\n+\n+{% block title %}Mail settings{% 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+\n+<div class=\"container\" style=\"margin-top: 1rem;\">\n+  <section class=\"block\">\n+    <h1 class=\"title\">\n+      Mail settings for {{ email }}\n+    </h1>\n+  </section>\n+\n+  <div class=\"content\">\n+    <p>\n+      You can configure your notification settings for Patchwork.\n+      Use this if you wish to opt out of all email from Patchwork,\n+      or if have previously opted out of Patchwork mail but now wish to\n+      receive notifications from Patchwork.\n+    </p>\n+    <p>\n+      If you opt out of email, Patchwork may still email you if you do certain\n+      actions yourself (such as create a new Patchwork account) but will not\n+      send you unsolicited email.\n+    </p>\n+    <p>\n+      When you submit a request, an email will be sent to your address with\n+      a link to click to finalise the request.\n+      Patchwork does this to prevent someone modifying your mail settings\n+      without your consent.\n+    </p>\n+\n+{% if is_optout %}\n+    <p>\n+      Patchwork <strong>may not</strong> send automated notifications to this address.\n+    </p>\n+{% else %}\n+    <p>\n+      Patchwork <strong>may</strong> send automated notifications to this address.\n+    </p>\n+{% endif %}\n+\n+    <form class=\"form\" method=\"post\">\n+      {% csrf_token %}\n+      <div class=\"field is-grouped\">\n+        <div class=\"control\">\n+          <input class=\"button\" type=\"submit\" name=\"optin\" value=\"Opt-in\"{% if not is_optout %} disabled{% endif %}/>\n+        </div>\n+        <div class=\"control\">\n+          <input class=\"button\" type=\"submit\" name=\"optout\" value=\"Opt-out\"{% if is_optout %} disabled{% endif %}/>\n+        </div>\n+      </div>\n+    </form>\n+  </div>\n+</div>\n+{% endblock %}\ndiff --git patchwork/templates/patchwork/mail-settings.html patchwork/templates/patchwork/mail-settings.html\nindex 58f567ac..858140ae 100644\n--- patchwork/templates/patchwork/mail-settings.html\n+++ patchwork/templates/patchwork/mail-settings.html\n@@ -1,37 +1,83 @@\n-{% extends \"base.html\" %}\n+{% extends \"base2.html\" %}\n \n {% block title %}Mail settings{% endblock %}\n-{% block heading %}Mail settings{% endblock %}\n \n {% block body %}\n-<p>Settings for <strong>{{ email }}</strong>:</p>\n-\n-<table class=\"horizontal\">\n-  <tr>\n-    <th>Opt-out list</th>\n-{% if is_optout %}\n-    <td>\n-      Patchwork <strong>may not</strong> send automated notifications to this address.\n-    </td>\n-    <td>\n-      <form method=\"post\" action=\"{% url 'mail-optin' %}\">\n-        {% csrf_token %}\n-        <input type=\"hidden\" name=\"email\" value=\"{{ email }}\"/>\n-        <input type=\"submit\" value=\"Opt-in\"/>\n-      </form>\n-    </td>\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-    <td>\n-      Patchwork <strong>may</strong> send automated notifications to this address.\n-    </td>\n-    <td>\n-      <form method=\"post\" action=\"{% url 'mail-optout' %}\">\n-        {% csrf_token %}\n-        <input type=\"hidden\" name=\"email\" value=\"{{ email }}\"/>\n-        <input type=\"submit\" value=\"Opt-out\"/>\n-      </form>\n-    </td>\n+<div class=\"notification\">\n {% endif %}\n-  </tr>\n-</table>\n+  {{ message }}\n+  <button class=\"delete\" onclick=\"dismiss(this);\"></button>\n+</div>\n+{% endfor %}\n+\n+<div class=\"container\" style=\"margin-top: 1rem;\">\n+  <section class=\"block\">\n+    <h1 class=\"title\">\n+      Mail settings\n+    </h1>\n+  </section>\n+\n+  <p>\n+    You can configure Patchwork to send you mail on certain events,\n+    or block automated mail altogether. Enter your email address to\n+    view or change your email settings.\n+  </p>\n+\n+  <div class=\"block\"></div>\n+\n+{% if form.non_field_errors %}\n+  <div class=\"notification is-danger is-light\">\n+    <button class=\"delete\" onclick=\"dismiss(this);\"></button>\n+    {{ form.non_field_errors }}\n+  </div>\n+{% endif %}\n+\n+  <form class=\"form\" method=\"post\">\n+    {% csrf_token %}\n+\n+    <div class=\"field is-horizontal\">\n+      <div class=\"field-label is-normal\">\n+        <label for=\"email\" class=\"label\">\n+          Email address\n+        </label>\n+      </div>\n+      <div class=\"field-body\">\n+        <div class=\"field\">\n+          <div class=\"control\">\n+            <input id=\"id_email\" type=\"email\" name=\"email\" class=\"input\" placeholder=\"e.g. bobsmith@example.com\">\n+          </div>\n+          <p class=\"help\">\n+            Your email address\n+          </p>\n+{% for error in form.email.errors %}\n+          <p class=\"help is-danger\">{{ error }}</p>\n+{% endfor %}\n+        </div>\n+      </div>\n+    </div>\n+\n+    <div class=\"field is-horizontal\">\n+      <div class=\"field-label\">\n+        <!-- Left empty for spacing -->\n+      </div>\n+      <div class=\"field-body\">\n+        <div class=\"field\">\n+          <div class=\"control\">\n+            <button class=\"button is-primary\">\n+              Submit\n+            </button>\n+          </div>\n+        </div>\n+      </div>\n+    </div>\n+  </form>\n+</div>\n {% endblock %}\ndiff --git patchwork/templates/patchwork/mail.html patchwork/templates/patchwork/mail.html\ndeleted file mode 100644\nindex a2ad23d1..00000000\n--- patchwork/templates/patchwork/mail.html\n+++ /dev/null\n@@ -1,37 +0,0 @@\n-{% extends \"base.html\" %}\n-\n-{% block title %}Mail settings{% endblock %}\n-{% block heading %}Mail settings{% endblock %}\n-\n-{% block body %}\n-<p>\n-  You can configure Patchwork to send you mail on certain events,\n-  or block automated mail altogether. Enter your email address to\n-  view or change your email settings.\n-</p>\n-\n-<form method=\"post\">\n-{% csrf_token %}\n-<table class=\"form registerform\">\n-{% if form.errors %}\n-  <tr>\n-    <td colspan=\"2\" class=\"error\">\n-      There was an error accessing your mail settings:\n-    </td>\n-  </tr>\n-{% endif %}\n-  <tr>\n-    <th>{{ form.email.label_tag }}</th>\n-    <td>\n-      {{ form.email }}\n-      {{ form.email.errors }}\n-    </td>\n-  </tr>\n-  <tr>\n-    <td colspan=\"2\" class=\"submitrow\">\n-      <input type=\"submit\" value=\"Access mail settings\"/>\n-    </td>\n-  </tr>\n-</table>\n-</form>\n-{% endblock %}\ndiff --git patchwork/templates/patchwork/optin-request.html patchwork/templates/patchwork/optin-request.html\ndeleted file mode 100644\nindex 36744c26..00000000\n--- patchwork/templates/patchwork/optin-request.html\n+++ /dev/null\n@@ -1,53 +0,0 @@\n-{% extends \"base.html\" %}\n-\n-{% load admins %}\n-\n-{% block title %}Opt-in{% endblock %}\n-{% block heading %}Opt-in{% endblock %}\n-\n-{% block body %}\n-{% if confirmation %}\n-<p><strong>Opt-in confirmation email sent</strong></p>\n-<p>\n-  An opt-in confirmation mail has been sent to\n-  <strong>{{ confirmation.email }}</strong>, containing a link. Please click on\n-  that link to confirm your opt-in.\n-</p>\n-{% else %}\n-{% if error %}\n-<p class=\"error\">{{ error }}</p>\n-{% endif %}\n-\n-{% if form %}\n-<p>\n-  This form allows you to opt-in to automated email from Patchwork. Use\n-  this if you have previously opted-out of Patchwork mail, but now want to\n-  received notifications from Patchwork.\n-</p>\n-<p>\n-  When you submit it, an email will be sent to your address with a link to\n-  click to finalise the opt-in. Patchwork does this to prevent someone opting\n-  you in without your consent.\n-</p>\n-\n-<form method=\"post\" action=\"\">\n-  {% csrf_token %}\n-  {{ form.email.errors }}\n-  <div style=\"padding: 0.5em 1em 2em;\">\n-    {{ form.email.label_tag }}: {{ form.email }}\n-  </div>\n-  <input type=\"submit\" value=\"Send me an opt-in link\">\n-</form>\n-{% endif %}\n-\n-{% if error and admins %}\n-<p>\n-  If you are having trouble opting in, please email {% site_admins %}.\n-</p>\n-{% endif %}\n-{% endif %}\n-\n-{% if user.is_authenticated %}\n-<p>Return to your <a href=\"{% url 'user-profile' %}\">user profile</a>.</p>\n-{% endif %}\n-{% endblock %}\ndiff --git patchwork/templates/patchwork/optin.html patchwork/templates/patchwork/optin.html\ndeleted file mode 100644\nindex 659bfccb..00000000\n--- patchwork/templates/patchwork/optin.html\n+++ /dev/null\n@@ -1,21 +0,0 @@\n-{% extends \"base.html\" %}\n-\n-{% block title %}Opt-in{% endblock %}\n-{% block heading %}Opt-in{% endblock %}\n-\n-{% block body %}\n-<p>\n-  <strong>Opt-in complete</strong>. You have successfully opted back in to\n-  automated email from this Patchwork system, using the address\n-  <strong>{{ email }}</strong>.\n-</p>\n-<p>\n-  If you later decide that you no longer want to receive automated mail from\n-  Patchwork, just visit\n-  <a href=\"{% url 'mail-settings' %}\">http://{{ site.domain }}{% url 'mail-settings' %}</a>,\n-  or visit the main Patchwork page and navigate from there.\n-</p>\n-{% if user.is_authenticated %}\n-<p>Return to your <a href=\"{% url 'user-profile' %}\">user profile</a>.</p>\n-{% endif %}\n-{% endblock %}\ndiff --git patchwork/templates/patchwork/optout-request.html patchwork/templates/patchwork/optout-request.html\ndeleted file mode 100644\nindex a89f72bb..00000000\n--- patchwork/templates/patchwork/optout-request.html\n+++ /dev/null\n@@ -1,56 +0,0 @@\n-{% extends \"base.html\" %}\n-\n-{% load admins %}\n-\n-{% block title %}Opt-out{% endblock %}\n-{% block heading %}Opt-out{% endblock %}\n-\n-{% block body %}\n-{% if confirmation %}\n-<p><strong>Opt-out confirmation email sent</strong></p>\n-<p>\n-  An opt-out confirmation mail has been sent to\n-  <strong>{{ confirmation.email }}</strong>, containing a link. Please click on\n-  that link to confirm your opt-out.\n-</p>\n-{% else %}\n-{% if error %}\n-<p class=\"error\">{{ error }}</p>\n-{% endif %}\n-\n-{% if form %}\n-<p>\n-  This form allows you to opt-out of automated email from Patchwork.\n-</p>\n-<p>\n-  If you opt-out of email, Patchwork may still email you if you do certain\n-  actions yourself (such as create a new Patchwork account), but will not\n-  send you unsolicited email.\n-</p>\n-<p>\n-  When you submit it, one email will be sent to your address with a link to\n-  click to finalise the opt-out. Patchwork does this to prevent someone\n-  opting you out without your consent.\n-</p>\n-<form method=\"post\" action=\"\">\n-  {% csrf_token %}\n-  {{ form.email.errors }}\n-  <div style=\"padding: 0.5em 1em 2em;\">\n-    {{ form.email.label_tag }}: {{ form.email }}\n-  </div>\n-  <input type=\"submit\" value=\"Send me an opt-out link\">\n-</form>\n-{% endif %}\n-\n-{% if error and admins %}\n-<p>\n-  If you are having trouble opting out, please email {% site_admins %}.\n-</p>\n-{% endif %}\n-{% endif %}\n-\n-{% if user.is_authenticated %}\n-<p>Return to your <a href=\"{% url 'user-profile' %}\">user profile</a>.</p>\n-{% endif %}\n-\n-{% endblock %}\ndiff --git patchwork/templates/patchwork/optout.html patchwork/templates/patchwork/optout.html\ndeleted file mode 100644\nindex 2d7e67e5..00000000\n--- patchwork/templates/patchwork/optout.html\n+++ /dev/null\n@@ -1,25 +0,0 @@\n-{% extends \"base.html\" %}\n-\n-{% block title %}Opt-out{% endblock %}\n-{% block heading %}Opt-out{% endblock %}\n-\n-{% block body %}\n-<p>\n-  <strong>Opt-out complete</strong>. You have successfully opted-out of\n-  automated notifications from this Patchwork system, from the address\n-  <strong>{{ email }}</strong>\n-</p>\n-<p>\n-  Please note that you may still receive email from other Patchwork setups at\n-  different sites, as they are run independently. You may need to opt-out of\n-  those separately.\n-</p>\n-<p>\n-  If you later decide to receive mail from Patchwork, just visit\n-  <a href=\"{% url 'mail-settings' %}\">http://{{ site.domain }}{% url 'mail-settings' %}</a>,\n-  or visit the main Patchwork page and navigate from there.\n-</p>\n-{% if user.is_authenticated %}\n-<p>Return to your <a href=\"{% url 'user-profile' %}\">user profile</a>.</p>\n-{% endif %}\n-{% endblock %}\ndiff --git patchwork/templates/patchwork/registration-confirm.html patchwork/templates/patchwork/registration-confirm.html\ndeleted file mode 100644\nindex e9219a5a..00000000\n--- patchwork/templates/patchwork/registration-confirm.html\n+++ /dev/null\n@@ -1,14 +0,0 @@\n-{% extends \"base.html\" %}\n-\n-{% block title %}Registration{% endblock %}\n-{% block heading %}Registration{% endblock %}\n-\n-{% block body %}\n-<p>Registration confirmed!</p>\n-\n-<p>\n-  Your Patchwork registration is complete. Head over to your\n-  <a href=\"{% url 'user-profile' %}\">profile</a> to start using\n-  Patchwork's extra features.\n-</p>\n-{% endblock %}\ndiff --git patchwork/urls.py patchwork/urls.py\nindex 30c070a9..e8ddba10 100644\n--- patchwork/urls.py\n+++ patchwork/urls.py\n@@ -185,8 +185,7 @@ urlpatterns = [\n     path('delegate/', api_views.delegates, name='api-delegates'),\n     # email setup\n     path('mail/', mail_views.settings, name='mail-settings'),\n-    path('mail/optout/', mail_views.optout, name='mail-optout'),\n-    path('mail/optin/', mail_views.optin, name='mail-optin'),\n+    path('mail/<path:email>/', mail_views.configure, name='mail-configure'),\n     # about\n     path('about/', about_views.about, name='about'),\n     # legacy redirects\ndiff --git patchwork/views/mail.py patchwork/views/mail.py\nindex 1a2019eb..d20f9d2f 100644\n--- patchwork/views/mail.py\n+++ patchwork/views/mail.py\n@@ -7,6 +7,7 @@ import smtplib\n \n from django.conf import settings as conf_settings\n from django.core.mail import send_mail\n+from django.contrib import messages\n from django.http import HttpResponseRedirect\n from django.shortcuts import render\n from django.template.loader import render_to_string\n@@ -22,12 +23,9 @@ def settings(request):\n         form = EmailForm(data=request.POST)\n         if form.is_valid():\n             email = form.cleaned_data['email']\n-            is_optout = EmailOptout.objects.filter(email=email).count() > 0\n-            context = {\n-                'email': email,\n-                'is_optout': is_optout,\n-            }\n-            return render(request, 'patchwork/mail-settings.html', context)\n+            return HttpResponseRedirect(\n+                reverse('mail-configure', kwargs={'email': email}),\n+            )\n     else:\n         form = EmailForm()\n \n@@ -35,91 +33,125 @@ def settings(request):\n         'form': form,\n     }\n \n-    return render(request, 'patchwork/mail.html', context)\n+    return render(request, 'patchwork/mail-settings.html', context)\n \n \n-def optout_confirm(request, conf):\n-    email = conf.email.strip().lower()\n-    # silently ignore duplicated optouts\n-    if EmailOptout.objects.filter(email=email).count() == 0:\n-        optout = EmailOptout(email=email)\n-        optout.save()\n+def _opt_in(request, email):\n+    EmailConfirmation.objects.filter(type='optin', email=email).delete()\n \n-    conf.deactivate()\n+    confirmation = EmailConfirmation(type='optin', email=email)\n+    confirmation.save()\n \n-    context = {\n-        'email': conf.email,\n-    }\n+    context = {'confirmation': confirmation}\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-    return render(request, 'patchwork/optout.html', context)\n+    try:\n+        send_mail(subject, message, conf_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+    messages.success(\n+        request,\n+        'Requested opt-in to email from Patchwork. '\n+        'Check your email for confirmation.',\n+    )\n \n-def optin_confirm(request, conf):\n-    email = conf.email.strip().lower()\n-    EmailOptout.objects.filter(email=email).delete()\n+    return True\n \n-    conf.deactivate()\n \n-    context = {\n-        'email': conf.email,\n-    }\n+def _opt_out(request, email):\n+    EmailConfirmation.objects.filter(type='optout', email=email).delete()\n \n-    return render(request, 'patchwork/optin.html', context)\n+    confirmation = EmailConfirmation(type='optout', email=email)\n+    confirmation.save()\n \n+    context = {'confirmation': confirmation}\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-def _optinout(request, action):\n-    context = {}\n-    mail_template = 'patchwork/mails/%s-request.txt' % action\n-    mail_subject_template = 'patchwork/mails/%s-request-subject.txt' % action\n-    html_template = 'patchwork/%s-request.html' % action\n+    try:\n+        send_mail(subject, message, conf_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-    if request.method != 'POST':\n-        return HttpResponseRedirect(reverse(settings))\n+    messages.success(\n+        request,\n+        'Requested opt-out of email from Patchwork. '\n+        'Check your email for confirmation.',\n+    )\n+\n+    return True\n \n-    form = EmailForm(data=request.POST)\n+\n+def configure(request, email):\n+    # Yes, we're kind of abusing forms here, but this is easier than doing our\n+    # own view-based validation\n+    form = EmailForm(data={'email': email})\n     if not form.is_valid():\n-        context['error'] = (\n-            'There was an error in the form. Please review ' 'and re-submit.'\n-        )\n-        context['form'] = form\n-        return render(request, html_template, context)\n+        # don't worry - Django escapes these by default\n+        messages.error(request, f'{email} is not a valid email address.')\n+        return HttpResponseRedirect(reverse(settings))\n \n     email = form.cleaned_data['email']\n-    if (\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-            \"opt-out list, so you don't need to opt back in\" % email\n-        )\n-        context['form'] = form\n-        return render(request, html_template, context)\n \n-    conf = EmailConfirmation(type=action, email=email)\n-    conf.save()\n+    if request.method == 'POST':\n+        if 'optin' in request.POST:\n+            if _opt_in(request, email):\n+                return HttpResponseRedirect(reverse('project-list'))\n+        elif 'optout' in request.POST:\n+            if _opt_out(request, email):\n+                return HttpResponseRedirect(reverse('project-list'))\n+        else:\n+            messages.error(request, 'Invalid request.')\n+\n+    is_optout = EmailOptout.objects.filter(email=email).count() > 0\n+    context = {\n+        'email': email,\n+        'is_optout': is_optout,\n+    }\n \n-    context['confirmation'] = conf\n+    return render(request, 'patchwork/mail-configure.html', context)\n \n-    subject = render_to_string(mail_subject_template)\n-    message = render_to_string(mail_template, context, request=request)\n \n-    try:\n-        send_mail(subject, message, conf_settings.DEFAULT_FROM_EMAIL, [email])\n-    except smtplib.SMTPException:\n-        context['confirmation'] = None\n-        context['error'] = (\n-            'An error occurred during confirmation. '\n-            'Please try again later.'\n-        )\n-        context['admins'] = conf_settings.ADMINS\n+def optout_confirm(request, confirmation):\n+    email = confirmation.email.strip().lower()\n+    # silently ignore duplicated optouts\n+    if EmailOptout.objects.filter(email=email).count() == 0:\n+        optout = EmailOptout(email=email)\n+        optout.save()\n \n-    return render(request, html_template, context)\n+    confirmation.deactivate()\n \n+    messages.success(\n+        request,\n+        'Successfully opted out of email from Patchwork.'\n+    )\n+\n+    return HttpResponseRedirect(reverse('project-list'))\n+\n+\n+def optin_confirm(request, confirmation):\n+    email = confirmation.email.strip().lower()\n+    EmailOptout.objects.filter(email=email).delete()\n \n-def optout(request):\n-    return _optinout(request, 'optout')\n+    confirmation.deactivate()\n \n+    messages.success(\n+        request,\n+        'Successfully opted into email from Patchwork.'\n+    )\n \n-def optin(request):\n-    return _optinout(request, 'optin')\n+    return HttpResponseRedirect(reverse('project-list'))\ndiff --git patchwork/views/notification.py patchwork/views/notification.py\nindex 4e023867..7c773ba7 100644\n--- patchwork/views/notification.py\n+++ patchwork/views/notification.py\n@@ -4,9 +4,9 @@\n #\n # SPDX-License-Identifier: GPL-2.0-or-later\n \n-from django.http import Http404\n-from django.shortcuts import get_object_or_404\n-from django.shortcuts import render\n+from django.contrib import messages\n+from django.http import HttpResponseRedirect\n+from django.urls import reverse\n \n from patchwork.models import EmailConfirmation\n from patchwork.views import mail\n@@ -22,18 +22,27 @@ def confirm(request, key):\n         'optin': mail.optin_confirm,\n     }\n \n-    conf = get_object_or_404(EmailConfirmation, key=key)\n+    try:\n+        conf = EmailConfirmation.objects.get(key=key)\n+    except EmailConfirmation.DoesNotExist:\n+        messages.error(\n+            request,\n+            'That request is invalid or expired. Please try again.'\n+        )\n+        return HttpResponseRedirect(reverse('project-list'))\n+\n     if conf.type not in views:\n-        raise Http404\n+        messages.error(\n+            request,\n+            'That request is invalid or expired. Please try again.'\n+        )\n+        return HttpResponseRedirect(reverse('project-list'))\n \n     if conf.active and conf.is_valid():\n         return views[conf.type](request, conf)\n \n-    context = {}\n-    context['conf'] = conf\n-    if not conf.active:\n-        context['error'] = 'inactive'\n-    elif not conf.is_valid():\n-        context['error'] = 'expired'\n-\n-    return render(request, 'patchwork/confirm-error.html', context)\n+    messages.error(\n+        request,\n+        'That request is invalid or expired. Please try again.'\n+    )\n+    return HttpResponseRedirect(reverse('project-list'))\ndiff --git patchwork/views/user.py patchwork/views/user.py\nindex 440aa38a..973061b7 100644\n--- patchwork/views/user.py\n+++ patchwork/views/user.py\n@@ -54,12 +54,12 @@ def register(request):\n             user.save()\n \n             # create confirmation\n-            conf = EmailConfirmation(\n+            confirmation = EmailConfirmation(\n                 type='registration', user=user, email=user.email\n             )\n-            conf.save()\n+            confirmation.save()\n \n-            context['confirmation'] = conf\n+            context['confirmation'] = confirmation\n \n             # send email\n             subject = render_to_string(\n@@ -67,12 +67,18 @@ def register(request):\n             )\n             message = render_to_string(\n                 'patchwork/mails/activation.txt',\n-                {'site': Site.objects.get_current(), 'confirmation': conf},\n+                {\n+                    'site': Site.objects.get_current(),\n+                    'confirmation': confirmation,\n+                },\n             )\n \n             try:\n                 send_mail(\n-                    subject, message, settings.DEFAULT_FROM_EMAIL, [conf.email]\n+                    subject,\n+                    message,\n+                    settings.DEFAULT_FROM_EMAIL,\n+                    [confirmation.email]\n                 )\n             except smtplib.SMTPException:\n                 context['confirmation'] = None\n@@ -88,26 +94,34 @@ def register(request):\n     return render(request, 'patchwork/registration.html', context)\n \n \n-def register_confirm(request, conf):\n-    conf.user.is_active = True\n-    conf.user.save()\n-    conf.deactivate()\n+def register_confirm(request, confirmation):\n+    confirmation.user.is_active = True\n+    confirmation.user.save()\n+    confirmation.deactivate()\n \n     try:\n-        person = Person.objects.get(email__iexact=conf.user.email)\n+        person = Person.objects.get(email__iexact=confirmation.user.email)\n     except Person.DoesNotExist:\n-        person = Person(email=conf.user.email, name=conf.user.profile.name)\n-    person.user = conf.user\n+        person = Person(\n+            email=confirmation.user.email, name=confirmation.user.profile.name,\n+        )\n+    person.user = confirmation.user\n     person.save()\n \n-    return render(request, 'patchwork/registration-confirm.html')\n+    messages.success(request, 'Successfully confirmed account.')\n+\n+    return HttpResponseRedirect(reverse('project-list'))\n \n \n def _send_confirmation_email(request, email):\n-    conf = EmailConfirmation(type='userperson', user=request.user, email=email)\n-    conf.save()\n+    confirmation = EmailConfirmation(\n+        type='userperson',\n+        user=request.user,\n+        email=email,\n+    )\n+    confirmation.save()\n \n-    context = {'confirmation': conf}\n+    context = {'confirmation': confirmation}\n     subject = render_to_string('patchwork/mails/user-link-subject.txt')\n     message = render_to_string(\n         'patchwork/mails/user-link.txt',\n@@ -273,15 +287,15 @@ def profile(request):\n \n \n @login_required\n-def link_confirm(request, conf):\n+def link_confirm(request, confirmation):\n     try:\n-        person = Person.objects.get(email__iexact=conf.email)\n+        person = Person.objects.get(email__iexact=confirmation.email)\n     except Person.DoesNotExist:\n-        person = Person(email=conf.email)\n+        person = Person(email=confirmation.email)\n \n-    person.link_to_user(conf.user)\n+    person.link_to_user(confirmation.user)\n     person.save()\n-    conf.deactivate()\n+    confirmation.deactivate()\n \n     messages.success(request, 'Successfully linked email to account.')\n \n",
    "prefixes": [
        "RFC",
        "v2",
        "18/19"
    ]
}