From 5d4ce64c9ec4526c43f9755eea32acf7423b6231 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 26 Aug 2019 22:37:05 -0500 Subject: [PATCH] Move IESG Statements under /about/group/iesg and out of /blog. Temporarily disable the recursive chown on container entry. --- docker-entry.sh | 6 +- ietf/blog/models.py | 53 ++-- ietf/iesg_statement/__init__.py | 0 .../commands/move_iesg_statements.py | 125 +++++++++ .../iesg_statement/migrations/0001_initial.py | 61 ++++ ietf/iesg_statement/migrations/__init__.py | 0 ietf/iesg_statement/models.py | 262 ++++++++++++++++++ .../iesg_statement_index_page.html | 33 +++ .../iesg_statement/iesg_statement_page.html | 215 ++++++++++++++ ietf/settings/base.py | 1 + 10 files changed, 737 insertions(+), 19 deletions(-) create mode 100644 ietf/iesg_statement/__init__.py create mode 100644 ietf/iesg_statement/management/commands/move_iesg_statements.py create mode 100644 ietf/iesg_statement/migrations/0001_initial.py create mode 100644 ietf/iesg_statement/migrations/__init__.py create mode 100644 ietf/iesg_statement/models.py create mode 100644 ietf/iesg_statement/templates/iesg_statement/iesg_statement_index_page.html create mode 100644 ietf/iesg_statement/templates/iesg_statement/iesg_statement_page.html diff --git a/docker-entry.sh b/docker-entry.sh index f1b0278b..c2cf11f5 100755 --- a/docker-entry.sh +++ b/docker-entry.sh @@ -10,9 +10,9 @@ if [ -n "${WWW_GID}" ]; then groupmod -g "${WWW_GID}" www fi -if [ -n "${WWWRUN_UID}" -o -n "${WWW_GID}" ]; then - chown -R wwwrun:www /code -fi +#if [ -n "${WWWRUN_UID}" -o -n "${WWW_GID}" ]; then +# chown -R wwwrun:www /code +#fi if [ ! -f "ietf/settings/local.py" ]; then echo "local.py not found. Exiting." diff --git a/ietf/blog/models.py b/ietf/blog/models.py index 7ea86712..c9f565b3 100644 --- a/ietf/blog/models.py +++ b/ietf/blog/models.py @@ -324,23 +324,44 @@ class BlogIndexPage(Page): Use it to organise :model:`blog.BlogPage` models. """ def serve(self, request, *args, **kwargs): - blogs = ordered_live_annotated_blogs() - first_blog_url = blogs.first().url - query_string = "?" - - # This is duplicated in BlogPage - for parameter, functions in parameter_functions_map.items(): - search_query = request.GET.get(parameter) - if search_query: - try: - related_object = functions[0](search_query) - blogs = functions[1](blogs, related_object) - query_string += "%s=%s&" % (parameter, search_query) - except (ValueError, ObjectDoesNotExist): - pass - if blogs: + # IESG statements were moved under the IESG about/groups page. Queries to the + # base /blog/ page that used a query string to filter for IESG statements can't + # be redirected through ordinary redirection, so we're doing it here. + if request.GET.get('primary_topic')=='7': + query_string = '' + topic = request.GET.get('secondary_topic') + if topic: + query_string = query_string + 'topic=' + topic + date_from = request.GET.get('date_from') + if date_from: + separator = '&' if query_string else '' + query_string = query_string + separator + 'date_from=' + date_from + date_to = request.GET.get('date_to') + if date_to: + separator = '&' if query_string else '' + query_string = query_string + separator + 'date_to' + date_to + target_url = '/about/groups/iesg/statements' + if query_string: + target_url = target_url + '?' + query_string + return redirect(target_url) + else: + blogs = ordered_live_annotated_blogs() first_blog_url = blogs.first().url - return redirect(first_blog_url + query_string) + query_string = "?" + + # This is duplicated in BlogPage + for parameter, functions in parameter_functions_map.items(): + search_query = request.GET.get(parameter) + if search_query: + try: + related_object = functions[0](search_query) + blogs = functions[1](blogs, related_object) + query_string += "%s=%s&" % (parameter, search_query) + except (ValueError, ObjectDoesNotExist): + pass + if blogs: + first_blog_url = blogs.first().url + return redirect(first_blog_url + query_string) search_fields = [] diff --git a/ietf/iesg_statement/__init__.py b/ietf/iesg_statement/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ietf/iesg_statement/management/commands/move_iesg_statements.py b/ietf/iesg_statement/management/commands/move_iesg_statements.py new file mode 100644 index 00000000..07f89f2f --- /dev/null +++ b/ietf/iesg_statement/management/commands/move_iesg_statements.py @@ -0,0 +1,125 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth.models import User + +from wagtail.contrib.redirects.models import Redirect + +from ietf.blog.models import BlogPage +from ietf.iesg_statement.models import IESGStatementPage, IESGStatementIndexPage, IESGStatementTopic +from ietf.standard.models import StandardPage +from ietf.snippets.models import PrimaryTopic + +slug_map = { + 'iesg-statement-maximizing-encrypted-access-ietf-information' : 'maximizing-encrypted-access', + 'iesg-statement-internet-draft-authorship' : 'internet-draft-authorship', + 'iesg-statement-designating-rfcs-historic' : 'designating-rfcs-historic-2014-07-20', + 'discuss-criteria-iesg-review' : 'iesg-discuss-criteria', + 'guidance-face-face-and-virtual-interim-meetings' : 'interim-meetings-guidance-2016-01-16', + 'writable-mib-module-iesg-statement' : 'writable-mib-module', + 'ietf-anti-harassment-policy' : 'anti-harassment-policy', + 'iesg-statement-removal-internet-draft-ietf-web-site' : 'internet-draft-removal', + 'iesg-statement-ethertypes' : 'ethertypes', + 'iesg-statement-designating-rfcs-historic-2011-10-20' : 'designating-rfcs-historic-2011-10-20', + 'iesg-statement-designating-rfcs-historic-2011-06-27' : 'designating-rfcs-historic-2011-06-27', + 'iesg-statement-iesg-processing-rfc-errata-concerning-rfc-metadata' : 'rfc-metadata-errata', + 'iesg-statement-document-shepherds' : 'document-shepherds', + 'iesg-statement-nomcom-eligibility-and-day-passes' : 'nomcom-eligibility-day-passes', + 'iesg-statement-usage-assignable-codepoints-addresses-and-names-specification-examples' : 'assignable-codepoints-addresses-names', + 'iesg-statement-copyright' : 'copyright-2009-09-08', + 'proposed-status-ietf-documents-reserving-resources-example-purposes' : 'reserving-resources-examples', + 'guidance-interim-meetings-conference-calls-and-jabber-sessions' : 'interim-meetings-guidance-2008-09-02', + 'iesg-processing-rfc-errata-ietf-stream' : 'processing-rfc-errata', + 'iesg-statement-spam-control-ietf-mailing-lists' : 'spam-control-2008-04-14', + 'guidance-spam-control-ietf-mailing-lists' : 'spam-control-2006-01-09', + 'iesg-guidance-moderation-ietf-working-group-mailing-lists' : 'mailing-lists-moderation', + 'iesg-statement-registration-requests-uris-containing-telephone-numbers' : 'registration-requests-uris', + 'iesg-statement-rfc3406-and-urn-namespaces-registry-review' : 'urn-namespaces-registry', + 'advice-wg-chairs-dealing-topic-postings' : 'off-topic-postings', + 'appeals-iesg-and-area-director-actions-and-decisions' : 'appeals-actions-decisions', + 'experimental-specification-new-congestion-control-algorithms' : 'experimental-congestion-control', + 'guidance-area-director-sponsoring-documents' : 'area-director-sponsoring-documents', + 'last-call-guidance-community' : 'last-call-guidance', + 'iesg-statement-normative-and-informative-references' : 'normative-informative-references', + 'iesg-statement-disruptive-posting' : 'disruptive-posting', + 'iesg-statement-auth48-state' : 'auth48', + 'syntax-format-definitions' : 'syntax-format-definitions', + 'iesg-statement-idn' : 'idn', + 'copyright-statement-mib-and-pib-modules' : 'copyright-2002-11-27', + 'guidance-spam-control-ietf-mailing-lists-2002-03-13' : 'spam-control-2002-03-13', + 'design-teams' : 'design-teams', + 'guidelines-use-formal-languages-ietf-specifications' : 'formal-languages-use', + 'establishment-temporary-sub-ip-area' : 'sub-ip-area-2001-03-21', + 'plans-organize-sub-ip-technologies-ietf' : 'sub-ip-area-2000-11-20', + 'new-ietf-work-area' : 'sub-ip-area-2000-12-06', + 'guidance-interim-ietf-working-group-meetings-and-conference-calls' : 'interim-meetings-guidance-2000-08-29', + 'ietf-meeting-photography-policy' : 'meeting-photography-policy', + 'support-documents-ietf-working-groups' : 'support-documents', + 'license-file-open-source-repositories' : 'open-source-repositories-license', +} + + +class Command(BaseCommand): + help = 'Moves the iesg statements from the blog app to the iesg_statements app' + + def handle(self, *args, **options): + if IESGStatementPage.objects.exists(): + print("IESGStatementPages exist. This command has probably already been run.") + print("Exiting without making any changes.") + return + + iesg_page = StandardPage.objects.get(pk=1210) + index_page = IESGStatementIndexPage( + title='IESG Statements', + slug='statements', + url_path=iesg_page.url_path+'/statements/' + ) + iesg_page.add_child(instance=index_page) + iesg_page.save() + for stmt in BlogPage.objects.filter(primary_topics__topic__title="IESG Statements"): + + if stmt.slug in slug_map: + new_slug = slug_map[stmt.slug] + else: + new_slug = stmt.slug[15:] if stmt.slug.startswith('iesg-statement-') else stmt.slug + + new_page = IESGStatementPage( + title = stmt.title, + slug = new_slug, + date_published = stmt.date_published, + introduction = stmt.introduction, + url_path = index_page.url_path+'/'+new_slug, + draft_title = stmt.draft_title, + owner = stmt.owner, + seo_title= stmt.seo_title, + live = stmt.live, + has_unpublished_changes = stmt.has_unpublished_changes, + show_in_menus = stmt.show_in_menus, + search_description = stmt.search_description, + go_live_at = stmt.go_live_at, + expire_at = stmt.expire_at, + expired = stmt.expired, + locked = stmt.locked, + first_published_at = stmt.first_published_at, + last_published_at = stmt.last_published_at, + latest_revision_created_at = stmt.latest_revision_created_at, + live_revision = stmt.live_revision, + ) + index_page.add_child(instance=new_page) + # Intentionally not creating/publishing a new revision + new_page.body = stmt.body + new_page.save() + for st in stmt.secondary_topics.all(): + IESGStatementTopic.objects.create(page=new_page,topic=st.topic) + + Redirect.objects.create( + old_path=stmt.url[:-1] if stmt.url.endswith('/') else stmt.url, + is_permanent=True, + redirect_page=new_page + ) + + + BlogPage.objects.filter(primary_topics__topic__title="IESG Statements").delete() + PrimaryTopic.objects.filter(title="IESG Statements").delete() + + # todo - make this robust in case the iesg page gets edited before this is run on production + iesg_page.key_info[6].value.source = '

