From dbe174943857739bb8d601e120e3f1a50fd4d346 Mon Sep 17 00:00:00 2001
From: Robert Sparks <rjsparks@nostrum.com>
Date: Sun, 23 Jul 2023 11:00:24 -0700
Subject: [PATCH] feat: IAB statements (#5940)

* feat: support iab and iesg statements. Import iab statements. (#5895)

* feat: infrastructure for statements doctype

* chore: basic test framework

* feat: basic statement document view

* feat: show replaced statements

* chore: black

* fix: state help for statements

* fix: cleanout non-relevant email expansions

* feat: import iab statements, provide group statements tab

* fix: guard against running import twice

* feat: build redirect csv for iab statements

* fix: set document state on import

* feat: show published date on main doc view

* feat: handle pdf statements

* feat: create new and update statements

* chore: copyright block updates

* chore: remove flakes

* chore: black

* feat: add edit/new buttons for the secretariat

* fix: address PR #5895 review comments

* fix: pin pydantic until inflect catches up (#5901) (#5902)

* chore: re-un-pin pydantic
---
 docker/configs/settings_local.py              |   2 +-
 ietf/doc/factories.py                         |  32 +-
 .../commands/import_iab_statements.py         | 320 ++++++++++++++++
 .../migrations/0005_alter_docevent_type.py    |  86 +++++
 ietf/doc/migrations/0006_statements.py        |  43 +++
 ietf/doc/models.py                            |  12 +-
 ietf/doc/tests_statement.py                   | 355 ++++++++++++++++++
 ietf/doc/urls.py                              |   6 +-
 ietf/doc/urls_statement.py                    |  10 +
 ietf/doc/views_doc.py                         |  35 +-
 ietf/doc/views_help.py                        |   4 +
 ietf/doc/views_statement.py                   | 274 ++++++++++++++
 ietf/group/urls.py                            |   3 +-
 ietf/group/utils.py                           |   4 +-
 ietf/group/views.py                           |  33 +-
 ietf/mailtrigger/utils.py                     |   4 +-
 ietf/name/fixtures/names.json                 |  54 ++-
 ietf/name/migrations/0004_statements.py       |  21 ++
 ietf/settings.py                              |   2 +-
 ietf/static/js/upload_statement.js            |  29 ++
 ietf/templates/doc/document_statement.html    | 134 +++++++
 .../doc/statement/new_statement.html          |  18 +
 .../doc/statement/statement_template.md       |   1 +
 .../doc/statement/upload_content.html         |  23 ++
 ietf/templates/group/statements.html          |  40 ++
 package.json                                  |   1 +
 26 files changed, 1521 insertions(+), 25 deletions(-)
 create mode 100644 ietf/doc/management/commands/import_iab_statements.py
 create mode 100644 ietf/doc/migrations/0005_alter_docevent_type.py
 create mode 100644 ietf/doc/migrations/0006_statements.py
 create mode 100644 ietf/doc/tests_statement.py
 create mode 100644 ietf/doc/urls_statement.py
 create mode 100644 ietf/doc/views_statement.py
 create mode 100644 ietf/name/migrations/0004_statements.py
 create mode 100644 ietf/static/js/upload_statement.js
 create mode 100644 ietf/templates/doc/document_statement.html
 create mode 100644 ietf/templates/doc/statement/new_statement.html
 create mode 100644 ietf/templates/doc/statement/statement_template.md
 create mode 100644 ietf/templates/doc/statement/upload_content.html
 create mode 100644 ietf/templates/group/statements.html

diff --git a/docker/configs/settings_local.py b/docker/configs/settings_local.py
index 647fcd5b2..8178db1ef 100644
--- a/docker/configs/settings_local.py
+++ b/docker/configs/settings_local.py
@@ -40,7 +40,7 @@ INTERNAL_IPS = [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips] + ['127.0.0.
 #    'ietf.context_processors.sql_debug',
 # ]
 
-DOCUMENT_PATH_PATTERN = '/assets/ietf-ftp/{doc.type_id}/'
+DOCUMENT_PATH_PATTERN = '/assets/ietfdata/doc/{doc.type_id}/'
 INTERNET_DRAFT_PATH = '/assets/ietf-ftp/internet-drafts/'
 RFC_PATH = '/assets/ietf-ftp/rfc/'
 CHARTER_PATH = '/assets/ietf-ftp/charter/'
diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py
index 95fbedfaa..a8c20b6bb 100644
--- a/ietf/doc/factories.py
+++ b/ietf/doc/factories.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2016-2020, All Rights Reserved
+# Copyright The IETF Trust 2016-2023, All Rights Reserved
 # -*- coding: utf-8 -*-
 
 
@@ -23,7 +23,6 @@ from ietf.utils.text import xslugify
 from ietf.utils.timezone import date_today
 
 
-
 def draft_name_generator(type_id,group,n):
         return '%s-%s-%s-%s%d'%( 
               type_id,
@@ -577,4 +576,31 @@ class EditorialRfcFactory(RgDraftFactory):
     def reset_canonical_name(obj, create, extracted, **kwargs): 
         if hasattr(obj, '_canonical_name'):
             del obj._canonical_name
-        return None          
+        return None
+    
+class StatementFactory(BaseDocumentFactory):
+    type_id = "statement"
+    title = factory.Faker("sentence")
+    group = factory.SubFactory("ietf.group.factories.GroupFactory", acronym="iab")
+
+    name = factory.LazyAttribute(
+        lambda o: "statement-%s-%s" % (xslugify(o.group.acronym), xslugify(o.title))
+    )
+    uploaded_filename = factory.LazyAttribute(lambda o: f"{o.name}-{o.rev}.md")
+
+    published_statement_event = factory.RelatedFactory(
+        "ietf.doc.factories.DocEventFactory",
+        "doc",
+        type="published_statement",
+        time=timezone.now() - datetime.timedelta(days=1),
+    )
+
+    @factory.post_generation
+    def states(obj, create, extracted, **kwargs):
+        if not create:
+            return
+        if extracted:
+            for state_type_id, state_slug in extracted:
+                obj.set_state(State.objects.get(type_id=state_type_id, slug=state_slug))
+        else:
+            obj.set_state(State.objects.get(type_id="statement", slug="active"))
diff --git a/ietf/doc/management/commands/import_iab_statements.py b/ietf/doc/management/commands/import_iab_statements.py
new file mode 100644
index 000000000..c268101fa
--- /dev/null
+++ b/ietf/doc/management/commands/import_iab_statements.py
@@ -0,0 +1,320 @@
+# Copyright The IETF Trust 2023, All Rights Reserved
+
+import debug  # pyflakes:ignore
+
+import csv
+import datetime
+import io
+import os
+import shutil
+import subprocess
+import tempfile
+
+from collections import defaultdict
+from pathlib import Path
+
+from django.conf import settings
+from django.core.management.base import BaseCommand
+
+from ietf.doc.models import Document, DocAlias, DocEvent, State
+from ietf.utils.text import xslugify
+
+
+class Command(BaseCommand):
+    help = "Performs a one-time import of IAB statements"
+
+    def handle(self, *args, **options):
+        if Document.objects.filter(type="statement", group__acronym="iab").exists():
+            print("IAB statement documents already exist - exiting")
+            exit(-1)
+        tmpdir = tempfile.mkdtemp()
+        process = subprocess.Popen(
+            ["git", "clone", "https://github.com/kesara/iab-scraper.git", tmpdir],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+        )
+        stdout, stderr = process.communicate()
+        if not Path(tmpdir).joinpath("iab_minutes", "2022-12-14.md").exists():
+            print("Git clone of the iab-scraper directory did not go as expected")
+            print("stdout:", stdout)
+            print("stderr:", stderr)
+            print(f"Clean up {tmpdir} manually")
+            exit(-1)
+
+        spreadsheet_rows = load_spreadsheet()
+        with open("iab_statement_redirects.csv", "w") as redirect_file:
+            redirect_writer = csv.writer(redirect_file)
+            for index, (file_fix, date_string, title, url, _) in enumerate(
+                spreadsheet_rows
+            ):
+                name = url.split("/")[6].lower()
+                if name.startswith("iabs"):
+                    name = name[5:]
+                elif name.startswith("iab"):
+                    name = name[4:]
+                if index == 1:
+                    name += "-archive"  # https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-statement-on-identifiers-and-unicode-7-0-0/archive/
+                if index == 100:
+                    name = (
+                        "2010-" + name
+                    )  # https://www.iab.org/documents/correspondence-reports-documents/docs2010/iab-statement-on-the-rpki/
+                if index == 152:
+                    name = (
+                        "2018-" + name
+                    )  # https://www.iab.org/documents/correspondence-reports-documents/2018-2/iab-statement-on-the-rpki/
+                docname = f"statement-iab-{xslugify(name)}"
+                ext = None
+                base_sourcename = (
+                    f"{date_string}-{file_fix}" if file_fix != "" else date_string
+                )
+                if (
+                    Path(tmpdir)
+                    .joinpath("iab_statements", f"{base_sourcename}.md")
+                    .exists()
+                ):
+                    ext = "md"
+                elif (
+                    Path(tmpdir)
+                    .joinpath("iab_statements", f"{base_sourcename}.pdf")
+                    .exists()
+                ):
+                    ext = "pdf"
+                if ext is None:
+                    debug.show(
+                        'f"Could not find {Path(tmpdir).joinpath("iab_statements", f"{base_path}.md")}"'
+                    )
+                    continue
+                filename = f"{docname}-00.{ext}"
+                # Create Document
+                doc = Document.objects.create(
+                    name=docname,
+                    type_id="statement",
+                    title=title,
+                    group_id=7,  # The IAB group
+                    rev="00",
+                    uploaded_filename=filename,
+                )
+                doc.set_state(State.objects.get(type_id="statement", slug="active"))
+                DocAlias.objects.create(name=doc.name).docs.add(doc)
+                year, month, day = [int(part) for part in date_string.split("-")]
+                e1 = DocEvent.objects.create(
+                    time=datetime.datetime(
+                        year, month, day, 12, 00, tzinfo=datetime.timezone.utc
+                    ),
+                    type="published_statement",
+                    doc=doc,
+                    rev="00",
+                    by_id=1,
+                    desc="Statement published (note: The 1200Z time of day is inaccurate - the actual time of day is not known)",
+                )
+                e2 = DocEvent.objects.create(
+                    type="added_comment",
+                    doc=doc,
+                    rev="00",
+                    by_id=1,  # The "(System)" person
+                    desc="Statement moved into datatracker from iab wordpress website",
+                )
+                doc.save_with_history([e1, e2])
+
+                # Put file in place
+                source = Path(tmpdir).joinpath(
+                    "iab_statements", f"{base_sourcename}.{ext}"
+                )
+                dest = Path(settings.DOCUMENT_PATH_PATTERN.format(doc=doc)).joinpath(
+                    filename
+                )
+                if dest.exists():
+                    print(f"WARNING: {dest} already exists - not overwriting it.")
+                else:
+                    os.makedirs(dest.parent, exist_ok=True)
+                    shutil.copy(source, dest)
+
+                redirect_writer.writerow(
+                    [
+                        url,
+                        f"https://datatracker.ietf.org/doc/{docname}",
+                    ]
+                )
+
+        shutil.rmtree(tmpdir)
+
+
+def load_spreadsheet():
+    csv_dump = '''2002-03-01,IAB RFC Publication Process Description(txt) March 2003,https://www.iab.org/documents/correspondence-reports-documents/docs2003/iab-rfc-publication-process/,deprecated
+2015-01-27,IAB Statement on Identifiers and Unicode 7.0.0 (archive),https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-statement-on-identifiers-and-unicode-7-0-0/archive/,deprecated
+2010-02-05,Response to the EC’s RFI on Forums and Consortiums,https://www.iab.org/documents/correspondence-reports-documents/docs2010/response-to-the-ecs-rfi-on-forums-and-consortiums/,https://www.iab.org/wp-content/IAB-uploads/2011/03/2010-02-05-IAB-Response-Euro-ICT-Questionnaire.pdf
+2011-03-30,IAB responds to NTIA Request for Comments on the IANA Functions,https://www.iab.org/documents/correspondence-reports-documents/2011-2/iab-responds-to-ntia-request-for-comments-on-the-iana-functions/,https://www.iab.org/wp-content/IAB-uploads/2011/04/2011-03-30-iab-iana-noi-response.pdf
+2011-07-28,IAB's response to the NTIA FNOI on IANA,https://www.iab.org/documents/correspondence-reports-documents/2011-2/iabs-response-to-the-ntia-fnoi-on-iana/,https://www.iab.org/wp-content/IAB-uploads/2011/07/IANA-IAB-FNOI-2011.pdf
+2011-12-16,"Questionnaire in support of the ICANN bid for the IANA function [Dec 16, 2011]",https://www.iab.org/documents/correspondence-reports-documents/2011-2/questionnaire-in-support-of-the-icann-bid-for-the-iana-function/,https://www.iab.org/wp-content/IAB-uploads/2011/12/IAB-Past-Performance-Questionnaire.pdf
+2012-04-03,IETF Oversight of the IANA Protocol Parameter Function,https://www.iab.org/documents/correspondence-reports-documents/2012-2/ietf-oversight-of-the-iana-protocol-parameter-function/,https://www.iab.org/wp-content/IAB-uploads/2012/04/IETF-IANA-Oversight.pdf
+2012-04-29,IETF and IAB comment on OMB Circular A-119,https://www.iab.org/documents/correspondence-reports-documents/2012-2/ietf-and-iab-comment-on-omb-circular-a-119/,https://www.iab.org/wp-content/IAB-uploads/2012/04/OMB-119.pdf
+2012-05-24,IAB submits updated ICANN performance evaluation,https://www.iab.org/documents/correspondence-reports-documents/2012-2/iab-submits-updated-icann-performance-evaluation/,https://www.iab.org/wp-content/IAB-uploads/2012/05/IAB-Past-Performance-Questionnaire-FINAL.pdf
+2013-07-02,Open letter to the European Commission and the European Parliament in the matter of the Transatlantic Trade and Investment Partnership (TTIP),https://www.iab.org/documents/correspondence-reports-documents/2013-2/open-letter-to-the-ec/,https://www.iab.org/wp-content/IAB-uploads/2013/07/TTIP_market_driven_standards_EU_letter.pdf
+2013-05-10,Comments In the matter of Transatlantic Trade and Investment Partnership (TTIP) (USTR-2013-0019),https://www.iab.org/documents/correspondence-reports-documents/2013-2/comments-in-the-matter-of-transatlantic-trade-and-investment-partnership-ttip-ustr-2013-0019/,https://www.iab.org/wp-content/IAB-uploads/2013/07/TTIP_market_driven_standards_FINAL.pdf
+2013-10-23,IAB Comments on Recommendation for Random Number Generation Using Deterministic Random Bit Generators,https://www.iab.org/documents/correspondence-reports-documents/2013-2/nist-sp-800-90a/,https://www.iab.org/wp-content/IAB-uploads/2013/10/IAB-NIST-FINAL.pdf
+2014-04-07,IAB Comments on NISTIR 7977,https://www.iab.org/documents/correspondence-reports-documents/2014-2/iab-comments-on-nistir-7977/,https://www.iab.org/wp-content/IAB-uploads/2014/04/IAB-NIST7977-20140407.pdf
+2014-04-29,Comments to ICANN on the Transition of NTIA’s Stewardship of the IANA Functions,https://www.iab.org/documents/correspondence-reports-documents/2014-2/iab-response-to-icann-iana-transition-proposal/,https://www.iab.org/wp-content/IAB-uploads/2014/04/iab-response-to-20140408-20140428a.pdf
+2016-05-27,"IAB Comments to US NTIA Request for Comments, ""The Benefits, Challenges, and Potential Roles for the Government in Fostering the Advancement of the Internet of Things""",https://www.iab.org/documents/correspondence-reports-documents/2016-2/iab-comments-to-ntia-request-for-comments-the-benefits-challenges-and-potential-roles-for-the-government/,https://www.iab.org/wp-content/IAB-uploads/2016/05/ntia-iot-20160525.pdf
+2016-05-24,"IAB Chair Testifies before the United States Senate Committee on Commerce, Science, and Transportation on ""Examining the Multistakeholder Plan for Transitioning the Internet Assigned Number Authority""",https://www.iab.org/documents/correspondence-reports-documents/2016-2/iab-chair-statement-before-us-senate-committee-on-iana-transition/,https://www.iab.org/wp-content/IAB-uploads/2016/05/sullivan-to-senate-commerce-20160524.pdf
+2018-07-16,IAB Response to NTIA Notice of Inquiry on International Internet Policy Priorities,https://www.iab.org/documents/correspondence-reports-documents/2018-2/iab-response-to-ntia-notice-of-inquiry-on-international-internet-policy-priorities-response/,https://www.iab.org/wp-content/IAB-uploads/2018/07/IAB-response-to-the-2018-NTIA-Notice-of-Inquiry.pdf
+2018-09-09,Internet Architecture Board Comments on the Australian Assistance and Access Bill 2018,https://www.iab.org/documents/correspondence-reports-documents/2018-2/internet-architecture-board-comments-on-the-australian-assistance-and-access-bill-2018/,https://www.iab.org/wp-content/IAB-uploads/2018/09/IAB-Comments-on-Australian-Assistance-and-Access-Bill-2018.pdf
+2023-03-03,IAB Response to the Office of the High Commissioner for Human Rights Call for Input  on “The relationship between human rights and technical standard-setting processes for new and emerging digital technologies”,https://www.iab.org/documents/correspondence-reports-documents/2023-2/iab-response-to-the-ohchr-call-for-input-on-the-relationship-between-human-rights-and-technical-standard/,https://www.iab.org/wp-content/IAB-uploads/2023/03/IAB-Response-to-OHCHR-consultation.pdf
+1998-12-09,"IAB Request to IANA for Delegating IPv6 Address Space, Mail Message, December 1998",https://www.iab.org/documents/correspondence-reports-documents/docs98/iab-request-to-iana-for-delegating-ipv6-address-space-mail-message-december-1998/,
+1998-12-18,"1998 Statements on Cryptography, Mail Message, December 1998.",https://www.iab.org/documents/correspondence-reports-documents/docs98/1998-statements-on-cryptography/,
+1999-02-22,Correspondence between Bradner and Dyson on Protocol Parameter Parameters,https://www.iab.org/documents/correspondence-reports-documents/docs99/correspondence-between-bradner-and-dyson-on-protocol-parameter-parameters/,
+1999-08-13,Comment on ICANN ASO membership,https://www.iab.org/documents/correspondence-reports-documents/docs99/comment-on-icann-aso-membership/,
+1999-10-19,Ad Hoc Group on Numbering,https://www.iab.org/documents/correspondence-reports-documents/docs99/ad-hoc-group-on-numbering/,
+2000-05-01,"IAB Statement on Infrastructure Domain and Subdomains, May 2000.",https://www.iab.org/documents/correspondence-reports-documents/docs2000/iab-statement-on-infrastructure-domain-and-subdomains-may-2000/,
+2002-05-01,"IETF and ITU-T Cooperation Arrangements, May 2002",https://www.iab.org/documents/correspondence-reports-documents/docs2002/ietf-and-itu-t-cooperation-arrangements-may-2002/,
+2002-05-03,"IAB replyto ENUM liaison statement, May 2002",https://www.iab.org/documents/correspondence-reports-documents/docs2002/enum-response/,
+2002-05-24,"Interim Approval for Internet Telephone Numbering System (ENUM) Provisioning, 24 May 2002",https://www.iab.org/documents/correspondence-reports-documents/docs2002/enum-pr/,
+2002-06-01,"IAB response to ICANN Evolution and Reform, June 2002",https://www.iab.org/documents/correspondence-reports-documents/docs2002/icann-response/,
+2002-09-01,"IAB response to ICANN Evolution and Reform Committee's Second Interim Report, September 2002",https://www.iab.org/documents/correspondence-reports-documents/docs2002/icann-response-2/,
+2002-10-01,"IAB response to ICANN Evolution and Reform Committee's Final Implementation Report, October 2002",https://www.iab.org/documents/correspondence-reports-documents/docs2002/icann-response-3/,
+2002-12-10,"IAB Response to RIRs request regarding 6bone address entries in ip6.arpa, December 2002",https://www.iab.org/documents/correspondence-reports-documents/docs2002/3ffe/,
+2003-01-03,"IETF Notice of Withdrawal from the Protocol Support Organization, January 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/icann-pso-notice/,
+2003-01-25,"IAB Response to Verisign GRS IDN Announcement, January 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/icann-vgrs-response/,
+2003-07-10,"Note: Unified Notification Protocol Considerations, July 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-07-10-iab-notification/,
+2003-08-01,Open Architectural Issues in the Development of the Internet,https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-08-architectural-issues/,
+2003-08-28,RFC Document editing/ queueing suggestion,https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-08-28-klensin-rfc-editor/,
+2003-09-02,"IAB Chair's announcement of an Advisory Committee, September 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-09-02-adv-committee/,
+2003-09-19,"IAB Commentary: Architectural Concerns on the Use of DNS Wildcards, September 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-09-20-dns-wildcards/,
+2003-09-24,"IAB to ICANN: IAB input related to the .cs code in ISO 3166, 24 September 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-09-25-icann-cs-code/,
+2003-09-24,"IAB to ISO: IAB comment on stability of ISO 3166 and other infrastructure standards, 24 September 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-09-25-iso-cs-code/,
+2003-09-25,"Correspondance to ISO concerning .cs code, and advice to ICANN, 25 September 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-09-25-icann-cs-code-2/,
+2003-09-25,"Correspondance to ISO concerning .cs code, and advice to ICANN, 25 September 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-09-25-iso-cs-code-2/,
+2003-09-26,ISO Codes,https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-09-23-isocodes/,
+2003-10-02,"IESG to IAB: Checking data for validity before usage in a protocol, 2 October 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-10-02-iesg-dns-validity-check-query/,
+2003-10-14,"Survey of Current Security Work in the IETF, October 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-10-14-security-survey/,
+2003-10-17,"IAB to ICANN SESAC:Wildcard entries in DNS domains, 17 October 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-10-17-crocker-wildcards-2/,
+2003-10-17,"IAB note to Steve Crocker, Chair, ICANN Security and Stability Advisory Committee, October 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-10-17-crocker-wildcards/,
+2003-10-18,"IAB concerns against permanent deployment of edge-based port filtering, October 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-10-18-edge-filters/,
+2003-11-08,"IAB Response to IESG architectural query: Checking data for validity before usage in protocol, November 2003",https://www.iab.org/documents/correspondence-reports-documents/docs2003/2003-11-08-iesg-dns-validity-check-query-response/,
+2004-01-19,"Number Resource Organisation (NRO) formation, 19 January 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-01-19-nro/,
+2004-01-22,"IAB to RIPE NCC:ENUM Administration, 22 January 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-01-22-enum-subcodes/,
+2004-02-09,"IAB to IANA: Instructions to IANA -Delegation of 2.0.0.2.ip6.arpa, 9 February, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-02-09-6to4-rev-delegation/,
+2004-02-26,The IETF -- what is it?,https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-02-26-ietf-defn/,
+2004-04-15,"IAB to ICANN: Validity checks for names, 15 April, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-04-15-icann-dns-validity-check/,
+2004-05-07,"IAB to IANA: IPv6 Allocation Policy , 7 May, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-05-07-iana-v6alloc/,
+2004-05-24,"IAB to IANA: Instructions to IANA -Delegation of 3.f.f.e.ip6.arpa, 24 May, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-05-24-3ffe-rev-delegation/,
+2004-05-27,"IAB to ICANN:Concerns regarding IANA Report, 27 May, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-05-27-iana-report/,
+2004-07-16,"Upcoming clarifications to RIPE NCC instructions for e164.arpa operation, 16 July 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-07-15-enum-instructions/,
+2004-07-16,"IANA Delegation Requests, 16 July 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-07-16-iana-delegation/,
+2004-08-06,OMA-IETF Standardization Collaboration,https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-08-draft-iab-oma-liaison-00/,
+2004-08-12,"IAB to RIPE NCC:Notice of revision of instructions concerning the ENUM Registry, 12 August, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-08-12-enum-instructions/,
+2004-08-12,"Response to your letter of August 4, 12 August 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-08-12-icann-wildcard/,
+2004-09-27,"IAB to ICANN:Report of Concerns over IANA Performance , 27 September, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-09-27-iana-concerns/,
+2004-09-27,"IAB Report of IETF IANA Functions , 27 September 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-09-27-iana-report/,
+2004-11-03,"IAB to IESG:Comments on Teredo , 3 November, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-11-03-teredo-comments/,
+2004-11-12,"IAB to ICANN:Response to ICANN Request for assistance with new TLD Policy , 12 November, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-11-12-icann-new-tld-policy/,
+2004-11-29,"The IETF and IPv6 Address Allocation , 29 November 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-11-29-ipv6-allocs/,
+2004-12-15,"IAB Comment to Internet AD:Comments on IANA Considerations in IPv6 ULA draft, 15 December, 2004",https://www.iab.org/documents/correspondence-reports-documents/docs2004/2004-12-15-ipv6-ula-iana-considerations/,
+2005-02-16,"IAB review of Structure of the IETF Administrative Support Activity, 16 February 2005",https://www.iab.org/documents/correspondence-reports-documents/docs2005/2005-02-16-iasa/,
+2005-08-26,"SiteFinder returns, 26 August 2005",https://www.iab.org/documents/correspondence-reports-documents/docs2005/2005-08-26-ssac-note/,
+2005-09-01,"Re: SiteFinder returns, 1 September 2005",https://www.iab.org/documents/correspondence-reports-documents/docs2005/2005-09-01-ssac-response/,
+2005-10-14,"IAB to ICANN: IAB comments on ICANN IDN Guidelines, 14 October 2005",https://www.iab.org/documents/correspondence-reports-documents/docs2005/2005-10-14-idn-guidelines/,
+2005-11-07,"IAB to ICANN – Nameserver change for e164.arpa, 7 November 2005",https://www.iab.org/documents/correspondence-reports-documents/docs2005/2005-11-07-nameserver-change/,
+2005-11-22,"IETF to ICANN – IANA structural status, 22 November 2005",https://www.iab.org/documents/correspondence-reports-documents/docs2005/2005-11-22-iana-structure/,
+2005-11-29,"IAB to IANA – Teredo prefix assignment, 29 November 2005",https://www.iab.org/documents/correspondence-reports-documents/docs2005/2005-11-29-teredo-prefix/,
+2005-12-22,"IAB to ICANN – dot arpa TLD management, 22 December 2005",https://www.iab.org/documents/correspondence-reports-documents/docs2005/2005-12-22-dot-arpa/,
+2006-03-06,"IAB Position on the IETF IANA Technical Parameter Function, 6 March 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/iab-iana-position/,
+2006-03-28,"IAB to ICANN – Name server changes for ip6.arpa, 28 March 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/2006-03-28-nameserver-change/,
+2006-04-20,"IAB to IANA – Administrative contact information change for arpa, 20 April 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/2006-04-20-update-to-administrative-contact-information-for-arpa-iana/,
+2006-04-20,"IAB to ITU TSB – FYI re contact info changes for e164.arpa, 20 April 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/2006-04-20-update-to-contact-information-for-e164-arpa-hill/,
+2006-04-20,"IAB to IANA – Contact information changes for e164.arpa, 20 April 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/2006-04-20-update-to-contact-information-for-e164-arpa-iana/,
+2006-05-15,"IAB to IANA – Request to IANA for status update on deployment of DNSSEC on IANA managed zones, 15 May 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/2006-05-15-iab-request-to-iana-to-sign-dnssec-zones/,
+2006-06-07,"The IAB announces the mailing list for the discussion of the independent submissions process, 7 June 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/2006-06-07-independent-submissions/,
+2006-06-19,"Procedural issues with liaison on nextsteps, 19 June 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/2006-06-16-response-to-idn-liaison-issues/,
+2006-10-12,"The IAB sends a note to the Registry Services Technical Evaluation Panel on the use of wildcards in the .travel TLD, 12 October 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/2006-10-12-rstep-note/,
+2006-10-19,"The IAB sends a note to the OIF Technical Committee Chair on IETF Protocol Extensions, 19 October 2006",https://www.iab.org/documents/correspondence-reports-documents/docs2006/2006-10-19-oifnote/,
+2007-05-21,"The IAB responds to ITU Consultation on Resolution 102, 21 May 2007",https://www.iab.org/documents/correspondence-reports-documents/docs2007/2007-05-21-itu-resolution-102/,
+2007-07-05,"Correspondence from the RIPE NCC regarding deployment of DNSSEC in the E164.ARPA zone, 5 July 2007",https://www.iab.org/documents/correspondence-reports-documents/docs2007/2007-07-05-ripe-ncc-dnssec-e164/,
+2007-07-24,"Correspondence from the IAB to the ITU-TSB Director regarding deployment of DNSSEC in the E164.ARPA zone, 24 July 2007",https://www.iab.org/documents/correspondence-reports-documents/docs2007/2007-07-24-iab-itu-dnssec-e164/,
+2007-10-10,"Follow-up work on NAT-PT, 10 October 2007",https://www.iab.org/documents/correspondence-reports-documents/docs2007/follow-up-work-on-nat-pt/,
+2008-02-15,"Correspondence from the IAB to the National Telecommunications and Information Administration, US Department of Commerce regarding the ICANN/DoC Joint Project Agreement, 15 February 2008",https://www.iab.org/documents/correspondence-reports-documents/docs2008/2008-02-15-midterm-view-icann-doc-jpa/,
+2008-03-07,"The IAB’s response to ICANN’s solicitation on DNS stability, 7 March 2008",https://www.iab.org/documents/correspondence-reports-documents/docs2008/2008-03-07-icann-new-gtlds/,
+2008-06-04,Proposed RFC Editor Structure,https://www.iab.org/documents/correspondence-reports-documents/docs2008/2008-06-04-rfc-editor-model/,
+2008-08-16,"The IAB’s response to Geoff Huston’s request concerning 32-bit AS numbers, 16 August 2008",https://www.iab.org/documents/correspondence-reports-documents/docs2008/2008-08-16-32bit-as-huston/,
+2008-09-05,Proposed RFC Editor Structure,https://www.iab.org/documents/correspondence-reports-documents/docs2008/2008-09-05-rfc-editor-model/,
+2008-11-18,"The IAB’s correspondence with NTIA on DNSSEC deployment at the root, 18 November 2008",https://www.iab.org/documents/correspondence-reports-documents/docs2008/2008-11-18-dnssec-deployment-at-the-root/,
+2008-12-04,"IAB correspondence with Geoff Huston on TAs, IANA, RIRs et al.., 4 December 2008",https://www.iab.org/documents/correspondence-reports-documents/docs2008/2008-12-04-huston-tas-iana-rirs/,
+2009-06-02,"IAB correspondence with IANA on the Signing of .ARPA, 2 June 2009",https://www.iab.org/documents/correspondence-reports-documents/docs2009/2009-06-02-roseman-signing-by-iana-of-arpa/,
+2009-10-14,"IAB correspondence with ICANN on their “Scaling the Root” study., 14 October 2009",https://www.iab.org/documents/correspondence-reports-documents/docs2009/2009-10-14-icann-scaling-the-root/,
+2010-01-27,IAB statement on the RPKI,https://www.iab.org/documents/correspondence-reports-documents/docs2010/iab-statement-on-the-rpki/,
+2010-07-30,Transition of IN-ADDR.ARPA generation,https://www.iab.org/documents/correspondence-reports-documents/docs2010/transition-of-in-addr-arpa-generation/,
+2011-06-22,Response to ARIN's request for guidance regarding Draft Policy ARIN-2011-5,https://www.iab.org/documents/correspondence-reports-documents/2011-2/response-to-arins-request-for-guidance-regarding-draft-policy-arin-2011-5/,
+2011-07-25,"IAB Response to ""Some IESG Thoughts on Liaisons""",https://www.iab.org/documents/correspondence-reports-documents/2011-2/iab-response-to-some-iesg-thoughts-on-liaisons/,
+2011-09-16,Letter to the European Commission on Global Interoperability in Emergency Services,https://www.iab.org/documents/correspondence-reports-documents/2011-2/letter-to-the-european-commission-on-global-interoperability-in-emergency-services/,
+2012-02-08,"IAB Statement: ""The interpretation of rules in the ICANN gTLD Applicant Guidebook""",https://www.iab.org/documents/correspondence-reports-documents/2012-2/iab-statement-the-interpretation-of-rules-in-the-icann-gtld-applicant-guidebook/,
+2012-03-26,"Response to ICANN questions concerning ""The interpretation of rules in the ICANN gTLD Applicant Guidebook""",https://www.iab.org/documents/correspondence-reports-documents/2012-2/response-to-icann-questions-concerning-the-interpretation-of-rules-in-the-icann-gtld-applicant-guidebook/,
+2012-03-30,IAB Member Roles in Evaluating New Work Proposals,https://www.iab.org/documents/correspondence-reports-documents/2012-2/iab-member-roles-in-evaluating-new-work-proposals/,
+2012-08-29,Leading Global Standards Organizations Endorse ‘OpenStand’ Principles that Drive Innovation and Borderless Commerce,https://www.iab.org/documents/correspondence-reports-documents/2012-2/leading-global-standards-organizations-endorse-%e2%80%98openstand/,
+2013-03-28,IAB Response to RSSAC restructure document (28 March 2013),https://www.iab.org/documents/correspondence-reports-documents/2013-2/iab-response-to-rssac-restructure-document-28-march-2013/,
+2013-05-28,Consultation on Root Zone KSK Rollover from the IAB,https://www.iab.org/documents/correspondence-reports-documents/2013-2/consultation-on-root-zone-ksk-rollover-from-the-iab/,
+2013-07-10,IAB Statement: Dotless Domains Considered Harmful,https://www.iab.org/documents/correspondence-reports-documents/2013-2/iab-statement-dotless-domains-considered-harmful/,
+2013-07-16,IAB Response to ICANN Consultation on the Source of Policies & User Instructions for Internet Number Resource Requests,https://www.iab.org/documents/correspondence-reports-documents/2013-2/iab-response-to-iana-policies-user-instructions-25jun13/,
+2013-10-03,Statement from the IAB on the Strengths of the OpenStand Principles,https://www.iab.org/documents/correspondence-reports-documents/2013-2/statement-from-openstand-on-the-strengths-of-the-openstand-principles/,
+2013-10-07,Montevideo Statement on the Future of Internet Cooperation,https://www.iab.org/documents/correspondence-reports-documents/2013-2/montevideo-statement-on-the-future-of-internet-cooperation/,
+2013-11-27,IAB Statement on draft-farrell-perpass-attack-00,https://www.iab.org/documents/correspondence-reports-documents/2013-2/iab-statement-on-draft-farrell-perpass-attack-00/,
+2014-01-23,IAB Comments Regarding the IRTF CFRG chair,https://www.iab.org/documents/correspondence-reports-documents/2014-2/0123-iab-comments-regarding-the-irtf-cfrg-chair/,
+2014-02-14,"Statement from the I* Leaders Coordination Meeting, Santa Monica, 14 February 2014",https://www.iab.org/documents/correspondence-reports-documents/2014-2/statement-from-the-i-leaders-coordination-meeting-santa-monica-14-february-2014/,
+2014-03-11,Re: Guiding the Evolution of the IANA Protocol Parameter Registries,https://www.iab.org/documents/correspondence-reports-documents/2014-2/re-guiding-the-evolution-of-the-iana-protocol-parameter-registries/,
+2014-03-14,Internet Technical Leaders Welcome IANA Globalization Progress,https://www.iab.org/documents/correspondence-reports-documents/2014-2/internet-technical-leaders-welcome-iana-globalization-progress/,
+2014-05-13,I* Post-NETmundial Meeting Statement,https://www.iab.org/documents/correspondence-reports-documents/2014-2/i-post-netmundial-meeting-statement/,
+2014-06-05,Comments on ICANN Board Member Compensation from the IAB,https://www.iab.org/documents/correspondence-reports-documents/2014-2/comments-on-icann-board-member-compensation/,
+2014-11-13,IAB Statement on Internet Confidentiality,https://www.iab.org/documents/correspondence-reports-documents/2014-2/iab-statement-on-internet-confidentiality/,
+2014-12-04,IAB statement on the NETmundial Initiative,https://www.iab.org/documents/correspondence-reports-documents/2014-2/iab-statement-on-the-netmundial-initiative/,
+2014-12-17,IAB Comments on CSTD Report Mapping International Internet Public Policy Issues,https://www.iab.org/documents/correspondence-reports-documents/2014-2/iab-comments-on-cstd-report-mapping-international-public-policy-issues/,
+2015-02-11,IAB liaison to ICANN Root Server System Advisory Committee (RSSAC),https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-liaison-to-icann-root-server-system-advisory-council-rssac/,
+2015-02-11,IAB Statement on Identifiers and Unicode 7.0.0,https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-statement-on-identifiers-and-unicode-7-0-0/,
+2015-03-02,IAB Statement on Liaison Compensation,https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-statement-on-liaison-compensation/,
+2015-04-09,IAB Comments on The HTTPS-Only Standard,https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-comments-on-the-https-only-standard/,
+2015-06-03,IAB comments on CCWG-Accountability Draft Report,https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-comments-on-ccwg-accountability-draft-report/,
+2015-06-12,IAB Statement on the Trade in Security Technologies,https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-statement-on-the-trade-in-security-technologies/,
+2015-06-24,"IAB Correspondence to U.S. Bureau of Industry and Security, re RIN 0694-AG49",https://www.iab.org/documents/correspondence-reports-documents/2015-2/rin-0694-ag49/,
+2015-09-07,Internet Architecture Board comments on the ICG Proposal,https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-comments-on-icg-proposal/,
+2015-09-09,IAB comments on the CCWG accountability 2d draft report,https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-comments-on-ccwg-accountability/,
+2015-10-07,IAB Comments to FCC on Rules regarding Authorization of Radiofrequency Equipment,https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-comments-on-fcc-15-92/,
+2015-12-16,IAB comments on the CCWG accountability 3d draft report,https://www.iab.org/documents/correspondence-reports-documents/2015-2/iab-comments-on-the-ccwg-accountability-3d-draft-report/,
+2016-01-13,"Comments from the Internet Architecture Board (IAB) on ""Registration Data Access Protocol (RDAP) Operational Profile for gTLD Registries and Registrars""",https://www.iab.org/documents/correspondence-reports-documents/2016-2/comments-from-the-internet-architecture-board-iab-on-registration-data-access-protocol-rdap-operational-profile-for-gtld-registries-and-registrars/,
+2016-05-04,IAB comments on Draft New ICANN Bylaws,https://www.iab.org/documents/correspondence-reports-documents/2016-2/iab-comments-on-draft-new-icann-bylaws/,
+2016-05-11,IAB Comments on Proposed Changes to Internet Society Bylaws,https://www.iab.org/documents/correspondence-reports-documents/2016-2/iab-comments-on-proposed-changes-to-internet-society-bylaws/,
+2016-07-17,Comments from the IAB on LGRs for second level,https://www.iab.org/documents/correspondence-reports-documents/2016-2/comments-from-the-iab-on-lgrs-for-second-level/,
+2016-09-01,IAB statement on IANA Intellectual Property Rights,https://www.iab.org/documents/correspondence-reports-documents/2016-2/iab-statement-on-iana-intellectual-property-rights/,
+2016-09-14,IAB Statement on the IANA Stewardship Transition,https://www.iab.org/documents/correspondence-reports-documents/2016-2/iab-statement-on-the-iana-stewardship-transition/,
+2016-11-07,IAB Statement on IPv6,https://www.iab.org/documents/correspondence-reports-documents/2016-2/iab-statement-on-ipv6/,
+2016-12-07,"IAB comment on ""Revised Proposed Implementation of GNSO Thick Whois Consensus Policy Requiring Consistent Labeling and Display of RDDS (Whois) Output for All gTLDs""",https://www.iab.org/documents/correspondence-reports-documents/2016-2/iab-comment-on-revised-proposed-implementation-of-gnso-thick-whois-consensus-policy-requiring-consistent-labeling-and-display-of-rdds-whois-output-for-all-gtlds/,
+2017-01-04,IAB comments on Identifier Technology Health Indicators: Definition,https://www.iab.org/documents/correspondence-reports-documents/2017-2/iab-comments-on-identifier-technology-health-indicators-definition/,
+2017-02-01,IAB Statement on OCSP Stapling,https://www.iab.org/documents/correspondence-reports-documents/2017-2/iab-statement-on-ocsp-stapling/,
+2017-02-16,Follow up on barriers to entry blog post,https://www.iab.org/documents/correspondence-reports-documents/2017-2/follow-up-on-barriers-to-entry-blog-post/,
+2017-03-02,IAB Comments to United States NTIA on the Green Paper: Fostering the Advancement of the Internet of Things,https://www.iab.org/documents/correspondence-reports-documents/2017-2/iab-comments-to-ntia-on-fostering-the-advancement-of-iot/,
+2017-03-30,Internet Architecture Board statement on the registration of special use names in the ARPA domain,https://www.iab.org/documents/correspondence-reports-documents/2017-2/iab-statement-on-the-registration-of-special-use-names-in-the-arpa-domain/,
+2017-05-01,Comments from the IAB on IDN Implementation Guidelines,https://www.iab.org/documents/correspondence-reports-documents/2017-2/comments-from-the-iab-on-idn-implementation-guidelines/,
+2017-07-31,IAB Response to FCC-17-89,https://www.iab.org/documents/correspondence-reports-documents/2017-2/iab-response-to-fcc-17-89/,
+2018-03-15,IAB Statement on Identifiers and Unicode,https://www.iab.org/documents/correspondence-reports-documents/2018-2/iab-statement-on-identifiers-and-unicode/,
+2018-04-03,IAB Statement on the RPKI,https://www.iab.org/documents/correspondence-reports-documents/2018-2/iab-statement-on-the-rpki/,
+2019-05-02,Revised Operating Instructions for e164.arpa (ENUM),https://www.iab.org/documents/correspondence-reports-documents/2019-2/revised-operating-instructions-for-e164-arpa-enum/,
+2019-06-26,Comments on Evolving the Governance of the Root Server System,https://www.iab.org/documents/correspondence-reports-documents/2019-2/comments-on-evolving-the-governance-of-the-root-server-system/,
+2019-09-04,Avoiding Unintended Harm to Internet Infrastructure,https://www.iab.org/documents/correspondence-reports-documents/2019-2/avoiding-unintended-harm-to-internet-infrastructure/,
+2020-07-01,"IAB correspondence with the National Telecommunications and Information Administration (NTIA) on DNSSEC deployment for the Root Zone [Docket No. 100603240-0240-01], 1 July 2010",https://www.iab.org/documents/correspondence-reports-documents/docs2010/2010-07-01-alexander-dnssec-deployment-for-the-root-zone/,
+2020-09-29,IAB Comments on the Draft Final Report on the new gTLD Subsequent Procedures Policy Development Process,https://www.iab.org/documents/correspondence-reports-documents/2020-2/iab-comments-on-new-gtld-subsequent-procedures/,
+2021-07-14,IAB Statement on Inclusive Language in IAB Stream Documents,https://www.iab.org/documents/correspondence-reports-documents/2021-2/iab-statement-on-inclusive-language-in-iab-stream-documents/,
+2022-04-08,IAB comment on Mandated Browser Root Certificates in the European Union’s eIDAS Regulation on the Internet,https://www.iab.org/documents/correspondence-reports-documents/2022-2/iab-comment-on-mandated-browser-root-certificates-in-the-european-unions-eidas-regulation-on-the-internet/,
+2022-04-08,"IAB Comments on A Notice by the Federal Communications Commission on Secure Internet Routing, issued 03/11/2022",https://www.iab.org/documents/correspondence-reports-documents/2022-2/iab-comments-on-a-notice-by-the-federal-communications-commission-on-secure-internet-routing-issued-03-11-2022/,
+2022-07-08,IAB Statement to OSTP on Privacy-Enhancing Technologies,https://www.iab.org/documents/correspondence-reports-documents/2022-2/iab-statement-to-ostp-on-privacy-enhancing-technologies/,
+2022-11-21,IAB Comments on a notice by the Federal Trade Commission on “Trade Regulation Rule on Commercial Surveillance and Data Security” (16 CFR Part 464),https://www.iab.org/documents/correspondence-reports-documents/2022-2/iab-comments-on-a-notice-by-the-federal-trade-commission-on-trade-regulation-rule-on-commercial-surveillance-and-data-security-16-cfr-part-464/,
+'''
+
+    rows = []
+    date_count = defaultdict(lambda: 0)
+    with io.StringIO(csv_dump) as csv_file:
+        reader = csv.reader(csv_file)
+        for row in reader:
+            date = row[0]
+            if date_count[date] == 0:
+                row.insert(0, "")
+            else:
+                row.insert(0, date_count[date])
+                date_count[date] += 1
+            rows.append(row)
+    return rows
diff --git a/ietf/doc/migrations/0005_alter_docevent_type.py b/ietf/doc/migrations/0005_alter_docevent_type.py
new file mode 100644
index 000000000..f8a3cfc79
--- /dev/null
+++ b/ietf/doc/migrations/0005_alter_docevent_type.py
@@ -0,0 +1,86 @@
+# Copyright The IETF Trust 2023, All Rights Reserved
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("doc", "0004_alter_dochistory_ad_alter_dochistory_shepherd_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="docevent",
+            name="type",
+            field=models.CharField(
+                choices=[
+                    ("new_revision", "Added new revision"),
+                    ("new_submission", "Uploaded new revision"),
+                    ("changed_document", "Changed document metadata"),
+                    ("added_comment", "Added comment"),
+                    ("added_message", "Added message"),
+                    ("edited_authors", "Edited the documents author list"),
+                    ("deleted", "Deleted document"),
+                    ("changed_state", "Changed state"),
+                    ("changed_stream", "Changed document stream"),
+                    ("expired_document", "Expired document"),
+                    ("extended_expiry", "Extended expiry of document"),
+                    ("requested_resurrect", "Requested resurrect"),
+                    ("completed_resurrect", "Completed resurrect"),
+                    ("changed_consensus", "Changed consensus"),
+                    ("published_rfc", "Published RFC"),
+                    (
+                        "added_suggested_replaces",
+                        "Added suggested replacement relationships",
+                    ),
+                    (
+                        "reviewed_suggested_replaces",
+                        "Reviewed suggested replacement relationships",
+                    ),
+                    ("changed_action_holders", "Changed action holders for document"),
+                    ("changed_group", "Changed group"),
+                    ("changed_protocol_writeup", "Changed protocol writeup"),
+                    ("changed_charter_milestone", "Changed charter milestone"),
+                    ("initial_review", "Set initial review time"),
+                    ("changed_review_announcement", "Changed WG Review text"),
+                    ("changed_action_announcement", "Changed WG Action text"),
+                    ("started_iesg_process", "Started IESG process on document"),
+                    ("created_ballot", "Created ballot"),
+                    ("closed_ballot", "Closed ballot"),
+                    ("sent_ballot_announcement", "Sent ballot announcement"),
+                    ("changed_ballot_position", "Changed ballot position"),
+                    ("changed_ballot_approval_text", "Changed ballot approval text"),
+                    ("changed_ballot_writeup_text", "Changed ballot writeup text"),
+                    ("changed_rfc_editor_note_text", "Changed RFC Editor Note text"),
+                    ("changed_last_call_text", "Changed last call text"),
+                    ("requested_last_call", "Requested last call"),
+                    ("sent_last_call", "Sent last call"),
+                    ("scheduled_for_telechat", "Scheduled for telechat"),
+                    ("iesg_approved", "IESG approved document (no problem)"),
+                    ("iesg_disapproved", "IESG disapproved document (do not publish)"),
+                    ("approved_in_minute", "Approved in minute"),
+                    ("iana_review", "IANA review comment"),
+                    ("rfc_in_iana_registry", "RFC is in IANA registry"),
+                    (
+                        "rfc_editor_received_announcement",
+                        "Announcement was received by RFC Editor",
+                    ),
+                    ("requested_publication", "Publication at RFC Editor requested"),
+                    (
+                        "sync_from_rfc_editor",
+                        "Received updated information from RFC Editor",
+                    ),
+                    ("requested_review", "Requested review"),
+                    ("assigned_review_request", "Assigned review request"),
+                    ("closed_review_request", "Closed review request"),
+                    ("closed_review_assignment", "Closed review assignment"),
+                    ("downref_approved", "Downref approved"),
+                    ("posted_related_ipr", "Posted related IPR"),
+                    ("removed_related_ipr", "Removed related IPR"),
+                    ("changed_editors", "Changed BOF Request editors"),
+                    ("published_statement", "Published statement"),
+                ],
+                max_length=50,
+            ),
+        ),
+    ]
diff --git a/ietf/doc/migrations/0006_statements.py b/ietf/doc/migrations/0006_statements.py
new file mode 100644
index 000000000..9a074292e
--- /dev/null
+++ b/ietf/doc/migrations/0006_statements.py
@@ -0,0 +1,43 @@
+# Copyright The IETF Trust 2023, All Rights Reserved
+
+from django.db import migrations
+
+
+def forward(apps, schema_editor):
+    StateType = apps.get_model("doc", "StateType")
+    State = apps.get_model("doc", "State")
+
+    StateType.objects.create(slug="statement", label="Statement State")
+    State.objects.create(
+        slug="active",
+        type_id="statement",
+        name="Active",
+        order=0,
+        desc="The statement is active",
+    )
+    State.objects.create(
+        slug="replaced",
+        type_id="statement",
+        name="Replaced",
+        order=0,
+        desc="The statement has been replaced",
+    )
+
+
+def reverse(apps, schema_editor):
+    StateType = apps.get_model("doc", "StateType")
+    State = apps.get_model("doc", "State")
+
+    State.objects.filter(type_id="statement").delete()
+    StateType.objects.filter(slug="statement").delete()
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("doc", "0005_alter_docevent_type"),
+        ("name", "0004_statements"),
+    ]
+
+    operations = [
+        migrations.RunPython(forward, reverse),
+    ]
diff --git a/ietf/doc/models.py b/ietf/doc/models.py
index 86d814d73..d48032676 100644
--- a/ietf/doc/models.py
+++ b/ietf/doc/models.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2010-2020, All Rights Reserved
+# Copyright The IETF Trust 2010-2023, All Rights Reserved
 # -*- coding: utf-8 -*-
 
 
@@ -162,7 +162,7 @@ class DocumentInfo(models.Model):
                 self._cached_file_path = settings.CONFLICT_REVIEW_PATH
             elif self.type_id == "statchg":
                 self._cached_file_path = settings.STATUS_CHANGE_PATH
-            elif self.type_id == "bofreq":
+            elif self.type_id == "bofreq": # TODO: This is probably unneeded, as is the separate path setting
                 self._cached_file_path = settings.BOFREQ_PATH
             else:
                 self._cached_file_path = settings.DOCUMENT_PATH_PATTERN.format(doc=self)
@@ -186,7 +186,7 @@ class DocumentInfo(models.Model):
             elif self.type_id == 'review':
                 # TODO: This will be wrong if a review is updated on the same day it was created (or updated more than once on the same day)
                 self._cached_base_name = "%s.txt" % self.name
-            elif self.type_id == 'bofreq':
+            elif self.type_id in ['bofreq', 'statement']:
                 self._cached_base_name = "%s-%s.md" % (self.name, self.rev)
             else:
                 if self.rev:
@@ -1290,7 +1290,11 @@ EVENT_TYPES = [
     ("removed_related_ipr", "Removed related IPR"),
 
     # Bofreq Editor events
-    ("changed_editors", "Changed BOF Request editors")
+    ("changed_editors", "Changed BOF Request editors"),
+
+    # Statement events
+    ("published_statement", "Published statement"),
+    
     ]
 
 class DocEvent(models.Model):
diff --git a/ietf/doc/tests_statement.py b/ietf/doc/tests_statement.py
new file mode 100644
index 000000000..48ab0da7d
--- /dev/null
+++ b/ietf/doc/tests_statement.py
@@ -0,0 +1,355 @@
+# Copyright The IETF Trust 2023, All Rights Reserved
+
+import debug  # pyflakes:ignore
+
+from pyquery import PyQuery
+
+from pathlib import Path
+
+from django.conf import settings
+from django.core.files.uploadedfile import SimpleUploadedFile
+from django.template.loader import render_to_string
+from django.urls import reverse as urlreverse
+
+from ietf.doc.factories import StatementFactory, DocEventFactory
+from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent
+from ietf.group.models import Group
+from ietf.person.factories import PersonFactory
+from ietf.utils.mail import outbox, empty_outbox
+from ietf.utils.test_utils import (
+    TestCase,
+    reload_db_objects,
+    login_testing_unauthorized,
+)
+
+
+class StatementsTestCase(TestCase):
+    settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [
+        "DOCUMENT_PATH_PATTERN"
+    ]
+
+    def extract_content(self, response):
+        if not hasattr(response, "_cached_extraction"):
+            response._cached_extraction = list(response.streaming_content)[0].decode(
+                "utf-8"
+            )
+        return response._cached_extraction
+
+    def write_statement_markdown_file(self, statement):
+        (
+            Path(settings.DOCUMENT_PATH_PATTERN.format(doc=statement))
+            / ("%s-%s.md" % (statement.name, statement.rev))
+        ).write_text(
+            """# This is a test statement.
+Version: {statement.rev}
+
+## A section
+
+This test section has some text.
+"""
+        )
+
+    def write_statement_pdf_file(self, statement):
+        (
+            Path(settings.DOCUMENT_PATH_PATTERN.format(doc=statement))
+            / ("%s-%s.pdf" % (statement.name, statement.rev))
+        ).write_text(
+            f"{statement.rev} This is not valid PDF, but the test does not need it to be"
+        )
+
+    def test_statement_doc_view(self):
+        doc = StatementFactory()
+        self.write_statement_markdown_file(doc)
+        url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        q = PyQuery(response.content)
+        self.assertEqual(q("#statement-state").text(), "Active")
+        self.assertEqual(q("#statement-type").text(), "IAB Statement")
+        self.assertIn("has some text", q(".card-body").text())
+        published = doc.docevent_set.filter(type="published_statement").last().time
+        self.assertIn(published.date().isoformat(), q("#published").text())
+
+        doc.set_state(State.objects.get(type_id="statement", slug="replaced"))
+        doc2 = StatementFactory()
+        doc2.relateddocument_set.create(
+            relationship_id="replaces", target=doc.docalias.first()
+        )
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        q = PyQuery(response.content)
+        self.assertEqual(q("#statement-state").text(), "Replaced")
+        self.assertEqual(q("#statement-type").text(), "Replaced IAB Statement")
+        self.assertEqual(q("#statement-type").next().text(), f"Replaced by {doc2.name}")
+
+        url = urlreverse(
+            "ietf.doc.views_doc.document_main", kwargs=dict(name=doc2.name)
+        )
+        response = self.client.get(url)
+        self.assertEqual(response.status_code, 200)
+        q = PyQuery(response.content)
+        self.assertEqual(q("#statement-type").text(), "IAB Statement")
+        self.assertEqual(q("#statement-type").next().text(), f"Replaces {doc.name}")
+
+    def test_serve_pdf(self):
+        url = urlreverse(
+            "ietf.doc.views_statement.serve_pdf",
+            kwargs=dict(name="statement-does-not-exist"),
+        )
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 404)
+
+        doc = StatementFactory()
+        url = urlreverse(
+            "ietf.doc.views_statement.serve_pdf", kwargs=dict(name=doc.name)
+        )
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 404)  # File not found
+
+        self.write_statement_pdf_file(doc)
+        doc.rev = "01"
+        e = DocEventFactory(type="published_statement", doc=doc, rev=doc.rev)
+        doc.save_with_history([e])
+        self.write_statement_pdf_file(doc)
+
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r.get("Content-Type"), "application/pdf")
+        self.assertTrue(
+            self.extract_content(r).startswith(doc.rev)
+        )  # relies on test doc not actually being pdf
+
+        url = urlreverse(
+            "ietf.doc.views_statement.serve_pdf", kwargs=dict(name=doc.name, rev="00")
+        )
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 200)
+        self.assertTrue(self.extract_content(r).startswith("00 "))
+        url = urlreverse(
+            "ietf.doc.views_statement.serve_pdf", kwargs=dict(name=doc.name, rev="01")
+        )
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 200)
+        self.assertTrue(self.extract_content(r).startswith("01 "))
+
+    def test_submit(self):
+        doc = StatementFactory()
+        url = urlreverse("ietf.doc.views_statement.submit", kwargs=dict(name=doc.name))
+
+        rev = doc.rev
+        r = self.client.post(
+            url, {"statement_submission": "enter", "statement_content": "# oiwefrase"}
+        )
+        self.assertEqual(r.status_code, 302)
+        doc = reload_db_objects(doc)
+        self.assertEqual(rev, doc.rev)
+
+        nobody = PersonFactory()
+        self.client.login(
+            username=nobody.user.username, password=nobody.user.username + "+password"
+        )
+        r = self.client.post(
+            url, {"statement_submission": "enter", "statement_content": "# oiwefrase"}
+        )
+        self.assertEqual(r.status_code, 403)
+        doc = reload_db_objects(doc)
+        self.assertEqual(rev, doc.rev)
+        self.client.logout()
+
+        for username in ["secretary"]:  # There is potential for expanding this list
+            self.client.login(username=username, password=username + "+password")
+            r = self.client.get(url)
+            self.assertEqual(r.status_code, 200)
+            file = SimpleUploadedFile(
+                "random.pdf",
+                b"not valid pdf",
+                content_type="application/pdf",
+            )
+            for postdict in [
+                {
+                    "statement_submission": "enter",
+                    "statement_content": f"# {username}",
+                },
+                {
+                    "statement_submission": "upload",
+                    "statement_file": file,
+                },
+            ]:
+                docevent_count = doc.docevent_set.count()
+                empty_outbox()
+                r = self.client.post(url, postdict)
+                self.assertEqual(r.status_code, 302)
+                doc = reload_db_objects(doc)
+                self.assertEqual("%02d" % (int(rev) + 1), doc.rev)
+                if postdict["statement_submission"] == "enter":
+                    self.assertEqual(f"# {username}", doc.text())
+                else:
+                    self.assertEqual("not valid pdf", doc.text())
+                self.assertEqual(docevent_count + 1, doc.docevent_set.count())
+                self.assertEqual(0, len(outbox))
+                rev = doc.rev
+            self.client.logout()
+
+    def test_start_new_statement(self):
+        url = urlreverse("ietf.doc.views_statement.new_statement")
+        login_testing_unauthorized(self, "secretary", url)
+        r = self.client.get(url)
+        self.assertContains(
+            r,
+            "Replace this with the content of the statement in markdown source",
+            status_code=200,
+        )
+        group = Group.objects.get(acronym="iab")
+        r = self.client.post(
+            url,
+            dict(
+                group=group.pk,
+                title="default",
+                statement_submission="enter",
+                statement_content=render_to_string(
+                    "doc/statement/statement_template.md", {"settings": settings}
+                ),
+            ),
+        )
+        self.assertContains(r, "The example content may not be saved.", status_code=200)
+
+        file = SimpleUploadedFile(
+            "random.pdf",
+            b"not valid pdf",
+            content_type="application/pdf",
+        )
+        group = Group.objects.get(acronym="iab")
+        for postdict in [
+            dict(
+                group=group.pk,
+                title="title one",
+                statement_submission="enter",
+                statement_content="some stuff",
+            ),
+            dict(
+                group=group.pk,
+                title="title two",
+                statement_submission="upload",
+                statement_file=file,
+            ),
+        ]:
+            empty_outbox()
+            r = self.client.post(url, postdict)
+            self.assertEqual(r.status_code, 302)
+            name = f"statement-{group.acronym}-{postdict['title']}".replace(
+                " ", "-"
+            )  # cheap slugification
+            statement = Document.objects.filter(
+                name=name, type_id="statement"
+            ).first()
+            self.assertIsNotNone(statement)
+            self.assertIsNotNone(DocAlias.objects.filter(name=name).first())
+            self.assertEqual(statement.title, postdict["title"])
+            self.assertEqual(statement.rev, "00")
+            self.assertEqual(statement.get_state_slug(), "active")
+            self.assertEqual(
+                statement.latest_event(NewRevisionDocEvent).rev, "00"
+            )
+            self.assertIsNotNone(statement.latest_event(type="published_statement"))
+            if postdict["statement_submission"] == "enter":
+                self.assertEqual(statement.text_or_error(), "some stuff")
+            else:
+                self.assertTrue(statement.uploaded_filename.endswith("pdf"))
+            self.assertEqual(len(outbox), 0)
+
+        existing_statement = StatementFactory()
+        for postdict in [
+            dict(
+                group=group.pk,
+                title="",
+                statement_submission="enter",
+                statement_content="some stuff",
+            ),
+            dict(
+                group=group.pk,
+                title="a title",
+                statement_submission="enter",
+                statement_content="",
+            ),
+            dict(
+                group=group.pk,
+                title=existing_statement.title,
+                statement_submission="enter",
+                statement_content="some stuff",
+            ),
+            dict(
+                group=group.pk,
+                title="森川",
+                statement_submission="enter",
+                statement_content="some stuff",
+            ),
+            dict(
+                group=group.pk,
+                title="a title",
+                statement_submission="",
+                statement_content="some stuff",
+            ),
+            dict(
+                group="",
+                title="a title",
+                statement_submission="enter",
+                statement_content="some stuff",
+            ),
+            dict(
+                group=0,
+                title="a title",
+                statement_submission="enter",
+                statement_content="some stuff",
+            ),
+        ]:
+            r = self.client.post(url, postdict)
+            self.assertEqual(r.status_code, 200, f"Wrong status_code for {postdict}")
+            q = PyQuery(r.content)
+            self.assertTrue(
+                q("form div.is-invalid"), f"Expected an error for {postdict}"
+            )
+
+    def test_submit_non_markdown_formats(self):
+        doc = StatementFactory()
+
+        file = SimpleUploadedFile(
+            "random.pdf",
+            b"01 This is not valid PDF, but the test does not need it to be",
+            content_type="application/pdf",
+        )
+
+        url = urlreverse("ietf.doc.views_statement.submit", kwargs=dict(name=doc.name))
+        login_testing_unauthorized(self, "secretary", url)
+
+        r = self.client.post(
+            url,
+            {
+                "statement_submission": "upload",
+                "statement_file": file,
+            },
+        )
+        self.assertEqual(r.status_code, 302)
+        self.assertEqual(
+            r["Location"],
+            urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name)),
+        )
+
+        doc = reload_db_objects(doc)
+        self.assertEqual(doc.rev, "01")
+        r = self.client.get(url)
+        self.assertEqual(r.status_code, 200)
+        q = PyQuery(r.content)
+        self.assertEqual(
+            q("#id_statement_content").text().strip(),
+            "The current revision of this statement is in pdf format",
+        )
+
+        file = SimpleUploadedFile(
+            "random.mp4", b"29ucdvn2o09hano5", content_type="video/mp4"
+        )
+        r = self.client.post(
+            url, {"statement_submission": "upload", "statement_file": file}
+        )
+        self.assertEqual(r.status_code, 200)
+        q = PyQuery(r.content)
+        self.assertTrue("Unexpected content" in q("#id_statement_file").next().text())
diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py
index f4b672804..c2addc5c9 100644
--- a/ietf/doc/urls.py
+++ b/ietf/doc/urls.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2009-2020, All Rights Reserved
+# Copyright The IETF Trust 2009-2023, All Rights Reserved
 # -*- coding: utf-8 -*-
 # Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
 # All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>
