From 9c83f354bbf3a12d8fbc02acbae6df0b7d44b43d Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Tue, 7 Jan 2020 11:19:52 -0500 Subject: [PATCH 01/50] begin adding page models --- cdhweb/pages/__init__.py | 6 ++ cdhweb/pages/admin.py | 3 + cdhweb/pages/apps.py | 5 + cdhweb/pages/migrations/__init__.py | 0 cdhweb/pages/models.py | 144 ++++++++++++++++++++++++++++ cdhweb/pages/tests.py | 3 + cdhweb/pages/views.py | 3 + 7 files changed, 164 insertions(+) create mode 100644 cdhweb/pages/__init__.py create mode 100644 cdhweb/pages/admin.py create mode 100644 cdhweb/pages/apps.py create mode 100644 cdhweb/pages/migrations/__init__.py create mode 100644 cdhweb/pages/models.py create mode 100644 cdhweb/pages/tests.py create mode 100644 cdhweb/pages/views.py diff --git a/cdhweb/pages/__init__.py b/cdhweb/pages/__init__.py new file mode 100644 index 000000000..651de021a --- /dev/null +++ b/cdhweb/pages/__init__.py @@ -0,0 +1,6 @@ +''' +Pages app for the CDH website. +Includes logic related to generic content pages & landing pages. +''' + +default_app_config = 'cdhweb.pages.apps.PagesConfig' diff --git a/cdhweb/pages/admin.py b/cdhweb/pages/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/cdhweb/pages/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/cdhweb/pages/apps.py b/cdhweb/pages/apps.py new file mode 100644 index 000000000..acdb96073 --- /dev/null +++ b/cdhweb/pages/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PagesConfig(AppConfig): + name = 'pages' diff --git a/cdhweb/pages/migrations/__init__.py b/cdhweb/pages/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cdhweb/pages/models.py b/cdhweb/pages/models.py new file mode 100644 index 000000000..a716580ab --- /dev/null +++ b/cdhweb/pages/models.py @@ -0,0 +1,144 @@ +from random import shuffle + +from django.db import models +from django.utils.text import slugify +from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel +from wagtail.core.blocks import (CharBlock, RichTextBlock, StreamBlock, + StructBlock, TextBlock) +from wagtail.core.fields import StreamField +from wagtail.core.models import Page +from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.images.blocks import ImageChooserBlock +from wagtail.images.edit_handlers import ImageChooserPanel +from wagtail.search import index + +from cdhweb.blog.models import BlogPost +from cdhweb.events.models import Event +from cdhweb.projects.models import Project + + +#: commonly allowed tags for RichTextBlocks +RICH_TEXT_TAGS = ['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'] + +#: help text for image alternative text +ALT_TEXT_HELP = """Alternative text for visually impaired users to +briefly communicate the intended message of the image in this context.""" + + +class LinkableSectionBlock(StructBlock): + ''':class:`~wagtail.core.blocks.StructBlock` for a rich text block and an + associated `title` that will render as an

. Creates an anchor () + so that the section can be directly linked to using a url fragment.''' + title = CharBlock() + anchor_text = CharBlock(help_text='Short label for anchor link') + body = RichTextBlock(features=RICH_TEXT_TAGS) + panels = [ + FieldPanel('title'), + FieldPanel('slug'), + FieldPanel('body'), + ] + + class Meta: + icon = 'form' + label = 'Linkable Section' + template = 'pages/snippets/linkable_section.html' + + def clean(self, value): + cleaned_values = super().clean(value) + # run slugify to ensure anchor text is a slug + cleaned_values['anchor_text'] = slugify(cleaned_values['anchor_text']) + return cleaned_values + + +class CaptionedImageBlock(StructBlock): + ''':class:`~wagtail.core.blocks.StructBlock` for an image with + alternative text and optional formatted caption, so + that both caption and alternative text can be context-specific.''' + image = ImageChooserBlock() + alternative_text = TextBlock(required=True, help_text=ALT_TEXT_HELP) + caption = RichTextBlock(features=['bold', 'italic', 'link'], required=False) + + class Meta: + icon = 'image' + + +class BodyContentBlock(StreamBlock): + '''Common set of blocks available in StreamFields for body text.''' + paragraph = RichTextBlock(features=RICH_TEXT_TAGS) + image = CaptionedImageBlock() + document = DocumentChooserBlock() + linkable_section = LinkableSectionBlock() + + +class ContentPage(Page): + '''Basic content page model.''' + + parent_page_types = ['LandingPage', 'ContentPage'] + subpage_types = ['ContentPage'] + + +class LandingPage(Page): + '''Page type that aggregates and displays multiple :class:`ContentPage`s.''' + + tagline = models.CharField(max_length=255) + body = StreamField(BodyContentBlock, blank=True) + image = models.ForeignKey('wagtailimages.image', null=True, blank=True, + on_delete=models.SET_NULL, related_name='+') # no reverse relationship + + search_fields = Page.search_fields + [ + index.SearchField('body'), + ] + + content_panels = Page.content_panels + [ + ImageChooserPanel('header_image'), + FieldPanel('tagline'), + StreamFieldPanel('body') + ] + + parent_page_types = ['HomePage'] + subpage_types = ['ContentPage'] + + +class HomePage(Page): + '''A home page that aggregates and displays featured content.''' + + tagline = models.CharField(max_length=255) + body = StreamField(BodyContentBlock, blank=True) + image = models.ForeignKey('wagtailimages.image', null=True, blank=True, + on_delete=models.SET_NULL, related_name='+') # no reverse relationship + + class Meta: + verbose_name = 'Homepage' + + def get_context(self, request): + '''Add featured content to the page context.''' + context = super().get_context(request) + + # add up to 6 featured updates, otherwise use 3 most recent updates + updates = BlogPost.objects.featured().published().recent()[:6] + if not updates.exists(): + updates = BlogPost.objects.published().recent()[:3] + context['updates'] = updates + + # add up to 4 highlighted, published projects + projects = list(Project.objects.published().highlighted()) + shuffle(projects) + context['projects'] = projects[:4] + + # add up to 3 upcoming, published events + context['events'] = Event.objects.published().upcoming()[:3] + + return context + + search_fields = Page.search_fields + [ + index.SearchField('body'), + ] + + content_panels = Page.content_panels + [ + ImageChooserPanel('header_image'), + FieldPanel('tagline'), + StreamFieldPanel('body') + ] + + parent_page_types = [Page] # only root + subpage_types = ['LandingPage', 'ContentPage'] diff --git a/cdhweb/pages/tests.py b/cdhweb/pages/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/cdhweb/pages/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cdhweb/pages/views.py b/cdhweb/pages/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/cdhweb/pages/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 4032485f9fb449a0aa03b4950535fb8137f510b7 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Tue, 7 Jan 2020 17:09:07 -0500 Subject: [PATCH 02/50] begin homepage migration testing --- cdhweb/pages/apps.py | 3 +- cdhweb/pages/migrations/0001_initial.py | 61 +++++++++++++++ cdhweb/pages/migrations/0002_homepage.py | 78 +++++++++++++++++++ cdhweb/pages/models.py | 98 ++++++++++++++++++------ cdhweb/pages/tests.py | 3 - cdhweb/pages/tests/test_migrations.py | 73 ++++++++++++++++++ cdhweb/settings.py | 1 + 7 files changed, 288 insertions(+), 29 deletions(-) create mode 100644 cdhweb/pages/migrations/0001_initial.py create mode 100644 cdhweb/pages/migrations/0002_homepage.py delete mode 100644 cdhweb/pages/tests.py create mode 100644 cdhweb/pages/tests/test_migrations.py diff --git a/cdhweb/pages/apps.py b/cdhweb/pages/apps.py index acdb96073..44ddee4d2 100644 --- a/cdhweb/pages/apps.py +++ b/cdhweb/pages/apps.py @@ -2,4 +2,5 @@ class PagesConfig(AppConfig): - name = 'pages' + name = 'cdhweb.pages' + label = 'cdhweb.pages' # will conflict with mezzanine pages otherwise diff --git a/cdhweb/pages/migrations/0001_initial.py b/cdhweb/pages/migrations/0001_initial.py new file mode 100644 index 000000000..32494d4a7 --- /dev/null +++ b/cdhweb/pages/migrations/0001_initial.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2020-01-07 18:57 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import wagtail.core.blocks +import wagtail.core.fields +import wagtail.images.blocks + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('wagtailcore', '0040_page_draft_title'), + ('wagtailimages', '0021_image_file_hash'), + ('blog', '0004_blogpost_is_featured'), + ('projects', '0007_membership_status_override'), + ('events', '0005_event_attendance'), + ('resources', '0004_attachments_optional') + ] + + operations = [ + migrations.CreateModel( + name='ContentPage', + 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')), + ('description', wagtail.core.fields.RichTextField(blank=True, help_text='Optional. Brief description for preview display. Will also be used for search description (without tags), if one is not entered.')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page', models.Model), + ), + migrations.CreateModel( + name='HomePage', + 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')), + ('body', wagtail.core.fields.StreamField([('paragraph', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'])), ('image', wagtail.core.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('alternative_text', wagtail.core.blocks.TextBlock(help_text='Alternative text for visually impaired users to\nbriefly communicate the intended message of the image in this context.', required=True)), ('caption', wagtail.core.blocks.RichTextBlock(features=['bold', 'italic', 'link'], required=False))])), ('linkable_section', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock()), ('anchor_text', wagtail.core.blocks.CharBlock(help_text='Short label for anchor link')), ('body', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote']))]))], blank=True)), + ], + options={ + 'verbose_name': 'Homepage', + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='LandingPage', + 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')), + ('tagline', models.CharField(max_length=255)), + ('body', wagtail.core.fields.StreamField([('paragraph', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'])), ('image', wagtail.core.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('alternative_text', wagtail.core.blocks.TextBlock(help_text='Alternative text for visually impaired users to\nbriefly communicate the intended message of the image in this context.', required=True)), ('caption', wagtail.core.blocks.RichTextBlock(features=['bold', 'italic', 'link'], required=False))])), ('linkable_section', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock()), ('anchor_text', wagtail.core.blocks.CharBlock(help_text='Short label for anchor link')), ('body', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote']))]))], blank=True)), + ('header_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.Image')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/cdhweb/pages/migrations/0002_homepage.py b/cdhweb/pages/migrations/0002_homepage.py new file mode 100644 index 000000000..96abc9f94 --- /dev/null +++ b/cdhweb/pages/migrations/0002_homepage.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + +# add_child utility method taken from: +# http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ +def add_child(apps, parent_page, klass, **kwargs): + + ContentType = apps.get_model('contenttypes.ContentType') + + page_content_type = ContentType.objects.get_or_create( + model=klass.__name__.lower(), + app_label=klass._meta.app_label, + )[0] + + url_path = '%s%s/' % (parent_page.url_path, kwargs.get('slug')) + + created_page = klass.objects.create( + content_type=page_content_type, + path='%s00%02d' % (parent_page.path, parent_page.numchild + 1), + depth=parent_page.depth + 1, + numchild=0, + url_path=url_path, + **kwargs + ) + + parent_page.numchild += 1 + parent_page.save() + + return created_page + +def create_homepage(apps, schema_editor): + '''Search for an existing mezzanine Page for the home page, if any, and save + its content. Create a new wagtail HomePage using this content and set the + site's root to the new homepage. Delete the default wagtail welcome page, + if it exists.''' + + # MezzaninePage = apps.get_model('mezzanine.pages', 'Page') + Page = apps.get_model('wagtailcore', 'Page') + HomePage = apps.get_model('cdhweb.pages', 'HomePage') + + # try: + # old_home = Page.objects.get(title='Home') + # content = old_home.richtextpage.content + # except (Page.DoesNotExist, AttributeError): + # content = '' + + root = Page.objects.get(title='Root') + new_home = add_child(apps, root, HomePage, title='Home') + # new_home.save_revision().publish() # create a new revision and publish + + welcome = Page.objects.get(title='Welcome to your new Wagtail site!') + welcome.delete() # delete welcome page + + +def revert_create_homepage(apps, schema_editor): + '''Delete the created wagtail HomePage and replace with a placeholder + welcome page.''' + + Page = apps.get_model('wagtailcore', 'Page') + HomePage = apps.get_model('cdhweb.pages', 'HomePage') + + root = Page.objects.get(title='Root') + add_child(apps, root, Page, title='Welcome to your new Wagtail site!') + + HomePage.objects.first().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('cdhweb.pages', '0001_initial') + ] + + operations = [ + migrations.RunPython(create_homepage, reverse_code=revert_create_homepage) + ] \ No newline at end of file diff --git a/cdhweb/pages/models.py b/cdhweb/pages/models.py index a716580ab..721bd8ff6 100644 --- a/cdhweb/pages/models.py +++ b/cdhweb/pages/models.py @@ -1,11 +1,13 @@ from random import shuffle +import bleach from django.db import models +from django.template.defaultfilters import striptags, truncatechars_html from django.utils.text import slugify from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel from wagtail.core.blocks import (CharBlock, RichTextBlock, StreamBlock, StructBlock, TextBlock) -from wagtail.core.fields import StreamField +from wagtail.core.fields import RichTextField, StreamField from wagtail.core.models import Page from wagtail.documents.blocks import DocumentChooserBlock from wagtail.images.blocks import ImageChooserBlock @@ -16,7 +18,6 @@ from cdhweb.events.models import Event from cdhweb.projects.models import Project - #: commonly allowed tags for RichTextBlocks RICH_TEXT_TAGS = ['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'] @@ -25,6 +26,67 @@ briefly communicate the intended message of the image in this context.""" +class PagePreviewDescriptionMixin(models.Model): + '''Page mixin with logic for page preview content. Adds an optional + richtext description field, and methods to get description and plain-text + description, for use in previews on the site and plain-text metadata + previews.''' + + # adapted from PPA; does not allow

tags in description + #: brief description for preview display + description = RichTextField( + blank=True, features=['bold', 'italic'], + help_text='Optional. Brief description for preview display. Will ' + + 'also be used for search description (without tags), if one is ' + + 'not entered.') + #: maximum length for description to be displayed + max_length = 225 + # (tags are omitted by subsetting default ALLOWED_TAGS) + #: allowed tags for bleach html stripping in description + allowed_tags = list((set(bleach.sanitizer.ALLOWED_TAGS) - \ + set(['a', 'blockquote']))) # additional tags to remove + + class Meta: + abstract = True + + def get_description(self): + '''Get formatted description for preview. Uses description field + if there is content, otherwise uses beginning of the body content.''' + + description = '' + + # use description field if set + # use striptags to check for empty paragraph) + if striptags(self.description): + description = self.description + + # if not, use beginning of body content + else: + # Iterate over blocks and use content from first paragraph content + for block in self.body: + if block.block_type == 'paragraph': + description = block + # stop after the first instead of using last + break + + description = bleach.clean( + str(description), + tags=self.allowed_tags, + strip=True + ) + # truncate either way + return truncatechars_html(description, self.max_length) + + def get_plaintext_description(self): + '''Get plain-text description for use in metadata. Uses + search_description field if set; otherwise uses the result of + :meth:`get_description` with tags stripped.''' + + if self.search_description.strip(): + return self.search_description + return striptags(self.get_description()) + + class LinkableSectionBlock(StructBlock): ''':class:`~wagtail.core.blocks.StructBlock` for a rich text block and an associated `title` that will render as an