List\xa0All\xa0\xa0| \xa0On\xa0Mailing\xa0Lists\xa0|\xa0On\xa0Meetings\xa0|\xa0On\xa0Procedures\xa0|\xa0On\xa0Technical\xa0Issues

' + iesg_page.save_revision(user=User.objects.get(username='robert.sparks')).publish() diff --git a/ietf/iesg_statement/migrations/0001_initial.py b/ietf/iesg_statement/migrations/0001_initial.py new file mode 100644 index 00000000..35122be2 --- /dev/null +++ b/ietf/iesg_statement/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.22 on 2019-08-21 19:22 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields +import wagtail.contrib.table_block.blocks +import wagtail.core.blocks +import wagtail.core.fields +import wagtail.embeds.blocks +import wagtail.images.blocks + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('snippets', '0026_auto_20170227_1401'), + ('wagtailcore', '0040_page_draft_title'), + ('images', '0009_ietfimage_file_hash'), + ] + + operations = [ + migrations.CreateModel( + name='IESGStatementIndexPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ], + options={ + 'verbose_name': 'IESG Statements Index Page', + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='IESGStatementPage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('social_text', models.CharField(blank=True, help_text='Description of this page as it should appear when shared on social networks, or in Google results', max_length=255)), + ('date_published', models.DateTimeField(blank=True, help_text='Use this field to override the date that the blog post appears to have been published.', null=True)), + ('introduction', models.CharField(help_text='The page introduction text.', max_length=511)), + ('body', wagtail.core.fields.StreamField([('heading', wagtail.core.blocks.CharBlock(icon='title')), ('paragraph', wagtail.core.blocks.RichTextBlock(icon='pilcrow')), ('image', wagtail.images.blocks.ImageChooserBlock(icon='image', template='includes/imageblock.html')), ('embed', wagtail.embeds.blocks.EmbedBlock(icon='code')), ('raw_html', wagtail.core.blocks.RawHTMLBlock(icon='placeholder')), ('table', wagtail.contrib.table_block.blocks.TableBlock(table_options={'renderer': 'html'}))])), + ('prepared_body', models.TextField(blank=True, help_text='The prepared body content after bibliography styling has been applied. Auto-generated on each save.', null=True)), + ('feed_image', models.ForeignKey(blank=True, help_text='This image will be used in listings and indexes across the site, if no feed image is added, the social image will be used.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.IETFImage')), + ('social_image', models.ForeignKey(blank=True, help_text="Image to appear alongside 'social text', particularly for sharing on social networks", null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='images.IETFImage')), + ], + options={ + 'verbose_name': 'IESG Statement Page', + }, + bases=('wagtailcore.page', models.Model), + ), + migrations.CreateModel( + name='IESGStatementTopic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('page', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='topics', to='iesg_statement.IESGStatementPage')), + ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='snippets.SecondaryTopic')), + ], + ), + ] diff --git a/ietf/iesg_statement/migrations/__init__.py b/ietf/iesg_statement/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ietf/iesg_statement/models.py b/ietf/iesg_statement/models.py new file mode 100644 index 00000000..16ff9881 --- /dev/null +++ b/ietf/iesg_statement/models.py @@ -0,0 +1,262 @@ +from datetime import datetime +from functools import partial + +from django.db import models +from django.db.models.functions import Coalesce +from django.shortcuts import redirect + +from django.core.exceptions import ObjectDoesNotExist +from django.utils import functional +from django.utils.safestring import mark_safe + +from modelcluster.fields import ParentalKey + +from wagtail.core.models import Page +from wagtail.core.fields import StreamField +from wagtail.snippets.edit_handlers import ( + SnippetChooserPanel +) +from wagtail.search import index +from wagtail.admin.edit_handlers import ( + StreamFieldPanel, FieldPanel, InlinePanel +) + +from ..bibliography.models import BibliographyMixin +#from ..utils.models import FeedSettings, PromoteMixin +from ..utils.models import PromoteMixin +from ..utils.blocks import StandardBlock +from ..snippets.models import SecondaryTopic + +def filter_pages_by_topic(pages, topic): + return pages.filter(topics__topic=topic) + + +def get_topic_by_id(id): + return SecondaryTopic.objects.get(id=id) + + +def filter_pages_by_date_from(pages, date_from): + return pages.filter(d__gte=date_from) + + +def filter_pages_by_date_to(pages, date_to): + return pages.filter(d__lte=date_to) + + +def parse_date_search_input(date): + return datetime.date(datetime.strptime(date, "%d/%m/%Y")) + + +def build_filter_text(**kwargs): + if any(kwargs): + text_fragments = [] + if kwargs.get('topic'): + text_fragments.append( + '{}'.format(kwargs.get('topic')) + ) + if kwargs.get('date_from') and kwargs.get('date_to'): + text_fragments.append( + 'dates between {} & {}'.format( + kwargs['date_from'], kwargs['date_to'] + ) + ) + elif kwargs.get('date_from'): + text_fragments.append('dates after {}'.format( + kwargs['date_from'] + )) + elif kwargs.get('date_to'): + text_fragments.append('dates before {}'.format( + kwargs['date_to'] + )) + return ', '.join(text_fragments) + else: + return "" + + +parameter_functions_map = { + 'topic': [get_topic_by_id, filter_pages_by_topic], + 'date_from': [parse_date_search_input, filter_pages_by_date_from], + 'date_to': [parse_date_search_input, filter_pages_by_date_to] +} + + +class IESGStatementTopic(models.Model): + + page = ParentalKey( + 'iesg_statement.IESGStatementPage', + related_name='topics' + ) + topic = models.ForeignKey( + 'snippets.SecondaryTopic', + related_name='+', + ) + + panels = [ + SnippetChooserPanel('topic') + ] + +class IESGStatementPage(Page, BibliographyMixin, PromoteMixin): + + date_published = models.DateTimeField( + null=True, blank=True, + help_text="Use this field to override the date that the " + "blog post appears to have been published." + ) + introduction = models.CharField( + max_length=511, + help_text="The page introduction text." + ) + body = StreamField(StandardBlock()) + + search_fields = Page.search_fields + [ + index.SearchField('introduction'), + index.SearchField('body'), + ] + + # for bibliography + prepared_body = models.TextField( + blank=True, null=True, + help_text="The prepared body content after bibliography styling has been applied. Auto-generated on each save.", + ) + CONTENT_FIELD_MAP = {'body': 'prepared_body'} + + parent_page_types = ['iesg_statement.IESGStatementIndexPage'] + subpage_types = [] + + + @property + def date(self): + return self.date_published or self.first_published_at + + @property + def next(self): + siblings = self.siblings.exclude(pk=self.pk) + if not siblings: + return [] + try: + return [ + sibling for sibling in self.siblings + if sibling.date < self.date + ][0] + except IndexError: + return siblings[0] + + @property + def previous(self): + siblings = list(self.siblings.exclude(pk=self.pk)) + if not siblings: + return [] + try: + return [ + sibling for sibling in self.siblings + if sibling.date > self.date + ][-1] + except IndexError: + return siblings[-1] + + @property + def feed_text(self): + return self.search_description or self.introduction + + @functional.cached_property + def siblings(self): + return self.__class__.objects.live().sibling_of(self).annotate( + d=Coalesce('date_published', 'first_published_at') + ).order_by('-d') + + def get_context(self, request, *args, **kwargs): + context = super(IESGStatementPage, self).get_context(request, *args, **kwargs) + siblings = self.siblings + query_string = "?" + filter_text_builder = build_filter_text + # TODO feed_settings = FeedSettings.for_site(request.site) + + for parameter, functions in parameter_functions_map.items(): + search_query = request.GET.get(parameter) + if search_query: + try: + related_object = functions[0](search_query) + siblings = functions[1](siblings, related_object) + query_string += "%s=%s&" % (parameter, search_query) + filter_text_builder = partial(filter_text_builder, + **{parameter: related_object.__str__()}) + except (ValueError, ObjectDoesNotExist): + pass + + filter_text = filter_text_builder() + + if filter_text: + if siblings: + filter_text = mark_safe("You have filtered by " + filter_text) + else: + filter_text = mark_safe("No results for " + filter_text + ", showing latest") + + context.update( + parent_url=self.get_parent().url, + filter_text = filter_text, + siblings=siblings, + topics=IESGStatementTopic.objects.all().values_list( + 'topic__pk', 'topic__title' + ).distinct(), + query_string=query_string, + # TODO blog_feed_title=feed_settings.blog_feed_title + ) + return context + + def serve_preview(self, request, mode_name): + """ This is another hack to overcome the MRO issue we were seeing """ + return BibliographyMixin.serve_preview(self, request, mode_name) + + class Meta: + verbose_name = "IESG Statement Page" + +IESGStatementPage.content_panels = Page.content_panels + [ + FieldPanel('date_published'), + FieldPanel('introduction'), + StreamFieldPanel('body'), + InlinePanel('topics', label="Topics"), +] + +IESGStatementPage.promote_panels = Page.promote_panels + PromoteMixin.panels + + +class IESGStatementIndexPage(Page): + + def get_context(self, request): + context = super().get_context(request) + context['statements'] = IESGStatementPage.objects.child_of(self).live().order_by('-date_published') + return context + + def serve(self, request, *args, **kwargs): + has_filter = False + for parameter, _ in parameter_functions_map.items(): + if request.GET.get(parameter): + has_filter = True + break + + if (has_filter): + statements = IESGStatementPage.objects.child_of(self).live().order_by('-date_published') + first_statement_url = statements.first().url + query_string = "?" + + for parameter, functions in parameter_functions_map.items(): + search_query = request.GET.get(parameter) + if search_query: + try: + related_object = functions[0](search_query) + statements = functions[1](statements, related_object) + query_string += "%s=%s&" % (parameter, search_query) + except (ValueError, ObjectDoesNotExist): + pass + if statements: + first_statement_url = statements.first().url + return redirect(first_statement_url + query_string) + else: + return super().serve(request,*args,**kwargs) + + + subpage_types = ['iesg_statement.IESGStatementPage'] + + class Meta: + verbose_name = "IESG Statements Index Page" + diff --git a/ietf/iesg_statement/templates/iesg_statement/iesg_statement_index_page.html b/ietf/iesg_statement/templates/iesg_statement/iesg_statement_index_page.html new file mode 100644 index 00000000..e682106a --- /dev/null +++ b/ietf/iesg_statement/templates/iesg_statement/iesg_statement_index_page.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + + +{% block main_content %} +{# sidebar #} +{% include "includes/social-sharebar.html" %} + +
+
+ {% include 'includes/breadcrumbs.html' %} +