@@ -37,7 +37,7 @@ from django.conf import settings
 from django.urls import include
 from django.views.generic import RedirectView
 
-from ietf.doc import views_search, views_draft, views_ballot, views_status_change, views_doc, views_downref, views_stats, views_help, views_bofreq
+from ietf.doc import views_search, views_draft, views_ballot, views_status_change, views_doc, views_downref, views_stats, views_help, views_bofreq, views_statement
 from ietf.utils.urls import url
 
 session_patterns = [
@@ -57,6 +57,7 @@ urlpatterns = [
     url(r'^start-rfc-status-change/(?:%(name)s/)?$' % settings.URL_REGEXPS, views_status_change.start_rfc_status_change),
     url(r'^bof-requests/?$', views_bofreq.bof_requests),
     url(r'^bof-requests/new/$', views_bofreq.new_bof_request),
+    url(r'^statement/new/$', views_statement.new_statement),
     url(r'^iesg/?$', views_search.drafts_in_iesg_process),
     url(r'^email-aliases/?$', views_doc.email_aliases),
     url(r'^downref/?$', views_downref.downref_registry),
@@ -169,6 +170,7 @@ urlpatterns = [
 
     url(r'^%(charter)s/' % settings.URL_REGEXPS, include('ietf.doc.urls_charter')),
     url(r'^%(bofreq)s/' % settings.URL_REGEXPS, include('ietf.doc.urls_bofreq')),
+    url(r'^%(statement)s/' % settings.URL_REGEXPS, include('ietf.doc.urls_statement')),
     url(r'^%(name)s/conflict-review/' % settings.URL_REGEXPS, include('ietf.doc.urls_conflict_review')),
     url(r'^%(name)s/status-change/' % settings.URL_REGEXPS, include('ietf.doc.urls_status_change')),
     url(r'^%(name)s/material/' % settings.URL_REGEXPS, include('ietf.doc.urls_material')),
diff --git a/ietf/doc/urls_statement.py b/ietf/doc/urls_statement.py
new file mode 100644
index 000000000..e130c453b
--- /dev/null
+++ b/ietf/doc/urls_statement.py
@@ -0,0 +1,10 @@
+# Copyright The IETF Trust 2023, All Rights Reserved
+
+from django.conf import settings
+from ietf.doc import views_statement
+from ietf.utils.urls import url
+
+urlpatterns = [
+    url(r"^(?:%(rev)s/)?pdf/$" % settings.URL_REGEXPS, views_statement.serve_pdf),
+    url(r"^submit/$", views_statement.submit),
+]
diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py
index f6c90111b..6adbe1532 100644
--- a/ietf/doc/views_doc.py
+++ b/ietf/doc/views_doc.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2009-2022, All Rights Reserved
+# Copyright The IETF Trust 2009-2023, All Rights Reserved
 # -*- coding: utf-8 -*-
 #
 # Parts Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies).
@@ -845,7 +845,40 @@ def document_main(request, name, rev=None, document_html=False):
             )
         )
 
+    if doc.type_id == "statement":
+        if doc.uploaded_filename:
+            basename = doc.uploaded_filename.split(".")[0] # strip extension
+        else:
+            basename = f"{doc.name}-{doc.rev}"
+        variants = set([match.name.split(".")[1] for match in Path(doc.get_file_path()).glob(f"{basename}.*")])
+        inlineable = any([ext in variants for ext in ["md", "txt"]])
+        if inlineable:
+            content = markdown.markdown(doc.text_or_error())
+        else:
+            content = "No format available to display inline"
+            if "pdf" in variants:
+                pdf_url = urlreverse(
+                    "ietf.doc.views_statement.serve_pdf",
+                    kwargs=dict(name=doc.name, rev=doc.rev),
+                )
+                content += f" - Download [pdf]({pdf_url})"
+            content = markdown.markdown(content)
+        can_manage = has_role(request.user,["Secretariat"]) # Add IAB or IESG as appropriate
+        interesting_relations_that, interesting_relations_that_doc = interesting_doc_relations(doc)
+        published = doc.latest_event(type="published_statement").time
 
+        return render(request, "doc/document_statement.html",
+                                  dict(doc=doc,
+                                       top=top,
+                                       revisions=revisions,
+                                       latest_rev=latest_rev,
+                                       published=published,
+                                       content=content,
+                                       snapshot=snapshot,
+                                       replaces=interesting_relations_that_doc.filter(relationship="replaces"),
+                                       replaced_by=interesting_relations_that.filter(relationship="replaces"),
+                                       can_manage=can_manage,
+                                       ))
 
     raise Http404("Document not found: %s" % (name + ("-%s"%rev if rev else "")))
 
diff --git a/ietf/doc/views_help.py b/ietf/doc/views_help.py
index 43c029ac7..fd46bda31 100644
--- a/ietf/doc/views_help.py
+++ b/ietf/doc/views_help.py
@@ -1,3 +1,5 @@
+# Copyright The IETF Trust 2013-2023, All Rights Reserved
+
 from django.shortcuts import render, get_object_or_404
 from django.http import Http404
 
@@ -5,6 +7,7 @@ from ietf.doc.models import State, StateType, IESG_SUBSTATE_TAGS
 from ietf.name.models import DocRelationshipName,  DocTagName
 from ietf.doc.utils import get_tags_for_stream_id
 
+# TODO: This is haphazardly implemented. Either flesh it out or reconsider the utility of showing these in these this way.
 def state_help(request, type):
     slug, title = {
         "draft-iesg": ("draft-iesg", "IESG States for Internet-Drafts"),
@@ -19,6 +22,7 @@ def state_help(request, type):
         "status-change": ("statchg", "RFC Status Change States"),
         "bofreq": ("bofreq", "BOF Request States"),
         "procmaterials": ("procmaterials", "Proceedings Materials States"),
+        "statement": {"statement", "Statement States"}
         }.get(type, (None, None))
     state_type = get_object_or_404(StateType, slug=slug)
 
diff --git a/ietf/doc/views_statement.py b/ietf/doc/views_statement.py
new file mode 100644
index 000000000..0ffa6a3c4
--- /dev/null
+++ b/ietf/doc/views_statement.py
@@ -0,0 +1,274 @@
+# Copyright The IETF Trust 2023, All Rights Reserved
+
+import debug  # pyflakes: ignore
+
+from pathlib import Path
+
+from django import forms
+from django.conf import settings
+from django.http import FileResponse, Http404
+from django.views.decorators.cache import cache_control
+from django.shortcuts import get_object_or_404, render, redirect
+from django.template.loader import render_to_string
+from ietf.utils import markdown
+from django.utils.html import escape
+
+from ietf.doc.models import Document, DocAlias, DocEvent, NewRevisionDocEvent, State
+from ietf.group.models import Group
+from ietf.ietfauth.utils import role_required
+from ietf.utils.text import xslugify
+from ietf.utils.textupload import get_cleaned_text_file_content
+
+CONST_PDF_REV_NOTICE = "The current revision of this statement is in pdf format"
+
+
+@cache_control(max_age=3600)
+def serve_pdf(self, name, rev=None):
+    doc = get_object_or_404(Document, name=name)
+    if rev is None:
+        rev = doc.rev
+    p = Path(doc.get_file_path()).joinpath(f"{doc.name}-{rev}.pdf")
+    if not p.exists():
+        raise Http404
+    else:
+        return FileResponse(p.open(mode="rb"), content_type="application/pdf")
+
+
+class StatementUploadForm(forms.Form):
+    ACTIONS = [
+        ("enter", "Enter content directly"),
+        ("upload", "Upload content from file"),
+    ]
+    statement_submission = forms.ChoiceField(choices=ACTIONS, widget=forms.RadioSelect)
+    statement_file = forms.FileField(
+        label="Markdown or PDF source file to upload", required=False
+    )
+    statement_content = forms.CharField(
+        widget=forms.Textarea(attrs={"rows": 30}), required=False, strip=False
+    )
+
+    def clean(self):
+        def require_field(f):
+            if not self.cleaned_data.get(f):
+                self.add_error(f, forms.ValidationError("You must fill in this field."))
+                return False
+            else:
+                return True
+
+        submission_method = self.cleaned_data.get("statement_submission")
+        markdown_content = ""
+        if submission_method == "enter":
+            if require_field("statement_content"):
+                markdown_content = self.cleaned_data["statement_content"].replace(
+                    "\r", ""
+                )
+                default_content = render_to_string(
+                    "doc/statement/statement_template.md", {}
+                )
+                if markdown_content == default_content:
+                    raise forms.ValidationError(
+                        "The example content may not be saved. Edit it to contain the next revision statement content."
+                    )
+                if markdown_content == CONST_PDF_REV_NOTICE:
+                    raise forms.ValidationError(
+                        "Not proceeding with the text noting that the current version is pdf. Did you mean to upload a new PDF?"
+                    )
+        elif submission_method == "upload":
+            if require_field("statement_file"):
+                content_type = self.cleaned_data["statement_file"].content_type
+                acceptable_types = (
+                    "application/pdf",
+                ) + settings.DOC_TEXT_FILE_VALID_UPLOAD_MIME_TYPES
+                if not content_type.startswith(
+                    acceptable_types
+                ):  # dances around decoration of types with encoding etc.
+                    self.add_error(
+                        "statement_file",
+                        forms.ValidationError(
+                            f"Unexpected content type: Expected one of {', '.join(acceptable_types)}"
+                        ),
+                    )
+                elif content_type != "application/pdf":
+                    markdown_content = get_cleaned_text_file_content(
+                        self.cleaned_data["statement_file"]
+                    )
+        if markdown_content != "":
+            try:
+                _ = markdown.markdown(markdown_content)
+            except Exception as e:
+                raise forms.ValidationError(f"Markdown processing failed: {e}")
+
+
+@role_required("Secretariat")
+def submit(request, name):
+    statement = get_object_or_404(Document, type="statement", name=name)
+
+    if request.method == "POST":
+        form = StatementUploadForm(request.POST, request.FILES)
+        if form.is_valid():
+            statement_submission = form.cleaned_data["statement_submission"]
+            writing_pdf = (
+                statement_submission == "upload"
+                and form.cleaned_data["statement_file"].content_type
+                == "application/pdf"
+            )
+
+            statement.rev = "%02d" % (int(statement.rev) + 1)
+            statement.uploaded_filename = (
+                f"{statement.name}-{statement.rev}.{'pdf' if writing_pdf else 'md'}"
+            )
+            e = NewRevisionDocEvent.objects.create(
+                type="new_revision",
+                doc=statement,
+                by=request.user.person,
+                rev=statement.rev,
+                desc="New revision available",
+            )
+            statement.save_with_history([e])
+            markdown_content = ""
+            if statement_submission == "upload":
+                if not writing_pdf:
+                    markdown_content = get_cleaned_text_file_content(
+                        form.cleaned_data["statement_file"]
+                    )
+            else:
+                markdown_content = form.cleaned_data["statement_content"]
+            with Path(statement.get_file_name()).open(
+                mode="wb" if writing_pdf else "w"
+            ) as destination:
+                if writing_pdf:
+                    for chunk in form.cleaned_data["statement_file"].chunks():
+                        destination.write(chunk)
+                else:
+                    destination.write(markdown_content)
+            return redirect("ietf.doc.views_doc.document_main", name=statement.name)
+
+    else:
+        if statement.uploaded_filename.endswith("pdf"):
+            text = CONST_PDF_REV_NOTICE
+        else:
+            text = statement.text_or_error()
+        init = {
+            "statement_content": text,
+            "statement_submission": "enter",
+        }
+        form = StatementUploadForm(initial=init)
+    return render(
+        request, "doc/statement/upload_content.html", {"form": form, "doc": statement}
+    )
+
+
+class NewStatementForm(StatementUploadForm):
+    group = forms.ModelChoiceField(
+        queryset=Group.objects.filter(acronym__in=["iab", "iesg"])
+    )
+    title = forms.CharField(max_length=255)
+    field_order = [
+        "group",
+        "title",
+        "statement_submission",
+        "statement_file",
+        "statement_content",
+    ]
+
+    def name_from_title_and_group(self, title, group):
+        title_slug = xslugify(title)
+        if title_slug.startswith(f"{group.acronym}-"):
+            title_slug = title_slug[len(f"{group.acronym}-") :]
+        name = f"statement-{group.acronym}-{title_slug[:240]}"
+        return name.replace("_", "-")
+
+    def clean(self):
+        if all([field in self.cleaned_data for field in ["title", "group"]]):
+            title = self.cleaned_data["title"]
+            group = self.cleaned_data["group"]
+            name = self.name_from_title_and_group(title, group)
+            if name == self.name_from_title_and_group("", group):
+                self.add_error(
+                    "title",
+                    forms.ValidationError(
+                        "The filename derived from this title is empty. Please include a few descriptive words using ascii or numeric characters"
+                    ),
+                )
+            if Document.objects.filter(name=name).exists():
+                self.add_error(
+                    "title",
+                    forms.ValidationError(
+                        "This title produces a filename already used by an existing statement"
+                    ),
+                )
+        return super().clean()
+
+
+@role_required("Secretariat")
+def new_statement(request):
+    if request.method == "POST":
+        form = NewStatementForm(request.POST, request.FILES)
+        if form.is_valid():
+            statement_submission = form.cleaned_data["statement_submission"]
+            writing_pdf = (
+                statement_submission == "upload"
+                and form.cleaned_data["statement_file"].content_type
+                == "application/pdf"
+            )
+
+            group = form.cleaned_data["group"]
+            title = form.cleaned_data["title"]
+            name = form.name_from_title_and_group(title, group)
+            statement = Document.objects.create(
+                type_id="statement",
+                group=group,
+                name=name,
+                title=title,
+                abstract="",
+                rev="00",
+                uploaded_filename=f"{name}-00.{'pdf' if writing_pdf else 'md'}",
+            )
+            statement.set_state(State.objects.get(type_id="statement", slug="active"))
+            e1 = NewRevisionDocEvent.objects.create(
+                type="new_revision",
+                doc=statement,
+                by=request.user.person,
+                rev=statement.rev,
+                desc="New revision available",
+                time=statement.time,
+            )
+            e2 = DocEvent.objects.create(
+                type="published_statement",
+                doc=statement,
+                rev=statement.rev,
+                by=request.user.person,
+                desc="Statement published",
+            )
+            statement.save_with_history([e1, e2])
+            alias = DocAlias.objects.create(name=name)
+            alias.docs.set([statement])
+            markdown_content = ""
+            if statement_submission == "upload":
+                if not writing_pdf:
+                    markdown_content = get_cleaned_text_file_content(
+                        form.cleaned_data["statement_file"]
+                    )
+            else:
+                markdown_content = form.cleaned_data["statement_content"]
+            with Path(statement.get_file_name()).open(
+                mode="wb" if writing_pdf else "w"
+            ) as destination:
+                if writing_pdf:
+                    for chunk in form.cleaned_data["statement_file"].chunks():
+                        destination.write(chunk)
+                else:
+                    destination.write(markdown_content)
+            return redirect("ietf.doc.views_doc.document_main", name=statement.name)
+
+    else:
+        init = {
+            "statement_content": escape(
+                render_to_string(
+                    "doc/statement/statement_template.md", {"settings": settings}
+                )
+            ),
+            "statement_submission": "enter",
+        }
+        form = NewStatementForm(initial=init)
+    return render(request, "doc/statement/new_statement.html", {"form": form})
diff --git a/ietf/group/urls.py b/ietf/group/urls.py
index 18a2ecd67..814fb9733 100644
--- a/ietf/group/urls.py
+++ b/ietf/group/urls.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2013-2020, All Rights Reserved
+# Copyright The IETF Trust 2013-2023, All Rights Reserved
 
 from django.conf import settings
 from django.urls import include
@@ -47,6 +47,7 @@ info_detail_urls = [
     url(r'^secretarysettings/$', views.change_review_secretary_settings),
     url(r'^reset_next_reviewer/$', views.reset_next_reviewer),
     url(r'^email-aliases/$', RedirectView.as_view(pattern_name=views.email,permanent=False),name='ietf.group.urls_info_details.redirect.email'),
+    url(r'^statements/$', views.statements),
 ]
 
 
diff --git a/ietf/group/utils.py b/ietf/group/utils.py
index b701d6a7c..b82474b49 100644
--- a/ietf/group/utils.py
+++ b/ietf/group/utils.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2012-2021, All Rights Reserved
+# Copyright The IETF Trust 2012-2023, All Rights Reserved
 # -*- coding: utf-8 -*-
 
 
@@ -233,6 +233,8 @@ def construct_group_menu_context(request, group, selected, group_type, others):
 
     if group.features.has_meetings:
         entries.append(("Meetings", urlreverse("ietf.group.views.meetings", kwargs=kwargs)))
+    if group.acronym in ["iab", "iesg"]:
+        entries.append(("Statements", urlreverse("ietf.group.views.statements", kwargs=kwargs)))
     entries.append(("History", urlreverse("ietf.group.views.history", kwargs=kwargs)))
     entries.append(("Photos", urlreverse("ietf.group.views.group_photos", kwargs=kwargs)))
     entries.append(("Email expansions", urlreverse("ietf.group.views.email", kwargs=kwargs)))
diff --git a/ietf/group/views.py b/ietf/group/views.py
index d67a49150..19c277c76 100644
--- a/ietf/group/views.py
+++ b/ietf/group/views.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright The IETF Trust 2009-2022, All Rights Reserved
+# Copyright The IETF Trust 2009-2023, All Rights Reserved
 #
 # Portion Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
 # All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>
@@ -48,7 +48,7 @@ from simple_history.utils import update_change_reason
 from django import forms
 from django.conf import settings
 from django.contrib.auth.decorators import login_required
-from django.db.models import Q, Count
+from django.db.models import Q, Count, OuterRef, Subquery
 from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse
 from django.shortcuts import render, redirect, get_object_or_404
 from django.template.loader import render_to_string
@@ -61,7 +61,7 @@ import debug                            # pyflakes:ignore
 
 from ietf.community.models import CommunityList, EmailSubscription
 from ietf.community.utils import docs_tracked_by_community_list
-from ietf.doc.models import DocTagName, State, DocAlias, RelatedDocument, Document
+from ietf.doc.models import DocTagName, State, DocAlias, RelatedDocument, Document, DocEvent
 from ietf.doc.templatetags.ietf_filters import clean_whitespace
 from ietf.doc.utils import get_chartering_type, get_tags_for_stream_id
 from ietf.doc.utils_charter import charter_name_for_group, replace_charter_of_replaced_group
@@ -2082,7 +2082,32 @@ def reset_next_reviewer(request, acronym, group_type=None):
 
     return render(request, 'group/reset_next_reviewer.html', { 'group':group, 'form': form,})
 
-
+def statements(request, acronym, group_type=None):
+    if not acronym in ["iab", "iesg"]:
+        raise Http404
+    group = get_group_or_404(acronym, group_type)
+    statements = group.document_set.filter(type_id="statement").annotate(
+        published=Subquery(
+            DocEvent.objects.filter(
+                doc=OuterRef("pk"),
+                type="published_statement"
+            ).order_by("-time").values("time")[:1]
+        )
+    ).order_by("-published")
+    return render(
+        request,
+        "group/statements.html",
+        construct_group_menu_context(
+            request,
+            group,
+            "statements",
+            group_type,
+            {
+                "group": group,
+                "statements": statements,
+            },
+        ),
+    )
 
 
 
diff --git a/ietf/mailtrigger/utils.py b/ietf/mailtrigger/utils.py
index 48d91ff6a..496f20dc2 100644
--- a/ietf/mailtrigger/utils.py
+++ b/ietf/mailtrigger/utils.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2015-2019, All Rights Reserved
+# Copyright The IETF Trust 2015-2023, All Rights Reserved
 
 from collections import namedtuple
 
@@ -70,7 +70,7 @@ def gather_relevant_expansions(**kwargs):
 
         relevant.add('doc_state_edited')
         
-        if not doc.type_id in ['bofreq',]:
+        if not doc.type_id in ['bofreq', 'statement']:
             relevant.update(['doc_telechat_details_changed','ballot_deferred','iesg_ballot_saved'])
 
         if doc.type_id in ['draft','statchg']:
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index 58bc3859c..d073991af 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -2539,6 +2539,32 @@
     "model": "doc.state",
     "pk": 174
   },