. Creates an anchor () @@ -66,14 +128,13 @@ class BodyContentBlock(StreamBlock): '''Common set of blocks available in StreamFields for body text.''' paragraph = RichTextBlock(features=RICH_TEXT_TAGS) image = CaptionedImageBlock() - document = DocumentChooserBlock() linkable_section = LinkableSectionBlock() -class ContentPage(Page): +class ContentPage(Page, PagePreviewDescriptionMixin): '''Basic content page model.''' - parent_page_types = ['LandingPage', 'ContentPage'] + parent_page_types = ['ContentPage'] subpage_types = ['ContentPage'] @@ -82,17 +143,14 @@ class LandingPage(Page): tagline = models.CharField(max_length=255) body = StreamField(BodyContentBlock, blank=True) - image = models.ForeignKey('wagtailimages.image', null=True, blank=True, - on_delete=models.SET_NULL, related_name='+') # no reverse relationship - - search_fields = Page.search_fields + [ - index.SearchField('body'), - ] + header_image = models.ForeignKey('wagtailimages.image', null=True, + blank=True, on_delete=models.SET_NULL, related_name='+') # no reverse relationship + search_fields = Page.search_fields + [index.SearchField('body')] content_panels = Page.content_panels + [ - ImageChooserPanel('header_image'), FieldPanel('tagline'), - StreamFieldPanel('body') + StreamFieldPanel('body'), + ImageChooserPanel('header_image'), ] parent_page_types = ['HomePage'] @@ -102,10 +160,7 @@ class LandingPage(Page): class HomePage(Page): '''A home page that aggregates and displays featured content.''' - tagline = models.CharField(max_length=255) body = StreamField(BodyContentBlock, blank=True) - image = models.ForeignKey('wagtailimages.image', null=True, blank=True, - on_delete=models.SET_NULL, related_name='+') # no reverse relationship class Meta: verbose_name = 'Homepage' @@ -130,15 +185,8 @@ def get_context(self, request): return context - search_fields = Page.search_fields + [ - index.SearchField('body'), - ] - - content_panels = Page.content_panels + [ - ImageChooserPanel('header_image'), - FieldPanel('tagline'), - StreamFieldPanel('body') - ] + search_fields = Page.search_fields + [index.SearchField('body')] + content_panels = Page.content_panels + [StreamFieldPanel('body')] parent_page_types = [Page] # only root subpage_types = ['LandingPage', 'ContentPage'] diff --git a/cdhweb/pages/tests.py b/cdhweb/pages/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/cdhweb/pages/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/cdhweb/pages/tests/test_migrations.py b/cdhweb/pages/tests/test_migrations.py new file mode 100644 index 000000000..4610a8b3b --- /dev/null +++ b/cdhweb/pages/tests/test_migrations.py @@ -0,0 +1,73 @@ +from django.apps import apps +from django.test import TestCase +from django.db.migrations.executor import MigrationExecutor +from django.db import connection + +# migration test case adapted from +# https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ +# and from winthrop-django + +class TestMigrations(TestCase): + + app = None + migrate_from = None + migrate_to = None + + def setUp(self): + assert self.migrate_from and self.migrate_to, \ + "TestCase '{}' must define migrate_from and migrate_to properties".format(type(self).__name__) + self.migrate_from = [(self.app, self.migrate_from)] + self.migrate_to = [(self.app, self.migrate_to)] + executor = MigrationExecutor(connection) + old_apps = executor.loader.project_state(self.migrate_from).apps + + # Reverse to the original migration + executor.migrate(self.migrate_from) + self.setUpBeforeMigration(old_apps) + + # Run the migration to test + executor.loader.build_graph() # reload. + executor.migrate(self.migrate_to) + + self.apps = executor.loader.project_state(self.migrate_to).apps + + def setUpBeforeMigration(self, apps): + pass + + +class TestCreateHomepage(TestMigrations): + + app = 'cdhweb.pages' + migrate_from = '0001_initial' + migrate_to = '0002_homepage' + + def test_new_homepage(self): + # should create one new HomePage + HomePage = self.apps.get_model('cdhweb.pages', 'HomePage') + self.assertEqual(HomePage.objects.count(), 1) + + def test_homepage_at_root(self): + # new HomePage should be located at root + HomePage = self.apps.get_model('cdhweb.pages', 'HomePage') + Page = self.apps.get_model('wagtailcore', 'Page') + home = HomePage.objects.first() + root = Page.objects.get(title='Root') + self.assertEqual(home.get_parent(), root) + + def test_delete_welcome_page(self): + # should delete wagtail default welcome page + Page = self.apps.get_model('wagtailcore', 'Page') + self.assertRaises(Page.DoesNotExist, Page.objects.get(pk=2)) + +# class TestMigrateHomepage(TestMigrations): + +# migrate_from = '0001_initial' +# migrate_to = '0002_homepage' + +# def setUpBeforeMigration(self, apps): +# # create a mezzanine home page with content +# pass + +# def test_migrate_homepage(self): +# # new HomePage should have migrated mezzanine content +# pass diff --git a/cdhweb/settings.py b/cdhweb/settings.py index 744bde6d7..b44ec7291 100644 --- a/cdhweb/settings.py +++ b/cdhweb/settings.py @@ -357,6 +357,7 @@ # mezzanine.blog; there are good and bad aspects to this; we certainly # don't want users to create the wrong kind of blog posts. "cdhweb.blog", + 'cdhweb.pages', ] # Add django debug toolbar to installed_apps if available From 8101d52f39240dd1b2db4222de8bb9561b13f3cf Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 10:35:33 -0500 Subject: [PATCH 03/50] fix tests for homepage creation migration --- cdhweb/pages/migrations/0002_homepage.py | 39 +++++++++++--------- cdhweb/pages/tests/test_migrations.py | 47 +++++++++++++++++------- 2 files changed, 54 insertions(+), 32 deletions(-) diff --git a/cdhweb/pages/migrations/0002_homepage.py b/cdhweb/pages/migrations/0002_homepage.py index 96abc9f94..ef11bd1b7 100644 --- a/cdhweb/pages/migrations/0002_homepage.py +++ b/cdhweb/pages/migrations/0002_homepage.py @@ -3,9 +3,11 @@ from django.db import migrations -# add_child utility method taken from: +# add_child utility method adapted from: # http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ def add_child(apps, parent_page, klass, **kwargs): + '''Create a new draft wagtail page of type klass as a child of page instance + parent_page, passing along kwargs to its create() function.''' ContentType = apps.get_model('contenttypes.ContentType') @@ -14,14 +16,12 @@ def add_child(apps, parent_page, klass, **kwargs): app_label=klass._meta.app_label, )[0] - url_path = '%s%s/' % (parent_page.url_path, kwargs.get('slug')) - created_page = klass.objects.create( content_type=page_content_type, path='%s00%02d' % (parent_page.path, parent_page.numchild + 1), depth=parent_page.depth + 1, numchild=0, - url_path=url_path, + live=False, # create as a draft so that URL is set correctly on publish **kwargs ) @@ -31,39 +31,41 @@ def add_child(apps, parent_page, klass, **kwargs): return created_page def create_homepage(apps, schema_editor): - '''Search for an existing mezzanine Page for the home page, if any, and save - its content. Create a new wagtail HomePage using this content and set the - site's root to the new homepage. Delete the default wagtail welcome page, - if it exists.''' + '''Create a new wagtail HomePage with any existing content from an old + Mezzanine home page, and delete Wagtail's default welcome page.''' - # MezzaninePage = apps.get_model('mezzanine.pages', 'Page') + RichTextPage = apps.get_model('pages', 'RichTextPage') Page = apps.get_model('wagtailcore', 'Page') HomePage = apps.get_model('cdhweb.pages', 'HomePage') - # try: - # old_home = Page.objects.get(title='Home') - # content = old_home.richtextpage.content - # except (Page.DoesNotExist, AttributeError): - # content = '' + # check for an existing mezzanine 'home' page and save its content + old_home = RichTextPage.objects.filter(title='Home') + if old_home.count() == 1: + content = old_home.first().content + # create the new homepage underneath site root and publish it root = Page.objects.get(title='Root') - new_home = add_child(apps, root, HomePage, title='Home') - # new_home.save_revision().publish() # create a new revision and publish + add_child(apps, root, HomePage, title='Home') + # new_home.save_revision().publish() + # delete the default welcome page welcome = Page.objects.get(title='Welcome to your new Wagtail site!') - welcome.delete() # delete welcome page + welcome.delete() def revert_create_homepage(apps, schema_editor): '''Delete the created wagtail HomePage and replace with a placeholder welcome page.''' + # NOTE this does not restore deleted mezzanine page! Page = apps.get_model('wagtailcore', 'Page') HomePage = apps.get_model('cdhweb.pages', 'HomePage') + # create a welcome page, since at least one child of root is required root = Page.objects.get(title='Root') add_child(apps, root, Page, title='Welcome to your new Wagtail site!') + # delete the created HomePage HomePage.objects.first().delete() @@ -74,5 +76,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(create_homepage, reverse_code=revert_create_homepage) + migrations.RunPython(create_homepage, + reverse_code=revert_create_homepage) ] \ No newline at end of file diff --git a/cdhweb/pages/tests/test_migrations.py b/cdhweb/pages/tests/test_migrations.py index 4610a8b3b..31614f372 100644 --- a/cdhweb/pages/tests/test_migrations.py +++ b/cdhweb/pages/tests/test_migrations.py @@ -3,10 +3,17 @@ from django.db.migrations.executor import MigrationExecutor from django.db import connection +def get_parent(apps, page): + '''Find the parent of a wagtail page using its `path` attribute.''' + # see for an explanation of django-treebeard & the `path` attribute: + # http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ + Page = apps.get_model('wagtailcore', 'Page') + return Page.objects.get(path=page.path[:4]) + + # migration test case adapted from # https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ # and from winthrop-django - class TestMigrations(TestCase): app = None @@ -49,25 +56,37 @@ def test_new_homepage(self): def test_homepage_at_root(self): # new HomePage should be located at root HomePage = self.apps.get_model('cdhweb.pages', 'HomePage') - Page = self.apps.get_model('wagtailcore', 'Page') home = HomePage.objects.first() - root = Page.objects.get(title='Root') - self.assertEqual(home.get_parent(), root) + parent = get_parent(apps, home) + self.assertEqual(parent.title, 'Root') def test_delete_welcome_page(self): # should delete wagtail default welcome page Page = self.apps.get_model('wagtailcore', 'Page') - self.assertRaises(Page.DoesNotExist, Page.objects.get(pk=2)) + with self.assertRaises(Page.DoesNotExist): + Page.objects.get(title='Welcome to your new Wagtail site!') + -# class TestMigrateHomepage(TestMigrations): +class TestMigrateHomepage(TestMigrations): -# migrate_from = '0001_initial' -# migrate_to = '0002_homepage' + app = 'cdhweb.pages' + migrate_from = '0001_initial' + migrate_to = '0002_homepage' -# def setUpBeforeMigration(self, apps): -# # create a mezzanine home page with content -# pass + def setUpBeforeMigration(self, apps): + # create a mezzanine home page with content + pass -# def test_migrate_homepage(self): -# # new HomePage should have migrated mezzanine content -# pass + def test_migrate_homepage(self): + # new HomePage should have migrated mezzanine content + pass + + +class TestCreateSite(TestMigrations): + + app = 'cdhweb.pages' + migrate_from = '0001_initial' + migrate_to = '0002_homepage' + + def test_create_site(self): + pass From baf2fb5cd795def21d310cd3fedc112c38af01d3 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:33:38 -0500 Subject: [PATCH 04/50] suppress pytest warning about junit --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index de88f501f..1b21a9d51 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,3 +4,5 @@ DJANGO_SETTINGS_MODULE=cdhweb.settings python_files = "cdhweb/**/tests.py" "cdhweb/**/tests/test_*.py" "cdhweb/tests.py" # limit testpath to speed up collecting step testpaths = cdhweb +# suppress junit warning +junit_family=legacy From 282b22d0a2989430b4169bab4064c4d2d7402fe0 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:34:27 -0500 Subject: [PATCH 05/50] unregister wagtail models from django admin --- cdhweb/pages/admin.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cdhweb/pages/admin.py b/cdhweb/pages/admin.py index 8c38f3f3d..7e012be0c 100644 --- a/cdhweb/pages/admin.py +++ b/cdhweb/pages/admin.py @@ -1,3 +1,13 @@ from django.contrib import admin +from wagtail.core.models import Page, Site +from wagtail.documents.models import Document +from wagtail.images.models import Image -# Register your models here. +# unregister wagtail content from django admin to avoid +# editing something in the wrong place and potentially causing +# problems + +admin.site.unregister(Page) +admin.site.unregister(Site) +admin.site.unregister(Image) +admin.site.unregister(Document) \ No newline at end of file From 1c6744934a0c7b34866c793ab35520791cd3c0cd Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:34:45 -0500 Subject: [PATCH 06/50] move migration utilities to module --- cdhweb/pages/migration_utilities.py | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 cdhweb/pages/migration_utilities.py diff --git a/cdhweb/pages/migration_utilities.py b/cdhweb/pages/migration_utilities.py new file mode 100644 index 000000000..e3a5790c0 --- /dev/null +++ b/cdhweb/pages/migration_utilities.py @@ -0,0 +1,68 @@ +from django.test import TestCase +from django.db.migrations.executor import MigrationExecutor +from django.db import connection + + +# see for an explanation of django-treebeard & the `path` attribute: +# http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ +def get_parent(apps, page): + '''Find the parent of a wagtail page using its `path` attribute.''' + Page = apps.get_model('wagtailcore', 'Page') + return Page.objects.get(path=page.path[:4]) + +# add_child utility method adapted from: +# http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ +def add_child(apps, parent_page, klass, **kwargs): + '''Create a new draft wagtail page of type klass as a child of page instance + parent_page, passing along kwargs to its create() function.''' + + ContentType = apps.get_model('contenttypes.ContentType') + + page_content_type = ContentType.objects.get_or_create( + model=klass.__name__.lower(), + app_label=klass._meta.app_label, + )[0] + + created_page = klass.objects.create( + content_type=page_content_type, + path='%s00%02d' % (parent_page.path, parent_page.numchild + 1), + depth=parent_page.depth + 1, + numchild=0, + live=False, # create as a draft so that URL is set correctly on publish + **kwargs + ) + + parent_page.numchild += 1 + parent_page.save() + + return created_page + +# migration test case adapted from +# https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ +# and from winthrop-django +class TestMigrations(TestCase): + + app = None + migrate_from = None + migrate_to = None + + def setUp(self): + assert self.migrate_from and self.migrate_to, \ + "TestCase '{}' must define migrate_from and migrate_to properties".format(type(self).__name__) + self.migrate_from = [(self.app, self.migrate_from)] + self.migrate_to = [(self.app, self.migrate_to)] + executor = MigrationExecutor(connection) + old_apps = executor.loader.project_state(self.migrate_from).apps + + # Reverse to the original migration + executor.migrate(self.migrate_from) + self.setUpBeforeMigration(old_apps) + + # Run the migration to test + executor.loader.build_graph() # reload. + executor.migrate(self.migrate_to) + + self.apps = executor.loader.project_state(self.migrate_to).apps + + def setUpBeforeMigration(self, apps): + pass \ No newline at end of file From 37323a6a3e3bfdcf912b7405b7ecce62b695a905 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:35:32 -0500 Subject: [PATCH 07/50] add blockquote feature to draftail editor --- cdhweb/pages/wagtail_hooks.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 cdhweb/pages/wagtail_hooks.py diff --git a/cdhweb/pages/wagtail_hooks.py b/cdhweb/pages/wagtail_hooks.py new file mode 100644 index 000000000..66259edc3 --- /dev/null +++ b/cdhweb/pages/wagtail_hooks.py @@ -0,0 +1,34 @@ +import wagtail.admin.rich_text.editors.draftail.features as draftail_features +from wagtail.admin.rich_text.converters.html_to_contentstate import \ + BlockElementHandler +from wagtail.core import hooks + +# blockquote registration example taken from: +# http://docs.wagtail.io/en/v2.4/advanced_topics/customisation/extending_draftail.html#creating-new-blocks + +@hooks.register('register_rich_text_features') +def register_blockquote_feature(features): + """ + Registering the `blockquote` feature, which uses the `blockquote` Draft.js block type, + and is stored as HTML with a `
` tag. + """ + feature_name = 'blockquote' + type_ = 'blockquote' + tag = 'blockquote' + + control = { + 'type': type_, + 'label': '❝', + 'description': 'Blockquote', + # Optionally, we can tell Draftail what element to use when displaying those blocks in the editor. + 'element': 'blockquote', + } + + features.register_editor_plugin( + 'draftail', feature_name, draftail_features.BlockFeature(control) + ) + + features.register_converter_rule('contentstate', feature_name, { + 'from_database_format': {tag: BlockElementHandler(type_)}, + 'to_database_format': {'block_map': {type_: tag}}, + }) From ef0d31e08cc68b481456b2105b1df6202552070f Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:35:48 -0500 Subject: [PATCH 08/50] update migration tests --- cdhweb/pages/tests/test_migrations.py | 41 +-------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/cdhweb/pages/tests/test_migrations.py b/cdhweb/pages/tests/test_migrations.py index 31614f372..faf62a41c 100644 --- a/cdhweb/pages/tests/test_migrations.py +++ b/cdhweb/pages/tests/test_migrations.py @@ -1,45 +1,6 @@ from django.apps import apps -from django.test import TestCase -from django.db.migrations.executor import MigrationExecutor -from django.db import connection -def get_parent(apps, page): - '''Find the parent of a wagtail page using its `path` attribute.''' - # see for an explanation of django-treebeard & the `path` attribute: - # http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ - Page = apps.get_model('wagtailcore', 'Page') - return Page.objects.get(path=page.path[:4]) - - -# migration test case adapted from -# https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ -# and from winthrop-django -class TestMigrations(TestCase): - - app = None - migrate_from = None - migrate_to = None - - def setUp(self): - assert self.migrate_from and self.migrate_to, \ - "TestCase '{}' must define migrate_from and migrate_to properties".format(type(self).__name__) - self.migrate_from = [(self.app, self.migrate_from)] - self.migrate_to = [(self.app, self.migrate_to)] - executor = MigrationExecutor(connection) - old_apps = executor.loader.project_state(self.migrate_from).apps - - # Reverse to the original migration - executor.migrate(self.migrate_from) - self.setUpBeforeMigration(old_apps) - - # Run the migration to test - executor.loader.build_graph() # reload. - executor.migrate(self.migrate_to) - - self.apps = executor.loader.project_state(self.migrate_to).apps - - def setUpBeforeMigration(self, apps): - pass +from cdhweb.pages.migration_utilities import TestMigrations, get_parent class TestCreateHomepage(TestMigrations): From e8baf1d521149d67b88f957a49b93dad5b2e1555 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:35:58 -0500 Subject: [PATCH 09/50] basic page model tests --- cdhweb/pages/tests/test_models.py | 96 +++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 cdhweb/pages/tests/test_models.py diff --git a/cdhweb/pages/tests/test_models.py b/cdhweb/pages/tests/test_models.py new file mode 100644 index 000000000..56c200960 --- /dev/null +++ b/cdhweb/pages/tests/test_models.py @@ -0,0 +1,96 @@ +from django.test import SimpleTestCase +from wagtail.core.models import Page +from wagtail.tests.utils import WagtailPageTests +from wagtail.tests.utils.form_data import (nested_form_data, rich_text, + streamfield) + +from cdhweb.pages.models import (ContentPage, HomePage, LandingPage, + LinkableSectionBlock) + + +class TestLinkableSectionBlock(SimpleTestCase): + + def test_clean(self): + block = LinkableSectionBlock() + cleaned_values = block.clean({'anchor_text': 'Working at the CDH'}) + assert cleaned_values['anchor_text'] == 'working-at-the-cdh' + + def test_render(self): + block = LinkableSectionBlock() + html = block.render(block.to_python({ + 'title': 'Working at the CDH', + 'body': 'Info about how to get a job working at the CDH', + 'anchor_text': 'working-at-the-cdh', + })) + expected_html = ''' +
+

Working at the CDH + +

+
+ Info about how to get a job working at the CDH +
+
+ ''' + + self.assertHTMLEqual(html, expected_html) + + +class TestHomePage(WagtailPageTests): + + def test_can_create(self): + root = Page.objects.get(title='Root') + self.assertCanCreate(root, HomePage, nested_form_data({ + 'title': 'Home 2', + 'slug': 'home-2', + 'body': streamfield([ + ('paragraph', rich_text('homepage body text')), + ]), + })) + + def test_parent_pages(self): + # only allowed parent is basic page (root) + self.assertAllowedParentPageTypes(HomePage, [Page]) + + def test_subpages(self): + # landing pages or content pages can be children + self.assertAllowedSubpageTypes(HomePage, [LandingPage, ContentPage]) + + def test_template(self): + pass + + +class TestLandingPage(WagtailPageTests): + + def test_can_create(self): + pass + + def test_parent_pages(self): + # only allowed parent is home + self.assertAllowedParentPageTypes(LandingPage, [HomePage]) + + def test_subpages(self): + # only allowed child is content page + self.assertAllowedSubpageTypes(LandingPage, [ContentPage]) + + def test_template(self): + pass + + +class TestContentPage(WagtailPageTests): + + def test_can_create(self): + pass + + def test_parent_pages(self): + # can be child of home, landing page, or another content page + self.assertAllowedParentPageTypes(ContentPage, + [HomePage, LandingPage, ContentPage]) + + def test_subpages(self): + # only allowed child is content page + self.assertAllowedSubpageTypes(ContentPage, [ContentPage]) + + def test_template(self): + pass From fb6bb36abf75a386c9bd3dfa274514e57c41c9c8 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:36:10 -0500 Subject: [PATCH 10/50] make tests module to fix pytest naming --- cdhweb/pages/tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cdhweb/pages/tests/__init__.py diff --git a/cdhweb/pages/tests/__init__.py b/cdhweb/pages/tests/__init__.py new file mode 100644 index 000000000..e69de29bb From 6722c04a698d030873256631d5d9f6fa659c3dd7 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:36:21 -0500 Subject: [PATCH 11/50] remove unused views.py --- cdhweb/pages/views.py | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 cdhweb/pages/views.py diff --git a/cdhweb/pages/views.py b/cdhweb/pages/views.py deleted file mode 100644 index 91ea44a21..000000000 --- a/cdhweb/pages/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. From b79d68bd25cd7b30e4f2e9f6368896f3911a65a7 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:36:39 -0500 Subject: [PATCH 12/50] update pages models --- cdhweb/pages/models.py | 86 ++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 57 deletions(-) diff --git a/cdhweb/pages/models.py b/cdhweb/pages/models.py index 721bd8ff6..ccbdeb2aa 100644 --- a/cdhweb/pages/models.py +++ b/cdhweb/pages/models.py @@ -18,12 +18,14 @@ from cdhweb.events.models import Event from cdhweb.projects.models import Project + #: commonly allowed tags for RichTextBlocks RICH_TEXT_TAGS = ['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'] -#: help text for image alternative text -ALT_TEXT_HELP = """Alternative text for visually impaired users to -briefly communicate the intended message of the image in this context.""" +class BodyContentBlock(StreamBlock): + '''Common set of blocks available in StreamFields for body text.''' + paragraph = RichTextBlock(features=RICH_TEXT_TAGS) + image = ImageChooserBlock() class PagePreviewDescriptionMixin(models.Model): @@ -56,7 +58,7 @@ def get_description(self): description = '' # use description field if set - # use striptags to check for empty paragraph) + # use striptags to check for empty paragraph if striptags(self.description): description = self.description @@ -87,62 +89,31 @@ def get_plaintext_description(self): return striptags(self.get_description()) -class LinkableSectionBlock(StructBlock): - ''':class:`~wagtail.core.blocks.StructBlock` for a rich text block and an - associated `title` that will render as an

. Creates an anchor () - so that the section can be directly linked to using a url fragment.''' - title = CharBlock() - anchor_text = CharBlock(help_text='Short label for anchor link') - body = RichTextBlock(features=RICH_TEXT_TAGS) - panels = [ - FieldPanel('title'), - FieldPanel('slug'), - FieldPanel('body'), - ] - - class Meta: - icon = 'form' - label = 'Linkable Section' - template = 'pages/snippets/linkable_section.html' - - def clean(self, value): - cleaned_values = super().clean(value) - # run slugify to ensure anchor text is a slug - cleaned_values['anchor_text'] = slugify(cleaned_values['anchor_text']) - return cleaned_values - - -class CaptionedImageBlock(StructBlock): - ''':class:`~wagtail.core.blocks.StructBlock` for an image with - alternative text and optional formatted caption, so - that both caption and alternative text can be context-specific.''' - image = ImageChooserBlock() - alternative_text = TextBlock(required=True, help_text=ALT_TEXT_HELP) - caption = RichTextBlock(features=['bold', 'italic', 'link'], required=False) - - class Meta: - icon = 'image' - +class ContentPage(Page, PagePreviewDescriptionMixin): + '''Basic content page model.''' -class BodyContentBlock(StreamBlock): - '''Common set of blocks available in StreamFields for body text.''' - paragraph = RichTextBlock(features=RICH_TEXT_TAGS) - image = CaptionedImageBlock() - linkable_section = LinkableSectionBlock() + #: main page text + body = StreamField(BodyContentBlock, blank=True) + # TODO attachments -class ContentPage(Page, PagePreviewDescriptionMixin): - '''Basic content page model.''' + content_panels = Page.content_panels + [ + FieldPanel('description'), + StreamFieldPanel('body'), + ] - parent_page_types = ['ContentPage'] + parent_page_types = ['HomePage', 'LandingPage', 'ContentPage'] subpage_types = ['ContentPage'] class LandingPage(Page): '''Page type that aggregates and displays multiple :class:`ContentPage`s.''' - tagline = models.CharField(max_length=255) + #: main page text body = StreamField(BodyContentBlock, blank=True) + #: short sentence overlaid on the header image + tagline = models.CharField(max_length=255) + #: image that will be used for the header header_image = models.ForeignKey('wagtailimages.image', null=True, blank=True, on_delete=models.SET_NULL, related_name='+') # no reverse relationship @@ -160,13 +131,20 @@ class LandingPage(Page): class HomePage(Page): '''A home page that aggregates and displays featured content.''' + #: main page text body = StreamField(BodyContentBlock, blank=True) + search_fields = Page.search_fields + [index.SearchField('body')] + content_panels = Page.content_panels + [StreamFieldPanel('body')] + + parent_page_types = [Page] # only root + subpage_types = ['LandingPage', 'ContentPage'] + class Meta: verbose_name = 'Homepage' def get_context(self, request): - '''Add featured content to the page context.''' + '''Add featured updates, projects, and events to the page context.''' context = super().get_context(request) # add up to 6 featured updates, otherwise use 3 most recent updates @@ -183,10 +161,4 @@ def get_context(self, request): # add up to 3 upcoming, published events context['events'] = Event.objects.published().upcoming()[:3] - return context - - search_fields = Page.search_fields + [index.SearchField('body')] - content_panels = Page.content_panels + [StreamFieldPanel('body')] - - parent_page_types = [Page] # only root - subpage_types = ['LandingPage', 'ContentPage'] + return context \ No newline at end of file From 83f84f3624d5cf7be81f1e50165d0e02872f4861 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:48:17 -0500 Subject: [PATCH 13/50] remove LinkableSectionBlock from tests --- cdhweb/pages/tests/test_models.py | 32 +------------------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/cdhweb/pages/tests/test_models.py b/cdhweb/pages/tests/test_models.py index 56c200960..30b29f357 100644 --- a/cdhweb/pages/tests/test_models.py +++ b/cdhweb/pages/tests/test_models.py @@ -4,37 +4,7 @@ from wagtail.tests.utils.form_data import (nested_form_data, rich_text, streamfield) -from cdhweb.pages.models import (ContentPage, HomePage, LandingPage, - LinkableSectionBlock) - - -class TestLinkableSectionBlock(SimpleTestCase): - - def test_clean(self): - block = LinkableSectionBlock() - cleaned_values = block.clean({'anchor_text': 'Working at the CDH'}) - assert cleaned_values['anchor_text'] == 'working-at-the-cdh' - - def test_render(self): - block = LinkableSectionBlock() - html = block.render(block.to_python({ - 'title': 'Working at the CDH', - 'body': 'Info about how to get a job working at the CDH', - 'anchor_text': 'working-at-the-cdh', - })) - expected_html = ''' -
-

Working at the CDH - -

-
- Info about how to get a job working at the CDH -
-
- ''' - - self.assertHTMLEqual(html, expected_html) +from cdhweb.pages.models import ContentPage, HomePage, LandingPage class TestHomePage(WagtailPageTests): From fe04bc5294133c64ada5db0b5de3b9e1d91110a2 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:50:44 -0500 Subject: [PATCH 14/50] update homepage template --- cdhweb/pages/templates/pages/home_page.html | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 cdhweb/pages/templates/pages/home_page.html diff --git a/cdhweb/pages/templates/pages/home_page.html b/cdhweb/pages/templates/pages/home_page.html new file mode 100644 index 000000000..f18aa6dc5 --- /dev/null +++ b/cdhweb/pages/templates/pages/home_page.html @@ -0,0 +1,49 @@ +{% extends 'base.html' %} +{% load wagtailcore_tags %} + +{% block page-title %}{% if page %}{{ page.meta_title }}{% else %}The Center for Digital Humanities at Princeton{% endif %}{% endblock %} + +{% block content %} {# add a class to main content for home-page specific styles #} +
+{% if updates %} +{% include 'snippets/carousel.html' %} +{% endif %} +{{ block.super }} +
+{% endblock %} + +{% block bodyattrs %}class="with-cards"{% endblock %} + +{% block main %} + +{# display editable page content; wrapped for formatting reasons #} +
+
+ {% for block in page.body %} + {% include_block block %} + {% endfor %} +
+
+ +
+

Upcoming Events

+{% for event in events %} + {% include 'events/snippets/event_card.html' %} +{% empty %} +
+

Next semester's events are being scheduled. + Check back later or view past events.

+
+{% endfor %} +
+ +
+

Projects

+{% for project in projects %} + {% include 'projects/snippets/project_card.html' %} + {# project tile still todo #} +{% endfor %} +
+ + +{% endblock %} \ No newline at end of file From e7f85c019338badfc209ce4c7d38caefe8f4a791 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 14:51:55 -0500 Subject: [PATCH 15/50] fixup initial pages migration --- cdhweb/pages/migrations/0001_initial.py | 11 ++-- cdhweb/pages/migrations/0002_homepage.py | 81 ------------------------ 2 files changed, 4 insertions(+), 88 deletions(-) delete mode 100644 cdhweb/pages/migrations/0002_homepage.py diff --git a/cdhweb/pages/migrations/0001_initial.py b/cdhweb/pages/migrations/0001_initial.py index 32494d4a7..f4684b480 100644 --- a/cdhweb/pages/migrations/0001_initial.py +++ b/cdhweb/pages/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.27 on 2020-01-07 18:57 +# Generated by Django 1.11.27 on 2020-01-08 19:51 from __future__ import unicode_literals from django.db import migrations, models @@ -16,10 +16,6 @@ class Migration(migrations.Migration): dependencies = [ ('wagtailcore', '0040_page_draft_title'), ('wagtailimages', '0021_image_file_hash'), - ('blog', '0004_blogpost_is_featured'), - ('projects', '0007_membership_status_override'), - ('events', '0005_event_attendance'), - ('resources', '0004_attachments_optional') ] operations = [ @@ -28,6 +24,7 @@ class Migration(migrations.Migration): 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')), ('description', wagtail.core.fields.RichTextField(blank=True, help_text='Optional. Brief description for preview display. Will also be used for search description (without tags), if one is not entered.')), + ('body', wagtail.core.fields.StreamField([('paragraph', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'])), ('image', wagtail.images.blocks.ImageChooserBlock())], blank=True)), ], options={ 'abstract': False, @@ -38,7 +35,7 @@ class Migration(migrations.Migration): name='HomePage', 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')), - ('body', wagtail.core.fields.StreamField([('paragraph', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'])), ('image', wagtail.core.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('alternative_text', wagtail.core.blocks.TextBlock(help_text='Alternative text for visually impaired users to\nbriefly communicate the intended message of the image in this context.', required=True)), ('caption', wagtail.core.blocks.RichTextBlock(features=['bold', 'italic', 'link'], required=False))])), ('linkable_section', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock()), ('anchor_text', wagtail.core.blocks.CharBlock(help_text='Short label for anchor link')), ('body', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote']))]))], blank=True)), + ('body', wagtail.core.fields.StreamField([('paragraph', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'])), ('image', wagtail.images.blocks.ImageChooserBlock())], blank=True)), ], options={ 'verbose_name': 'Homepage', @@ -49,8 +46,8 @@ class Migration(migrations.Migration): name='LandingPage', 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')), + ('body', wagtail.core.fields.StreamField([('paragraph', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'])), ('image', wagtail.images.blocks.ImageChooserBlock())], blank=True)), ('tagline', models.CharField(max_length=255)), - ('body', wagtail.core.fields.StreamField([('paragraph', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'])), ('image', wagtail.core.blocks.StructBlock([('image', wagtail.images.blocks.ImageChooserBlock()), ('alternative_text', wagtail.core.blocks.TextBlock(help_text='Alternative text for visually impaired users to\nbriefly communicate the intended message of the image in this context.', required=True)), ('caption', wagtail.core.blocks.RichTextBlock(features=['bold', 'italic', 'link'], required=False))])), ('linkable_section', wagtail.core.blocks.StructBlock([('title', wagtail.core.blocks.CharBlock()), ('anchor_text', wagtail.core.blocks.CharBlock(help_text='Short label for anchor link')), ('body', wagtail.core.blocks.RichTextBlock(features=['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote']))]))], blank=True)), ('header_image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.Image')), ], options={ diff --git a/cdhweb/pages/migrations/0002_homepage.py b/cdhweb/pages/migrations/0002_homepage.py deleted file mode 100644 index ef11bd1b7..000000000 --- a/cdhweb/pages/migrations/0002_homepage.py +++ /dev/null @@ -1,81 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations - -# add_child utility method adapted from: -# http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ -def add_child(apps, parent_page, klass, **kwargs): - '''Create a new draft wagtail page of type klass as a child of page instance - parent_page, passing along kwargs to its create() function.''' - - ContentType = apps.get_model('contenttypes.ContentType') - - page_content_type = ContentType.objects.get_or_create( - model=klass.__name__.lower(), - app_label=klass._meta.app_label, - )[0] - - created_page = klass.objects.create( - content_type=page_content_type, - path='%s00%02d' % (parent_page.path, parent_page.numchild + 1), - depth=parent_page.depth + 1, - numchild=0, - live=False, # create as a draft so that URL is set correctly on publish - **kwargs - ) - - parent_page.numchild += 1 - parent_page.save() - - return created_page - -def create_homepage(apps, schema_editor): - '''Create a new wagtail HomePage with any existing content from an old - Mezzanine home page, and delete Wagtail's default welcome page.''' - - RichTextPage = apps.get_model('pages', 'RichTextPage') - Page = apps.get_model('wagtailcore', 'Page') - HomePage = apps.get_model('cdhweb.pages', 'HomePage') - - # check for an existing mezzanine 'home' page and save its content - old_home = RichTextPage.objects.filter(title='Home') - if old_home.count() == 1: - content = old_home.first().content - - # create the new homepage underneath site root and publish it - root = Page.objects.get(title='Root') - add_child(apps, root, HomePage, title='Home') - # new_home.save_revision().publish() - - # delete the default welcome page - welcome = Page.objects.get(title='Welcome to your new Wagtail site!') - welcome.delete() - - -def revert_create_homepage(apps, schema_editor): - '''Delete the created wagtail HomePage and replace with a placeholder - welcome page.''' - # NOTE this does not restore deleted mezzanine page! - - Page = apps.get_model('wagtailcore', 'Page') - HomePage = apps.get_model('cdhweb.pages', 'HomePage') - - # create a welcome page, since at least one child of root is required - root = Page.objects.get(title='Root') - add_child(apps, root, Page, title='Welcome to your new Wagtail site!') - - # delete the created HomePage - HomePage.objects.first().delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('cdhweb.pages', '0001_initial') - ] - - operations = [ - migrations.RunPython(create_homepage, - reverse_code=revert_create_homepage) - ] \ No newline at end of file From 2b466a5ae2acacf95880005c0f2164e9b87fa77f Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 16:09:05 -0500 Subject: [PATCH 16/50] update app label to fix NoReverseMatch issue --- cdhweb/pages/apps.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cdhweb/pages/apps.py b/cdhweb/pages/apps.py index 44ddee4d2..3345e8bbb 100644 --- a/cdhweb/pages/apps.py +++ b/cdhweb/pages/apps.py @@ -3,4 +3,7 @@ class PagesConfig(AppConfig): name = 'cdhweb.pages' - label = 'cdhweb.pages' # will conflict with mezzanine pages otherwise + # NOTE we have to relabel this to avoid a conflict with mezzanine's pages; + # using anything with a '.' in the name will confuse the RegexResolver and + # result in NoReverseMatch errors when adding pages. + label = 'cdhpages' \ No newline at end of file From 4037eb7138a6b7c937444e517cfb526fc9d76e58 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 16:33:29 -0500 Subject: [PATCH 17/50] add sample pages fixture --- cdhweb/pages/fixtures/sample_pages.json | 74 +++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 cdhweb/pages/fixtures/sample_pages.json diff --git a/cdhweb/pages/fixtures/sample_pages.json b/cdhweb/pages/fixtures/sample_pages.json new file mode 100644 index 000000000..3a92a6bc4 --- /dev/null +++ b/cdhweb/pages/fixtures/sample_pages.json @@ -0,0 +1,74 @@ +[ + { + "model": "wagtailcore.page", + "pk": 3, + "fields": { + "path": "00010001", + "depth": 2, + "numchild": 1, + "title": "Home", + "draft_title": "Home", + "slug": "home", + "content_type": ["cdhpages", "homepage"], + "live": true, + "has_unpublished_changes": false, + "url_path": "/home/", + "owner": null, + "seo_title": "", + "show_in_menus": true, + "search_description": "", + "go_live_at": null, + "expire_at": null, + "expired": false, + "locked": false, + "first_published_at": null, + "last_published_at": null, + "latest_revision_created_at": null, + "live_revision": null + } + }, + { + "model": "wagtailcore.page", + "pk": 4, + "fields": { + "path": "000100010001", + "depth": 3, + "numchild": 0, + "title": "Research", + "draft_title": "Research", + "slug": "research", + "content_type": ["cdhpages", "landingpage"], + "live": true, + "has_unpublished_changes": false, + "url_path": "/home/research", + "owner": null, + "seo_title": "", + "show_in_menus": true, + "search_description": "", + "go_live_at": null, + "expire_at": null, + "expired": false, + "locked": false, + "first_published_at": null, + "last_published_at": null, + "latest_revision_created_at": null, + "live_revision": null + } + }, + { + "model": "cdhpages.homepage", + "pk": 3, + "fields": { + "body": "[]" + } + }, + { + "model": "cdhpages.landingpage", + "pk": 4, + "fields": { + "tagline": "Establishing best practices in technical research and design", + "body": "[]", + "header_image": null + } + } +] \ No newline at end of file From 722d2e262919cb72dfaad82e0bfc01d399fe709f Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 16:34:12 -0500 Subject: [PATCH 18/50] update page model tests --- cdhweb/pages/tests/test_models.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/cdhweb/pages/tests/test_models.py b/cdhweb/pages/tests/test_models.py index 30b29f357..26885e868 100644 --- a/cdhweb/pages/tests/test_models.py +++ b/cdhweb/pages/tests/test_models.py @@ -32,9 +32,18 @@ def test_template(self): class TestLandingPage(WagtailPageTests): + fixtures = ['sample_pages'] def test_can_create(self): - pass + home = HomePage.objects.get(title='Home') + self.assertCanCreate(home, LandingPage, nested_form_data({ + 'title': 'Engage', + 'slug': 'engage', + 'tagline': 'Consult, collaborate, and work with us', + 'body': streamfield([ + ('paragraph', rich_text('engage page text')), + ]), + })) def test_parent_pages(self): # only allowed parent is home @@ -49,9 +58,17 @@ def test_template(self): class TestContentPage(WagtailPageTests): - + fixtures = ['sample_pages'] + def test_can_create(self): - pass + research = LandingPage.objects.get(title='Research') + self.assertCanCreate(research, ContentPage, nested_form_data({ + 'title': 'Data Curation', + 'slug': 'data-curation', + 'body': streamfield([ + ('paragraph', rich_text('data curation page text')), + ]), + })) def test_parent_pages(self): # can be child of home, landing page, or another content page From 2788225dbffc02608933dad597f5ea8123e79733 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 17:11:11 -0500 Subject: [PATCH 19/50] add site changes to homepage migration --- cdhweb/pages/migrations/0002_homepage.py | 71 ++++++++++++++++++++++++ cdhweb/pages/tests/test_migrations.py | 27 +++++---- 2 files changed, 84 insertions(+), 14 deletions(-) create mode 100644 cdhweb/pages/migrations/0002_homepage.py diff --git a/cdhweb/pages/migrations/0002_homepage.py b/cdhweb/pages/migrations/0002_homepage.py new file mode 100644 index 000000000..74624de03 --- /dev/null +++ b/cdhweb/pages/migrations/0002_homepage.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2020-01-08 19:59 +from __future__ import unicode_literals + +from django.db import migrations +from cdhweb.pages.migration_utilities import add_child + + +def create_homepage(apps, schema_editor): + '''Create a new wagtail HomePage with any existing content from an old + Mezzanine home page, and delete Wagtail's default welcome page. Point the + default Wagtail site at the new HomePage.''' + + Page = apps.get_model('wagtailcore', 'Page') + Site = apps.get_model('wagtailcore', 'Site') + HomePage = apps.get_model('cdhpages', 'HomePage') + + # check for an existing mezzanine 'home' page and save its content + try: + RichTextPage = apps.get_model('pages', 'RichTextPage') + old_home = RichTextPage.objects.get(title='Home') + content = old_home.content + except (LookupError, RichTextPage.DoesNotExist): + content = '' + + # create the new homepage underneath site root + root = Page.objects.get(title='Root') + home = add_child(apps, root, HomePage, title='Home', body=content) + + # point the default site at the new homepage + site = Site.objects.first() + site.root_page = home + site.root_page_id = home.id + site.save() + + # delete the default welcome page + welcome = Page.objects.get(title='Welcome to your new Wagtail site!') + welcome.delete() + + +def revert_homepage(apps, schema_editor): + '''Delete the created wagtail HomePage and replace with a placeholder + welcome page. Point the default wagtail site back at the welcome page.''' + + Page = apps.get_model('wagtailcore', 'Page') + Site = apps.get_model('wagtailcore', 'Site') + HomePage = apps.get_model('cdhpages', 'HomePage') + + # create a welcome page, since at least one child of root is required + root = Page.objects.get(title='Root') + welcome = add_child(apps, root, Page, title='Welcome to your new Wagtail site!') + + # point the default site at the welcome page + site = Site.objects.first() + site.root_page = welcome + site.root_page_id = welcome.id + site.save() + + # delete the created HomePage + HomePage.objects.first().delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ('cdhpages', '0001_initial') + ] + + operations = [ + migrations.RunPython(create_homepage, reverse_code=revert_homepage) + ] \ No newline at end of file diff --git a/cdhweb/pages/tests/test_migrations.py b/cdhweb/pages/tests/test_migrations.py index faf62a41c..33302a221 100644 --- a/cdhweb/pages/tests/test_migrations.py +++ b/cdhweb/pages/tests/test_migrations.py @@ -5,18 +5,18 @@ class TestCreateHomepage(TestMigrations): - app = 'cdhweb.pages' + app = 'cdhpages' migrate_from = '0001_initial' migrate_to = '0002_homepage' def test_new_homepage(self): # should create one new HomePage - HomePage = self.apps.get_model('cdhweb.pages', 'HomePage') + HomePage = self.apps.get_model('cdhpages', 'HomePage') self.assertEqual(HomePage.objects.count(), 1) def test_homepage_at_root(self): # new HomePage should be located at root - HomePage = self.apps.get_model('cdhweb.pages', 'HomePage') + HomePage = self.apps.get_model('cdhpages', 'HomePage') home = HomePage.objects.first() parent = get_parent(apps, home) self.assertEqual(parent.title, 'Root') @@ -27,10 +27,19 @@ def test_delete_welcome_page(self): with self.assertRaises(Page.DoesNotExist): Page.objects.get(title='Welcome to your new Wagtail site!') + def test_site_root_page(self): + # default site should point to new home page + Site = apps.get_model('wagtailcore', 'Site') + HomePage = self.apps.get_model('cdhpages', 'HomePage') + home = HomePage.objects.first() + site = Site.objects.first() + self.assertEqual(site.root_page, home) + self.assertEqual(site.root_page_id, home.id) + class TestMigrateHomepage(TestMigrations): - app = 'cdhweb.pages' + app = 'cdhpages' migrate_from = '0001_initial' migrate_to = '0002_homepage' @@ -41,13 +50,3 @@ def setUpBeforeMigration(self, apps): def test_migrate_homepage(self): # new HomePage should have migrated mezzanine content pass - - -class TestCreateSite(TestMigrations): - - app = 'cdhweb.pages' - migrate_from = '0001_initial' - migrate_to = '0002_homepage' - - def test_create_site(self): - pass From 446ffc36b15de70e30230308ad7d52f5054795a4 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 8 Jan 2020 17:11:23 -0500 Subject: [PATCH 20/50] minor edits --- cdhweb/pages/migrations/0001_initial.py | 4 ++-- cdhweb/pages/tests/test_models.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cdhweb/pages/migrations/0001_initial.py b/cdhweb/pages/migrations/0001_initial.py index f4684b480..e88c880ac 100644 --- a/cdhweb/pages/migrations/0001_initial.py +++ b/cdhweb/pages/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.27 on 2020-01-08 19:51 +# Generated by Django 1.11.27 on 2020-01-08 21:05 from __future__ import unicode_literals from django.db import migrations, models @@ -14,8 +14,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('wagtailcore', '0040_page_draft_title'), ('wagtailimages', '0021_image_file_hash'), + ('wagtailcore', '0040_page_draft_title'), ] operations = [ diff --git a/cdhweb/pages/tests/test_models.py b/cdhweb/pages/tests/test_models.py index 26885e868..dd5ddfb8b 100644 --- a/cdhweb/pages/tests/test_models.py +++ b/cdhweb/pages/tests/test_models.py @@ -27,7 +27,7 @@ def test_subpages(self): # landing pages or content pages can be children self.assertAllowedSubpageTypes(HomePage, [LandingPage, ContentPage]) - def test_template(self): + def test_template_used(self): pass @@ -53,7 +53,7 @@ def test_subpages(self): # only allowed child is content page self.assertAllowedSubpageTypes(LandingPage, [ContentPage]) - def test_template(self): + def test_template_used(self): pass @@ -79,5 +79,5 @@ def test_subpages(self): # only allowed child is content page self.assertAllowedSubpageTypes(ContentPage, [ContentPage]) - def test_template(self): + def test_template_used(self): pass From 60d5763def4a0f2c89e033a7a31960566921efad Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Thu, 9 Jan 2020 12:45:13 -0500 Subject: [PATCH 21/50] update migration utils and tests --- cdhweb/pages/fixtures/sample_pages.json | 38 +++++++++-- ...ration_utilities.py => migration_utils.py} | 46 +++++++++++-- cdhweb/pages/migrations/0002_homepage.py | 2 +- cdhweb/pages/tests/test_migration_utils.py | 66 +++++++++++++++++++ cdhweb/pages/tests/test_migrations.py | 2 +- 5 files changed, 142 insertions(+), 12 deletions(-) rename cdhweb/pages/{migration_utilities.py => migration_utils.py} (57%) create mode 100644 cdhweb/pages/tests/test_migration_utils.py diff --git a/cdhweb/pages/fixtures/sample_pages.json b/cdhweb/pages/fixtures/sample_pages.json index 3a92a6bc4..3eb949f39 100644 --- a/cdhweb/pages/fixtures/sample_pages.json +++ b/cdhweb/pages/fixtures/sample_pages.json @@ -1,7 +1,35 @@ [ { "model": "wagtailcore.page", - "pk": 3, + "pk": 1, + "fields": { + "path": "0001", + "depth": 1, + "numchild": 1, + "title": "Root", + "draft_title": "Root", + "slug": "root", + "content_type": ["wagtailcore", "page"], + "live": true, + "has_unpublished_changes": false, + "url_path": "/", + "owner": null, + "seo_title": "", + "show_in_menus": false, + "search_description": "", + "go_live_at": null, + "expire_at": null, + "expired": false, + "locked": false, + "first_published_at": null, + "last_published_at": null, + "latest_revision_created_at": null, + "live_revision": null + } + }, + { + "model": "wagtailcore.page", + "pk": 2, "fields": { "path": "00010001", "depth": 2, @@ -29,7 +57,7 @@ }, { "model": "wagtailcore.page", - "pk": 4, + "pk": 3, "fields": { "path": "000100010001", "depth": 3, @@ -40,7 +68,7 @@ "content_type": ["cdhpages", "landingpage"], "live": true, "has_unpublished_changes": false, - "url_path": "/home/research", + "url_path": "/home/research/", "owner": null, "seo_title": "", "show_in_menus": true, @@ -57,14 +85,14 @@ }, { "model": "cdhpages.homepage", - "pk": 3, + "pk": 2, "fields": { "body": "[]" } }, { "model": "cdhpages.landingpage", - "pk": 4, + "pk": 3, "fields": { "tagline": "Establishing best practices in technical research and design", "body": "[]", diff --git a/cdhweb/pages/migration_utilities.py b/cdhweb/pages/migration_utils.py similarity index 57% rename from cdhweb/pages/migration_utilities.py rename to cdhweb/pages/migration_utils.py index e3a5790c0..4117291fa 100644 --- a/cdhweb/pages/migration_utilities.py +++ b/cdhweb/pages/migration_utils.py @@ -6,9 +6,15 @@ # see for an explanation of django-treebeard & the `path` attribute: # http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ def get_parent(apps, page): - '''Find the parent of a wagtail page using its `path` attribute.''' + '''Find the parent of a wagtail page instance. Always returns the underlying + :class:`wagtail.core.models.Page` rather than a subclass.''' + # if the path is 4 digits, we're at the root, so there is no parent + if len(page.path) == 4: + return None + + # otherwise remove the final 4 digits to get the parent's id (pk) Page = apps.get_model('wagtailcore', 'Page') - return Page.objects.get(path=page.path[:4]) + return Page.objects.get(path=page.path[:-4]) # add_child utility method adapted from: # http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ @@ -16,28 +22,58 @@ def add_child(apps, parent_page, klass, **kwargs): '''Create a new draft wagtail page of type klass as a child of page instance parent_page, passing along kwargs to its create() function.''' + # add/set the correct content type for the new page ContentType = apps.get_model('contenttypes.ContentType') - page_content_type = ContentType.objects.get_or_create( model=klass.__name__.lower(), app_label=klass._meta.app_label, )[0] + # create the new page created_page = klass.objects.create( content_type=page_content_type, - path='%s00%02d' % (parent_page.path, parent_page.numchild + 1), + path='%s%04d' % (parent_page.path, parent_page.numchild + 1), depth=parent_page.depth + 1, numchild=0, live=False, # create as a draft so that URL is set correctly on publish **kwargs ) + # update the parent's child count parent_page.numchild += 1 parent_page.save() return created_page -# migration test case adapted from +# adapted from `copy_page_data_to_content_streamfield`: +# https://www.caktusgroup.com/blog/2019/09/12/wagtail-data-migrations/ +def html_to_streamfield(html): + # create and save a new RichTextBlock with the html content + # generate JSON referencing the block to store as a page's streamfield + return [ + {'type': 'body', 'value': [ + + ]} + ] + +# adapted from `copy_page_data_to_content_streamfield`: +# https://www.caktusgroup.com/blog/2019/09/12/wagtail-data-migrations/ +def create_revision(page, content): + # create the revision object + revision = { + 'fields': { + 'approved_go_live_at': None, # needs to become json null + # 'content_json': json.dumps(content), + # 'created_at': date, # current timestamp, or pass in? + 'page': page.id, + 'submitted_for_moderation': False, + # 'user': admin # get admin user or pass in? + } + } + # add the new revision to the page and return it + return page + +# adapted from `TestMigrations` # https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ # and from winthrop-django class TestMigrations(TestCase): diff --git a/cdhweb/pages/migrations/0002_homepage.py b/cdhweb/pages/migrations/0002_homepage.py index 74624de03..7f2d556ac 100644 --- a/cdhweb/pages/migrations/0002_homepage.py +++ b/cdhweb/pages/migrations/0002_homepage.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.db import migrations -from cdhweb.pages.migration_utilities import add_child +from cdhweb.pages.migration_utils import add_child def create_homepage(apps, schema_editor): diff --git a/cdhweb/pages/tests/test_migration_utils.py b/cdhweb/pages/tests/test_migration_utils.py new file mode 100644 index 000000000..2a6609e6e --- /dev/null +++ b/cdhweb/pages/tests/test_migration_utils.py @@ -0,0 +1,66 @@ +from django.apps import apps +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase +from wagtail.core.models import Page + +from cdhweb.pages.migration_utils import add_child, get_parent +from cdhweb.pages.models import ContentPage, HomePage + + +class TestGetParent(TestCase): + fixtures = ['sample_pages'] + + def test_root(self): + # should return None for root since it has no parent + root = Page.objects.get(title='Root') + self.assertIsNone(get_parent(apps, root)) + + def test_single_parent(self): + # should return root for first-level node + root = Page.objects.get(title='Root') + home = Page.objects.get(title='Home') + self.assertEqual(get_parent(apps, home), root) + + def test_nested(self): + # should return immediate parent for nested node + home = Page.objects.get(title='Home') + research = Page.objects.get(title='Research') + self.assertEqual(get_parent(apps, research), home) + + +class TestAddChild(TestCase): + fixtures = ['sample_pages'] + + def test_add_at_root(self): + # should add at the root with correct props and update root child count + root = Page.objects.get(title='Root') + home_type = ContentType.objects.get(model='homepage') + add_child(apps, root, HomePage, title='New Home') + new_home = HomePage.objects.get(title='New Home') + + self.assertEqual(HomePage.objects.count(), 2) # added second homepage + self.assertEqual(new_home.content_type, home_type) # correct type + self.assertEqual(new_home.live, False) # is draft + self.assertEqual(new_home.numchild, 0) # no children yet + self.assertEqual(new_home.path, '00010002') # correct path + self.assertEqual(new_home.title, 'New Home') # passed kwarg + self.assertEqual(new_home.get_parent(), root) # correct parent + self.assertEqual(root.numchild, 2) # root has 2 children + + def test_add_nested(self): + # should add nested with correct props and update parent child count + research = Page.objects.get(title='Research') + cp_type = ContentType.objects.get(model='contentpage') + add_child(apps, research, ContentPage, title='CDH Software') + software = ContentPage.objects.get(title='CDH Software') + + self.assertEqual(software.content_type, cp_type) # correct type + self.assertEqual(software.live, False) # is draft + self.assertEqual(software.numchild, 0) # no children yet + self.assertEqual(software.path, '0001000100010001') # correct path + self.assertEqual(software.title, 'CDH Software') # passed kwarg + self.assertEqual(software.get_parent(), research) # correct parent + self.assertEqual(research.numchild, 1) # parent has 1 child + + def test_with_kwargs(self): + pass diff --git a/cdhweb/pages/tests/test_migrations.py b/cdhweb/pages/tests/test_migrations.py index 33302a221..4661e0911 100644 --- a/cdhweb/pages/tests/test_migrations.py +++ b/cdhweb/pages/tests/test_migrations.py @@ -1,6 +1,6 @@ from django.apps import apps -from cdhweb.pages.migration_utilities import TestMigrations, get_parent +from cdhweb.pages.migration_utils import TestMigrations, get_parent class TestCreateHomepage(TestMigrations): From 52a33f518037ae93e037d87ac4cff390f4e1aa8e Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Thu, 9 Jan 2020 15:41:21 -0500 Subject: [PATCH 22/50] PEP8 comment style cleanup --- cdhweb/blog/views.py | 2 +- cdhweb/events/views.py | 2 +- cdhweb/pages/models.py | 6 +++--- cdhweb/people/tests.py | 6 ++---- cdhweb/projects/views.py | 2 +- 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/cdhweb/blog/views.py b/cdhweb/blog/views.py index 82a4dae2f..a0ce25adb 100644 --- a/cdhweb/blog/views.py +++ b/cdhweb/blog/views.py @@ -19,7 +19,7 @@ class BlogPostMixinView(object): def get_queryset(self): # use displayable manager to find published events only # (or draft profiles for logged in users with permission to view) - return BlogPost.objects.published() # TODO: published(for_user=self.request.user) + return BlogPost.objects.published() # TODO: published(for_user=self.request.user) class BlogPostArchiveMixin(BlogPostMixinView, LastModifiedListMixin): diff --git a/cdhweb/events/views.py b/cdhweb/events/views.py index 3dcb29eeb..f05e07f33 100644 --- a/cdhweb/events/views.py +++ b/cdhweb/events/views.py @@ -22,7 +22,7 @@ class EventMixinView(object): def get_queryset(self): '''use displayable manager to find published events only''' # (or draft profiles for logged in users with permission to view) - return Event.objects.published() # TODO: published(for_user=self.request.user) + return Event.objects.published() # TODO: published(for_user=self.request.user) class EventSemesterDates(object): diff --git a/cdhweb/pages/models.py b/cdhweb/pages/models.py index ccbdeb2aa..a6f947548 100644 --- a/cdhweb/pages/models.py +++ b/cdhweb/pages/models.py @@ -46,7 +46,7 @@ class PagePreviewDescriptionMixin(models.Model): # (tags are omitted by subsetting default ALLOWED_TAGS) #: allowed tags for bleach html stripping in description allowed_tags = list((set(bleach.sanitizer.ALLOWED_TAGS) - \ - set(['a', 'blockquote']))) # additional tags to remove + set(['a', 'blockquote']))) # additional tags to remove class Meta: abstract = True @@ -115,7 +115,7 @@ class LandingPage(Page): tagline = models.CharField(max_length=255) #: image that will be used for the header header_image = models.ForeignKey('wagtailimages.image', null=True, - blank=True, on_delete=models.SET_NULL, related_name='+') # no reverse relationship + blank=True, on_delete=models.SET_NULL, related_name='+') # no reverse relationship search_fields = Page.search_fields + [index.SearchField('body')] content_panels = Page.content_panels + [ @@ -137,7 +137,7 @@ class HomePage(Page): search_fields = Page.search_fields + [index.SearchField('body')] content_panels = Page.content_panels + [StreamFieldPanel('body')] - parent_page_types = [Page] # only root + parent_page_types = [Page] # only root subpage_types = ['LandingPage', 'ContentPage'] class Meta: diff --git a/cdhweb/people/tests.py b/cdhweb/people/tests.py index 9ab7c3463..7abafbad7 100644 --- a/cdhweb/people/tests.py +++ b/cdhweb/people/tests.py @@ -651,12 +651,10 @@ def test_profile_detail(self): coauth_post = staffer2.blogposts.first() self.assertContains(response, solo_post.title) # show both blog posts self.assertContains(response, solo_post.title) - # indicate that one post has another author - self.assertContains(response, staffer2.profile.title) + self.assertContains(response, staffer2.profile.title) # indicate that one post has another author response = self.client.get(staffer2.get_absolute_url()) - # only posts from this author - self.assertNotContains(response, solo_post.title) + self.assertNotContains(response, solo_post.title) # only posts from this author self.assertContains(response, coauth_post.title) self.assertContains(response, staffer.profile.title) diff --git a/cdhweb/projects/views.py b/cdhweb/projects/views.py index 5684461a6..e7dc984a5 100644 --- a/cdhweb/projects/views.py +++ b/cdhweb/projects/views.py @@ -15,7 +15,7 @@ class ProjectMixinView(object): def get_queryset(self): # use displayable manager to find published events only # (or draft profiles for logged in users with permission to view) - return Project.objects.published() # TODO: published(for_user=self.request.user) + return Project.objects.published() # TODO: published(for_user=self.request.user) class ProjectListMixinView(ProjectMixinView, ListView, LastModifiedListMixin): From 5af748112428c7e1e72eaeba8a74d3d99f727416 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Thu, 9 Jan 2020 15:41:50 -0500 Subject: [PATCH 23/50] implement create_revision, add logging to migration utils --- cdhweb/pages/migration_utils.py | 72 +++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/cdhweb/pages/migration_utils.py b/cdhweb/pages/migration_utils.py index 4117291fa..4fcc9fff4 100644 --- a/cdhweb/pages/migration_utils.py +++ b/cdhweb/pages/migration_utils.py @@ -1,13 +1,19 @@ -from django.test import TestCase -from django.db.migrations.executor import MigrationExecutor +import json +import logging +from datetime import datetime + from django.db import connection +from django.db.migrations.executor import MigrationExecutor +from django.test import TestCase +logger = logging.getLogger('wagtail.core') # see for an explanation of django-treebeard & the `path` attribute: # http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ def get_parent(apps, page): '''Find the parent of a wagtail page instance. Always returns the underlying :class:`wagtail.core.models.Page` rather than a subclass.''' + # if the path is 4 digits, we're at the root, so there is no parent if len(page.path) == 4: return None @@ -18,6 +24,8 @@ def get_parent(apps, page): # add_child utility method adapted from: # http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ +# see also: +# https://github.com/wagtail/wagtail/blob/stable/2.3.x/wagtail/core/models.py#L442 def add_child(apps, parent_page, klass, **kwargs): '''Create a new draft wagtail page of type klass as a child of page instance parent_page, passing along kwargs to its create() function.''' @@ -30,12 +38,12 @@ def add_child(apps, parent_page, klass, **kwargs): )[0] # create the new page - created_page = klass.objects.create( + page = klass.objects.create( content_type=page_content_type, path='%s%04d' % (parent_page.path, parent_page.numchild + 1), depth=parent_page.depth + 1, numchild=0, - live=False, # create as a draft so that URL is set correctly on publish + live=False, # create as a draft so that URL is set correctly on publish **kwargs ) @@ -43,7 +51,17 @@ def add_child(apps, parent_page, klass, **kwargs): parent_page.numchild += 1 parent_page.save() - return created_page + # log the creation and return the page + logger.info( + "Page created: \"%s\" id=%d content_type=%s.%s path=%s", + page.title, + page.id, + klass._meta.app_label, + klass.__name__, + page.url_path + ) + + return page # adapted from `copy_page_data_to_content_streamfield`: # https://www.caktusgroup.com/blog/2019/09/12/wagtail-data-migrations/ @@ -56,22 +74,34 @@ def html_to_streamfield(html): ]} ] -# adapted from `copy_page_data_to_content_streamfield`: -# https://www.caktusgroup.com/blog/2019/09/12/wagtail-data-migrations/ -def create_revision(page, content): +# adapted from wagtail's `save_revision`: +# https://github.com/wagtail/wagtail/blob/stable/2.3.x/wagtail/core/models.py#L631 +def create_revision(apps, page, content=[], user=None, created_at=datetime.now()): + '''Add a :class:`wagtail.core.models.PageRevision` to document changes to a + Page associated with a particular user and timestamp.''' + + PageRevision = apps.get_model('wagtailcore', 'PageRevision') + page.full_clean() + # create the revision object - revision = { - 'fields': { - 'approved_go_live_at': None, # needs to become json null - # 'content_json': json.dumps(content), - # 'created_at': date, # current timestamp, or pass in? - 'page': page.id, - 'submitted_for_moderation': False, - # 'user': admin # get admin user or pass in? - } - } - # add the new revision to the page and return it - return page + revision = PageRevision.create( + content_json=json.dumps(content), + user=user, + submitted_for_moderation=False, + approved_go_live_at=None, + created_at=created_at + ) + + # update and save the page + page.latest_revision_created_at = created_at + page.draft_title = page.title + page.has_unpublished_changes = True + + # log the revision and return it + logger.info("Page edited: \"%s\" id=%d revision_id=%d", page.title, + page.id, revision.id) + + return revision # adapted from `TestMigrations` # https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ @@ -101,4 +131,4 @@ def setUp(self): self.apps = executor.loader.project_state(self.migrate_to).apps def setUpBeforeMigration(self, apps): - pass \ No newline at end of file + pass From c80d02d962469720d02d4694760f574ba5d0c629 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Thu, 9 Jan 2020 16:36:49 -0500 Subject: [PATCH 24/50] add wagtail site to sample pages fixture --- cdhweb/pages/fixtures/sample_pages.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cdhweb/pages/fixtures/sample_pages.json b/cdhweb/pages/fixtures/sample_pages.json index 3eb949f39..8a3288520 100644 --- a/cdhweb/pages/fixtures/sample_pages.json +++ b/cdhweb/pages/fixtures/sample_pages.json @@ -98,5 +98,15 @@ "body": "[]", "header_image": null } + }, + { + "model": "wagtailcore.site", + "fields": { + "hostname": "localhost", + "port": 8000, + "site_name": "test", + "root_page": 2, + "is_default_site": true + } } ] \ No newline at end of file From 3947ff542ee93c7fb7c38b4a831474104e46e13d Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Thu, 9 Jan 2020 17:01:57 -0500 Subject: [PATCH 25/50] fix homepage creation migration test --- cdhweb/pages/tests/test_migrations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cdhweb/pages/tests/test_migrations.py b/cdhweb/pages/tests/test_migrations.py index 4661e0911..497d18e28 100644 --- a/cdhweb/pages/tests/test_migrations.py +++ b/cdhweb/pages/tests/test_migrations.py @@ -33,7 +33,6 @@ def test_site_root_page(self): HomePage = self.apps.get_model('cdhpages', 'HomePage') home = HomePage.objects.first() site = Site.objects.first() - self.assertEqual(site.root_page, home) self.assertEqual(site.root_page_id, home.id) From 026da5c3203a2e4db7b3e98737cdc5818631c6c1 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Thu, 9 Jan 2020 17:02:12 -0500 Subject: [PATCH 26/50] add page_to_dict helper and update create_revision --- cdhweb/pages/migration_utils.py | 47 +++++++++++---- cdhweb/pages/tests/test_migration_utils.py | 66 ++++++++++++++++------ 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/cdhweb/pages/migration_utils.py b/cdhweb/pages/migration_utils.py index 4fcc9fff4..8fe43cb80 100644 --- a/cdhweb/pages/migration_utils.py +++ b/cdhweb/pages/migration_utils.py @@ -63,25 +63,50 @@ def add_child(apps, parent_page, klass, **kwargs): return page -# adapted from `copy_page_data_to_content_streamfield`: -# https://www.caktusgroup.com/blog/2019/09/12/wagtail-data-migrations/ -def html_to_streamfield(html): - # create and save a new RichTextBlock with the html content - # generate JSON referencing the block to store as a page's streamfield - return [ - {'type': 'body', 'value': [ - ]} - ] +def page_to_dict(page): + return { + 'pk': page.pk, + 'live': page.live, + 'slug': page.slug, + 'path': page.path, + 'title': page.title, + 'depth': page.depth, + 'owner': page.owner, + 'locked': page.locked, + 'expired': page.expired, + 'numchild': page.numchild, + 'url_path': page.url_path, + 'expire_at': page.expire_at, + 'seo_title': page.seo_title, + 'go_live_at': page.go_live_at, + 'draft_title': page.draft_title, + 'content_type': page.content_type, + 'show_in_menus': page.show_in_menus, + 'live_revision': page.live_revision, + 'last_published_at': page.last_published_at, + 'search_description': page.search_description, + 'first_published_at': page.first_published_at, + 'has_unpublished_changes': page.has_unpublished_changes, + 'latest_revision_created_at': page.latest_revision_created_at, + } # adapted from wagtail's `save_revision`: # https://github.com/wagtail/wagtail/blob/stable/2.3.x/wagtail/core/models.py#L631 -def create_revision(apps, page, content=[], user=None, created_at=datetime.now()): +def create_revision(apps, page, user=None, created_at=datetime.now(), **kwargs): '''Add a :class:`wagtail.core.models.PageRevision` to document changes to a Page associated with a particular user and timestamp.''' PageRevision = apps.get_model('wagtailcore', 'PageRevision') - page.full_clean() + + # serialize the page's current state and apply changes from kwargs + content = page_to_dict(page) + for (key, value) in kwargs.items(): + try: + page[key] = value + content[key] = value + except (KeyError, AttributeError): + continue # create the revision object revision = PageRevision.create( diff --git a/cdhweb/pages/tests/test_migration_utils.py b/cdhweb/pages/tests/test_migration_utils.py index 2a6609e6e..89d3bea89 100644 --- a/cdhweb/pages/tests/test_migration_utils.py +++ b/cdhweb/pages/tests/test_migration_utils.py @@ -1,9 +1,9 @@ from django.apps import apps from django.contrib.contenttypes.models import ContentType from django.test import TestCase -from wagtail.core.models import Page +from wagtail.core.models import Page, PageRevision -from cdhweb.pages.migration_utils import add_child, get_parent +from cdhweb.pages.migration_utils import add_child, get_parent, create_revision from cdhweb.pages.models import ContentPage, HomePage @@ -38,14 +38,14 @@ def test_add_at_root(self): add_child(apps, root, HomePage, title='New Home') new_home = HomePage.objects.get(title='New Home') - self.assertEqual(HomePage.objects.count(), 2) # added second homepage - self.assertEqual(new_home.content_type, home_type) # correct type - self.assertEqual(new_home.live, False) # is draft - self.assertEqual(new_home.numchild, 0) # no children yet - self.assertEqual(new_home.path, '00010002') # correct path - self.assertEqual(new_home.title, 'New Home') # passed kwarg - self.assertEqual(new_home.get_parent(), root) # correct parent - self.assertEqual(root.numchild, 2) # root has 2 children + self.assertEqual(HomePage.objects.count(), 2) # added second homepage + self.assertEqual(new_home.content_type, home_type) # correct type + self.assertEqual(new_home.live, False) # is draft + self.assertEqual(new_home.numchild, 0) # no children yet + self.assertEqual(new_home.path, '00010002') # correct path + self.assertEqual(new_home.title, 'New Home') # passed kwarg + self.assertEqual(new_home.get_parent(), root) # correct parent + self.assertEqual(root.numchild, 2) # root has 2 children def test_add_nested(self): # should add nested with correct props and update parent child count @@ -54,13 +54,45 @@ def test_add_nested(self): add_child(apps, research, ContentPage, title='CDH Software') software = ContentPage.objects.get(title='CDH Software') - self.assertEqual(software.content_type, cp_type) # correct type - self.assertEqual(software.live, False) # is draft - self.assertEqual(software.numchild, 0) # no children yet - self.assertEqual(software.path, '0001000100010001') # correct path - self.assertEqual(software.title, 'CDH Software') # passed kwarg - self.assertEqual(software.get_parent(), research) # correct parent - self.assertEqual(research.numchild, 1) # parent has 1 child + self.assertEqual(software.content_type, cp_type) # correct type + self.assertEqual(software.live, False) # is draft + self.assertEqual(software.numchild, 0) # no children yet + self.assertEqual(software.path, '0001000100010001') # correct path + self.assertEqual(software.title, 'CDH Software') # passed kwarg + self.assertEqual(software.get_parent(), research) # correct parent + self.assertEqual(research.numchild, 1) # parent has 1 child def test_with_kwargs(self): pass + + +def TestCreateRevision(TestCase): + fixtures = ['sample_pages'] + + def test_create(self): + # create an empty revision of the homepage + home = Page.objects.get(title='Home') + revision = create_revision(apps, home) + self.assertEqual(home.revisions.count(), 1) # now has 1 revision + + def test_update_page(self): + # check that the page associated with the revision is updated + home = Page.objects.get(title='Home') + revision = create_revision(apps, home) + self.assertEqual(home.latest_revision_created_at, revision.created_at) + self.assertTrue(home.has_unpublished_changes) + + def test_content(self): + # check that the provided content is included in the revision + home = Page.objects.get(title='Home') + revision = create_revision(apps, home, content=[]) + pass + + def test_user(self): + pass + + def test_timestamp(self): + pass + + def test_logging(self): + pass \ No newline at end of file From 723dc2bad46bc0ea0bc8afb4bafad8459506ebf5 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Thu, 23 Jan 2020 16:21:13 -0500 Subject: [PATCH 27/50] improve migration_utils and tests --- cdhweb/pages/migration_utils.py | 55 ++++++---------------- cdhweb/pages/migrations/0002_homepage.py | 7 ++- cdhweb/pages/tests/test_migration_utils.py | 49 ++++++++++++++----- cdhweb/pages/tests/test_migrations.py | 17 ++++++- 4 files changed, 70 insertions(+), 58 deletions(-) diff --git a/cdhweb/pages/migration_utils.py b/cdhweb/pages/migration_utils.py index 8fe43cb80..af04bd70d 100644 --- a/cdhweb/pages/migration_utils.py +++ b/cdhweb/pages/migration_utils.py @@ -2,6 +2,7 @@ import logging from datetime import datetime +from django.core.serializers import serialize from django.db import connection from django.db.migrations.executor import MigrationExecutor from django.test import TestCase @@ -63,64 +64,36 @@ def add_child(apps, parent_page, klass, **kwargs): return page - -def page_to_dict(page): - return { - 'pk': page.pk, - 'live': page.live, - 'slug': page.slug, - 'path': page.path, - 'title': page.title, - 'depth': page.depth, - 'owner': page.owner, - 'locked': page.locked, - 'expired': page.expired, - 'numchild': page.numchild, - 'url_path': page.url_path, - 'expire_at': page.expire_at, - 'seo_title': page.seo_title, - 'go_live_at': page.go_live_at, - 'draft_title': page.draft_title, - 'content_type': page.content_type, - 'show_in_menus': page.show_in_menus, - 'live_revision': page.live_revision, - 'last_published_at': page.last_published_at, - 'search_description': page.search_description, - 'first_published_at': page.first_published_at, - 'has_unpublished_changes': page.has_unpublished_changes, - 'latest_revision_created_at': page.latest_revision_created_at, - } - # adapted from wagtail's `save_revision`: # https://github.com/wagtail/wagtail/blob/stable/2.3.x/wagtail/core/models.py#L631 def create_revision(apps, page, user=None, created_at=datetime.now(), **kwargs): '''Add a :class:`wagtail.core.models.PageRevision` to document changes to a Page associated with a particular user and timestamp.''' - PageRevision = apps.get_model('wagtailcore', 'PageRevision') - - # serialize the page's current state and apply changes from kwargs - content = page_to_dict(page) + # apply provided kwargs as changes to the page, if any for (key, value) in kwargs.items(): - try: - page[key] = value - content[key] = value - except (KeyError, AttributeError): - continue - - # create the revision object - revision = PageRevision.create( - content_json=json.dumps(content), + setattr(page, key, value) + + # serialize current page state as content of a new revision + content_json = serialize('json', [page]) + + # create the revision object and save it + PageRevision = apps.get_model('wagtailcore', 'PageRevision') + revision = PageRevision( + page_id=page.id, + content_json=content_json, user=user, submitted_for_moderation=False, approved_go_live_at=None, created_at=created_at ) + revision.save() # update and save the page page.latest_revision_created_at = created_at page.draft_title = page.title page.has_unpublished_changes = True + page.save() # log the revision and return it logger.info("Page edited: \"%s\" id=%d revision_id=%d", page.title, diff --git a/cdhweb/pages/migrations/0002_homepage.py b/cdhweb/pages/migrations/0002_homepage.py index 7f2d556ac..0461ac523 100644 --- a/cdhweb/pages/migrations/0002_homepage.py +++ b/cdhweb/pages/migrations/0002_homepage.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.db import migrations -from cdhweb.pages.migration_utils import add_child +from cdhweb.pages.migration_utils import add_child, create_revision def create_homepage(apps, schema_editor): @@ -25,7 +25,10 @@ def create_homepage(apps, schema_editor): # create the new homepage underneath site root root = Page.objects.get(title='Root') - home = add_child(apps, root, HomePage, title='Home', body=content) + home = add_child(apps, root, HomePage, title='Home') + + # create a new revision for the page to apply content & mark migration + create_revision(apps, home, content=content) # point the default site at the new homepage site = Site.objects.first() diff --git a/cdhweb/pages/tests/test_migration_utils.py b/cdhweb/pages/tests/test_migration_utils.py index 89d3bea89..9b5468bfd 100644 --- a/cdhweb/pages/tests/test_migration_utils.py +++ b/cdhweb/pages/tests/test_migration_utils.py @@ -1,9 +1,13 @@ +import json +from datetime import datetime + from django.apps import apps +from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.test import TestCase from wagtail.core.models import Page, PageRevision -from cdhweb.pages.migration_utils import add_child, get_parent, create_revision +from cdhweb.pages.migration_utils import add_child, create_revision, get_parent from cdhweb.pages.models import ContentPage, HomePage @@ -66,14 +70,17 @@ def test_with_kwargs(self): pass -def TestCreateRevision(TestCase): +class TestCreateRevision(TestCase): fixtures = ['sample_pages'] def test_create(self): - # create an empty revision of the homepage + # create some empty revisions of the homepage home = Page.objects.get(title='Home') - revision = create_revision(apps, home) + rev1 = create_revision(apps, home) self.assertEqual(home.revisions.count(), 1) # now has 1 revision + rev2 = create_revision(apps, home) + self.assertEqual(home.revisions.count(), 2) # now 2 revisions + self.assertEqual(home.latest_revision_created_at, rev2.created_at) def test_update_page(self): # check that the page associated with the revision is updated @@ -82,17 +89,33 @@ def test_update_page(self): self.assertEqual(home.latest_revision_created_at, revision.created_at) self.assertTrue(home.has_unpublished_changes) - def test_content(self): - # check that the provided content is included in the revision - home = Page.objects.get(title='Home') - revision = create_revision(apps, home, content=[]) - pass - def test_user(self): - pass + # check creating a revision associated with an arbitrary user + research = Page.objects.get(title='Research') + bob = User.objects.create_user('bob', 'bob@example.com', 'password') + create_revision(apps, research, user=bob) + revision = research.get_latest_revision() + self.assertEqual(revision.user, bob) def test_timestamp(self): - pass + # check creating a revision with an arbitrary creation date + research = Page.objects.get(title='Research') + old_date = datetime(1991, 12, 1) + revision = create_revision(apps, research, created_at=old_date) + self.assertEqual(research.latest_revision_created_at, old_date) def test_logging(self): - pass \ No newline at end of file + # check that the newly created revision is logged + pass + + def test_content(self): + # check that the provided page content is included in the revision + # NOTE the actual fields that store page content (in this case 'body') + # are usually defined on the model that inherits `Page`, not `Page`! + research = Page.objects.get(title='Research') + content = [] + create_revision(apps, research, body=json.dumps(content)) + revision = research.get_latest_revision_as_page() + self.assertEqual(revision.body, content) + pass + diff --git a/cdhweb/pages/tests/test_migrations.py b/cdhweb/pages/tests/test_migrations.py index 497d18e28..495556dbc 100644 --- a/cdhweb/pages/tests/test_migrations.py +++ b/cdhweb/pages/tests/test_migrations.py @@ -1,6 +1,7 @@ from django.apps import apps from cdhweb.pages.migration_utils import TestMigrations, get_parent +from mezzanine.pages.models import RichTextPage class TestCreateHomepage(TestMigrations): @@ -44,8 +45,20 @@ class TestMigrateHomepage(TestMigrations): def setUpBeforeMigration(self, apps): # create a mezzanine home page with content - pass + content = """ +

The Center for Digital Humanities is a DH research center.

+ """ + # RichTextPage = apps.get_model('pages', 'RichTextPage') + RichTextPage.objects.create(title='Home', content=content) + + def test_delete_mezzanine_home(self): + # mezzanine page should be deleted + RichTextPage = apps.get_model('pages', 'RichTextPage') + with self.assertRaises(RichTextPage.DoesNotExist): + old_home = RichTextPage.objects.get(title='Home') def test_migrate_homepage(self): # new HomePage should have migrated mezzanine content - pass + HomePage = self.apps.get_model('cdhpages', 'HomePage') + self.assertEqual(HomePage.body, 'The Center for Digital Humanities is \ + a DH research center.') From 3883d3ac0e71e476fce8317b0c4e875175ae411d Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Tue, 28 Jan 2020 09:30:02 -0500 Subject: [PATCH 28/50] add utils for converting btw. html and StreamField --- cdhweb/pages/migration_utils.py | 28 +++++++++++++++++++--- cdhweb/pages/tests/test_migration_utils.py | 20 +++++++++------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/cdhweb/pages/migration_utils.py b/cdhweb/pages/migration_utils.py index af04bd70d..55d2e7ab0 100644 --- a/cdhweb/pages/migration_utils.py +++ b/cdhweb/pages/migration_utils.py @@ -13,7 +13,8 @@ # http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ def get_parent(apps, page): '''Find the parent of a wagtail page instance. Always returns the underlying - :class:`wagtail.core.models.Page` rather than a subclass.''' + :class:`wagtail.core.models.Page` rather than a subclass. + ''' # if the path is 4 digits, we're at the root, so there is no parent if len(page.path) == 4: @@ -29,7 +30,8 @@ def get_parent(apps, page): # https://github.com/wagtail/wagtail/blob/stable/2.3.x/wagtail/core/models.py#L442 def add_child(apps, parent_page, klass, **kwargs): '''Create a new draft wagtail page of type klass as a child of page instance - parent_page, passing along kwargs to its create() function.''' + parent_page, passing along kwargs to its create() function. + ''' # add/set the correct content type for the new page ContentType = apps.get_model('contenttypes.ContentType') @@ -68,7 +70,8 @@ def add_child(apps, parent_page, klass, **kwargs): # https://github.com/wagtail/wagtail/blob/stable/2.3.x/wagtail/core/models.py#L631 def create_revision(apps, page, user=None, created_at=datetime.now(), **kwargs): '''Add a :class:`wagtail.core.models.PageRevision` to document changes to a - Page associated with a particular user and timestamp.''' + Page associated with a particular user and timestamp. + ''' # apply provided kwargs as changes to the page, if any for (key, value) in kwargs.items(): @@ -101,6 +104,25 @@ def create_revision(apps, page, user=None, created_at=datetime.now(), **kwargs): return revision +# adapted from wagtail docs on migrating to `StreamField`: +# https://docs.wagtail.io/en/v2.5/topics/streamfield.html#migrating-richtextfields-to-streamfield +def html_to_streamfield(apps, content): + '''Convert an HTML string of rich-text content into the contents of a + :class:`wagtail.core.fields.StreamField`.''' + BodyContentBlock = apps.get_model('cdhpages', 'BodyContentBlock') + return [('rich_text', RichText(content))] + +# adapted from wagtail docs on migrating to `StreamField`: +# https://docs.wagtail.io/en/v2.5/topics/streamfield.html#migrating-richtextfields-to-streamfield +def streamfield_to_html(field): + '''Concatenate the HTML source of all rich text blocks contained in a + :class:`wagtail.core.fields.StreamField` and return as a string.''' + if field.raw_text is None: + return ''.join([ + block.value.source for block in field if block.block_type == 'rich_text' + ]) + return field.raw_text + # adapted from `TestMigrations` # https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ # and from winthrop-django diff --git a/cdhweb/pages/tests/test_migration_utils.py b/cdhweb/pages/tests/test_migration_utils.py index 9b5468bfd..e15154c87 100644 --- a/cdhweb/pages/tests/test_migration_utils.py +++ b/cdhweb/pages/tests/test_migration_utils.py @@ -7,8 +7,10 @@ from django.test import TestCase from wagtail.core.models import Page, PageRevision -from cdhweb.pages.migration_utils import add_child, create_revision, get_parent -from cdhweb.pages.models import ContentPage, HomePage +from cdhweb.pages.migration_utils import (add_child, create_revision, + get_parent, html_to_streamfield, + streamfield_to_html) +from cdhweb.pages.models import ContentPage, HomePage, LandingPage class TestGetParent(TestCase): @@ -112,10 +114,12 @@ def test_content(self): # check that the provided page content is included in the revision # NOTE the actual fields that store page content (in this case 'body') # are usually defined on the model that inherits `Page`, not `Page`! - research = Page.objects.get(title='Research') - content = [] - create_revision(apps, research, body=json.dumps(content)) + research = LandingPage.objects.get(title='Research') + content = """ +

Here is a heading

+

Some basic paragraph content

+
  1. list
  2. items
+ """ + create_revision(apps, research, body=html_to_streamfield(content)) revision = research.get_latest_revision_as_page() - self.assertEqual(revision.body, content) - pass - + self.assertEqual(streamfield_to_html(revision.body), content) From c990005bf08202d4be1c720d7dbccb7b7bcb33dd Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Tue, 15 Dec 2020 16:18:32 -0500 Subject: [PATCH 29/50] Proof-of-concept exodus script --- cdhweb/pages/apps.py | 2 +- cdhweb/pages/management/commands/exodus.py | 51 ++++++ cdhweb/pages/migration_utils.py | 154 ------------------ cdhweb/pages/migrations/0002_homepage.py | 74 --------- .../{pages => cdhpages}/home_page.html | 0 cdhweb/pages/tests/test_migration_utils.py | 125 -------------- cdhweb/pages/tests/test_migrations.py | 64 -------- cdhweb/urls.py | 12 +- 8 files changed, 58 insertions(+), 424 deletions(-) create mode 100644 cdhweb/pages/management/commands/exodus.py delete mode 100644 cdhweb/pages/migration_utils.py delete mode 100644 cdhweb/pages/migrations/0002_homepage.py rename cdhweb/pages/templates/{pages => cdhpages}/home_page.html (100%) delete mode 100644 cdhweb/pages/tests/test_migration_utils.py delete mode 100644 cdhweb/pages/tests/test_migrations.py diff --git a/cdhweb/pages/apps.py b/cdhweb/pages/apps.py index 3345e8bbb..15c3864ab 100644 --- a/cdhweb/pages/apps.py +++ b/cdhweb/pages/apps.py @@ -6,4 +6,4 @@ class PagesConfig(AppConfig): # NOTE we have to relabel this to avoid a conflict with mezzanine's pages; # using anything with a '.' in the name will confuse the RegexResolver and # result in NoReverseMatch errors when adding pages. - label = 'cdhpages' \ No newline at end of file + label = 'cdhpages' diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py new file mode 100644 index 000000000..d71a17eb2 --- /dev/null +++ b/cdhweb/pages/management/commands/exodus.py @@ -0,0 +1,51 @@ +"""Convert mezzanine-based pages to wagtail page models.""" + +import json + +from django.core.management.base import BaseCommand, CommandError +from cdhweb.pages.models import HomePage, LandingPage, ContentPage +from cdhweb.resources.models import LandingPage as OldLandingPage +from mezzanine.pages.models import Page as MezzaninePage +from wagtail.core.blocks import RichTextBlock + + +class Command(BaseCommand): + help = __file__.__doc__ + + def add_arguments(self, parser): + pass + + def handle(self, *args, **options): + homepage = HomePage.objects.get() + + for olp in OldLandingPage.objects.all(): + lp = LandingPage( + title=olp.title, + tagline=olp.tagline, + slug=olp.slug, + seo_title=olp._meta_title or olp.title, + body=json.dumps([{ + "type": "paragraph", + "value": olp.content, + }]), + # Only store description if it's not auto-generated + search_description=olp.description, + first_published_at=olp.created, + last_published_at=olp.updated, + # TODO not dealing with images yet + # TODO not setting parent/child hierarchy yet + # TODO not setting menu placement yet + # TODO search keywords? + # NOTE not login-restricting pages since we don't use it + ) + # TODO create a revision that sets the body content of the page + # TODO set the correct visibility status for the new revision + # TODO set the correct publication date for the new revision + # TODO set the created date for the new revision + # TODO set the last updated date for the new revision + # NOTE not setting expiry date; handled manually + # NOTE inclusion in sitemap being handled by sitemap itself + # NOTE set has_unpublished_changes on page? + homepage.add_child(instance=lp) + + homepage.save() diff --git a/cdhweb/pages/migration_utils.py b/cdhweb/pages/migration_utils.py deleted file mode 100644 index 55d2e7ab0..000000000 --- a/cdhweb/pages/migration_utils.py +++ /dev/null @@ -1,154 +0,0 @@ -import json -import logging -from datetime import datetime - -from django.core.serializers import serialize -from django.db import connection -from django.db.migrations.executor import MigrationExecutor -from django.test import TestCase - -logger = logging.getLogger('wagtail.core') - -# see for an explanation of django-treebeard & the `path` attribute: -# http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ -def get_parent(apps, page): - '''Find the parent of a wagtail page instance. Always returns the underlying - :class:`wagtail.core.models.Page` rather than a subclass. - ''' - - # if the path is 4 digits, we're at the root, so there is no parent - if len(page.path) == 4: - return None - - # otherwise remove the final 4 digits to get the parent's id (pk) - Page = apps.get_model('wagtailcore', 'Page') - return Page.objects.get(path=page.path[:-4]) - -# add_child utility method adapted from: -# http://www.agilosoftware.com/blog/django-treebard-and-wagtail-page-creation/ -# see also: -# https://github.com/wagtail/wagtail/blob/stable/2.3.x/wagtail/core/models.py#L442 -def add_child(apps, parent_page, klass, **kwargs): - '''Create a new draft wagtail page of type klass as a child of page instance - parent_page, passing along kwargs to its create() function. - ''' - - # add/set the correct content type for the new page - ContentType = apps.get_model('contenttypes.ContentType') - page_content_type = ContentType.objects.get_or_create( - model=klass.__name__.lower(), - app_label=klass._meta.app_label, - )[0] - - # create the new page - page = klass.objects.create( - content_type=page_content_type, - path='%s%04d' % (parent_page.path, parent_page.numchild + 1), - depth=parent_page.depth + 1, - numchild=0, - live=False, # create as a draft so that URL is set correctly on publish - **kwargs - ) - - # update the parent's child count - parent_page.numchild += 1 - parent_page.save() - - # log the creation and return the page - logger.info( - "Page created: \"%s\" id=%d content_type=%s.%s path=%s", - page.title, - page.id, - klass._meta.app_label, - klass.__name__, - page.url_path - ) - - return page - -# adapted from wagtail's `save_revision`: -# https://github.com/wagtail/wagtail/blob/stable/2.3.x/wagtail/core/models.py#L631 -def create_revision(apps, page, user=None, created_at=datetime.now(), **kwargs): - '''Add a :class:`wagtail.core.models.PageRevision` to document changes to a - Page associated with a particular user and timestamp. - ''' - - # apply provided kwargs as changes to the page, if any - for (key, value) in kwargs.items(): - setattr(page, key, value) - - # serialize current page state as content of a new revision - content_json = serialize('json', [page]) - - # create the revision object and save it - PageRevision = apps.get_model('wagtailcore', 'PageRevision') - revision = PageRevision( - page_id=page.id, - content_json=content_json, - user=user, - submitted_for_moderation=False, - approved_go_live_at=None, - created_at=created_at - ) - revision.save() - - # update and save the page - page.latest_revision_created_at = created_at - page.draft_title = page.title - page.has_unpublished_changes = True - page.save() - - # log the revision and return it - logger.info("Page edited: \"%s\" id=%d revision_id=%d", page.title, - page.id, revision.id) - - return revision - -# adapted from wagtail docs on migrating to `StreamField`: -# https://docs.wagtail.io/en/v2.5/topics/streamfield.html#migrating-richtextfields-to-streamfield -def html_to_streamfield(apps, content): - '''Convert an HTML string of rich-text content into the contents of a - :class:`wagtail.core.fields.StreamField`.''' - BodyContentBlock = apps.get_model('cdhpages', 'BodyContentBlock') - return [('rich_text', RichText(content))] - -# adapted from wagtail docs on migrating to `StreamField`: -# https://docs.wagtail.io/en/v2.5/topics/streamfield.html#migrating-richtextfields-to-streamfield -def streamfield_to_html(field): - '''Concatenate the HTML source of all rich text blocks contained in a - :class:`wagtail.core.fields.StreamField` and return as a string.''' - if field.raw_text is None: - return ''.join([ - block.value.source for block in field if block.block_type == 'rich_text' - ]) - return field.raw_text - -# adapted from `TestMigrations` -# https://www.caktusgroup.com/blog/2016/02/02/writing-unit-tests-django-migrations/ -# and from winthrop-django -class TestMigrations(TestCase): - - app = None - migrate_from = None - migrate_to = None - - def setUp(self): - assert self.migrate_from and self.migrate_to, \ - "TestCase '{}' must define migrate_from and migrate_to properties".format(type(self).__name__) - self.migrate_from = [(self.app, self.migrate_from)] - self.migrate_to = [(self.app, self.migrate_to)] - executor = MigrationExecutor(connection) - old_apps = executor.loader.project_state(self.migrate_from).apps - - # Reverse to the original migration - executor.migrate(self.migrate_from) - self.setUpBeforeMigration(old_apps) - - # Run the migration to test - executor.loader.build_graph() # reload. - executor.migrate(self.migrate_to) - - self.apps = executor.loader.project_state(self.migrate_to).apps - - def setUpBeforeMigration(self, apps): - pass diff --git a/cdhweb/pages/migrations/0002_homepage.py b/cdhweb/pages/migrations/0002_homepage.py deleted file mode 100644 index 0461ac523..000000000 --- a/cdhweb/pages/migrations/0002_homepage.py +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.27 on 2020-01-08 19:59 -from __future__ import unicode_literals - -from django.db import migrations -from cdhweb.pages.migration_utils import add_child, create_revision - - -def create_homepage(apps, schema_editor): - '''Create a new wagtail HomePage with any existing content from an old - Mezzanine home page, and delete Wagtail's default welcome page. Point the - default Wagtail site at the new HomePage.''' - - Page = apps.get_model('wagtailcore', 'Page') - Site = apps.get_model('wagtailcore', 'Site') - HomePage = apps.get_model('cdhpages', 'HomePage') - - # check for an existing mezzanine 'home' page and save its content - try: - RichTextPage = apps.get_model('pages', 'RichTextPage') - old_home = RichTextPage.objects.get(title='Home') - content = old_home.content - except (LookupError, RichTextPage.DoesNotExist): - content = '' - - # create the new homepage underneath site root - root = Page.objects.get(title='Root') - home = add_child(apps, root, HomePage, title='Home') - - # create a new revision for the page to apply content & mark migration - create_revision(apps, home, content=content) - - # point the default site at the new homepage - site = Site.objects.first() - site.root_page = home - site.root_page_id = home.id - site.save() - - # delete the default welcome page - welcome = Page.objects.get(title='Welcome to your new Wagtail site!') - welcome.delete() - - -def revert_homepage(apps, schema_editor): - '''Delete the created wagtail HomePage and replace with a placeholder - welcome page. Point the default wagtail site back at the welcome page.''' - - Page = apps.get_model('wagtailcore', 'Page') - Site = apps.get_model('wagtailcore', 'Site') - HomePage = apps.get_model('cdhpages', 'HomePage') - - # create a welcome page, since at least one child of root is required - root = Page.objects.get(title='Root') - welcome = add_child(apps, root, Page, title='Welcome to your new Wagtail site!') - - # point the default site at the welcome page - site = Site.objects.first() - site.root_page = welcome - site.root_page_id = welcome.id - site.save() - - # delete the created HomePage - HomePage.objects.first().delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('cdhpages', '0001_initial') - ] - - operations = [ - migrations.RunPython(create_homepage, reverse_code=revert_homepage) - ] \ No newline at end of file diff --git a/cdhweb/pages/templates/pages/home_page.html b/cdhweb/pages/templates/cdhpages/home_page.html similarity index 100% rename from cdhweb/pages/templates/pages/home_page.html rename to cdhweb/pages/templates/cdhpages/home_page.html diff --git a/cdhweb/pages/tests/test_migration_utils.py b/cdhweb/pages/tests/test_migration_utils.py deleted file mode 100644 index e15154c87..000000000 --- a/cdhweb/pages/tests/test_migration_utils.py +++ /dev/null @@ -1,125 +0,0 @@ -import json -from datetime import datetime - -from django.apps import apps -from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType -from django.test import TestCase -from wagtail.core.models import Page, PageRevision - -from cdhweb.pages.migration_utils import (add_child, create_revision, - get_parent, html_to_streamfield, - streamfield_to_html) -from cdhweb.pages.models import ContentPage, HomePage, LandingPage - - -class TestGetParent(TestCase): - fixtures = ['sample_pages'] - - def test_root(self): - # should return None for root since it has no parent - root = Page.objects.get(title='Root') - self.assertIsNone(get_parent(apps, root)) - - def test_single_parent(self): - # should return root for first-level node - root = Page.objects.get(title='Root') - home = Page.objects.get(title='Home') - self.assertEqual(get_parent(apps, home), root) - - def test_nested(self): - # should return immediate parent for nested node - home = Page.objects.get(title='Home') - research = Page.objects.get(title='Research') - self.assertEqual(get_parent(apps, research), home) - - -class TestAddChild(TestCase): - fixtures = ['sample_pages'] - - def test_add_at_root(self): - # should add at the root with correct props and update root child count - root = Page.objects.get(title='Root') - home_type = ContentType.objects.get(model='homepage') - add_child(apps, root, HomePage, title='New Home') - new_home = HomePage.objects.get(title='New Home') - - self.assertEqual(HomePage.objects.count(), 2) # added second homepage - self.assertEqual(new_home.content_type, home_type) # correct type - self.assertEqual(new_home.live, False) # is draft - self.assertEqual(new_home.numchild, 0) # no children yet - self.assertEqual(new_home.path, '00010002') # correct path - self.assertEqual(new_home.title, 'New Home') # passed kwarg - self.assertEqual(new_home.get_parent(), root) # correct parent - self.assertEqual(root.numchild, 2) # root has 2 children - - def test_add_nested(self): - # should add nested with correct props and update parent child count - research = Page.objects.get(title='Research') - cp_type = ContentType.objects.get(model='contentpage') - add_child(apps, research, ContentPage, title='CDH Software') - software = ContentPage.objects.get(title='CDH Software') - - self.assertEqual(software.content_type, cp_type) # correct type - self.assertEqual(software.live, False) # is draft - self.assertEqual(software.numchild, 0) # no children yet - self.assertEqual(software.path, '0001000100010001') # correct path - self.assertEqual(software.title, 'CDH Software') # passed kwarg - self.assertEqual(software.get_parent(), research) # correct parent - self.assertEqual(research.numchild, 1) # parent has 1 child - - def test_with_kwargs(self): - pass - - -class TestCreateRevision(TestCase): - fixtures = ['sample_pages'] - - def test_create(self): - # create some empty revisions of the homepage - home = Page.objects.get(title='Home') - rev1 = create_revision(apps, home) - self.assertEqual(home.revisions.count(), 1) # now has 1 revision - rev2 = create_revision(apps, home) - self.assertEqual(home.revisions.count(), 2) # now 2 revisions - self.assertEqual(home.latest_revision_created_at, rev2.created_at) - - def test_update_page(self): - # check that the page associated with the revision is updated - home = Page.objects.get(title='Home') - revision = create_revision(apps, home) - self.assertEqual(home.latest_revision_created_at, revision.created_at) - self.assertTrue(home.has_unpublished_changes) - - def test_user(self): - # check creating a revision associated with an arbitrary user - research = Page.objects.get(title='Research') - bob = User.objects.create_user('bob', 'bob@example.com', 'password') - create_revision(apps, research, user=bob) - revision = research.get_latest_revision() - self.assertEqual(revision.user, bob) - - def test_timestamp(self): - # check creating a revision with an arbitrary creation date - research = Page.objects.get(title='Research') - old_date = datetime(1991, 12, 1) - revision = create_revision(apps, research, created_at=old_date) - self.assertEqual(research.latest_revision_created_at, old_date) - - def test_logging(self): - # check that the newly created revision is logged - pass - - def test_content(self): - # check that the provided page content is included in the revision - # NOTE the actual fields that store page content (in this case 'body') - # are usually defined on the model that inherits `Page`, not `Page`! - research = LandingPage.objects.get(title='Research') - content = """ -

Here is a heading

-

Some basic paragraph content

-
  1. list
  2. items
- """ - create_revision(apps, research, body=html_to_streamfield(content)) - revision = research.get_latest_revision_as_page() - self.assertEqual(streamfield_to_html(revision.body), content) diff --git a/cdhweb/pages/tests/test_migrations.py b/cdhweb/pages/tests/test_migrations.py deleted file mode 100644 index 495556dbc..000000000 --- a/cdhweb/pages/tests/test_migrations.py +++ /dev/null @@ -1,64 +0,0 @@ -from django.apps import apps - -from cdhweb.pages.migration_utils import TestMigrations, get_parent -from mezzanine.pages.models import RichTextPage - - -class TestCreateHomepage(TestMigrations): - - app = 'cdhpages' - migrate_from = '0001_initial' - migrate_to = '0002_homepage' - - def test_new_homepage(self): - # should create one new HomePage - HomePage = self.apps.get_model('cdhpages', 'HomePage') - self.assertEqual(HomePage.objects.count(), 1) - - def test_homepage_at_root(self): - # new HomePage should be located at root - HomePage = self.apps.get_model('cdhpages', 'HomePage') - home = HomePage.objects.first() - parent = get_parent(apps, home) - self.assertEqual(parent.title, 'Root') - - def test_delete_welcome_page(self): - # should delete wagtail default welcome page - Page = self.apps.get_model('wagtailcore', 'Page') - with self.assertRaises(Page.DoesNotExist): - Page.objects.get(title='Welcome to your new Wagtail site!') - - def test_site_root_page(self): - # default site should point to new home page - Site = apps.get_model('wagtailcore', 'Site') - HomePage = self.apps.get_model('cdhpages', 'HomePage') - home = HomePage.objects.first() - site = Site.objects.first() - self.assertEqual(site.root_page_id, home.id) - - -class TestMigrateHomepage(TestMigrations): - - app = 'cdhpages' - migrate_from = '0001_initial' - migrate_to = '0002_homepage' - - def setUpBeforeMigration(self, apps): - # create a mezzanine home page with content - content = """ -

The Center for Digital Humanities is a DH research center.

- """ - # RichTextPage = apps.get_model('pages', 'RichTextPage') - RichTextPage.objects.create(title='Home', content=content) - - def test_delete_mezzanine_home(self): - # mezzanine page should be deleted - RichTextPage = apps.get_model('pages', 'RichTextPage') - with self.assertRaises(RichTextPage.DoesNotExist): - old_home = RichTextPage.objects.get(title='Home') - - def test_migrate_homepage(self): - # new HomePage should have migrated mezzanine content - HomePage = self.apps.get_model('cdhpages', 'HomePage') - self.assertEqual(HomePage.body, 'The Center for Digital Humanities is \ - a DH research center.') diff --git a/cdhweb/urls.py b/cdhweb/urls.py index f4ebe6039..8f097ce78 100644 --- a/cdhweb/urls.py +++ b/cdhweb/urls.py @@ -84,12 +84,12 @@ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # add django debug toolbar urls if DEBUG = True -if settings.DEBUG: - try: - import debug_toolbar - urlpatterns += path("__debug__/", include(debug_toolbar.urls)), - except ImportError: - pass +# if settings.DEBUG: +# try: +# import debug_toolbar +# urlpatterns += path("__debug__/", include(debug_toolbar.urls)), +# except ImportError: +# pass # Adds ``STATIC_URL`` to the context of error pages, so that error # pages can use JS, CSS and images. From 00ff73a1cda339825721a134f148ae034ff42ef4 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Tue, 15 Dec 2020 17:54:56 -0500 Subject: [PATCH 30/50] Add homepage exodus --- cdhweb/pages/management/commands/exodus.py | 40 +++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index d71a17eb2..b9da0efa9 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -7,6 +7,7 @@ from cdhweb.resources.models import LandingPage as OldLandingPage from mezzanine.pages.models import Page as MezzaninePage from wagtail.core.blocks import RichTextBlock +from wagtail.core.models import Site, Page class Command(BaseCommand): @@ -16,8 +17,30 @@ def add_arguments(self, parser): pass def handle(self, *args, **options): - homepage = HomePage.objects.get() + # TODO clear out all pages except root for idempotency + # Page.objects.filter(depth__gt=1).delete() + + root = Page.objects.get(depth=1) + + # migrate the homepage + ohp = MezzaninePage.objects.get(slug="/") + homepage = HomePage( + title=ohp.title, + slug=ohp.slug, + seo_title=ohp._meta_title or ohp.title, + body=json.dumps([{ + "type": "paragraph", + "value": ohp.richtextpage.content, + }]), + # Store description as search_description even if auto-generated + search_description=ohp.description, + first_published_at=ohp.created, + last_published_at=ohp.updated, + ) + root.add_child(instance=homepage) + root.save() + # migrate all landing pages for olp in OldLandingPage.objects.all(): lp = LandingPage( title=olp.title, @@ -28,24 +51,17 @@ def handle(self, *args, **options): "type": "paragraph", "value": olp.content, }]), - # Only store description if it's not auto-generated search_description=olp.description, first_published_at=olp.created, last_published_at=olp.updated, # TODO not dealing with images yet - # TODO not setting parent/child hierarchy yet # TODO not setting menu placement yet # TODO search keywords? + # TODO set the correct visibility status # NOTE not login-restricting pages since we don't use it + # NOTE not setting expiry date; handled manually + # NOTE inclusion in sitemap being handled by sitemap itself + # NOTE set has_unpublished_changes on page? ) - # TODO create a revision that sets the body content of the page - # TODO set the correct visibility status for the new revision - # TODO set the correct publication date for the new revision - # TODO set the created date for the new revision - # TODO set the last updated date for the new revision - # NOTE not setting expiry date; handled manually - # NOTE inclusion in sitemap being handled by sitemap itself - # NOTE set has_unpublished_changes on page? homepage.add_child(instance=lp) - homepage.save() From 9b33d2972148dbd12465bf0115cdc7922485904f Mon Sep 17 00:00:00 2001 From: Kevin McElwee Date: Wed, 16 Dec 2020 14:49:37 -0500 Subject: [PATCH 31/50] Make exodus script idempotent --- cdhweb/pages/management/commands/exodus.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index b9da0efa9..bba0540e4 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -17,8 +17,10 @@ def add_arguments(self, parser): pass def handle(self, *args, **options): - # TODO clear out all pages except root for idempotency - # Page.objects.filter(depth__gt=1).delete() + # clear out all pages except root for idempotency + site = Site.objects.get() + old_root_page = site.root_page + Page.objects.filter(depth__gt=2).delete() root = Page.objects.get(depth=1) @@ -26,7 +28,7 @@ def handle(self, *args, **options): ohp = MezzaninePage.objects.get(slug="/") homepage = HomePage( title=ohp.title, - slug=ohp.slug, + slug='', # slug of / is invalid in wagtail seo_title=ohp._meta_title or ohp.title, body=json.dumps([{ "type": "paragraph", @@ -40,6 +42,12 @@ def handle(self, *args, **options): root.add_child(instance=homepage) root.save() + # Point site at the new root page before deleting the old one to avoid + # deleting in a cascade + site.root_page = homepage + site.save() + old_root_page.delete() + # migrate all landing pages for olp in OldLandingPage.objects.all(): lp = LandingPage( @@ -65,3 +73,5 @@ def handle(self, *args, **options): ) homepage.add_child(instance=lp) homepage.save() + + From 7ddbf9eeda32027e256d80227d142c47edcb9866 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 16 Dec 2020 17:16:24 -0500 Subject: [PATCH 32/50] Restructure exodus command; fix slug conversion --- cdhweb/pages/management/commands/exodus.py | 145 ++++++++++++++------- 1 file changed, 96 insertions(+), 49 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index bba0540e4..7dc3d0f95 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -2,76 +2,123 @@ import json -from django.core.management.base import BaseCommand, CommandError -from cdhweb.pages.models import HomePage, LandingPage, ContentPage +from cdhweb.pages.models import ContentPage, HomePage, LandingPage from cdhweb.resources.models import LandingPage as OldLandingPage -from mezzanine.pages.models import Page as MezzaninePage -from wagtail.core.blocks import RichTextBlock -from wagtail.core.models import Site, Page +from django.core.management.base import BaseCommand +from mezzanine.pages import models as mezz_page_models +from wagtail.core.models import Page, Site class Command(BaseCommand): help = __file__.__doc__ - def add_arguments(self, parser): - pass + def convert_slug(self, slug): + """Convert a Mezzanine slug into a Wagtail slug.""" + # wagtail stores only the final portion of a URL: "" + # remove trailing slash, then return final portion without slashes + return slug.rstrip("/").split("/")[-1] + + def create_homepage(self, page): + """Create and return a Wagtail homepage based on a Mezzanine page.""" + return HomePage( + title=page.title, + slug=self.convert_slug(page.slug), + seo_title=page._meta_title or page.title, + body=json.dumps([{ + "type": "paragraph", + "value": page.richtextpage.content, # access via richtextpage + }]), + search_description=page.description, # store even if generated + first_published_at=page.created, + last_published_at=page.updated, + ) + + def create_landingpage(self, page): + """Create and return a Wagtail landing page based on a Mezzanine page.""" + return LandingPage( + title=page.title, + tagline=page.tagline, # landing pages have a tagline + slug=self.convert_slug(page.slug), + seo_title=page._meta_title or page.title, + body=json.dumps([{ + "type": "paragraph", + "value": page.content, + }]), + search_description=page.description, # store even if generated + first_published_at=page.created, + last_published_at=page.updated, + # TODO not dealing with images yet + # TODO not setting menu placement yet + # TODO search keywords? + ) + + def create_contentpage(self, page): + """Create and return a Wagtail content page based on a Mezzanine page.""" + return ContentPage( + title=page.title, + slug=self.convert_slug(page.slug), + seo_title=page._meta_title or page.title, + body=json.dumps([{ + "type": "paragraph", + "value": page.richtextpage.content, + }]), + search_description=page.description, # store even if generated + first_published_at=page.created, + last_published_at=page.updated, + # TODO not dealing with images yet + # TODO not setting menu placement yet + # TODO search keywords? + # TODO set the correct visibility status + # NOTE not login-restricting pages since we don't use it + # NOTE not setting expiry date; handled manually + # NOTE inclusion in sitemap being handled by sitemap itself + # NOTE set has_unpublished_changes on page? + ) + + def walk(self, page, depth): + """Recursively create Wagtail content pages.""" + # create the new version of the page + new_page = self.create_contentpage(page) + new_page.depth = depth + # recursively create the new versions of all the page's children + for child in page.children.all(): + new_page.add_child(instance=self.walk(child, depth + 1)) + # return the new page + return new_page def handle(self, *args, **options): - # clear out all pages except root for idempotency + """Convert all existing Mezzanine pages to Wagtail pages.""" + # clear out all pages except root and homepage for idempotency site = Site.objects.get() old_root_page = site.root_page Page.objects.filter(depth__gt=2).delete() - root = Page.objects.get(depth=1) # migrate the homepage - ohp = MezzaninePage.objects.get(slug="/") - homepage = HomePage( - title=ohp.title, - slug='', # slug of / is invalid in wagtail - seo_title=ohp._meta_title or ohp.title, - body=json.dumps([{ - "type": "paragraph", - "value": ohp.richtextpage.content, - }]), - # Store description as search_description even if auto-generated - search_description=ohp.description, - first_published_at=ohp.created, - last_published_at=ohp.updated, - ) + old_homepage = mezz_page_models.Page.objects.get(slug="/") + homepage = self.create_homepage(old_homepage) root.add_child(instance=homepage) root.save() - # Point site at the new root page before deleting the old one to avoid - # deleting in a cascade + # point site at the new root page before deleting the old one to avoid + # deleting the site in a cascade site.root_page = homepage site.save() old_root_page.delete() # migrate all landing pages - for olp in OldLandingPage.objects.all(): - lp = LandingPage( - title=olp.title, - tagline=olp.tagline, - slug=olp.slug, - seo_title=olp._meta_title or olp.title, - body=json.dumps([{ - "type": "paragraph", - "value": olp.content, - }]), - search_description=olp.description, - first_published_at=olp.created, - last_published_at=olp.updated, - # TODO not dealing with images yet - # TODO not setting menu placement yet - # TODO search keywords? - # TODO set the correct visibility status - # NOTE not login-restricting pages since we don't use it - # NOTE not setting expiry date; handled manually - # NOTE inclusion in sitemap being handled by sitemap itself - # NOTE set has_unpublished_changes on page? - ) - homepage.add_child(instance=lp) + for old_landingpage in OldLandingPage.objects.all(): + landingpage = self.create_landingpage(old_landingpage) + homepage.add_child(instance=landingpage) homepage.save() - + # migrate all content pages + # direct children of homepage + for child in old_homepage.children.all(): + if child.richtextpage: + self.walk(child, 3) + # children of landingpage + for old_landingpage in OldLandingPage.objects.all(): + for child in old_landingpage.children.all(): + self.walk(child, 4) + homepage.save() From ce2cfddecccf0e405c0213fd1b0e4969f3c086e2 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Wed, 16 Dec 2020 20:10:21 -0500 Subject: [PATCH 33/50] Update exodus script to use BFS traversal --- cdhweb/pages/management/commands/exodus.py | 84 ++++++++++++---------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index 7dc3d0f95..1641d78f6 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -1,6 +1,7 @@ """Convert mezzanine-based pages to wagtail page models.""" import json +from collections import defaultdict from cdhweb.pages.models import ContentPage, HomePage, LandingPage from cdhweb.resources.models import LandingPage as OldLandingPage @@ -75,50 +76,59 @@ def create_contentpage(self, page): # NOTE set has_unpublished_changes on page? ) - def walk(self, page, depth): - """Recursively create Wagtail content pages.""" - # create the new version of the page - new_page = self.create_contentpage(page) - new_page.depth = depth - # recursively create the new versions of all the page's children - for child in page.children.all(): - new_page.add_child(instance=self.walk(child, depth + 1)) - # return the new page - return new_page - def handle(self, *args, **options): - """Convert all existing Mezzanine pages to Wagtail pages.""" - # clear out all pages except root and homepage for idempotency - site = Site.objects.get() - old_root_page = site.root_page + """Create Wagtail pages for all extant Mezzanine pages.""" + # clear out wagtail pages for idempotency Page.objects.filter(depth__gt=2).delete() - root = Page.objects.get(depth=1) - # migrate the homepage + # create the new homepage old_homepage = mezz_page_models.Page.objects.get(slug="/") homepage = self.create_homepage(old_homepage) + root = Page.objects.get(depth=1) root.add_child(instance=homepage) root.save() - # point site at the new root page before deleting the old one to avoid - # deleting the site in a cascade + # point the default site at the new homepage + site = Site.objects.get() site.root_page = homepage site.save() - old_root_page.delete() - - # migrate all landing pages - for old_landingpage in OldLandingPage.objects.all(): - landingpage = self.create_landingpage(old_landingpage) - homepage.add_child(instance=landingpage) - homepage.save() - - # migrate all content pages - # direct children of homepage - for child in old_homepage.children.all(): - if child.richtextpage: - self.walk(child, 3) - # children of landingpage - for old_landingpage in OldLandingPage.objects.all(): - for child in old_landingpage.children.all(): - self.walk(child, 4) - homepage.save() + Page.objects.filter(depth=2).exclude(pk=homepage.pk).delete() + + # track content pages to migrate and their parents + queue = [] + visited = set([homepage]) + parent = defaultdict(Page) + parent[old_homepage] = root + + # add all top-level content pages and landing pages to the queue + for page in list(old_homepage.children.all()) + \ + list(OldLandingPage.objects.all()): + queue.append(page) + parent[page] = homepage + + # perform breadth-first search of all content pages + while queue: + # get the next page; skip if we've seen it already + page = queue.pop(0) + if page in visited: + continue + + # figure out what page type to create, and create it + if hasattr(page, "richtextpage"): + new_page = self.create_contentpage(page) + else: + new_page = self.create_landingpage(page) + try: + parent[page].add_child(instance=new_page) + parent[page].save() + # add all the pages at the next level down to the queue + for child in page.children.all(): + if child not in visited: + queue.append(child) + parent[child] = new_page + # FIXME slugs + except: + pass + + # mark this page as visited + visited.add(page) From 60baf76448eb5fd4bf51cbf09815f0a40c8ad3fc Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Thu, 17 Dec 2020 09:16:39 -0500 Subject: [PATCH 34/50] Remove visited check for exodus script --- cdhweb/pages/management/commands/exodus.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index 1641d78f6..2d3a13d6c 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -15,7 +15,7 @@ class Command(BaseCommand): def convert_slug(self, slug): """Convert a Mezzanine slug into a Wagtail slug.""" - # wagtail stores only the final portion of a URL: "" + # wagtail stores only the final portion of a URL with no slashes # remove trailing slash, then return final portion without slashes return slug.rstrip("/").split("/")[-1] @@ -61,7 +61,7 @@ def create_contentpage(self, page): seo_title=page._meta_title or page.title, body=json.dumps([{ "type": "paragraph", - "value": page.richtextpage.content, + "value": page.richtextpage.content, # access via richtextpage }]), search_description=page.description, # store even if generated first_published_at=page.created, @@ -88,7 +88,9 @@ def handle(self, *args, **options): root.add_child(instance=homepage) root.save() - # point the default site at the new homepage + # point the default site at the new homepage and delete old homepage(s). + # if they are deleted prior to switching site.root_page, the site will + # also be deleted in a cascade, which we don't want site = Site.objects.get() site.root_page = homepage site.save() @@ -96,7 +98,6 @@ def handle(self, *args, **options): # track content pages to migrate and their parents queue = [] - visited = set([homepage]) parent = defaultdict(Page) parent[old_homepage] = root @@ -108,12 +109,8 @@ def handle(self, *args, **options): # perform breadth-first search of all content pages while queue: - # get the next page; skip if we've seen it already - page = queue.pop(0) - if page in visited: - continue - # figure out what page type to create, and create it + page = queue.pop(0) if hasattr(page, "richtextpage"): new_page = self.create_contentpage(page) else: @@ -123,12 +120,9 @@ def handle(self, *args, **options): parent[page].save() # add all the pages at the next level down to the queue for child in page.children.all(): - if child not in visited: - queue.append(child) - parent[child] = new_page + queue.append(child) + parent[child] = new_page # FIXME slugs except: pass - # mark this page as visited - visited.add(page) From dcb606878b594b55c11bb3cc4ad89a9d8d847ce1 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Thu, 17 Dec 2020 10:48:27 -0500 Subject: [PATCH 35/50] Add handling for project/event child pages --- cdhweb/pages/management/commands/exodus.py | 68 ++++++++++++++++------ 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index 2d3a13d6c..d5d5c132f 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -6,6 +6,7 @@ from cdhweb.pages.models import ContentPage, HomePage, LandingPage from cdhweb.resources.models import LandingPage as OldLandingPage from django.core.management.base import BaseCommand +from django.db.models import Q from mezzanine.pages import models as mezz_page_models from wagtail.core.models import Page, Site @@ -38,12 +39,12 @@ def create_landingpage(self, page): """Create and return a Wagtail landing page based on a Mezzanine page.""" return LandingPage( title=page.title, - tagline=page.tagline, # landing pages have a tagline + tagline=page.landingpage.tagline, # landing pages have a tagline slug=self.convert_slug(page.slug), seo_title=page._meta_title or page.title, body=json.dumps([{ "type": "paragraph", - "value": page.content, + "value": page.landingpage.content, }]), search_description=page.description, # store even if generated first_published_at=page.created, @@ -96,16 +97,50 @@ def handle(self, *args, **options): site.save() Page.objects.filter(depth=2).exclude(pk=homepage.pk).delete() - # track content pages to migrate and their parents + # track content pages to migrate and their parents. + # parent maps mezzanine pages to the parent of their wagtail counterpart + # so that you can call save() after adding a child to it queue = [] - parent = defaultdict(Page) - parent[old_homepage] = root + parent = {old_homepage: root} + + # create a dummy top-level projects/ page for project pages to go under + projects = ContentPage( + title="Sponsored Projects", + slug="projects", + seo_title="Sponsored Projects", + ) + homepage.add_child(instance=projects) + homepage.save() + + # create a dummy top-level events/ page for event pages to go under + events = ContentPage( + title="Events", + slug="events", + seo_title="Events" + ) + homepage.add_child(instance=events) + homepage.save() # add all top-level content pages and landing pages to the queue for page in list(old_homepage.children.all()) + \ - list(OldLandingPage.objects.all()): - queue.append(page) - parent[page] = homepage + list(OldLandingPage.objects.all()): + # use the base page type on landingpages for consistency + if hasattr(page, "page_ptr"): + queue.append(page.page_ptr) + parent[page.page_ptr] = homepage + else: + queue.append(page) + parent[page] = homepage + + # set all the project pages to have the dummy project page as a parent + project_pages = mezz_page_models.Page.objects.filter(slug__startswith="projects/") + for page in project_pages: + parent[page] = projects + + # set all the event pages to have the dummy events page as a parent + event_pages = mezz_page_models.Page.objects.filter(Q(slug__startswith="events/") | Q(slug="year-of-data")) + for page in event_pages: + parent[page] = events # perform breadth-first search of all content pages while queue: @@ -115,14 +150,9 @@ def handle(self, *args, **options): new_page = self.create_contentpage(page) else: new_page = self.create_landingpage(page) - try: - parent[page].add_child(instance=new_page) - parent[page].save() - # add all the pages at the next level down to the queue - for child in page.children.all(): - queue.append(child) - parent[child] = new_page - # FIXME slugs - except: - pass - + parent[page].add_child(instance=new_page) + parent[page].save() + # add all the pages at the next level down to the queue + for child in page.children.all(): + queue.append(child) + parent[child] = new_page From ba4d2261442f478aca84b94dac677dd40d72fa9d Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 17 Dec 2020 12:35:45 -0500 Subject: [PATCH 36/50] Recursive version of exodus script --- cdhweb/pages/management/commands/exodus.py | 100 ++++++++++++--------- 1 file changed, 60 insertions(+), 40 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index d5d5c132f..ae45e09d3 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -1,7 +1,6 @@ """Convert mezzanine-based pages to wagtail page models.""" import json -from collections import defaultdict from cdhweb.pages.models import ContentPage, HomePage, LandingPage from cdhweb.resources.models import LandingPage as OldLandingPage @@ -14,6 +13,9 @@ class Command(BaseCommand): help = __file__.__doc__ + # list to track migrated mezzanine pages by pk + migrated = [] + def convert_slug(self, slug): """Convert a Mezzanine slug into a Wagtail slug.""" # wagtail stores only the final portion of a URL with no slashes @@ -97,12 +99,6 @@ def handle(self, *args, **options): site.save() Page.objects.filter(depth=2).exclude(pk=homepage.pk).delete() - # track content pages to migrate and their parents. - # parent maps mezzanine pages to the parent of their wagtail counterpart - # so that you can call save() after adding a child to it - queue = [] - parent = {old_homepage: root} - # create a dummy top-level projects/ page for project pages to go under projects = ContentPage( title="Sponsored Projects", @@ -111,6 +107,7 @@ def handle(self, *args, **options): ) homepage.add_child(instance=projects) homepage.save() + self.migrated.append(mezz_page_models.Page.objects.get(slug='projects').pk) # create a dummy top-level events/ page for event pages to go under events = ContentPage( @@ -120,39 +117,62 @@ def handle(self, *args, **options): ) homepage.add_child(instance=events) homepage.save() + # mark events content page as migrated + self.migrated.append(mezz_page_models.Page.objects.get(slug='events').pk) - # add all top-level content pages and landing pages to the queue - for page in list(old_homepage.children.all()) + \ - list(OldLandingPage.objects.all()): - # use the base page type on landingpages for consistency - if hasattr(page, "page_ptr"): - queue.append(page.page_ptr) - parent[page.page_ptr] = homepage - else: - queue.append(page) - parent[page] = homepage - - # set all the project pages to have the dummy project page as a parent - project_pages = mezz_page_models.Page.objects.filter(slug__startswith="projects/") - for page in project_pages: - parent[page] = projects + # migrate children of homepage + for page in old_homepage.children.all(): + self.migrate_pages(page, homepage) - # set all the event pages to have the dummy events page as a parent - event_pages = mezz_page_models.Page.objects.filter(Q(slug__startswith="events/") | Q(slug="year-of-data")) + # special cases + # - migrate event pages but specify new events page as parent + event_pages = mezz_page_models.Page.objects \ + .filter(Q(slug__startswith="events/") | Q(slug="year-of-data")) for page in event_pages: - parent[page] = events - - # perform breadth-first search of all content pages - while queue: - # figure out what page type to create, and create it - page = queue.pop(0) - if hasattr(page, "richtextpage"): - new_page = self.create_contentpage(page) - else: - new_page = self.create_landingpage(page) - parent[page].add_child(instance=new_page) - parent[page].save() - # add all the pages at the next level down to the queue - for child in page.children.all(): - queue.append(child) - parent[child] = new_page + self.migrate_pages(page, events) + # - migrate project pages but specify new projects page as parent + project_pages = mezz_page_models.Page.objects \ + .filter(slug__startswith="projects/") + for page in project_pages: + self.migrate_pages(page, projects) + + # migrate all remaining pages, starting with pages with no parent + # (i.e., top level pages) + for page in mezz_page_models.Page.objects.filter(parent__isnull=True): + self.migrate_pages(page, homepage) + + # report on unmigrated pages + unmigrated = mezz_page_models.Page.objects.exclude(pk__in=self.migrated) + print('%d unmigrated mezzanine pages:' % unmigrated.count()) + for page in unmigrated: + print('\t%s — slug/url %s)' % (page, page.slug)) + + def migrate_pages(self, page, parent): + """Recursively convert a mezzanine page and all its descendants + to Wagtail pages with the same hierarchy. + + :params page: mezzanine page to convert + :params parent: wagtail page the new page should be added to + """ + + # if a page has already been migrated, skip it + if page.pk in self.migrated: + return + # create the new version of the page according to page type + if hasattr(page, "richtextpage"): + new_page = self.create_contentpage(page) + elif hasattr(page, "landingpage"): + new_page = self.create_landingpage(page) + else: + print('WARN: page conversion not yet handled for %s page' % (page)) + # bail out for now + return + + parent.add_child(instance=new_page) + parent.save() + # add to list of migrated pages + self.migrated.append(page.pk) + + # recursively create and add new versions of all this page's children + for child in page.children.all(): + self.migrate_pages(child, new_page) From a91713c825b7167f924dc1d209aa7a1ece91384c Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 17 Dec 2020 13:36:42 -0500 Subject: [PATCH 37/50] Fix project page double-nesting --- cdhweb/pages/management/commands/exodus.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index ae45e09d3..7efb9e48d 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -130,9 +130,10 @@ def handle(self, *args, **options): .filter(Q(slug__startswith="events/") | Q(slug="year-of-data")) for page in event_pages: self.migrate_pages(page, events) - # - migrate project pages but specify new projects page as parent + # - migrate project pages but specify new projects list page as parent + # - process about page last so project pages don't nest project_pages = mezz_page_models.Page.objects \ - .filter(slug__startswith="projects/") + .filter(slug__startswith="projects/").order_by('-slug') for page in project_pages: self.migrate_pages(page, projects) From 8086c621c57f2b2495be8ff3e03eaec775e46fd5 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 17 Dec 2020 13:53:28 -0500 Subject: [PATCH 38/50] Convert all pages/links to generic wagtail page; don't migrate home 2x --- cdhweb/pages/management/commands/exodus.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index 7efb9e48d..b12606a4a 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -64,7 +64,8 @@ def create_contentpage(self, page): seo_title=page._meta_title or page.title, body=json.dumps([{ "type": "paragraph", - "value": page.richtextpage.content, # access via richtextpage + # access via richtextpage when present + "value": page.richtextpage.content if hasattr(page, 'richtextpage') else '', }]), search_description=page.description, # store even if generated first_published_at=page.created, @@ -90,6 +91,8 @@ def handle(self, *args, **options): root = Page.objects.get(depth=1) root.add_child(instance=homepage) root.save() + # mark home page as migrated + self.migrated.append(old_homepage.pk) # point the default site at the new homepage and delete old homepage(s). # if they are deleted prior to switching site.root_page, the site will @@ -159,15 +162,15 @@ def migrate_pages(self, page, parent): # if a page has already been migrated, skip it if page.pk in self.migrated: return + # create the new version of the page according to page type - if hasattr(page, "richtextpage"): - new_page = self.create_contentpage(page) - elif hasattr(page, "landingpage"): + if hasattr(page, "landingpage"): new_page = self.create_landingpage(page) else: - print('WARN: page conversion not yet handled for %s page' % (page)) - # bail out for now - return + # treat everything else as page / richtexpage + if hasattr(page, "link"): + print('WARN: converting link page to content page %s ' % (page)) + new_page = self.create_contentpage(page) parent.add_child(instance=new_page) parent.save() From 0de07abd5c4cedea67e55411f99d3a3364a31c51 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Thu, 17 Dec 2020 14:13:09 -0500 Subject: [PATCH 39/50] Placeholder for consult/cosponsor form logic --- cdhweb/pages/management/commands/exodus.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index b12606a4a..5644f153e 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -58,6 +58,7 @@ def create_landingpage(self, page): def create_contentpage(self, page): """Create and return a Wagtail content page based on a Mezzanine page.""" + return ContentPage( title=page.title, slug=self.convert_slug(page.slug), @@ -72,7 +73,7 @@ def create_contentpage(self, page): last_published_at=page.updated, # TODO not dealing with images yet # TODO not setting menu placement yet - # TODO search keywords? + # NOTE: not migrating search keywords # TODO set the correct visibility status # NOTE not login-restricting pages since we don't use it # NOTE not setting expiry date; handled manually @@ -145,12 +146,17 @@ def handle(self, *args, **options): for page in mezz_page_models.Page.objects.filter(parent__isnull=True): self.migrate_pages(page, homepage) + # special cases — consult/co-sponsor form + self.form_pages() + # report on unmigrated pages unmigrated = mezz_page_models.Page.objects.exclude(pk__in=self.migrated) print('%d unmigrated mezzanine pages:' % unmigrated.count()) for page in unmigrated: print('\t%s — slug/url %s)' % (page, page.slug)) + # delete mezzanine pages here? (but keep for testing migration) + def migrate_pages(self, page, parent): """Recursively convert a mezzanine page and all its descendants to Wagtail pages with the same hierarchy. @@ -180,3 +186,11 @@ def migrate_pages(self, page, parent): # recursively create and add new versions of all this page's children for child in page.children.all(): self.migrate_pages(child, new_page) + + def form_pages(self): + # migrate embedded google forms from mezzanine templates + consults = ContentPage.objects.get(slug='consult') + # add new paragraph to body with iframe from engage/consult template and save + + # do the same for cosponsorship page + From a8074477f242948a893fe0cd2b6086f406f27f03 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Fri, 18 Dec 2020 11:00:47 -0500 Subject: [PATCH 40/50] Create "migration" block and alter allowed tags --- cdhweb/pages/models.py | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/cdhweb/pages/models.py b/cdhweb/pages/models.py index a6f947548..32322b9c5 100644 --- a/cdhweb/pages/models.py +++ b/cdhweb/pages/models.py @@ -1,31 +1,38 @@ from random import shuffle import bleach +from cdhweb.blog.models import BlogPost +from cdhweb.events.models import Event +from cdhweb.projects.models import Project +from django.conf import settings from django.db import models from django.template.defaultfilters import striptags, truncatechars_html -from django.utils.text import slugify from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel -from wagtail.core.blocks import (CharBlock, RichTextBlock, StreamBlock, - StructBlock, TextBlock) +from wagtail.core.blocks import RichTextBlock, StreamBlock from wagtail.core.fields import RichTextField, StreamField from wagtail.core.models import Page from wagtail.documents.blocks import DocumentChooserBlock +from wagtail.embeds.blocks import EmbedBlock from wagtail.images.blocks import ImageChooserBlock from wagtail.images.edit_handlers import ImageChooserPanel from wagtail.search import index -from cdhweb.blog.models import BlogPost -from cdhweb.events.models import Event -from cdhweb.projects.models import Project +#: common features for paragraph text +PARAGRAPH_FEATURES = [ + 'h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', + 'hr', 'blockquote', 'document', 'superscript', 'subscript', + 'strikethrough', 'code' +] -#: commonly allowed tags for RichTextBlocks -RICH_TEXT_TAGS = ['h3', 'h4', 'bold', 'italic', 'link', 'ol', 'ul', 'blockquote'] - class BodyContentBlock(StreamBlock): '''Common set of blocks available in StreamFields for body text.''' - paragraph = RichTextBlock(features=RICH_TEXT_TAGS) + paragraph = RichTextBlock(features=PARAGRAPH_FEATURES) image = ImageChooserBlock() + document = DocumentChooserBlock() + embed = EmbedBlock() + migrated = RichTextBlock( + features=settings.RICHTEXT_ALLOWED_TAGS, icon="warning") class PagePreviewDescriptionMixin(models.Model): @@ -45,8 +52,8 @@ class PagePreviewDescriptionMixin(models.Model): max_length = 225 # (tags are omitted by subsetting default ALLOWED_TAGS) #: allowed tags for bleach html stripping in description - allowed_tags = list((set(bleach.sanitizer.ALLOWED_TAGS) - \ - set(['a', 'blockquote']))) # additional tags to remove + allowed_tags = list((set(bleach.sanitizer.ALLOWED_TAGS) - + set(['a', 'blockquote']))) # additional tags to remove class Meta: abstract = True @@ -109,13 +116,13 @@ class ContentPage(Page, PagePreviewDescriptionMixin): class LandingPage(Page): '''Page type that aggregates and displays multiple :class:`ContentPage`s.''' - #: main page text - body = StreamField(BodyContentBlock, blank=True) #: short sentence overlaid on the header image tagline = models.CharField(max_length=255) #: image that will be used for the header header_image = models.ForeignKey('wagtailimages.image', null=True, - blank=True, on_delete=models.SET_NULL, related_name='+') # no reverse relationship + blank=True, on_delete=models.SET_NULL, related_name='+') # no reverse relationship + #: main page text + body = StreamField(BodyContentBlock, blank=True) search_fields = Page.search_fields + [index.SearchField('body')] content_panels = Page.content_panels + [ @@ -161,4 +168,4 @@ def get_context(self, request): # add up to 3 upcoming, published events context['events'] = Event.objects.published().upcoming()[:3] - return context \ No newline at end of file + return context From 2ec73c2203f36216ac37f67d49553ac76430117c Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Fri, 18 Dec 2020 11:02:36 -0500 Subject: [PATCH 41/50] Update exodus script to use new migrated field and transfer form iframes --- cdhweb/pages/management/commands/exodus.py | 56 ++++++++++++++-------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index 5644f153e..79fc7a73c 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -3,10 +3,11 @@ import json from cdhweb.pages.models import ContentPage, HomePage, LandingPage -from cdhweb.resources.models import LandingPage as OldLandingPage from django.core.management.base import BaseCommand from django.db.models import Q +from mezzanine.core.models import CONTENT_STATUS_PUBLISHED from mezzanine.pages import models as mezz_page_models +from wagtail.core.blocks import RichTextBlock from wagtail.core.models import Page, Site @@ -29,12 +30,10 @@ def create_homepage(self, page): slug=self.convert_slug(page.slug), seo_title=page._meta_title or page.title, body=json.dumps([{ - "type": "paragraph", + "type": "migrated", "value": page.richtextpage.content, # access via richtextpage }]), search_description=page.description, # store even if generated - first_published_at=page.created, - last_published_at=page.updated, ) def create_landingpage(self, page): @@ -45,12 +44,10 @@ def create_landingpage(self, page): slug=self.convert_slug(page.slug), seo_title=page._meta_title or page.title, body=json.dumps([{ - "type": "paragraph", + "type": "migrated", "value": page.landingpage.content, }]), search_description=page.description, # store even if generated - first_published_at=page.created, - last_published_at=page.updated, # TODO not dealing with images yet # TODO not setting menu placement yet # TODO search keywords? @@ -58,19 +55,16 @@ def create_landingpage(self, page): def create_contentpage(self, page): """Create and return a Wagtail content page based on a Mezzanine page.""" - return ContentPage( title=page.title, slug=self.convert_slug(page.slug), seo_title=page._meta_title or page.title, body=json.dumps([{ - "type": "paragraph", + "type": "migrated", # access via richtextpage when present - "value": page.richtextpage.content if hasattr(page, 'richtextpage') else '', + "value": page.richtextpage.content if hasattr(page, "richtextpage") else "", }]), search_description=page.description, # store even if generated - first_published_at=page.created, - last_published_at=page.updated, # TODO not dealing with images yet # TODO not setting menu placement yet # NOTE: not migrating search keywords @@ -83,8 +77,9 @@ def create_contentpage(self, page): def handle(self, *args, **options): """Create Wagtail pages for all extant Mezzanine pages.""" - # clear out wagtail pages for idempotency + # clear out wagtail pages and revisions for idempotency Page.objects.filter(depth__gt=2).delete() + # PageRevision.objects.all().delete() # create the new homepage old_homepage = mezz_page_models.Page.objects.get(slug="/") @@ -111,7 +106,8 @@ def handle(self, *args, **options): ) homepage.add_child(instance=projects) homepage.save() - self.migrated.append(mezz_page_models.Page.objects.get(slug='projects').pk) + self.migrated.append( + mezz_page_models.Page.objects.get(slug='projects').pk) # create a dummy top-level events/ page for event pages to go under events = ContentPage( @@ -122,7 +118,8 @@ def handle(self, *args, **options): homepage.add_child(instance=events) homepage.save() # mark events content page as migrated - self.migrated.append(mezz_page_models.Page.objects.get(slug='events').pk) + self.migrated.append( + mezz_page_models.Page.objects.get(slug='events').pk) # migrate children of homepage for page in old_homepage.children.all(): @@ -150,7 +147,8 @@ def handle(self, *args, **options): self.form_pages() # report on unmigrated pages - unmigrated = mezz_page_models.Page.objects.exclude(pk__in=self.migrated) + unmigrated = mezz_page_models.Page.objects.exclude( + pk__in=self.migrated) print('%d unmigrated mezzanine pages:' % unmigrated.count()) for page in unmigrated: print('\t%s — slug/url %s)' % (page, page.slug)) @@ -180,17 +178,33 @@ def migrate_pages(self, page, parent): parent.add_child(instance=new_page) parent.save() + + # set publication status + if page.status != CONTENT_STATUS_PUBLISHED: + new_page.unpublish() + # add to list of migrated pages self.migrated.append(page.pk) # recursively create and add new versions of all this page's children for child in page.children.all(): self.migrate_pages(child, new_page) + def form_pages(self): # migrate embedded google forms from mezzanine templates - consults = ContentPage.objects.get(slug='consult') - # add new paragraph to body with iframe from engage/consult template and save - - # do the same for cosponsorship page - + # add new migrated to body with iframe from engage/consult template and save + # set a height on the iframe to ensure it renders correctly + consults = ContentPage.objects.get(slug="consult") + consults.body = json.dumps([ + {"type": "migrated", "value": consults.body[0].value.source}, + {"type": "migrated", "value": ''} + ]) + consults.save() + # # do the same for cosponsorship page + cosponsor = ContentPage.objects.get(slug="cosponsor") + cosponsor.body = json.dumps([ + {"type": "migrated", "value": cosponsor.body[0].value.source}, + {"type": "migrated", "value": ''} + ]) + cosponsor.save() From 276c9f5524afd6524e5f2767d4e1869eec1b529e Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Fri, 18 Dec 2020 11:02:53 -0500 Subject: [PATCH 42/50] Add basic content page template --- .../pages/templates/cdhpages/content_page.html | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 cdhweb/pages/templates/cdhpages/content_page.html diff --git a/cdhweb/pages/templates/cdhpages/content_page.html b/cdhweb/pages/templates/cdhpages/content_page.html new file mode 100644 index 000000000..f1d493b82 --- /dev/null +++ b/cdhweb/pages/templates/cdhpages/content_page.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load wagtailcore_tags %} + +{% block page-title %}{{ page.seo_title }}{% endblock %} + +{% block bodyattrs %}class="richtextpage"{% endblock %} + +{% block main %} +{# ensure content is always wrapped in a div for formatting reasons #} +

{{ page.title }}

+
+ {% for block in page.body %} + {% include_block block %} + {% endfor %} +
+{% endblock %} From dfbd1350f0aaa3fe374f885a209c0c2cf544f3e2 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Fri, 18 Dec 2020 11:03:50 -0500 Subject: [PATCH 43/50] Update home page template --- cdhweb/pages/templates/cdhpages/home_page.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdhweb/pages/templates/cdhpages/home_page.html b/cdhweb/pages/templates/cdhpages/home_page.html index f18aa6dc5..31d2aa685 100644 --- a/cdhweb/pages/templates/cdhpages/home_page.html +++ b/cdhweb/pages/templates/cdhpages/home_page.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% load wagtailcore_tags %} -{% block page-title %}{% if page %}{{ page.meta_title }}{% else %}The Center for Digital Humanities at Princeton{% endif %}{% endblock %} +{% block page-title %}{{ page.seo_title }}{% endblock %} {% block content %} {# add a class to main content for home-page specific styles #}
From 11d675f411beea10f2c1a88176b4c747b915ca60 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Fri, 18 Dec 2020 11:04:07 -0500 Subject: [PATCH 44/50] Update landing page template --- .../templates/cdhpages/landing_page.html | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 cdhweb/pages/templates/cdhpages/landing_page.html diff --git a/cdhweb/pages/templates/cdhpages/landing_page.html b/cdhweb/pages/templates/cdhpages/landing_page.html new file mode 100644 index 000000000..a949d2831 --- /dev/null +++ b/cdhweb/pages/templates/cdhpages/landing_page.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} +{% load wagtailcore_tags %} + +{% block page-title %}{{ page.title }}{% endblock %} + +{% block bodyattrs %}class="with-cards small-no-cards"{% endblock %} + +{% block content %} {# wrap content in an article tag #} +
+{{ block.super }} +
+{% endblock %} + +{% block content-header %} +
+
+ /

{{ page.title }}

+

{{ page.tagline }}

+
+
+{% endblock %} + +{% block main %} + {% for block in page.body %} + {% include_block block %} + {% endfor %} +{% endblock %} From ab0a1192c8a1081ad5217dc72dc47e2939f2b107 Mon Sep 17 00:00:00 2001 From: Nick Budak Date: Fri, 18 Dec 2020 11:04:36 -0500 Subject: [PATCH 45/50] Generalize iframe css, shift form height into attributes --- sitemedia/scss/base/_forms.scss | 14 -------------- sitemedia/scss/base/_media.scss | 5 +++++ sitemedia/scss/site.scss | 8 -------- 3 files changed, 5 insertions(+), 22 deletions(-) diff --git a/sitemedia/scss/base/_forms.scss b/sitemedia/scss/base/_forms.scss index a3bddc2ea..8e3c744f1 100644 --- a/sitemedia/scss/base/_forms.scss +++ b/sitemedia/scss/base/_forms.scss @@ -93,17 +93,3 @@ select { outline-offset: $focus-outline-offset; } } - -.consult iframe, -.cosponsor iframe { - width: 100%; - border: none; -} - -.consult iframe { - height: 2300px; -} - -.cosponsor iframe { - height: 3200px; -} \ No newline at end of file diff --git a/sitemedia/scss/base/_media.scss b/sitemedia/scss/base/_media.scss index dfa22eaed..6b4d9f1ab 100644 --- a/sitemedia/scss/base/_media.scss +++ b/sitemedia/scss/base/_media.scss @@ -7,3 +7,8 @@ picture { margin: 0; max-width: 100%; } + +iframe { + width: 100%; + border: none; +} \ No newline at end of file diff --git a/sitemedia/scss/site.scss b/sitemedia/scss/site.scss index 896504fd2..4bfb9645f 100644 --- a/sitemedia/scss/site.scss +++ b/sitemedia/scss/site.scss @@ -1704,14 +1704,6 @@ body.richtextpage { &.consult { section { @include content-page; - - iframe { - margin: 0; - padding: 0; - border: 0; - width: 100%; - height: 750px; - } } } From 55ce56f95834373aabe28fc5af125222cb1919e7 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 18 Dec 2020 15:19:06 -0500 Subject: [PATCH 46/50] Image exodus: convert media uploads to wagtail images #248 --- cdhweb/pages/management/commands/exodus.py | 135 ++++++++++++++++++++- 1 file changed, 131 insertions(+), 4 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index 79fc7a73c..5c6cbd7b4 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -1,14 +1,23 @@ """Convert mezzanine-based pages to wagtail page models.""" - +import filecmp +import glob import json +import os +import os.path +import shutil +from collections import defaultdict -from cdhweb.pages.models import ContentPage, HomePage, LandingPage +from django.conf import settings +from django.core.files.images import ImageFile from django.core.management.base import BaseCommand from django.db.models import Q from mezzanine.core.models import CONTENT_STATUS_PUBLISHED from mezzanine.pages import models as mezz_page_models from wagtail.core.blocks import RichTextBlock -from wagtail.core.models import Page, Site +from wagtail.core.models import Page, Site, Collection, get_root_collection_id +from wagtail.images.models import Image + +from cdhweb.pages.models import ContentPage, HomePage, LandingPage class Command(BaseCommand): @@ -153,6 +162,9 @@ def handle(self, *args, **options): for page in unmigrated: print('\t%s — slug/url %s)' % (page, page.slug)) + # convert media images to wagtail images + self.image_exodus() + # delete mezzanine pages here? (but keep for testing migration) def migrate_pages(self, page, parent): @@ -189,7 +201,6 @@ def migrate_pages(self, page, parent): # recursively create and add new versions of all this page's children for child in page.children.all(): self.migrate_pages(child, new_page) - def form_pages(self): # migrate embedded google forms from mezzanine templates @@ -208,3 +219,119 @@ def form_pages(self): {"type": "migrated", "value": ''} ]) cosponsor.save() + + # cached collections used for migrated media + collections = { + # get root collection so we can add children to it + 'root': Collection.objects.get(pk=get_root_collection_id()) + } + + def get_collection(self, name): + # if we don't already have this collection, get it + if name not in self.collections: + # try to get it if it already exists + coll = Collection.objects.filter(name=name).first() + # otherwise, create it + if not coll: + coll = Collection(name=name) + self.collections['root'].add_child(instance=coll) + self.collections['root'].save() + + self.collections[name] = coll + + return self.collections[name] + + def image_exodus(self): + # generate wagtail images for all uploaded media + + # mezzanine/filebrowser_safe doesn't seem to have useful objects + # or track metadata, so just import from the filesystem + + # delete all images prior to run (clear out past migration attempts) + Image.objects.all().delete() + # also delete any wagtail image files, since they are not deleted + # by removing the objects + shutil.rmtree('%s/images' % settings.MEDIA_ROOT, ignore_errors=True) + shutil.rmtree('%s/original_images' % settings.MEDIA_ROOT, ignore_errors=True) + + # get media filenames to migrate, with duplicates filtered out + media_filenames = self.get_media_files() + + for imgpath in media_filenames: + extension = os.path.splitext(imgpath)[1] + # skip unsupported files based on file extension + # NOTE: leaving this here in case we want to handle + # documents the same way + if extension in ['.pdf', '.svg', '.docx']: + continue + + # if image is in a subdirectory under uploads (e.g. projects, blog) + # add it to a collection with that name + relative_path = os.path.dirname(imgpath) \ + .replace('%s/uploads/' % settings.MEDIA_ROOT, '') + + # there are two variants of Slavic DH, one with and one + # without a space; remove the space so they'll be in one collection + basedir = relative_path.split('/')[0].replace(' ', '') + collection = None + if basedir: + collection = self.get_collection(basedir) + + with open(imgpath, 'rb') as imgfilehandle: + title = os.path.basename(imgpath) + # passing collection=None errors, so + # only specify collection option when we have one + extra_opts = {} + if collection: + extra_opts['collection'] = collection + try: + Image.objects.create( + title=title, + file=ImageFile(imgfilehandle, name=title), + **extra_opts) + except Exception as err: + # seems to mean that height/width calculation failed + # (usually non-images) + print('Error creating image for %s: %s' % (imgpath, err)) + + def get_media_files(self): + # wagtail images support: GIF, JPEG, PNG, WEBP + imgfile_path = '%s/**/*.*' % settings.MEDIA_ROOT + # get filenames for all uploaded files + filenames = glob.glob(imgfile_path, recursive=True) + # aggregate files by basename to identify files with the same + # name in different locations + unique_filenames = defaultdict(list) + for path in filenames: + unique_filenames[os.path.basename(path)].append(path) + + # check files with the same name in multiple locations + for key, val in unique_filenames.items(): + if len(val) > 1: + samefile = filecmp.cmp(val[0], val[1], shallow=False) + # if the files are the same + if samefile: + # keep the first one and remove the others from the + # list of files to be migrated + extra_copies = val[1:] + + # if not all the same, identify the largest + # (all are variant/cropped versions of the same image) + else: + largest_file = None + largest_size = 0 + for filepath in val: + size = os.stat(filepath).st_size + if size > largest_size: + largest_size = size + largest_file = filepath + + extra_copies = [f for f in val if f != largest_file] + + # remove duplicate and variant images that + # will not be imported into wagtail + for extra_copy in extra_copies: + filenames.remove(extra_copy) + + return filenames + From 7210971a2b706a742a422e3b2116dd1c3f68be40 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 18 Dec 2020 16:25:02 -0500 Subject: [PATCH 47/50] Update landing page creation to include wagtail header image --- cdhweb/pages/management/commands/exodus.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index 5c6cbd7b4..e4e002f65 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -50,6 +50,7 @@ def create_landingpage(self, page): return LandingPage( title=page.title, tagline=page.landingpage.tagline, # landing pages have a tagline + header_image=self.get_wagtail_image(page.landingpage.image), slug=self.convert_slug(page.slug), seo_title=page._meta_title or page.title, body=json.dumps([{ @@ -90,6 +91,9 @@ def handle(self, *args, **options): Page.objects.filter(depth__gt=2).delete() # PageRevision.objects.all().delete() + # convert media images to wagtail images + self.image_exodus() + # create the new homepage old_homepage = mezz_page_models.Page.objects.get(slug="/") homepage = self.create_homepage(old_homepage) @@ -162,9 +166,6 @@ def handle(self, *args, **options): for page in unmigrated: print('\t%s — slug/url %s)' % (page, page.slug)) - # convert media images to wagtail images - self.image_exodus() - # delete mezzanine pages here? (but keep for testing migration) def migrate_pages(self, page, parent): @@ -335,3 +336,9 @@ def get_media_files(self): return filenames + def get_wagtail_image(self, image): + # get the migrated wagtail image for a foreign-key image + # using image file basename, which is migrated as image title + return Image.objects.get(title=os.path.basename(image.name)) + + From 7d7f9578c0f905641a3830485727abf7c2ebee37 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 18 Dec 2020 16:25:27 -0500 Subject: [PATCH 48/50] Use wagtail image tags to generate landing page header background image --- cdhweb/pages/templates/cdhpages/landing_page.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/cdhweb/pages/templates/cdhpages/landing_page.html b/cdhweb/pages/templates/cdhpages/landing_page.html index a949d2831..09ea553c1 100644 --- a/cdhweb/pages/templates/cdhpages/landing_page.html +++ b/cdhweb/pages/templates/cdhpages/landing_page.html @@ -1,5 +1,5 @@ {% extends 'base.html' %} -{% load wagtailcore_tags %} +{% load wagtailcore_tags wagtailimages_tags %} {% block page-title %}{{ page.title }}{% endblock %} @@ -12,7 +12,15 @@ {% endblock %} {% block content-header %} -
/

{{ page.title }}

From 4328ebca2a2458143473e92f5740a3131dc3d612 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Mon, 21 Dec 2020 17:25:20 -0500 Subject: [PATCH 49/50] Remove old files not removed by merge from develop --- cdhweb/pages/fixtures/sample_pages.json | 112 ------------------------ cdhweb/pages/wagtail_hooks.py | 34 ------- 2 files changed, 146 deletions(-) delete mode 100644 cdhweb/pages/fixtures/sample_pages.json delete mode 100644 cdhweb/pages/wagtail_hooks.py diff --git a/cdhweb/pages/fixtures/sample_pages.json b/cdhweb/pages/fixtures/sample_pages.json deleted file mode 100644 index 8a3288520..000000000 --- a/cdhweb/pages/fixtures/sample_pages.json +++ /dev/null @@ -1,112 +0,0 @@ -[ - { - "model": "wagtailcore.page", - "pk": 1, - "fields": { - "path": "0001", - "depth": 1, - "numchild": 1, - "title": "Root", - "draft_title": "Root", - "slug": "root", - "content_type": ["wagtailcore", "page"], - "live": true, - "has_unpublished_changes": false, - "url_path": "/", - "owner": null, - "seo_title": "", - "show_in_menus": false, - "search_description": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": null, - "last_published_at": null, - "latest_revision_created_at": null, - "live_revision": null - } - }, - { - "model": "wagtailcore.page", - "pk": 2, - "fields": { - "path": "00010001", - "depth": 2, - "numchild": 1, - "title": "Home", - "draft_title": "Home", - "slug": "home", - "content_type": ["cdhpages", "homepage"], - "live": true, - "has_unpublished_changes": false, - "url_path": "/home/", - "owner": null, - "seo_title": "", - "show_in_menus": true, - "search_description": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": null, - "last_published_at": null, - "latest_revision_created_at": null, - "live_revision": null - } - }, - { - "model": "wagtailcore.page", - "pk": 3, - "fields": { - "path": "000100010001", - "depth": 3, - "numchild": 0, - "title": "Research", - "draft_title": "Research", - "slug": "research", - "content_type": ["cdhpages", "landingpage"], - "live": true, - "has_unpublished_changes": false, - "url_path": "/home/research/", - "owner": null, - "seo_title": "", - "show_in_menus": true, - "search_description": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": null, - "last_published_at": null, - "latest_revision_created_at": null, - "live_revision": null - } - }, - { - "model": "cdhpages.homepage", - "pk": 2, - "fields": { - "body": "[]" - } - }, - { - "model": "cdhpages.landingpage", - "pk": 3, - "fields": { - "tagline": "Establishing best practices in technical research and design", - "body": "[]", - "header_image": null - } - }, - { - "model": "wagtailcore.site", - "fields": { - "hostname": "localhost", - "port": 8000, - "site_name": "test", - "root_page": 2, - "is_default_site": true - } - } -] \ No newline at end of file diff --git a/cdhweb/pages/wagtail_hooks.py b/cdhweb/pages/wagtail_hooks.py deleted file mode 100644 index 66259edc3..000000000 --- a/cdhweb/pages/wagtail_hooks.py +++ /dev/null @@ -1,34 +0,0 @@ -import wagtail.admin.rich_text.editors.draftail.features as draftail_features -from wagtail.admin.rich_text.converters.html_to_contentstate import \ - BlockElementHandler -from wagtail.core import hooks - -# blockquote registration example taken from: -# http://docs.wagtail.io/en/v2.4/advanced_topics/customisation/extending_draftail.html#creating-new-blocks - -@hooks.register('register_rich_text_features') -def register_blockquote_feature(features): - """ - Registering the `blockquote` feature, which uses the `blockquote` Draft.js block type, - and is stored as HTML with a `
` tag. - """ - feature_name = 'blockquote' - type_ = 'blockquote' - tag = 'blockquote' - - control = { - 'type': type_, - 'label': '❝', - 'description': 'Blockquote', - # Optionally, we can tell Draftail what element to use when displaying those blocks in the editor. - 'element': 'blockquote', - } - - features.register_editor_plugin( - 'draftail', feature_name, draftail_features.BlockFeature(control) - ) - - features.register_converter_rule('contentstate', feature_name, { - 'from_database_format': {tag: BlockElementHandler(type_)}, - 'to_database_format': {'block_map': {type_: tag}}, - }) From 71e357c18868680bdd8b05b221548e71957bfbca Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Tue, 22 Dec 2020 10:28:34 -0500 Subject: [PATCH 50/50] Clear out collections (except root collection) when running exodus --- cdhweb/pages/management/commands/exodus.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cdhweb/pages/management/commands/exodus.py b/cdhweb/pages/management/commands/exodus.py index 735c6c746..4fcf379ff 100644 --- a/cdhweb/pages/management/commands/exodus.py +++ b/cdhweb/pages/management/commands/exodus.py @@ -259,8 +259,11 @@ def image_exodus(self): # mezzanine/filebrowser_safe doesn't seem to have useful objects # or track metadata, so just import from the filesystem - # delete all images prior to run (clear out past migration attempts) + # delete all images and collections prior to run + # (clear out past migration attempts) Image.objects.all().delete() + Collection.objects.exclude(pk=get_root_collection_id()).delete() + # also delete any wagtail image files, since they are not deleted # by removing the objects shutil.rmtree('%s/images' % settings.MEDIA_ROOT, ignore_errors=True)