{{self.title}}

+ + {% for statement in statements %} + + + + + {% endfor %} +
{{statement.date_published|date:"Y-m-d"}}{{statement.title}}
+ +
+
+ +{% endblock %} + +{% block extra_css %} + +{% endblock %} + diff --git a/ietf/iesg_statement/templates/iesg_statement/iesg_statement_page.html b/ietf/iesg_statement/templates/iesg_statement/iesg_statement_page.html new file mode 100644 index 00000000..843269f4 --- /dev/null +++ b/ietf/iesg_statement/templates/iesg_statement/iesg_statement_page.html @@ -0,0 +1,215 @@ +{% extends "base.html" %} +{% load bibliography %} + +{% comment %} +{% block extra_meta %} + +{% endblock extra_meta %} +{% endcomment %} + +{% block main_content %} +{# sidebar #} +{% include "includes/social-sharebar.html" %} + +
+
+ + {# search #} +
+
+
+
+
+
    +
  • + TOPICS + +
  • +
  • + + +
  • +
+
+ +
+
+
+
+ {% if filter_text %} +

{{ filter_text }}

+ {% endif %} + {% include 'includes/breadcrumbs.html' %} +

{{ self.title }}

+
    +
  • {{ self.date|date:"DATE_FORMAT" }}
  • +
+

{{ self.introduction }}

+ + {{ self.prepared_body|safe }} + + {% bibliography self %} + +
+
+ +
+
+ + +{# highlights mobile #} +
+ +
+{# highlights desktop #} +
+ +
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/ietf/settings/base.py b/ietf/settings/base.py index 6980e59e..9da53f54 100644 --- a/ietf/settings/base.py +++ b/ietf/settings/base.py @@ -40,6 +40,7 @@ 'ietf.bibliography', 'ietf.images', 'ietf.documents', + 'ietf.iesg_statement', 'wagtail.contrib.forms', 'wagtail.contrib.redirects',