+  {
+    "fields": {
+      "desc": "The statement is active",
+      "name": "Active",
+      "next_states": [],
+      "order": 0,
+      "slug": "active",
+      "type": "statement",
+      "used": true
+    },
+    "model": "doc.state",
+    "pk": 175
+  },
+  {
+    "fields": {
+      "desc": "The statement has been replaced",
+      "name": "Replaced",
+      "next_states": [],
+      "order": 0,
+      "slug": "replaced",
+      "type": "statement",
+      "used": true
+    },
+    "model": "doc.state",
+    "pk": 176
+  },
   {
     "fields": {
       "label": "State"
@@ -2742,6 +2768,13 @@
     "model": "doc.statetype",
     "pk": "statchg"
   },
+  {
+    "fields": {
+      "label": "Statement State"
+    },
+    "model": "doc.statetype",
+    "pk": "statement"
+  },
   {
     "fields": {
       "about_page": "ietf.group.views.group_about",
@@ -10593,6 +10626,17 @@
     "model": "name.doctypename",
     "pk": "statchg"
   },
+  {
+    "fields": {
+      "desc": "",
+      "name": "Statement",
+      "order": 0,
+      "prefix": "statement",
+      "used": true
+    },
+    "model": "name.doctypename",
+    "pk": "statement"
+  },
   {
     "fields": {
       "desc": "",
@@ -16385,7 +16429,7 @@
     "fields": {
       "command": "xym",
       "switch": "--version",
-      "time": "2023-05-14T07:09:32.713Z",
+      "time": "2023-06-21T07:09:38.578Z",
       "used": true,
       "version": "xym 0.7.0"
     },
@@ -16396,7 +16440,7 @@
     "fields": {
       "command": "pyang",
       "switch": "--version",
-      "time": "2023-05-14T07:09:33.045Z",
+      "time": "2023-06-21T07:09:38.928Z",
       "used": true,
       "version": "pyang 2.5.3"
     },
@@ -16407,7 +16451,7 @@
     "fields": {
       "command": "yanglint",
       "switch": "--version",
-      "time": "2023-05-14T07:09:33.065Z",
+      "time": "2023-06-21T07:09:38.948Z",
       "used": true,
       "version": "yanglint SO 1.9.2"
     },
@@ -16418,9 +16462,9 @@
     "fields": {
       "command": "xml2rfc",
       "switch": "--version",
-      "time": "2023-05-14T07:09:33.970Z",
+      "time": "2023-06-21T07:09:39.903Z",
       "used": true,
-      "version": "xml2rfc 3.17.1"
+      "version": "xml2rfc 3.17.3"
     },
     "model": "utils.versioninfo",
     "pk": 4
diff --git a/ietf/name/migrations/0004_statements.py b/ietf/name/migrations/0004_statements.py
new file mode 100644
index 000000000..ea3779974
--- /dev/null
+++ b/ietf/name/migrations/0004_statements.py
@@ -0,0 +1,21 @@
+# Copyright The IETF Trust 2023, All Rights Reserved
+
+from django.db import migrations
+
+def forward(apps, schema_editor):
+    DocTypeName = apps.get_model("name", "DocTypeName")
+    DocTypeName.objects.create(slug="statement", name="Statement", prefix="statement", desc="", used=True)
+
+
+def reverse(apps, schema_editor):
+    DocTypeName = apps.get_model("name", "DocTypeName")
+    DocTypeName.objects.filter(slug="statement").delete()
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("name", "0003_populate_telechatagendasectionname"),
+    ]
+
+    operations = [
+        migrations.RunPython(forward, reverse),
+    ]
diff --git a/ietf/settings.py b/ietf/settings.py
index abb7cde1d..be2d62c7b 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -657,6 +657,7 @@ URL_REGEXPS = {
     "acronym": r"(?P<acronym>[-a-z0-9]+)",
     "bofreq": r"(?P<name>bofreq-[-a-z0-9]+)",
     "charter": r"(?P<name>charter-[-a-z0-9]+)",
+    "statement": r"(?P<name>statement-[-a-z0-9]+)",
     "date": r"(?P<date>\d{4}-\d{2}-\d{2})",
     "name": r"(?P<name>[A-Za-z0-9._+-]+?)",
     "document": r"(?P<document>[a-z][-a-z0-9]+)", # regular document names
@@ -668,7 +669,6 @@ URL_REGEXPS = {
 # Override this in settings_local.py if needed
 # *_PATH variables ends with a slash/ .
 
-#DOCUMENT_PATH_PATTERN = '/a/www/ietf-ftp/{doc.type_id}/'
 DOCUMENT_PATH_PATTERN = '/a/ietfdata/doc/{doc.type_id}/'
 INTERNET_DRAFT_PATH = '/a/ietfdata/doc/draft/repository'
 INTERNET_DRAFT_PDF_PATH = '/a/www/ietf-datatracker/pdf/'
diff --git a/ietf/static/js/upload_statement.js b/ietf/static/js/upload_statement.js
new file mode 100644
index 000000000..7e8a4e864
--- /dev/null
+++ b/ietf/static/js/upload_statement.js
@@ -0,0 +1,29 @@
+$(document)
+    .ready(function () {
+        var form = $("form.upload-content");
+        // review submission selection
+        form.find("[name=statement_submission]")
+            .on("click change", function () {
+                var val = form.find("[name=statement_submission]:checked")
+                    .val();
+
+                var shouldBeVisible = {
+                    enter: ['[name="statement_content"]'],
+                    upload: ['[name="statement_file"]'],
+                };
+
+                for (var v in shouldBeVisible) {
+                    for (var i in shouldBeVisible[v]) {
+                        var selector = shouldBeVisible[v][i];
+                        var row = form.find(selector);
+                        if (!row.is(".row"))
+                            row = row.closest(".row");
+                        if ($.inArray(selector, shouldBeVisible[val]) != -1)
+                            row.show();
+                        else
+                            row.hide();
+                    }
+                }
+            })
+            .trigger("change");
+    });
diff --git a/ietf/templates/doc/document_statement.html b/ietf/templates/doc/document_statement.html
new file mode 100644
index 000000000..7e6d5b947
--- /dev/null
+++ b/ietf/templates/doc/document_statement.html
@@ -0,0 +1,134 @@
+{% extends "base.html" %}
+{# Copyright The IETF Trust 2023, All Rights Reserved #}
+{% load origin %}
+{% load static %}
+{% load ietf_filters %}
+{% load person_filters textfilters %}
+{% block title %}{{ doc.title }}{% endblock %}
+{% block content %}
+    {% origin %}
+    {{ top|safe }}
+    {% include "doc/revisions_list.html" %}
+    <div id="timeline"></div>
+    {% if doc.rev != latest_rev %}
+        <div class="alert alert-warning my-3">The information below is for an older version of this statement.</div>
+    {% endif %}
+    <table class="table table-sm table-borderless">
+        <tbody class="meta border-top">
+            <tr>
+                <th scope="row">Document</th>
+                <th scope="row">Type</th>
+                <td class="edit"></td>
+                <td>
+                    <span id="statement-type">{% if doc.get_state.slug != "active" %}{{doc.get_state.name}} {% endif %}{% if doc.group %}{{doc.group.acronym|upper}} {%endif%}Statement</span>
+                    {% if snapshot %}<span class="badge rounded-pill bg-warning">Snapshot</span>{% endif %}
+                    {% if replaced_by %}<div>Replaced by {{ replaced_by|urlize_related_source_list:False|join:", " }}</div>{% endif %}
+                    {% if replaces %}<div>Replaces {{ replaces|urlize_related_target_list:False|join:", " }}</div>{% endif %}
+                </td>
+            </tr>
+            <tr>
+                <td></td>
+                <th scope="row">Title</th>
+                <td class="edit"></td>
+                <th scope="row" >{{ doc.title }}</th>
+            </tr>
+            <tr>
+                <td></td>
+                <th scope="row">Published</th>
+                <td class="edit"></td>
+                <td id="published">{{ published|date:"Y-m-d" }}</td>
+            </tr>
+            <tr>
+                <td></td>
+                <th scope="row">Metadata last updated</th>
+                <td class="edit"></td>
+                <td>{{ doc.time|date:"Y-m-d" }}</td>
+            </tr>
+            <tr>
+                <td></td>
+                <th scope="row">
+                    <a href="{% url 'ietf.doc.views_help.state_help' type='statement' %}">State</a>
+                </th>
+                <td class="edit"></td>
+                <td id="statement-state">
+                    {% if doc.get_state %}
+                        <span title="{{ doc.get_state.desc }}" class="{% if doc.get_state.name|slugify == 'active' %}text-success{% else %}text-danger{% endif %}">{{ doc.get_state.name }}</span>
+                    {% else %}
+                        No document state
+                    {% endif %}
+                </td>
+            </tr>
+            {% with doc.docextresource_set.all as resources %}
+                {% if resources or can_manage %}
+                    <tr>
+                        <td></td>
+                        <th scope="row">Additional resources</th>
+                        <td class="edit">
+                            {% if can_manage %}
+                                <a class="btn btn-primary btn-sm"
+                                   href="{% url 'ietf.doc.views_draft.edit_doc_extresources' name=doc.name %}">Edit</a>
+                            {% endif %}
+                        </td>
+                        <td>
+                            {% if resources %}
+                                {% for resource in resources|dictsort:"display_name" %}
+                                    {% if resource.name.type.slug == 'url' or resource.name.type.slug == 'email' %}
+                                        <a href="{{ resource.value }}" title="{{ resource.name.name }}">
+                                            {% firstof resource.display_name resource.name.name %}
+                                        </a>
+                                        <br>
+                                        {# Maybe make how a resource displays itself a method on the class so templates aren't doing this switching #}
+                                    {% else %}
+                                        <span title="{{ resource.name.name }}">{% firstof resource.display_name resource.name.name %}: {{ resource.value|escape }}</span>
+                                        <br>
+                                    {% endif %}
+                                {% endfor %}
+                            {% endif %}
+                        </td>
+                    </tr>
+                {% endif %}
+            {% endwith %}
+            <tr>
+                <td></td>
+                <th scope="row">
+                    Send notices to
+                </th>
+                <td class="edit">
+                    {% if not snapshot %}
+                        {% if can_manage %}
+                            {% doc_edit_button 'ietf.doc.views_doc.edit_notify' name=doc.name %}
+                        {% endif %}
+                    {% endif %}
+                </td>
+                <td>
+                    {{ doc.notify|default:'<span class="text-muted">(None)</span>' }}
+                </td>
+            </tr>
+        </tbody>
+    </table>
+    {% if not snapshot %}
+    {% if request.user|has_role:"Secretariat" %}
+        <p id="change-statement">
+            <a class="btn btn-primary"
+               href="{% url 'ietf.doc.views_statement.submit' name=doc.name %}">
+                Edit or upload revised statement text
+            </a>
+        </p>
+    {% endif %}
+{% endif %}
+
+    <div class="card mt-5">
+        <div class="card-header">
+            {{ doc.name }}-{{ doc.rev }}
+        </div>
+        <div class="card-body text-break">
+            {{ content }}
+        </div>
+    </div>
+{% endblock %}
+{% block js %}
+    <script src="{% static 'ietf/js/d3.js' %}">
+    </script>
+    <script src="{% static 'ietf/js/document_timeline.js' %}">
+    </script>
+{% endblock %}
\ No newline at end of file
diff --git a/ietf/templates/doc/statement/new_statement.html b/ietf/templates/doc/statement/new_statement.html
new file mode 100644
index 000000000..0742314ed
--- /dev/null
+++ b/ietf/templates/doc/statement/new_statement.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{# Copyright The IETF Trust 2023, All Rights Reserved #}
+{% load origin django_bootstrap5 static textfilters %}
+{% block title %}Start a new Statement{% endblock %}
+{% block content %}
+    {% origin %}
+    <h1>Start a new Statement</h1>
+    <form class="upload-content form-horizontal"
+          method="post"
+          enctype="multipart/form-data">
+        {% csrf_token %}
+        {% bootstrap_form form layout="horizontal" %}
+        <button type="submit" class="btn btn-primary">Submit</button>
+    </form>
+{% endblock %}
+{% block js %}
+    <script src="{% static 'ietf/js/upload_statement.js' %}"></script>
+{% endblock %}
\ No newline at end of file
diff --git a/ietf/templates/doc/statement/statement_template.md b/ietf/templates/doc/statement/statement_template.md
new file mode 100644
index 000000000..cc311530e
--- /dev/null
+++ b/ietf/templates/doc/statement/statement_template.md
@@ -0,0 +1 @@
+Replace this with the content of the statement in markdown source
diff --git a/ietf/templates/doc/statement/upload_content.html b/ietf/templates/doc/statement/upload_content.html
new file mode 100644
index 000000000..712c44043
--- /dev/null
+++ b/ietf/templates/doc/statement/upload_content.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+{# Copyright The IETF Trust 2023, All Rights Reserved #}
+{% load origin django_bootstrap5 static %}
+{% block title %}Upload new revision: {{ doc.name }}{% endblock %}
+{% block content %}
+    {% origin %}
+    <h1>
+        Upload New Revision
+        <br>
+        <small class="text-muted">{{ doc.name }}</small>
+    </h1>
+    <form class="upload-content form-horizontal mt-3"
+          method="post"
+          enctype="multipart/form-data">
+        {% csrf_token %}
+        {% bootstrap_form form layout="horizontal" %}
+        <button type="submit" class="btn btn-primary">Submit</button>
+        <a class="btn btn-secondary float-end" href="{{ doc.get_absolute_url }}">Back</a>
+    </form>
+{% endblock %}
+{% block js %}
+    <script src="{% static 'ietf/js/upload_statement.js' %}"></script>
+{% endblock %}
\ No newline at end of file
diff --git a/ietf/templates/group/statements.html b/ietf/templates/group/statements.html
new file mode 100644
index 000000000..4e0fc6153
--- /dev/null
+++ b/ietf/templates/group/statements.html
@@ -0,0 +1,40 @@
+{% extends "group/group_base.html" %}
+{# Copyright The IETF Trust 2023, All Rights Reserved #}
+{% load origin %}
+{% load ietf_filters person_filters textfilters %}
+{% load static %}
+{% block pagehead %}
+    <link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
+{% endblock %}
+{% block group_content %}
+    {% origin %}
+    <h2 class="my-3">{{group.acronym|upper}} Statements</h2>
+    {% if request.user|has_role:"Secretariat" %}
+    <div class="buttonlist">
+        <a id="start_button"
+           class="btn btn-primary"
+           href="{% url 'ietf.doc.views_statement.new_statement' %}">
+            Start New Statement
+        </a>
+    </div>
+    {% endif %}
+    <table class="my-3 table table-sm table-striped tablesorter">
+        <thead>
+            <tr>
+                <th class="col-1" scope="col" data-sort="date">Date</th>
+                <th scope="col" data-sort="statement">Statement</th>
+            </tr>
+        </thead>
+        <tbody>
+            {% for statement in statements %}
+            <tr>
+                <td title="{{ statement.published|date:'Y-m-d H:i:s O' }}">{{ statement.published|date:"Y-m-d" }}</td>
+                <td><a href="{% url 'ietf.doc.views_doc.document_main' name=statement.name %}">{{statement.title}}</a></td>
+            </tr>
+            {% endfor %}
+        </tbody>
+    </table>
+{% endblock %}
+{% block js %}
+    <script src="{% static "ietf/js/list.js" %}"></script>
+{% endblock %}
\ No newline at end of file
diff --git a/package.json b/package.json
index 30323c6be..1d8365ce0 100644
--- a/package.json
+++ b/package.json
@@ -148,6 +148,7 @@
         "ietf/static/js/upcoming.js",
         "ietf/static/js/upload-material.js",
         "ietf/static/js/upload_bofreq.js",
+        "ietf/static/js/upload_statement.js",
         "ietf/static/js/zxcvbn.js"
       ]
     },