From 27c605c00f9e928e182a9b96f57145dd01fbb8a3 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 3 Mar 2015 17:43:45 +0000 Subject: [PATCH 01/48] initial work on allowing jquery.autosize to work with our new streamfield textblock --- wagtail/wagtailadmin/widgets.py | 11 +++++++++++ wagtail/wagtailcore/blocks/field_block.py | 15 ++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/wagtail/wagtailadmin/widgets.py b/wagtail/wagtailadmin/widgets.py index 4e65201f2..69282378d 100644 --- a/wagtail/wagtailadmin/widgets.py +++ b/wagtail/wagtailadmin/widgets.py @@ -15,6 +15,17 @@ from wagtail.wagtailcore.models import Page from taggit.forms import TagWidget +class AdminAutoHeightTextInput(WidgetWithScript, widgets.Textarea): + def __init__(self, attrs=None): + # Use slightly better defaults than HTML's 20x2 box + default_attrs = {'rows': '1'} + if attrs: + default_attrs.update(attrs) + super(AdminAutoHeightTextInput, self).__init__(default_attrs) + + def render_js_init(self, id_, name, value): + return '$("#{0}").autosize();'.format(json.dumps(id_)) + class AdminDateInput(WidgetWithScript, widgets.DateInput): def render_js_init(self, id_, name, value): return 'initDateChooser({0});'.format(json.dumps(id_)) diff --git a/wagtail/wagtailcore/blocks/field_block.py b/wagtail/wagtailcore/blocks/field_block.py index c6dd7b166..76c2dd9f4 100644 --- a/wagtail/wagtailcore/blocks/field_block.py +++ b/wagtail/wagtailcore/blocks/field_block.py @@ -68,13 +68,18 @@ class CharBlock(FieldBlock): class TextBlock(FieldBlock): - def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs): - self.field = forms.CharField( - widget=forms.Textarea(), - required=required, help_text=help_text, - max_length=max_length, min_length=min_length) + def __init__(self, required=True, help_text=None, rows=1, max_length=None, min_length=None, **kwargs): + self.field_options = {'required': required, 'help_text': help_text, 'max_length': max_length, 'min_length': min_length} + self.rows = rows super(TextBlock, self).__init__(**kwargs) + @cached_property + def field(self): + from wagtail.wagtailadmin.widgets import AdminAutoHeightTextInput + field_kwargs = {'widget': AdminAutoHeightTextInput(attrs={'rows':self.rows})} + field_kwargs.update(self.field_options) + return forms.CharField(**field_kwargs) + def get_searchable_content(self, value): return [force_text(value)] From 2d38f23fb09c4928fee8319ba296a8ceeaaa9e96 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Wed, 4 Mar 2015 09:23:34 +0000 Subject: [PATCH 02/48] ensuring autosize fires. styling field --- .../static/wagtailadmin/scss/layouts/page-editor.scss | 3 ++- wagtail/wagtailadmin/widgets.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss index 241932c84..1263cdb40 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss @@ -267,7 +267,8 @@ } .struct-block .widget-text_input > label, - .struct-block .widget-textarea > label{ + .struct-block .widget-textarea > label, + .struct-block .widget-admin_auto_height_text_input > label{ display:none; } diff --git a/wagtail/wagtailadmin/widgets.py b/wagtail/wagtailadmin/widgets.py index 69282378d..6a2f146b4 100644 --- a/wagtail/wagtailadmin/widgets.py +++ b/wagtail/wagtailadmin/widgets.py @@ -17,14 +17,15 @@ from taggit.forms import TagWidget class AdminAutoHeightTextInput(WidgetWithScript, widgets.Textarea): def __init__(self, attrs=None): - # Use slightly better defaults than HTML's 20x2 box + # Use more appropriate rows default, given autoheight will alter this anyway default_attrs = {'rows': '1'} if attrs: default_attrs.update(attrs) + super(AdminAutoHeightTextInput, self).__init__(default_attrs) def render_js_init(self, id_, name, value): - return '$("#{0}").autosize();'.format(json.dumps(id_)) + return '$("#{0}").autosize();'.format(id_) class AdminDateInput(WidgetWithScript, widgets.DateInput): def render_js_init(self, id_, name, value): From cdbae1f575ccc6cec4c4659d9da35fe13548e8ce Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 12 Mar 2015 10:57:54 +0000 Subject: [PATCH 03/48] Added first_published_at field to Page --- .../0011_page_first_published_at.py | 20 +++++++++++++++++++ wagtail/wagtailcore/models.py | 1 + 2 files changed, 21 insertions(+) create mode 100644 wagtail/wagtailcore/migrations/0011_page_first_published_at.py diff --git a/wagtail/wagtailcore/migrations/0011_page_first_published_at.py b/wagtail/wagtailcore/migrations/0011_page_first_published_at.py new file mode 100644 index 000000000..c1466240a --- /dev/null +++ b/wagtail/wagtailcore/migrations/0011_page_first_published_at.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0010_change_page_owner_to_null_on_delete'), + ] + + operations = [ + migrations.AddField( + model_name='page', + name='first_published_at', + field=models.DateTimeField(editable=False, null=True), + preserve_default=True, + ), + ] diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 2688f4b1e..2268a9e87 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -284,6 +284,7 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed locked = models.BooleanField(default=False, editable=False) + first_published_at = models.DateTimeField(null=True, editable=False) latest_revision_created_at = models.DateTimeField(null=True, editable=False) search_fields = ( From 0bbd8edf68ff27cc86b6cc4ce012a37478b8dd8d Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 12 Mar 2015 13:54:23 +0000 Subject: [PATCH 04/48] Set first_published_at on first publish --- wagtail/wagtailcore/models.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 2268a9e87..35122105a 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -1140,6 +1140,11 @@ class PageRevision(models.Model): # If page goes live clear the approved_go_live_at of all revisions page.revisions.update(approved_go_live_at=None) page.expired = False # When a page is published it can't be expired + + # Set first_published_at if the page is being published now + if page.live and page.first_published_at is None: + page.first_published_at = timezone.now() + page.save() self.submitted_for_moderation = False page.revisions.update(submitted_for_moderation=False) From 00e5138885cd5702deeac8af9119d8a41d4c5fa5 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 12 Mar 2015 13:55:06 +0000 Subject: [PATCH 05/48] Some tests for first_published_at --- wagtail/wagtailadmin/tests/test_pages_views.py | 4 ++++ wagtail/wagtailcore/tests/test_management_commands.py | 1 + 2 files changed, 5 insertions(+) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 0d4b5e93c..0824b9d77 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -212,6 +212,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): self.assertEqual(page.title, post_data['title']) self.assertIsInstance(page, SimplePage) self.assertFalse(page.live) + self.assertFalse(page.first_published_at) # treebeard should report no consistency problems with the tree self.assertFalse(any(Page.find_problems()), 'treebeard found consistency problems') @@ -298,6 +299,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): self.assertEqual(page.title, post_data['title']) self.assertIsInstance(page, SimplePage) self.assertTrue(page.live) + self.assertTrue(page.first_published_at) # Check that the page_published signal was fired self.assertTrue(signal_fired[0]) @@ -333,6 +335,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): self.assertTrue(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists()) # But Page won't be live self.assertFalse(page.live) + self.assertFalse(page.first_published_at) self.assertTrue(page.status_string, "scheduled") def test_create_simplepage_post_submit(self): @@ -357,6 +360,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): self.assertEqual(page.title, post_data['title']) self.assertIsInstance(page, SimplePage) self.assertFalse(page.live) + self.assertFalse(page.first_published_at) # The latest revision for the page should now be in moderation self.assertTrue(page.get_latest_revision().submitted_for_moderation) diff --git a/wagtail/wagtailcore/tests/test_management_commands.py b/wagtail/wagtailcore/tests/test_management_commands.py index d631617b3..91fc727dc 100644 --- a/wagtail/wagtailcore/tests/test_management_commands.py +++ b/wagtail/wagtailcore/tests/test_management_commands.py @@ -188,6 +188,7 @@ class TestPublishScheduledPagesCommand(TestCase): p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) + self.assertTrue(p.first_published_at) self.assertFalse(p.has_unpublished_changes) self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) From 8d9e72de09341fb48b7e2a396111b4d4b14fc6db Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Sun, 15 Mar 2015 20:04:33 +0000 Subject: [PATCH 06/48] Pass fields attribute to inlineformset_factory --- wagtail/wagtailusers/views/groups.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailusers/views/groups.py b/wagtail/wagtailusers/views/groups.py index 21eb08c76..3a7ba8cde 100644 --- a/wagtail/wagtailusers/views/groups.py +++ b/wagtail/wagtailusers/views/groups.py @@ -83,7 +83,8 @@ def create(request): Group, GroupPagePermission, formset=BaseGroupPagePermissionFormSet, - extra=0 + extra=0, + fields=('page', 'permission_type'), ) if request.POST: form = GroupForm(request.POST) @@ -115,7 +116,8 @@ def edit(request, group_id): Group, GroupPagePermission, formset=BaseGroupPagePermissionFormSet, - extra=0 + extra=0, + fields=('page', 'permission_type'), ) if request.POST: form = GroupForm(request.POST, instance=group) From af7c1de978a7647d6c2023dda43133d3523bb103 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 16 Mar 2015 11:58:34 +0000 Subject: [PATCH 07/48] Removed page modes tests These views were already being tested in TestFormSubmission. I don't think anyone will ever call the modes directly making these tests a bit pointless. They also use the assertTemplateUsed method badly (these should be used on a response). --- wagtail/wagtailforms/tests.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py index 6b064fd35..25571bcc1 100644 --- a/wagtail/wagtailforms/tests.py +++ b/wagtail/wagtailforms/tests.py @@ -79,29 +79,6 @@ class TestFormSubmission(TestCase): self.assertIn("baz", submission[0].form_data) -class TestPageModes(TestCase): - fixtures = ['test.json'] - - def setUp(self): - self.form_page = Page.objects.get(url_path='/home/contact-us/').specific - - def test_form(self): - response = self.form_page.serve_preview(self.form_page.dummy_request(), 'form') - - # Check response - self.assertContains(response, """""") - self.assertTemplateUsed(response, 'tests/form_page.html') - self.assertTemplateNotUsed(response, 'tests/form_page_landing.html') - - def test_landing(self): - response = self.form_page.serve_preview(self.form_page.dummy_request(), 'landing') - - # Check response - self.assertContains(response, "Thank you for your feedback.") - self.assertTemplateNotUsed(response, 'tests/form_page.html') - self.assertTemplateUsed(response, 'tests/form_page_landing.html') - - class TestFormBuilder(TestCase): fixtures = ['test.json'] From 6f916a611b9ff8867f9c24d68fa795ec3c996cd9 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 17 Mar 2015 11:49:42 +0000 Subject: [PATCH 08/48] Make sure first_published_at is using live copy When publishing, the first_published at gets set on the live copy, not in the revision. This caused it to be set again on next publish because it is reading from the revision. --- wagtail/wagtailcore/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 35122105a..2753842b7 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -1100,6 +1100,7 @@ class PageRevision(models.Model): obj.owner = self.page.owner obj.locked = self.page.locked obj.latest_revision_created_at = self.page.latest_revision_created_at + obj.first_published_at = self.page.first_published_at return obj From 24e633e8e7b7cc7887228c2ad076257908a97f23 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 17 Mar 2015 13:51:17 +0000 Subject: [PATCH 09/48] Test republishing a page wont set first_published --- .../wagtailadmin/tests/test_pages_views.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 0824b9d77..2a28ca420 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -442,12 +442,13 @@ class TestPageEdit(TestCase, WagtailTestUtils): self.root_page = Page.objects.get(id=2) # Add child page - self.child_page = SimplePage() - self.child_page.title = "Hello world!" - self.child_page.slug = "hello-world" - self.child_page.live = True - self.root_page.add_child(instance=self.child_page) - self.child_page.save_revision() + child_page = SimplePage( + title="Hello world!", + slug="hello-world", + ) + self.root_page.add_child(instance=child_page) + child_page.save_revision().publish() + self.child_page = SimplePage.objects.get(id=child_page.id) # Add event page (to test edit handlers) self.event_page = EventPage() @@ -588,6 +589,9 @@ class TestPageEdit(TestCase, WagtailTestUtils): self.child_page.has_unpublished_changes = True self.child_page.save() + # Save current value of first_published_at so we can check that it doesn't change + first_published_at = SimplePage.objects.get(id=self.child_page.id).first_published_at + # Tests publish from edit page post_data = { 'title': "I've been edited!", @@ -612,6 +616,9 @@ class TestPageEdit(TestCase, WagtailTestUtils): # The page shouldn't have "has_unpublished_changes" flag set self.assertFalse(child_page_new.has_unpublished_changes) + # first_published_at should not change as it was already set + self.assertEqual(first_published_at, child_page_new.first_published_at) + def test_edit_post_publish_scheduled(self): go_live_at = timezone.now() + timedelta(days=1) expire_at = timezone.now() + timedelta(days=2) From 3abfa6304680962fb57021c284846a40cce8e562 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 17 Mar 2015 14:49:29 +0000 Subject: [PATCH 10/48] Specify 1.7.1 as the minimum supported Django version --- README.rst | 2 +- setup.py | 2 +- tox.ini | 2 +- wagtail/project_template/requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 910036050..1f6eb8b4e 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,7 @@ Ask your questions on our `Google Group `_. diff --git a/setup.py b/setup.py index 2cf645026..0ecc19863 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ PY3 = sys.version_info[0] == 3 install_requires = [ - "Django>=1.7.0,<1.8", + "Django>=1.7.1,<1.8", "django-compressor>=1.4", "django-modelcluster>=0.5", "django-taggit==0.12.3", diff --git a/tox.ini b/tox.ini index 49db8ec7c..1f40d9017 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ base = coverage dj17 = - Django>=1.7,<1.8 + Django>=1.7.1,<1.8 dj18 = https://github.com/django/django/archive/stable/1.8.x.zip#egg=django diff --git a/wagtail/project_template/requirements.txt b/wagtail/project_template/requirements.txt index 844a4df90..b5dfbfea6 100644 --- a/wagtail/project_template/requirements.txt +++ b/wagtail/project_template/requirements.txt @@ -1,5 +1,5 @@ # Minimal requirements -Django>=1.7,<1.8 +Django>=1.7.1,<1.8 wagtail==0.8.6 django-libsass>=0.2 Pillow>=2.6.1 From 846b3095938843c0af1ae627c16272ea171873c7 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 17 Mar 2015 16:26:43 +0000 Subject: [PATCH 11/48] Release note for #1054 --- CHANGELOG.txt | 1 + docs/releases/0.9.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e90148ae7..259cd1b9d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -31,6 +31,7 @@ Changelog * The `document_served` signal now correctly passes the Document class as `sender` and the document as `instance` * Image/Document edit page no longer throws OSError when the original image is missing * Page classes can specify an edit_handler property to override the default Content / Promote / Settings tabbed interface + * The Page model now records the date/time that a page was first published, as the field `first_published_at` 0.8.6 (10.03.2015) diff --git a/docs/releases/0.9.rst b/docs/releases/0.9.rst index 1a526d305..7d50adda1 100644 --- a/docs/releases/0.9.rst +++ b/docs/releases/0.9.rst @@ -22,6 +22,7 @@ Core ---- * Added validation to prevent pages being created with only whitespace characters in their title fields + * The Page model now records the date/time that a page was first published, as the field ``first_published_at`` Admin From 62f755054a5c1aac1e38f795640f71188d31a3b6 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 10:08:01 +0000 Subject: [PATCH 12/48] Add some more detail to a 0.9 release note Fixes #1065 --- docs/releases/0.9.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releases/0.9.rst b/docs/releases/0.9.rst index 7d50adda1..dcb45c628 100644 --- a/docs/releases/0.9.rst +++ b/docs/releases/0.9.rst @@ -117,6 +117,12 @@ Password reset view name has changed from ``password_reset`` to ``wagtailadmin_p You no longer need ``LOGIN_URL`` and ``LOGIN_REDIRECT_URL`` to point to Wagtail admin. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If you are upgrading from an older version of Wagtail, you probably want to remove these from your project settings. + +Prevously, these two settings needed to be set to ``wagtailadmin_login`` and ``wagtailadmin_dashboard`` +respectively or Wagtail would become very tricky to log in to. This is no longer the case and Wagtail +should work fine without them. + Javascript includes in admin backend have been moved ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 58f89d2a9135533e48db3de489f456ee180829b1 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 10:08:48 +0000 Subject: [PATCH 13/48] Admin menu => Left hand menu --- docs/releases/0.9.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/releases/0.9.rst b/docs/releases/0.9.rst index dcb45c628..bedf3fccb 100644 --- a/docs/releases/0.9.rst +++ b/docs/releases/0.9.rst @@ -30,7 +30,7 @@ Admin **UI** - * Improvements to the layout of the admin menu footer + * Improvements to the layout of the left-hand menu footer * Added thousands separator for counters on dashboard * Added contextual links to admin notification messages * When copying pages, it is now possible to specify a place to copy to From ed9aad1418a75c5e7cbac4e3125af64545e09d0c Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 10:12:39 +0000 Subject: [PATCH 14/48] Move login url settings release note up one space --- docs/releases/0.9.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/releases/0.9.rst b/docs/releases/0.9.rst index bedf3fccb..76ba59232 100644 --- a/docs/releases/0.9.rst +++ b/docs/releases/0.9.rst @@ -106,14 +106,6 @@ It is no longer necessary to pass the base model as a parameter, so this declara The old format is now deprecated; all existing ``InlinePanel`` declarations should be updated to the new format. -Login/Password reset views renamed -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It was previously possible to reverse the Wagtail login using django.contrib.auth.views.login. -This is no longer possible. Update any references to ``wagtailadmin_login``. - -Password reset view name has changed from ``password_reset`` to ``wagtailadmin_password_reset``. - You no longer need ``LOGIN_URL`` and ``LOGIN_REDIRECT_URL`` to point to Wagtail admin. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -123,6 +115,14 @@ Prevously, these two settings needed to be set to ``wagtailadmin_login`` and ``w respectively or Wagtail would become very tricky to log in to. This is no longer the case and Wagtail should work fine without them. +Login/Password reset views renamed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It was previously possible to reverse the Wagtail login using django.contrib.auth.views.login. +This is no longer possible. Update any references to ``wagtailadmin_login``. + +Password reset view name has changed from ``password_reset`` to ``wagtailadmin_password_reset``. + Javascript includes in admin backend have been moved ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From bd28d9288ab1d718e2e3a3a78d5e9a263c244fa7 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 18 Mar 2015 10:49:46 +0000 Subject: [PATCH 15/48] Increase the maximum length of a page slug from 50 to 255 characters - fixes #787 --- CHANGELOG.txt | 1 + docs/releases/0.9.rst | 1 + .../migrations/0012_extend_page_slug_field.py | 20 +++++++++++++++++++ wagtail/wagtailcore/models.py | 2 +- 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 wagtail/wagtailcore/migrations/0012_extend_page_slug_field.py diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 259cd1b9d..cbfefcd17 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -32,6 +32,7 @@ Changelog * Image/Document edit page no longer throws OSError when the original image is missing * Page classes can specify an edit_handler property to override the default Content / Promote / Settings tabbed interface * The Page model now records the date/time that a page was first published, as the field `first_published_at` + * Increased the maximum length of a page slug from 50 to 255 characters 0.8.6 (10.03.2015) diff --git a/docs/releases/0.9.rst b/docs/releases/0.9.rst index 76ba59232..fa2869bc3 100644 --- a/docs/releases/0.9.rst +++ b/docs/releases/0.9.rst @@ -23,6 +23,7 @@ Core * Added validation to prevent pages being created with only whitespace characters in their title fields * The Page model now records the date/time that a page was first published, as the field ``first_published_at`` + * Increased the maximum length of a page slug from 50 to 255 characters Admin diff --git a/wagtail/wagtailcore/migrations/0012_extend_page_slug_field.py b/wagtail/wagtailcore/migrations/0012_extend_page_slug_field.py new file mode 100644 index 000000000..7fa57b132 --- /dev/null +++ b/wagtail/wagtailcore/migrations/0012_extend_page_slug_field.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0011_page_first_published_at'), + ] + + operations = [ + migrations.AlterField( + model_name='page', + name='slug', + field=models.SlugField(help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255), + preserve_default=True, + ), + ] diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 2753842b7..a761be828 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -265,7 +265,7 @@ class PageBase(models.base.ModelBase): @python_2_unicode_compatible class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed)): title = models.CharField(max_length=255, help_text=_("The page title as you'd like it to be seen by the public")) - slug = models.SlugField(help_text=_("The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/")) + slug = models.SlugField(max_length=255, help_text=_("The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/")) # TODO: enforce uniqueness on slug field per parent (will have to be done at the Django # level rather than db, since there is no explicit parent relation in the db) content_type = models.ForeignKey('contenttypes.ContentType', related_name='pages') From 838affad84506ececcb5887fc7ba12c8117b2bce Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 18 Mar 2015 17:19:58 +0000 Subject: [PATCH 16/48] Use AdminAutoHeightTextInput on plain TextFields - fixes #1041 --- wagtail/wagtailadmin/edit_handlers.py | 31 +++++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index a694d9b5b..4990485b0 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -25,26 +25,43 @@ from wagtail.wagtailcore.utils import camelcase_to_underscore, resolve_model_str from wagtail.utils.deprecation import RemovedInWagtail11Warning +# Form field properties to override whenever we encounter a model field +# that matches one of these types - including subclasses FORM_FIELD_OVERRIDES = { models.DateField: {'widget': widgets.AdminDateInput}, models.TimeField: {'widget': widgets.AdminTimeInput}, models.DateTimeField: {'widget': widgets.AdminDateTimeInput}, - TaggableManager: {'widget': widgets.AdminTagWidget} + TaggableManager: {'widget': widgets.AdminTagWidget}, +} + +# Form field properties to override whenever we encounter a model field +# that matches one of these types exactly, ignoring subclasses. +# (This allows us to override the widget for models.TextField, but leave +# the RichTextField widget alone) +DIRECT_FORM_FIELD_OVERRIDES = { + models.TextField: {'widget': widgets.AdminAutoHeightTextInput}, } # Callback to allow us to override the default form fields provided for each model field. def formfield_for_dbfield(db_field, **kwargs): - # snarfed from django/contrib/admin/options.py + # adapted from django/contrib/admin/options.py + + overrides = None # If we've got overrides for the formfield defined, use 'em. **kwargs # passed to formfield_for_dbfield override the defaults. - for klass in db_field.__class__.mro(): - if klass in FORM_FIELD_OVERRIDES: - kwargs = dict(copy.deepcopy(FORM_FIELD_OVERRIDES[klass]), **kwargs) - return db_field.formfield(**kwargs) + if db_field.__class__ in DIRECT_FORM_FIELD_OVERRIDES: + overrides = DIRECT_FORM_FIELD_OVERRIDES[db_field.__class__] + else: + for klass in db_field.__class__.mro(): + if klass in FORM_FIELD_OVERRIDES: + overrides = FORM_FIELD_OVERRIDES[klass] + break + + if overrides: + kwargs = dict(copy.deepcopy(overrides), **kwargs) - # For any other type of field, just call its formfield() method. return db_field.formfield(**kwargs) From 2b57c8c43f22ee8085b20aa49e2e381897f46e3a Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 18 Mar 2015 17:30:55 +0000 Subject: [PATCH 17/48] test for 838affa --- .../wagtailadmin/tests/test_edit_handlers.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailadmin/tests/test_edit_handlers.py b/wagtail/wagtailadmin/tests/test_edit_handlers.py index f9cbdd020..0e38456ed 100644 --- a/wagtail/wagtailadmin/tests/test_edit_handlers.py +++ b/wagtail/wagtailadmin/tests/test_edit_handlers.py @@ -16,10 +16,11 @@ from wagtail.wagtailadmin.edit_handlers import ( InlinePanel, ) -from wagtail.wagtailadmin.widgets import AdminPageChooser, AdminDateInput +from wagtail.wagtailadmin.widgets import AdminPageChooser, AdminDateInput, AdminAutoHeightTextInput from wagtail.wagtailimages.edit_handlers import ImageChooserPanel from wagtail.wagtailcore.models import Page, Site -from wagtail.tests.models import PageChooserModel, EventPageChooserModel, EventPage, EventPageSpeaker +from wagtail.wagtailcore.fields import RichTextArea +from wagtail.tests.models import PageChooserModel, EventPageChooserModel, EventPage, EventPageSpeaker, SimplePage from wagtail.tests.utils import WagtailTestUtils from wagtail.utils.deprecation import RemovedInWagtail11Warning @@ -43,6 +44,22 @@ class TestGetFormForModel(TestCase): self.assertIn('speakers', form.formsets) self.assertIn('related_links', form.formsets) + def test_direct_form_field_overrides(self): + # Test that field overrides defined through DIRECT_FORM_FIELD_OVERRIDES + # are applied + + SimplePageForm = get_form_for_model(SimplePage) + simple_form = SimplePageForm() + # plain TextFields should use AdminAutoHeightTextInput as the widget + self.assertEqual(type(simple_form.fields['content'].widget), AdminAutoHeightTextInput) + + # This override should NOT be applied to subclasses of TextField such as + # RichTextField - they should retain their default widgets + EventPageForm = get_form_for_model(EventPage) + event_form = EventPageForm() + self.assertEqual(type(event_form.fields['body'].widget), RichTextArea) + + def test_get_form_for_model_with_specific_fields(self): EventPageForm = get_form_for_model(EventPage, fields=['date_from'], formsets=['speakers']) form = EventPageForm() From a447e53ba7ed81207eaf19eb22f711388b6ac7ed Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 19:03:43 +0000 Subject: [PATCH 18/48] Run tests with SQLite by default SQLite doesn't require any initial setup and is significantly faster than PostgreSQL Also removed USER/PASSWORD/HOST vars. Postgres users can pass in PGUSER, PGPASSWORD and PGHOST environment variables instead --- wagtail/tests/settings.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/wagtail/tests/settings.py b/wagtail/tests/settings.py index 96d919cbd..03f2d72e8 100644 --- a/wagtail/tests/settings.py +++ b/wagtail/tests/settings.py @@ -11,13 +11,8 @@ MEDIA_URL = '/media/' DATABASES = { 'default': { - 'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.db.backends.postgresql_psycopg2'), - 'NAME': os.environ.get('DATABASE_NAME', 'wagtaildemo'), - 'TEST_NAME': os.environ.get('DATABASE_NAME', 'test_wagtaildemo'), - 'USER': os.environ.get('DATABASE_USER', 'postgres'), - 'PASSWORD': os.environ.get('DATABASE_PASS', None), - 'HOST': os.environ.get('DATABASE_HOST', None), - 'PORT': os.environ.get('DATABASE_PORT', None), + 'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.db.backends.sqlite3'), + 'NAME': os.environ.get('DATABASE_NAME', 'wagtail'), } } From bff488d8fb5d27e578781ff28d1a71f2cc437d0c Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 19:05:31 +0000 Subject: [PATCH 19/48] Removed .drone.yml --- .drone.yml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .drone.yml diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index c9af87684..000000000 --- a/.drone.yml +++ /dev/null @@ -1,11 +0,0 @@ -image: kaedroho/django-base -env: - - DATABASE_HOST=postgres - - ELASTICSEARCH_URL=http://elasticsearch:9200/ -script: - - pip3.4 install mock python-dateutil pytz elasticsearch - - python3.4 setup.py install - - python3.4 runtests.py -services: - - postgres - - dockerfile/elasticsearch From 55ce9f761e375c3b6d5111157a93e0776ea5797c Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 19:06:15 +0000 Subject: [PATCH 20/48] Added *.swp and /venv to gitignore .swp files are made automatically by vim and are easy to accidentally commit /venv is a very common place to put a virtual environment which we wouldn't want to be committed --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index bd904efc9..c8ceb2743 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.swp *.pyc .DS_Store /.coverage @@ -6,3 +7,4 @@ /wagtail.egg-info/ /docs/_build/ /.tox/ +/venv From 23ee87243b64157ae2a658c37a35f92514f45c8d Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 19:13:20 +0000 Subject: [PATCH 21/48] Added Pillow to dev requirements --- requirements-dev.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 166dd353d..d4a4634a4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,9 @@ -# For coverage and PEP8 linting -coverage>=3.7.0 -flake8>=2.2.0 +# Required for running the tests mock>=1.0.0 python-dateutil>=2.2 pytz>=2014.7 +Pillow>=2.7.0 + +# For coverage and PEP8 linting +coverage>=3.7.0 +flake8>=2.2.0 From 5aa6ed2c2a91a709ac256ee63eee981fc71ad3bb Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 19:44:30 +0000 Subject: [PATCH 22/48] Docs for running the tests Fixes #1004 --- docs/howto/contributing.rst | 64 +++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/howto/contributing.rst b/docs/howto/contributing.rst index 797176997..f43c63d46 100644 --- a/docs/howto/contributing.rst +++ b/docs/howto/contributing.rst @@ -18,6 +18,70 @@ Coding guidelines * Python 2 and 3 compatibility. All contributions should support Python 2 and 3 and we recommend using the `six `_ compatibility library (use the pip version installed as a dependency, not the version bundled with Django). * Tests. Wagtail has a suite of tests, which we are committed to improving and expanding. We run continuous integration at `travis-ci.org/torchbox/wagtail `_ to ensure that no commits or pull requests introduce test failures. If your contributions add functionality to Wagtail, please include the additional tests to cover it; if your contributions alter existing functionality, please update the relevant tests accordingly. +Running the unit tests +~~~~~~~~~~~~~~~~~~~~~~ + +In order to run Wagtail's test suite, you will need to install some dependencies first. We recommend installing these into a virtual environment. + + +**Setting up the virtual environment** + +If you are using Python 3.3 or above, run the following commands in your shell +at the root of the Wagtail repo:: + + pyvenv venv + source venv/bin/activate + python setup.py develop + pip install -r requirements-dev.txt + +For Python 2, you will need to install the ``virtualenv`` package and replace +the first line above with: + + virtualenv venv + +**Running the tests** + +With your virtual environment active, run the following command to run all the +tests:: + + python runtests.py + +**Running only some of the tests** + +At the time of writing, Wagtail has nearly 1000 tests which takes a while to +run. You can run tests for only one part of Wagtail by passing in the path as +an argument to ``runtests.py``:: + + python runtests.py wagtail.wagtailcore + +**Testing against PostgreSQL** + +By default, Wagtail tests against SQLite. If you need to test against a +different database, set the ``DATABASE_ENGINE`` environment variable to the +name of the Django database backend to test against:: + + DATABASE_ENGINE=django.db.backends.postgresql_psycopg2 python runtests.py + +This will create a new database called ``test_wagtail`` in PostgreSQL and run +the tests against it. + +If you need to use a different user, password or host. Use the ``PGUSER``, ``PGPASSWORD`` and ``PGHOST`` environment variables. + +**Testing Elasticsearch** + +To test Elasticsearch, you need to have the ``elasticsearch`` package installed. + +Once installed, Wagtail will attempt to connect to a local instance of +Elasticsearch (``http://localhost:9200``) and use the index ``test_wagtail``. + +If your Elasticsearch instance is located somewhere else, you can set the +``ELASTICSEARCH_URL`` environment variable to point to its location:: + + ELASTICSEARCH_URL=http://my-elasticsearch-instance:9200 python runtests.py + +If you no longer want Wagtail to test against Elasticsearch, uninstall the +``elasticsearch`` package. + Styleguide ~~~~~~~~~~ From 5705ace2c60cd53f726cab88612f2fbabd27f739 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 22:55:53 +0000 Subject: [PATCH 23/48] Move GroupPagePermissionFormSet into users/forms.py --- wagtail/wagtailusers/forms.py | 10 ++++++++++ wagtail/wagtailusers/views/groups.py | 18 +----------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/wagtail/wagtailusers/forms.py b/wagtail/wagtailusers/forms.py index 3c7c3acb5..3fc1b3096 100644 --- a/wagtail/wagtailusers/forms.py +++ b/wagtail/wagtailusers/forms.py @@ -3,6 +3,7 @@ from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm from django.utils.translation import ugettext_lazy as _ from django.contrib.auth import get_user_model from django.contrib.auth.models import Group, Permission +from django.forms.models import inlineformset_factory from wagtail.wagtailcore import hooks from wagtail.wagtailadmin.widgets import AdminPageChooser @@ -213,6 +214,15 @@ class BaseGroupPagePermissionFormSet(forms.models.BaseInlineFormSet): return empty_form +GroupPagePermissionFormSet = inlineformset_factory( + Group, + GroupPagePermission, + formset=BaseGroupPagePermissionFormSet, + extra=0, + fields=('page', 'permission_type'), +) + + class NotificationPreferencesForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(NotificationPreferencesForm, self).__init__(*args, **kwargs) diff --git a/wagtail/wagtailusers/views/groups.py b/wagtail/wagtailusers/views/groups.py index 3a7ba8cde..6c8654216 100644 --- a/wagtail/wagtailusers/views/groups.py +++ b/wagtail/wagtailusers/views/groups.py @@ -5,12 +5,10 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from django.views.decorators.vary import vary_on_headers -from django.forms.models import inlineformset_factory from wagtail.wagtailadmin import messages from wagtail.wagtailadmin.forms import SearchForm -from wagtail.wagtailusers.forms import GroupForm, BaseGroupPagePermissionFormSet -from wagtail.wagtailcore.models import GroupPagePermission +from wagtail.wagtailusers.forms import GroupForm, GroupPagePermissionFormSet def user_has_group_model_perm(user): @@ -79,13 +77,6 @@ def index(request): @permission_required('auth.add_group') def create(request): - GroupPagePermissionFormSet = inlineformset_factory( - Group, - GroupPagePermission, - formset=BaseGroupPagePermissionFormSet, - extra=0, - fields=('page', 'permission_type'), - ) if request.POST: form = GroupForm(request.POST) formset = GroupPagePermissionFormSet(request.POST) @@ -112,13 +103,6 @@ def create(request): @permission_required('auth.change_group') def edit(request, group_id): group = get_object_or_404(Group, id=group_id) - GroupPagePermissionFormSet = inlineformset_factory( - Group, - GroupPagePermission, - formset=BaseGroupPagePermissionFormSet, - extra=0, - fields=('page', 'permission_type'), - ) if request.POST: form = GroupForm(request.POST, instance=group) formset = GroupPagePermissionFormSet(request.POST, instance=group) From fc6fe17c8cd1a63be0566928dde44d488188e6b3 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Thu, 19 Mar 2015 11:12:44 +0000 Subject: [PATCH 24/48] release note for #1070 --- CHANGELOG.txt | 1 + docs/releases/0.9.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index cbfefcd17..e05cd9045 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -33,6 +33,7 @@ Changelog * Page classes can specify an edit_handler property to override the default Content / Promote / Settings tabbed interface * The Page model now records the date/time that a page was first published, as the field `first_published_at` * Increased the maximum length of a page slug from 50 to 255 characters + * Plain text fields in the page editor now use auto-expanding text areas 0.8.6 (10.03.2015) diff --git a/docs/releases/0.9.rst b/docs/releases/0.9.rst index fa2869bc3..58898fa3b 100644 --- a/docs/releases/0.9.rst +++ b/docs/releases/0.9.rst @@ -37,6 +37,7 @@ Admin * When copying pages, it is now possible to specify a place to copy to * Added pagination to the snippets listing and chooser * Page / document / image / snippet choosers now include a link to edit the chosen item + * Plain text fields in the page editor now use auto-expanding text areas **Page editor** From 550d4e94daa98fd3c53548f3a6b91109619d1423 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Sat, 21 Feb 2015 17:37:14 +0000 Subject: [PATCH 25/48] Standardise date/time formats between admin datetime widgets and Javascript code. The JS for the datetime picker was previously updated to include seconds, to match Django's default format and thus fix #479 - however, the same bug also affected the time picker, which was left unfixed. Since we typically don't want seconds in our datetimes anyhow, we reverse that fix here, and instead update AdminDateWidget / AdminTimeWidget / AdminDateTimeWidget to explicitly use the seconds-less format that the JS expects. This also ensures that the JS behaviour will not break as a result of a site owner setting a non-standard value for DATE_INPUT_FORMATS / TIME_INPUT_FORMATS / DATETIME_INPUT_FORMATS in their site settings. --- .../static/wagtailadmin/js/page-editor.js | 4 +-- wagtail/wagtailadmin/widgets.py | 12 +++++++++ .../0011_update_golive_expire_help_text.py | 26 +++++++++++++++++++ wagtail/wagtailcore/models.py | 4 +-- 4 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 wagtail/wagtailcore/migrations/0011_update_golive_expire_help_text.py diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js b/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js index 4f53792b9..448d4c052 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js @@ -104,7 +104,7 @@ function initDateTimeChooser(id) { if (window.dateTimePickerTranslations) { $('#' + id).datetimepicker({ closeOnDateSelect: true, - format: 'Y-m-d H:i:s', + format: 'Y-m-d H:i', scrollInput:false, i18n: { lang: window.dateTimePickerTranslations @@ -113,7 +113,7 @@ function initDateTimeChooser(id) { }); } else { $('#' + id).datetimepicker({ - format: 'Y-m-d H:i:s', + format: 'Y-m-d H:i', }); } } diff --git a/wagtail/wagtailadmin/widgets.py b/wagtail/wagtailadmin/widgets.py index 6a2f146b4..19c35a7e0 100644 --- a/wagtail/wagtailadmin/widgets.py +++ b/wagtail/wagtailadmin/widgets.py @@ -28,16 +28,28 @@ class AdminAutoHeightTextInput(WidgetWithScript, widgets.Textarea): return '$("#{0}").autosize();'.format(id_) class AdminDateInput(WidgetWithScript, widgets.DateInput): + # Set a default date format to match the one that our JS date picker expects - + # it can still be overridden explicitly, but this way it won't be affected by + # the DATE_INPUT_FORMATS setting + def __init__(self, attrs=None, format='%Y-%m-%d'): + super(AdminDateInput, self).__init__(attrs=attrs, format=format) + def render_js_init(self, id_, name, value): return 'initDateChooser({0});'.format(json.dumps(id_)) class AdminTimeInput(WidgetWithScript, widgets.TimeInput): + def __init__(self, attrs=None, format='%H:%M'): + super(AdminTimeInput, self).__init__(attrs=attrs, format=format) + def render_js_init(self, id_, name, value): return 'initTimeChooser({0});'.format(json.dumps(id_)) class AdminDateTimeInput(WidgetWithScript, widgets.DateTimeInput): + def __init__(self, attrs=None, format='%Y-%m-%d %H:%M'): + super(AdminDateTimeInput, self).__init__(attrs=attrs, format=format) + def render_js_init(self, id_, name, value): return 'initDateTimeChooser({0});'.format(json.dumps(id_)) diff --git a/wagtail/wagtailcore/migrations/0011_update_golive_expire_help_text.py b/wagtail/wagtailcore/migrations/0011_update_golive_expire_help_text.py new file mode 100644 index 000000000..b16ed85f1 --- /dev/null +++ b/wagtail/wagtailcore/migrations/0011_update_golive_expire_help_text.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0010_change_page_owner_to_null_on_delete'), + ] + + operations = [ + migrations.AlterField( + model_name='page', + name='expire_at', + field=models.DateTimeField(help_text='Please add a date-time in the form YYYY-MM-DD hh:mm.', null=True, verbose_name='Expiry date/time', blank=True), + preserve_default=True, + ), + migrations.AlterField( + model_name='page', + name='go_live_at', + field=models.DateTimeField(help_text='Please add a date-time in the form YYYY-MM-DD hh:mm.', null=True, verbose_name='Go live date/time', blank=True), + preserve_default=True, + ), + ] diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index a761be828..7889a5b57 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -278,8 +278,8 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed show_in_menus = models.BooleanField(default=False, help_text=_("Whether a link to this page will appear in automatically generated menus")) search_description = models.TextField(blank=True) - go_live_at = models.DateTimeField(verbose_name=_("Go live date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm:ss."), blank=True, null=True) - expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm:ss."), blank=True, null=True) + go_live_at = models.DateTimeField(verbose_name=_("Go live date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True) + expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True) expired = models.BooleanField(default=False, editable=False) locked = models.BooleanField(default=False, editable=False) From 3d705dc43d6f666183a7606671a8b7ddf30c3c22 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Thu, 19 Mar 2015 11:37:14 +0000 Subject: [PATCH 26/48] renumber migration --- ...pire_help_text.py => 0013_update_golive_expire_help_text.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename wagtail/wagtailcore/migrations/{0011_update_golive_expire_help_text.py => 0013_update_golive_expire_help_text.py} (92%) diff --git a/wagtail/wagtailcore/migrations/0011_update_golive_expire_help_text.py b/wagtail/wagtailcore/migrations/0013_update_golive_expire_help_text.py similarity index 92% rename from wagtail/wagtailcore/migrations/0011_update_golive_expire_help_text.py rename to wagtail/wagtailcore/migrations/0013_update_golive_expire_help_text.py index b16ed85f1..dea1ca318 100644 --- a/wagtail/wagtailcore/migrations/0011_update_golive_expire_help_text.py +++ b/wagtail/wagtailcore/migrations/0013_update_golive_expire_help_text.py @@ -7,7 +7,7 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('wagtailcore', '0010_change_page_owner_to_null_on_delete'), + ('wagtailcore', '0012_extend_page_slug_field'), ] operations = [ From 8e3ea58b25dda4e22acbc09d60095746773a5a97 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Thu, 19 Mar 2015 11:53:43 +0000 Subject: [PATCH 27/48] release note for #1018 --- CHANGELOG.txt | 1 + docs/releases/0.9.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e05cd9045..12b8febd7 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -34,6 +34,7 @@ Changelog * The Page model now records the date/time that a page was first published, as the field `first_published_at` * Increased the maximum length of a page slug from 50 to 255 characters * Plain text fields in the page editor now use auto-expanding text areas + * Date / time pickers now consistently use times without seconds, to prevent Javascript behaviour glitches when focusing / unfocusing fields 0.8.6 (10.03.2015) diff --git a/docs/releases/0.9.rst b/docs/releases/0.9.rst index 58898fa3b..1274cf428 100644 --- a/docs/releases/0.9.rst +++ b/docs/releases/0.9.rst @@ -63,6 +63,7 @@ Admin * Removed the need to add permission check on admin views (now automated) * Reversing ``django.contrib.auth.admin.login`` will no longer lead to Wagtails login view (making it easier to have front end views) * Added cache-control headers to all admin views. This allows Varnish/Squid/CDN to run on vanilla settings in front of a Wagtail site + * Date / time pickers now consistently use times without seconds, to prevent Javascript behaviour glitches when focusing / unfocusing fields Project template From 156f715be6784e8e7513f098ee7f1ccc641ef106 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Fri, 20 Mar 2015 20:58:52 +0000 Subject: [PATCH 28/48] within StreamBlock's default rendering, call force_text on children to prevent spurious escaping - fixes #1078 --- wagtail/wagtailcore/blocks/stream_block.py | 4 ++-- wagtail/wagtailcore/tests/test_blocks.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/wagtail/wagtailcore/blocks/stream_block.py b/wagtail/wagtailcore/blocks/stream_block.py index ad3a0a7c8..c491c4aa1 100644 --- a/wagtail/wagtailcore/blocks/stream_block.py +++ b/wagtail/wagtailcore/blocks/stream_block.py @@ -6,7 +6,7 @@ from django import forms from django.core.exceptions import ValidationError from django.forms.utils import ErrorList from django.template.loader import render_to_string -from django.utils.encoding import python_2_unicode_compatible +from django.utils.encoding import python_2_unicode_compatible, force_text from django.utils.html import format_html_join from django.utils.safestring import mark_safe @@ -192,7 +192,7 @@ class BaseStreamBlock(Block): def render_basic(self, value): return format_html_join('\n', '
{0}
', - [(child, child.block_type) for child in value] + [(force_text(child), child.block_type) for child in value] ) def get_searchable_content(self, value): diff --git a/wagtail/wagtailcore/tests/test_blocks.py b/wagtail/wagtailcore/tests/test_blocks.py index f1a839af4..3262d5674 100644 --- a/wagtail/wagtailcore/tests/test_blocks.py +++ b/wagtail/wagtailcore/tests/test_blocks.py @@ -663,7 +663,7 @@ class TestStreamBlock(unittest.TestCase): def render_article(self, data): class ArticleBlock(blocks.StreamBlock): heading = blocks.CharBlock() - paragraph = blocks.CharBlock() + paragraph = blocks.RichTextBlock() block = ArticleBlock() value = block.to_python(data) @@ -678,7 +678,7 @@ class TestStreamBlock(unittest.TestCase): }, { 'type': 'paragraph', - 'value': 'My first paragraph', + 'value': 'My first paragraph', }, { 'type': 'paragraph', @@ -687,8 +687,8 @@ class TestStreamBlock(unittest.TestCase): ]) self.assertIn('
My title
', html) - self.assertIn('
My first paragraph
', html) - self.assertIn('
My second paragraph
', html) + self.assertIn('
My first paragraph
', html) + self.assertIn('
My second paragraph
', html) def test_render_unknown_type(self): # This can happen if a developer removes a type from their StreamBlock @@ -704,7 +704,7 @@ class TestStreamBlock(unittest.TestCase): ]) self.assertNotIn('foo', html) self.assertNotIn('Hello', html) - self.assertIn('
My first paragraph
', html) + self.assertIn('
My first paragraph
', html) def render_form(self): class ArticleBlock(blocks.StreamBlock): From adb703ca2b2e2c2f82b250ed53355740d5c5a1ee Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 21:43:53 +0000 Subject: [PATCH 29/48] South/django admin no longer required --- docs/howto/settings.rst | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/howto/settings.rst b/docs/howto/settings.rst index 61c546eb2..8324e9952 100644 --- a/docs/howto/settings.rst +++ b/docs/howto/settings.rst @@ -63,11 +63,9 @@ Apps (settings.py) 'django.contrib.messages', 'django.contrib.staticfiles', - 'south', 'compressor', 'taggit', 'modelcluster', - 'django.contrib.admin', 'wagtail.wagtailcore', 'wagtail.wagtailadmin', @@ -89,11 +87,6 @@ Wagtail requires several Django app modules, third-party apps, and defines sever Third-Party Apps ---------------- -``south`` - Used for database migrations. See `South Documentation`_. - -.. _South Documentation: http://south.readthedocs.org/en/latest/ - ``compressor`` Static asset combiner and minifier for Django. Compressor also enables for the use of preprocessors. See `Compressor Documentation`_. @@ -109,9 +102,6 @@ Third-Party Apps .. _the django-modelcluster github project page: https://github.com/torchbox/django-modelcluster -``django.contrib.admin`` - The Django admin module. While Wagtail will eventually provide a sites-editing interface, the Django admin is included for now to provide that functionality. - Wagtail Apps ------------ @@ -438,11 +428,9 @@ settings.py 'django.contrib.messages', 'django.contrib.staticfiles', - 'south', 'compressor', 'taggit', 'modelcluster', - 'django.contrib.admin', 'wagtail.wagtailcore', 'wagtail.wagtailadmin', From 6a968049f732084eb2173c14c900772178b90e5c Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 21:46:20 +0000 Subject: [PATCH 30/48] Search urls have changed --- docs/howto/settings.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/howto/settings.rst b/docs/howto/settings.rst index 8324e9952..54a2c4b04 100644 --- a/docs/howto/settings.rst +++ b/docs/howto/settings.rst @@ -260,13 +260,13 @@ URL Patterns from wagtail.wagtailcore import urls as wagtail_urls from wagtail.wagtailadmin import urls as wagtailadmin_urls from wagtail.wagtaildocs import urls as wagtaildocs_urls - from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls + from wagtail.wagtailsearch import urls as wagtailsearch_urls urlpatterns = patterns('', url(r'^django-admin/', include(admin.site.urls)), url(r'^admin/', include(wagtailadmin_urls)), - url(r'^search/', include(wagtailsearch_frontend_urls)), + url(r'^search/', include(wagtailsearch_urls)), url(r'^documents/', include(wagtaildocs_urls)), # Optional urlconf for including your own vanilla Django urls/views @@ -526,14 +526,14 @@ urls.py from wagtail.wagtailcore import urls as wagtail_urls from wagtail.wagtailadmin import urls as wagtailadmin_urls from wagtail.wagtaildocs import urls as wagtaildocs_urls - from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls + from wagtail.wagtailsearch import urls as wagtailsearch__urls urlpatterns = patterns('', url(r'^django-admin/', include(admin.site.urls)), url(r'^admin/', include(wagtailadmin_urls)), - url(r'^search/', include(wagtailsearch_frontend_urls)), + url(r'^search/', include(wagtailsearch_urls)), url(r'^documents/', include(wagtaildocs_urls)), # For anything not caught by a more specific rule above, hand over to From b2b38013712eaeaf5ee4ca0e4f01f16e40550179 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 22:10:44 +0000 Subject: [PATCH 31/48] Fixed capitalisation of Elasticsearch --- docs/getting_started/installation.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/getting_started/installation.rst b/docs/getting_started/installation.rst index 213c0bb7c..579b70bc7 100644 --- a/docs/getting_started/installation.rst +++ b/docs/getting_started/installation.rst @@ -8,7 +8,7 @@ Before you start You can get basic Wagtail setup installed on your machine with only a few prerequisites. See the full `Dependencies`_ list below. -There are various optional components that will improve the performance and feature set of Wagtail. Our recommended software stack includes the PostgreSQL database, ElasticSearch (for free-text searching), the OpenCV library (for image feature detection), and Redis (as a cache and message queue backend). This would be a lot to install in one go. For this reason we provide a virtual machine image to use with `Vagrant `__, with all of these components ready installed. +There are various optional components that will improve the performance and feature set of Wagtail. Our recommended software stack includes the PostgreSQL database, Elasticsearch (for free-text searching), the OpenCV library (for image feature detection), and Redis (as a cache and message queue backend). This would be a lot to install in one go. For this reason we provide a virtual machine image to use with `Vagrant `__, with all of these components ready installed. Whether you just want to try out the demo site, or you're ready to dive in and create a Wagtail site with all bells and whistles enabled, we strongly recommend the Vagrant approach. Nevertheless, if you're the sort of person who balks at the idea of downloading a whole operating system just to run a web app, we've got you covered too. Start from `Install Python`_. @@ -107,12 +107,12 @@ To enable Postgres for your project, uncomment the ``psycopg2`` line from your p This assumes that your PostgreSQL instance is configured to allow you to connect as the 'postgres' user - if not, you'll need to adjust the ``createdb`` line and the database settings in settings/base.py accordingly. -ElasticSearch +Elasticsearch ------------- -Wagtail integrates with ElasticSearch to provide full-text searching of your content, both within the Wagtail interface and on your site's front-end. If ElasticSearch is not available, Wagtail will fall back to much more basic search functionality using database queries. ElasticSearch is pre-installed as part of the Vagrant virtual machine image; non-Vagrant users can use the `debian.sh `__ or `ubuntu.sh `__ installation scripts as a guide. +Wagtail integrates with Elasticsearch to provide full-text searching of your content, both within the Wagtail interface and on your site's front-end. If Elasticsearch is not available, Wagtail will fall back to much more basic search functionality using database queries. Elasticsearch is pre-installed as part of the Vagrant virtual machine image; non-Vagrant users can use the `debian.sh `__ or `ubuntu.sh `__ installation scripts as a guide. -To enable ElasticSearch for your project, uncomment the ``elasticsearch`` line from your project's requirements.txt, and in ``myprojectname/settings/base.py``, uncomment the WAGTAILSEARCH_BACKENDS section. Then run:: +To enable Elasticsearch for your project, uncomment the ``elasticsearch`` line from your project's requirements.txt, and in ``myprojectname/settings/base.py``, uncomment the WAGTAILSEARCH_BACKENDS section. Then run:: pip install -r requirements.txt ./manage.py update_index From 667fee8d15d5e6b897ce494baf7c330eabeba3ff Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 18 Mar 2015 22:15:49 +0000 Subject: [PATCH 32/48] Replaced django-redis-cache with django-redis Fixes #1068 --- docs/howto/performance.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/howto/performance.rst b/docs/howto/performance.rst index 57bac2131..924e49d9f 100644 --- a/docs/howto/performance.rst +++ b/docs/howto/performance.rst @@ -13,14 +13,14 @@ We have tried to minimise external dependencies for a working installation of Wa Cache ----- -We recommend `Redis `_ as a fast, persistent cache. Install Redis through your package manager (on Debian or Ubuntu: ``sudo apt-get install redis-server``), add ``django-redis-cache`` to your requirements.txt, and enable it as a cache backend:: +We recommend `Redis `_ as a fast, persistent cache. Install Redis through your package manager (on Debian or Ubuntu: ``sudo apt-get install redis-server``), add ``django-redis`` to your requirements.txt, and enable it as a cache backend:: CACHES = { 'default': { - 'BACKEND': 'redis_cache.cache.RedisCache', + 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': '127.0.0.1:6379', 'OPTIONS': { - 'CLIENT_CLASS': 'redis_cache.client.DefaultClient', + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', } } } From 0b9eeae1aded965e7fa8dee743a1a80134f305c1 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 20 Mar 2015 10:11:41 +0000 Subject: [PATCH 33/48] Removed roadmap Moved to: https://github.com/torchbox/wagtail/wiki/Roadmap --- docs/releases/index.rst | 1 - docs/releases/roadmap.rst | 28 ---------------------------- 2 files changed, 29 deletions(-) delete mode 100644 docs/releases/roadmap.rst diff --git a/docs/releases/index.rst b/docs/releases/index.rst index b6e231e7d..8c6fd48d9 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -4,7 +4,6 @@ Release notes .. toctree:: :maxdepth: 1 - roadmap 0.9 0.8.6 0.8.5 diff --git a/docs/releases/roadmap.rst b/docs/releases/roadmap.rst deleted file mode 100644 index 9efcfd9da..000000000 --- a/docs/releases/roadmap.rst +++ /dev/null @@ -1,28 +0,0 @@ -Roadmap -------- - -The story so far -~~~~~~~~~~~~~~~~ - -Wagtail was developed for the Royal College of Art in 2013, and launched as an open source project in February 2014. The changes since its launch are recorded in the codebase: - -https://raw.github.com/torchbox/wagtail/master/CHANGELOG.txt - -In summary: - - * February 2014: Reduced dependencies, basic documentation, translations, tests - -What's next -~~~~~~~~~~~ - -The `issue list `_ gives a detailed view of the immediate tasks, but our current broad priorities are: - - * Better documentation: simple setup guides for all levels of user, a manual for editors and administrators, in-depth intstructions for Django developers. - * Block-level editing UI (see `Sir Trevor `_) - * Support for an HTML content type - * Simple inline stats - -You decide -~~~~~~~~~~ - -Please help us focus on the right things by raising issues for new features, or joining the discussion on existing issues. From ab3b53adfeee2b226ecb2c1ee0c8016cedbbd85f Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Sun, 22 Mar 2015 16:37:44 +0000 Subject: [PATCH 34/48] Use mocks when testing signal handlers --- .../wagtailadmin/tests/test_pages_views.py | 94 +++++++++---------- 1 file changed, 44 insertions(+), 50 deletions(-) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 2a28ca420..8bd3634a7 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -1,5 +1,6 @@ from datetime import timedelta import unittest +import mock from django.test import TestCase from django.core.urlresolvers import reverse @@ -274,12 +275,8 @@ class TestPageCreation(TestCase, WagtailTestUtils): def test_create_simplepage_post_publish(self): # Connect a mock signal handler to page_published signal - signal_fired = [False] - signal_page = [None] - def page_published_handler(sender, instance, **kwargs): - signal_fired[0] = True - signal_page[0] = instance - page_published.connect(page_published_handler) + mock_handler = mock.MagicMock() + page_published.connect(mock_handler) # Post post_data = { @@ -302,9 +299,12 @@ class TestPageCreation(TestCase, WagtailTestUtils): self.assertTrue(page.first_published_at) # Check that the page_published signal was fired - self.assertTrue(signal_fired[0]) - self.assertEqual(signal_page[0], page) - self.assertEqual(signal_page[0], signal_page[0].specific) + self.assertEqual(mock_handler.call_count, 1) + mock_call = mock_handler.mock_calls[0][2] + + self.assertEqual(mock_call['sender'], page.specific_class) + self.assertEqual(mock_call['instance'], page) + self.assertIsInstance(mock_call['instance'], page.specific_class) # treebeard should report no consistency problems with the tree self.assertFalse(any(Page.find_problems()), 'treebeard found consistency problems') @@ -577,12 +577,8 @@ class TestPageEdit(TestCase, WagtailTestUtils): def test_page_edit_post_publish(self): # Connect a mock signal handler to page_published signal - signal_fired = [False] - signal_page = [None] - def page_published_handler(sender, instance, **kwargs): - signal_fired[0] = True - signal_page[0] = instance - page_published.connect(page_published_handler) + mock_handler = mock.MagicMock() + page_published.connect(mock_handler) # Set has_unpublished_changes=True on the existing record to confirm that the publish action # is resetting it (and not just leaving it alone) @@ -609,9 +605,12 @@ class TestPageEdit(TestCase, WagtailTestUtils): self.assertEqual(child_page_new.title, post_data['title']) # Check that the page_published signal was fired - self.assertTrue(signal_fired[0]) - self.assertEqual(signal_page[0], child_page_new) - self.assertEqual(signal_page[0], signal_page[0].specific) + self.assertEqual(mock_handler.call_count, 1) + mock_call = mock_handler.mock_calls[0][2] + + self.assertEqual(mock_call['sender'], child_page_new.specific_class) + self.assertEqual(mock_call['instance'], child_page_new) + self.assertIsInstance(mock_call['instance'], child_page_new.specific_class) # The page shouldn't have "has_unpublished_changes" flag set self.assertFalse(child_page_new.has_unpublished_changes) @@ -913,12 +912,8 @@ class TestPageDelete(TestCase, WagtailTestUtils): def test_page_delete_post(self): # Connect a mock signal handler to page_unpublished signal - signal_fired = [False] - signal_page = [None] - def page_unpublished_handler(sender, instance, **kwargs): - signal_fired[0] = True - signal_page[0] = instance - page_unpublished.connect(page_unpublished_handler) + mock_handler = mock.MagicMock() + page_unpublished.connect(mock_handler) # Post response = self.client.post(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, ))) @@ -933,9 +928,12 @@ class TestPageDelete(TestCase, WagtailTestUtils): self.assertEqual(Page.objects.filter(path__startswith=self.root_page.path, slug='hello-world').count(), 0) # Check that the page_unpublished signal was fired - self.assertTrue(signal_fired[0]) - self.assertEqual(signal_page[0], self.child_page) - self.assertEqual(signal_page[0], signal_page[0].specific) + self.assertEqual(mock_handler.call_count, 1) + mock_call = mock_handler.mock_calls[0][2] + + self.assertEqual(mock_call['sender'], self.child_page.specific_class) + self.assertEqual(mock_call['instance'], self.child_page) + self.assertIsInstance(mock_call['instance'], self.child_page.specific_class) def test_page_delete_notlive_post(self): # Same as above, but this makes sure the page_unpublished signal is not fired @@ -946,10 +944,8 @@ class TestPageDelete(TestCase, WagtailTestUtils): self.child_page.save() # Connect a mock signal handler to page_unpublished signal - signal_fired = [False] - def page_unpublished_handler(sender, instance, **kwargs): - signal_fired[0] = True - page_unpublished.connect(page_unpublished_handler) + mock_handler = mock.MagicMock() + page_unpublished.connect(mock_handler) # Post response = self.client.post(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, ))) @@ -964,7 +960,7 @@ class TestPageDelete(TestCase, WagtailTestUtils): self.assertEqual(Page.objects.filter(path__startswith=self.root_page.path, slug='hello-world').count(), 0) # Check that the page_unpublished signal was not fired - self.assertFalse(signal_fired[0]) + self.assertEqual(mock_handler.call_count, 0) def test_subpage_deletion(self): # Connect mock signal handlers to page_unpublished, pre_delete and post_delete signals @@ -1483,12 +1479,8 @@ class TestPageUnpublish(TestCase, WagtailTestUtils): This posts to the unpublish view and checks that the page was unpublished """ # Connect a mock signal handler to page_unpublished signal - signal_fired = [False] - signal_page = [None] - def page_unpublished_handler(sender, instance, **kwargs): - signal_fired[0] = True - signal_page[0] = instance - page_unpublished.connect(page_unpublished_handler) + mock_handler = mock.MagicMock() + page_unpublished.connect(mock_handler) # Post to the unpublish page response = self.client.post(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, ))) @@ -1500,9 +1492,12 @@ class TestPageUnpublish(TestCase, WagtailTestUtils): self.assertFalse(SimplePage.objects.get(id=self.page.id).live) # Check that the page_unpublished signal was fired - self.assertTrue(signal_fired[0]) - self.assertEqual(signal_page[0], self.page) - self.assertEqual(signal_page[0], signal_page[0].specific) + self.assertEqual(mock_handler.call_count, 1) + mock_call = mock_handler.mock_calls[0][2] + + self.assertEqual(mock_call['sender'], self.page.specific_class) + self.assertEqual(mock_call['instance'], self.page) + self.assertIsInstance(mock_call['instance'], self.page.specific_class) class TestApproveRejectModeration(TestCase, WagtailTestUtils): @@ -1533,12 +1528,8 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils): This posts to the approve moderation view and checks that the page was approved """ # Connect a mock signal handler to page_published signal - signal_fired = [False] - signal_page = [None] - def page_published_handler(sender, instance, **kwargs): - signal_fired[0] = True - signal_page[0] = instance - page_published.connect(page_published_handler) + mock_handler = mock.MagicMock() + page_published.connect(mock_handler) # Post response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, ))) @@ -1553,9 +1544,12 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils): self.assertFalse(page.has_unpublished_changes, "Approving moderation failed to set has_unpublished_changes=False") # Check that the page_published signal was fired - self.assertTrue(signal_fired[0]) - self.assertEqual(signal_page[0], self.page) - self.assertEqual(signal_page[0], signal_page[0].specific) + self.assertEqual(mock_handler.call_count, 1) + mock_call = mock_handler.mock_calls[0][2] + + self.assertEqual(mock_call['sender'], self.page.specific_class) + self.assertEqual(mock_call['instance'], self.page) + self.assertIsInstance(mock_call['instance'], self.page.specific_class) def test_approve_moderation_when_later_revision_exists(self): self.page.title = "Goodbye world!" From 00c1b573f4c7ae27ecfe4e17d73581430d63e214 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 23 Mar 2015 15:40:23 +0000 Subject: [PATCH 35/48] implement hooks for defining your own embed / link handlers for the rich text editor --- wagtail/wagtailcore/rich_text.py | 35 ++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/wagtail/wagtailcore/rich_text.py b/wagtail/wagtailcore/rich_text.py index 16d64be51..9ca238454 100644 --- a/wagtail/wagtailcore/rich_text.py +++ b/wagtail/wagtailcore/rich_text.py @@ -156,6 +156,33 @@ LINK_HANDLERS = { 'document': DocumentLinkHandler, } +has_loaded_embed_handlers = False +has_loaded_link_handlers = False + +def get_embed_handler(embed_type): + global EMBED_HANDLERS, has_loaded_embed_handlers + + if not has_loaded_embed_handlers: + for hook in hooks.get_hooks('register_rich_text_embed_handler'): + handler_name, handler = hook() + EMBED_HANDLERS[handler_name] = handler + + has_loaded_embed_handlers = True + + return EMBED_HANDLERS[embed_type] + +def get_link_handler(link_type): + global LINK_HANDLERS, has_loaded_link_handlers + + if not has_loaded_link_handlers: + for hook in hooks.get_hooks('register_rich_text_link_handler'): + handler_name, handler = hook() + LINK_HANDLERS[handler_name] = handler + + has_loaded_link_handlers = True + + return LINK_HANDLERS[link_type] + class DbWhitelister(Whitelister): """ @@ -189,7 +216,7 @@ class DbWhitelister(Whitelister): if 'data-embedtype' in tag.attrs: embed_type = tag['data-embedtype'] # fetch the appropriate embed handler for this embedtype - embed_handler = EMBED_HANDLERS[embed_type] + embed_handler = get_embed_handler(embed_type) embed_attrs = embed_handler.get_db_attributes(tag) embed_attrs['embedtype'] = embed_type @@ -202,7 +229,7 @@ class DbWhitelister(Whitelister): cls.clean_node(doc, child) link_type = tag['data-linktype'] - link_handler = LINK_HANDLERS[link_type] + link_handler = get_link_handler(link_type) link_attrs = link_handler.get_db_attributes(tag) link_attrs['linktype'] = link_type tag.attrs.clear() @@ -238,12 +265,12 @@ def expand_db_html(html, for_editor=False): if 'linktype' not in attrs: # return unchanged return m.group(0) - handler = LINK_HANDLERS[attrs['linktype']] + handler = get_link_handler(attrs['linktype']) return handler.expand_db_attributes(attrs, for_editor) def replace_embed_tag(m): attrs = extract_attrs(m.group(1)) - handler = EMBED_HANDLERS[attrs['embedtype']] + handler = get_embed_handler(attrs['embedtype']) return handler.expand_db_attributes(attrs, for_editor) html = FIND_A_TAG.sub(replace_a_tag, html) From 24fb80d4a9d69f57e6bf74092d05b4885a0981a0 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 23 Mar 2015 19:55:59 +0000 Subject: [PATCH 36/48] Register document/image/embed rich text handlers via hooks --- wagtail/wagtailcore/rich_text.py | 109 +------------ wagtail/wagtailcore/tests/test_rich_text.py | 145 ------------------ wagtail/wagtaildocs/rich_text.py | 23 +++ wagtail/wagtaildocs/tests.py | 38 +++++ wagtail/wagtaildocs/wagtail_hooks.py | 6 + wagtail/wagtailembeds/rich_text.py | 30 ++++ wagtail/wagtailembeds/tests.py | 56 ++++++- wagtail/wagtailembeds/wagtail_hooks.py | 6 + wagtail/wagtailimages/rich_text.py | 44 ++++++ wagtail/wagtailimages/tests/test_rich_text.py | 60 ++++++++ wagtail/wagtailimages/wagtail_hooks.py | 6 + 11 files changed, 269 insertions(+), 254 deletions(-) create mode 100644 wagtail/wagtaildocs/rich_text.py create mode 100644 wagtail/wagtailembeds/rich_text.py create mode 100644 wagtail/wagtailimages/rich_text.py create mode 100644 wagtail/wagtailimages/tests/test_rich_text.py diff --git a/wagtail/wagtailcore/rich_text.py b/wagtail/wagtailcore/rich_text.py index 9ca238454..d4ac8d656 100644 --- a/wagtail/wagtailcore/rich_text.py +++ b/wagtail/wagtailcore/rich_text.py @@ -4,15 +4,6 @@ from django.utils.html import escape from wagtail.wagtailcore.whitelist import Whitelister from wagtail.wagtailcore.models import Page - -from wagtail.wagtaildocs.models import Document - -# FIXME: we don't really want to import wagtailimages within core. -# For that matter, we probably don't want core to be concerned about translating -# HTML for the benefit of the hallo.js editor... -from wagtail.wagtailimages.models import get_image_model -from wagtail.wagtailimages.formats import get_image_format - from wagtail.wagtailcore import hooks @@ -22,80 +13,6 @@ from wagtail.wagtailcore import hooks # elsewhere in the database and is liable to change - from real HTML representation # to DB representation and back again. -class ImageEmbedHandler(object): - """ - ImageEmbedHandler will be invoked whenever we encounter an element in HTML content - with an attribute of data-embedtype="image". The resulting element in the database - representation will be: - - """ - @staticmethod - def get_db_attributes(tag): - """ - Given a tag that we've identified as an image embed (because it has a - data-embedtype="image" attribute), return a dict of the attributes we should - have on the resulting element. - """ - return { - 'id': tag['data-id'], - 'format': tag['data-format'], - 'alt': tag['data-alt'], - } - - @staticmethod - def expand_db_attributes(attrs, for_editor): - """ - Given a dict of attributes from the tag, return the real HTML - representation. - """ - Image = get_image_model() - try: - image = Image.objects.get(id=attrs['id']) - format = get_image_format(attrs['format']) - - if for_editor: - try: - return format.image_to_editor_html(image, attrs['alt']) - except: - return '' - else: - return format.image_to_html(image, attrs['alt']) - - except Image.DoesNotExist: - return "" - - -class MediaEmbedHandler(object): - """ - MediaEmbedHandler will be invoked whenever we encounter an element in HTML content - with an attribute of data-embedtype="media". The resulting element in the database - representation will be: - - """ - @staticmethod - def get_db_attributes(tag): - """ - Given a tag that we've identified as a media embed (because it has a - data-embedtype="media" attribute), return a dict of the attributes we should - have on the resulting element. - """ - return { - 'url': tag['data-url'], - } - - @staticmethod - def expand_db_attributes(attrs, for_editor): - """ - Given a dict of attributes from the tag, return the real HTML - representation. - """ - from wagtail.wagtailembeds import format - if for_editor: - return format.embed_to_editor_html(attrs['url']) - else: - return format.embed_to_frontend_html(attrs['url']) - - class PageLinkHandler(object): """ PageLinkHandler will be invoked whenever we encounter an element in HTML content @@ -127,33 +44,9 @@ class PageLinkHandler(object): return "" -class DocumentLinkHandler(object): - @staticmethod - def get_db_attributes(tag): - return {'id': tag['data-id']} - - @staticmethod - def expand_db_attributes(attrs, for_editor): - try: - doc = Document.objects.get(id=attrs['id']) - - if for_editor: - editor_attrs = 'data-linktype="document" data-id="%d" ' % doc.id - else: - editor_attrs = '' - - return '' % (editor_attrs, escape(doc.url)) - except Document.DoesNotExist: - return "" - - -EMBED_HANDLERS = { - 'image': ImageEmbedHandler, - 'media': MediaEmbedHandler, -} +EMBED_HANDLERS = {} LINK_HANDLERS = { 'page': PageLinkHandler, - 'document': DocumentLinkHandler, } has_loaded_embed_handlers = False diff --git a/wagtail/wagtailcore/tests/test_rich_text.py b/wagtail/wagtailcore/tests/test_rich_text.py index 9c98c6d3f..8ffb917d8 100644 --- a/wagtail/wagtailcore/tests/test_rich_text.py +++ b/wagtail/wagtailcore/tests/test_rich_text.py @@ -3,10 +3,7 @@ from mock import patch from django.test import TestCase from wagtail.wagtailcore.rich_text import ( - ImageEmbedHandler, - MediaEmbedHandler, PageLinkHandler, - DocumentLinkHandler, DbWhitelister, extract_attrs, expand_db_html @@ -14,112 +11,6 @@ from wagtail.wagtailcore.rich_text import ( from bs4 import BeautifulSoup -class TestImageEmbedHandler(TestCase): - fixtures = ['wagtail/tests/fixtures/test.json'] - - def test_get_db_attributes(self): - soup = BeautifulSoup( - 'foo' - ) - tag = soup.b - result = ImageEmbedHandler.get_db_attributes(tag) - self.assertEqual(result, - {'alt': 'test-alt', - 'id': 'test-id', - 'format': 'test-format'}) - - def test_expand_db_attributes_page_does_not_exist(self): - result = ImageEmbedHandler.expand_db_attributes( - {'id': 0}, - False - ) - self.assertEqual(result, '') - - @patch('wagtail.wagtailimages.models.Image') - @patch('django.core.files.File') - def test_expand_db_attributes_not_for_editor(self, mock_file, mock_image): - result = ImageEmbedHandler.expand_db_attributes( - {'id': 1, - 'alt': 'test-alt', - 'format': 'left'}, - False - ) - self.assertIn('foo' - ) - tag = soup.b - result = MediaEmbedHandler.get_db_attributes(tag) - self.assertEqual(result, - {'url': 'test-url'}) - - @patch('wagtail.wagtailembeds.embeds.oembed') - def test_expand_db_attributes_for_editor(self, oembed): - oembed.return_value = { - 'title': 'test title', - 'author_name': 'test author name', - 'provider_name': 'test provider name', - 'type': 'test type', - 'thumbnail_url': 'test thumbnail url', - 'width': 'test width', - 'height': 'test height', - 'html': 'test html' - } - result = MediaEmbedHandler.expand_db_attributes( - {'url': 'http://www.youtube.com/watch/'}, - True - ) - self.assertIn('
', result) - self.assertIn('

test title

', result) - self.assertIn('

URL: http://www.youtube.com/watch/

', result) - self.assertIn('

Provider: test provider name

', result) - self.assertIn('

Author: test author name

', result) - self.assertIn('test title', result) - - @patch('wagtail.wagtailembeds.embeds.oembed') - def test_expand_db_attributes_not_for_editor(self, oembed): - oembed.return_value = { - 'title': 'test title', - 'author_name': 'test author name', - 'provider_name': 'test provider name', - 'type': 'test type', - 'thumbnail_url': 'test thumbnail url', - 'width': 'test width', - 'height': 'test height', - 'html': 'test html' - } - result = MediaEmbedHandler.expand_db_attributes( - {'url': 'http://www.youtube.com/watch/'}, - False - ) - self.assertIn('test html', result) - - class TestPageLinkHandler(TestCase): fixtures = ['wagtail/tests/fixtures/test.json'] @@ -155,42 +46,6 @@ class TestPageLinkHandler(TestCase): self.assertEqual(result, '
') -class TestDocumentLinkHandler(TestCase): - fixtures = ['wagtail/tests/fixtures/test.json'] - - def test_get_db_attributes(self): - soup = BeautifulSoup( - 'foo' - ) - tag = soup.a - result = DocumentLinkHandler.get_db_attributes(tag) - self.assertEqual(result, - {'id': 'test-id'}) - - def test_expand_db_attributes_document_does_not_exist(self): - result = DocumentLinkHandler.expand_db_attributes( - {'id': 0}, - False - ) - self.assertEqual(result, '') - - def test_expand_db_attributes_for_editor(self): - result = DocumentLinkHandler.expand_db_attributes( - {'id': 1}, - True - ) - self.assertEqual(result, - '') - - def test_expand_db_attributes_not_for_editor(self): - result = DocumentLinkHandler.expand_db_attributes( - {'id': 1}, - False - ) - self.assertEqual(result, - '') - - class TestDbWhiteLister(TestCase): def test_clean_tag_node_div(self): soup = BeautifulSoup( diff --git a/wagtail/wagtaildocs/rich_text.py b/wagtail/wagtaildocs/rich_text.py new file mode 100644 index 000000000..44f42a2e9 --- /dev/null +++ b/wagtail/wagtaildocs/rich_text.py @@ -0,0 +1,23 @@ +from django.utils.html import escape + +from wagtail.wagtaildocs.models import Document + + +class DocumentLinkHandler(object): + @staticmethod + def get_db_attributes(tag): + return {'id': tag['data-id']} + + @staticmethod + def expand_db_attributes(attrs, for_editor): + try: + doc = Document.objects.get(id=attrs['id']) + + if for_editor: + editor_attrs = 'data-linktype="document" data-id="%d" ' % doc.id + else: + editor_attrs = '' + + return '' % (editor_attrs, escape(doc.url)) + except Document.DoesNotExist: + return "" diff --git a/wagtail/wagtaildocs/tests.py b/wagtail/wagtaildocs/tests.py index 1a38f9ea3..00a8227e0 100644 --- a/wagtail/wagtaildocs/tests.py +++ b/wagtail/wagtaildocs/tests.py @@ -1,6 +1,7 @@ from six import b import unittest import mock +from bs4 import BeautifulSoup from django.test import TestCase from django.contrib.auth import get_user_model @@ -16,6 +17,7 @@ from wagtail.tests.models import EventPage, EventPageRelatedLink from wagtail.wagtaildocs.models import Document from wagtail.wagtaildocs import models +from wagtail.wagtaildocs.rich_text import DocumentLinkHandler class TestDocumentPermissions(TestCase): @@ -576,3 +578,39 @@ class TestServeView(TestCase): def test_with_incorrect_filename(self): response = self.client.get(reverse('wagtaildocs_serve', args=(self.document.id, 'incorrectfilename'))) self.assertEqual(response.status_code, 404) + + +class TestDocumentLinkHandler(TestCase): + fixtures = ['wagtail/tests/fixtures/test.json'] + + def test_get_db_attributes(self): + soup = BeautifulSoup( + 'foo' + ) + tag = soup.a + result = DocumentLinkHandler.get_db_attributes(tag) + self.assertEqual(result, + {'id': 'test-id'}) + + def test_expand_db_attributes_document_does_not_exist(self): + result = DocumentLinkHandler.expand_db_attributes( + {'id': 0}, + False + ) + self.assertEqual(result, '') + + def test_expand_db_attributes_for_editor(self): + result = DocumentLinkHandler.expand_db_attributes( + {'id': 1}, + True + ) + self.assertEqual(result, + '') + + def test_expand_db_attributes_not_for_editor(self): + result = DocumentLinkHandler.expand_db_attributes( + {'id': 1}, + False + ) + self.assertEqual(result, + '') diff --git a/wagtail/wagtaildocs/wagtail_hooks.py b/wagtail/wagtaildocs/wagtail_hooks.py index 41ce020bf..597d1652d 100644 --- a/wagtail/wagtaildocs/wagtail_hooks.py +++ b/wagtail/wagtaildocs/wagtail_hooks.py @@ -10,6 +10,7 @@ from wagtail.wagtailcore import hooks from wagtail.wagtailadmin.menu import MenuItem from wagtail.wagtaildocs import admin_urls +from wagtail.wagtaildocs.rich_text import DocumentLinkHandler @hooks.register('register_admin_urls') @@ -53,3 +54,8 @@ def register_permissions(): document_content_type = ContentType.objects.get(app_label='wagtaildocs', model='document') document_permissions = Permission.objects.filter(content_type = document_content_type) return document_permissions + + +@hooks.register('register_rich_text_link_handler') +def register_document_link_handler(): + return ('document', DocumentLinkHandler) diff --git a/wagtail/wagtailembeds/rich_text.py b/wagtail/wagtailembeds/rich_text.py new file mode 100644 index 000000000..e099d5dd6 --- /dev/null +++ b/wagtail/wagtailembeds/rich_text.py @@ -0,0 +1,30 @@ +from wagtail.wagtailembeds import format + +class MediaEmbedHandler(object): + """ + MediaEmbedHandler will be invoked whenever we encounter an element in HTML content + with an attribute of data-embedtype="media". The resulting element in the database + representation will be: + + """ + @staticmethod + def get_db_attributes(tag): + """ + Given a tag that we've identified as a media embed (because it has a + data-embedtype="media" attribute), return a dict of the attributes we should + have on the resulting element. + """ + return { + 'url': tag['data-url'], + } + + @staticmethod + def expand_db_attributes(attrs, for_editor): + """ + Given a dict of attributes from the tag, return the real HTML + representation. + """ + if for_editor: + return format.embed_to_editor_html(attrs['url']) + else: + return format.embed_to_frontend_html(attrs['url']) diff --git a/wagtail/wagtailembeds/tests.py b/wagtail/wagtailembeds/tests.py index 93231372e..b0799da38 100644 --- a/wagtail/wagtailembeds/tests.py +++ b/wagtail/wagtailembeds/tests.py @@ -2,8 +2,10 @@ import six.moves.urllib.request from six.moves.urllib.error import URLError from mock import patch -import warnings import unittest +from bs4 import BeautifulSoup + +from wagtail.wagtailembeds.rich_text import MediaEmbedHandler try: import embedly @@ -320,3 +322,55 @@ class TestEmbedBlock(TestCase): # Check that the embed was in the returned HTML self.assertIn('

Hello world!

', html) + + +class TestMediaEmbedHandler(TestCase): + def test_get_db_attributes(self): + soup = BeautifulSoup( + 'foo' + ) + tag = soup.b + result = MediaEmbedHandler.get_db_attributes(tag) + self.assertEqual(result, + {'url': 'test-url'}) + + @patch('wagtail.wagtailembeds.embeds.oembed') + def test_expand_db_attributes_for_editor(self, oembed): + oembed.return_value = { + 'title': 'test title', + 'author_name': 'test author name', + 'provider_name': 'test provider name', + 'type': 'test type', + 'thumbnail_url': 'test thumbnail url', + 'width': 'test width', + 'height': 'test height', + 'html': 'test html' + } + result = MediaEmbedHandler.expand_db_attributes( + {'url': 'http://www.youtube.com/watch/'}, + True + ) + self.assertIn('
', result) + self.assertIn('

test title

', result) + self.assertIn('

URL: http://www.youtube.com/watch/

', result) + self.assertIn('

Provider: test provider name

', result) + self.assertIn('

Author: test author name

', result) + self.assertIn('test title', result) + + @patch('wagtail.wagtailembeds.embeds.oembed') + def test_expand_db_attributes_not_for_editor(self, oembed): + oembed.return_value = { + 'title': 'test title', + 'author_name': 'test author name', + 'provider_name': 'test provider name', + 'type': 'test type', + 'thumbnail_url': 'test thumbnail url', + 'width': 'test width', + 'height': 'test height', + 'html': 'test html' + } + result = MediaEmbedHandler.expand_db_attributes( + {'url': 'http://www.youtube.com/watch/'}, + False + ) + self.assertIn('test html', result) diff --git a/wagtail/wagtailembeds/wagtail_hooks.py b/wagtail/wagtailembeds/wagtail_hooks.py index 955f8216b..863544c5a 100644 --- a/wagtail/wagtailembeds/wagtail_hooks.py +++ b/wagtail/wagtailembeds/wagtail_hooks.py @@ -5,6 +5,7 @@ from django.utils.html import format_html from wagtail.wagtailcore import hooks from wagtail.wagtailembeds import urls +from wagtail.wagtailembeds.rich_text import MediaEmbedHandler @hooks.register('register_admin_urls') @@ -27,3 +28,8 @@ def editor_js(): 'wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js', urlresolvers.reverse('wagtailembeds_chooser') ) + + +@hooks.register('register_rich_text_embed_handler') +def register_media_embed_handler(): + return ('media', MediaEmbedHandler) diff --git a/wagtail/wagtailimages/rich_text.py b/wagtail/wagtailimages/rich_text.py new file mode 100644 index 000000000..aa2f9edd0 --- /dev/null +++ b/wagtail/wagtailimages/rich_text.py @@ -0,0 +1,44 @@ +from wagtail.wagtailimages.models import get_image_model +from wagtail.wagtailimages.formats import get_image_format + +class ImageEmbedHandler(object): + """ + ImageEmbedHandler will be invoked whenever we encounter an element in HTML content + with an attribute of data-embedtype="image". The resulting element in the database + representation will be: + + """ + @staticmethod + def get_db_attributes(tag): + """ + Given a tag that we've identified as an image embed (because it has a + data-embedtype="image" attribute), return a dict of the attributes we should + have on the resulting element. + """ + return { + 'id': tag['data-id'], + 'format': tag['data-format'], + 'alt': tag['data-alt'], + } + + @staticmethod + def expand_db_attributes(attrs, for_editor): + """ + Given a dict of attributes from the tag, return the real HTML + representation. + """ + Image = get_image_model() + try: + image = Image.objects.get(id=attrs['id']) + format = get_image_format(attrs['format']) + + if for_editor: + try: + return format.image_to_editor_html(image, attrs['alt']) + except: + return '' + else: + return format.image_to_html(image, attrs['alt']) + + except Image.DoesNotExist: + return "" diff --git a/wagtail/wagtailimages/tests/test_rich_text.py b/wagtail/wagtailimages/tests/test_rich_text.py new file mode 100644 index 000000000..a9f91a468 --- /dev/null +++ b/wagtail/wagtailimages/tests/test_rich_text.py @@ -0,0 +1,60 @@ +from django.test import TestCase + +from bs4 import BeautifulSoup +from mock import patch + +from wagtail.wagtailimages.rich_text import ImageEmbedHandler + + +class TestImageEmbedHandler(TestCase): + fixtures = ['wagtail/tests/fixtures/test.json'] + + def test_get_db_attributes(self): + soup = BeautifulSoup( + 'foo' + ) + tag = soup.b + result = ImageEmbedHandler.get_db_attributes(tag) + self.assertEqual(result, + {'alt': 'test-alt', + 'id': 'test-id', + 'format': 'test-format'}) + + def test_expand_db_attributes_page_does_not_exist(self): + result = ImageEmbedHandler.expand_db_attributes( + {'id': 0}, + False + ) + self.assertEqual(result, '') + + @patch('wagtail.wagtailimages.models.Image') + @patch('django.core.files.File') + def test_expand_db_attributes_not_for_editor(self, mock_file, mock_image): + result = ImageEmbedHandler.expand_db_attributes( + {'id': 1, + 'alt': 'test-alt', + 'format': 'left'}, + False + ) + self.assertIn(' Date: Mon, 23 Mar 2015 20:10:41 +0000 Subject: [PATCH 37/48] Import FileWrapper from wsgiref.util Not actually implemented in Django. See: https://github.com/django/django/commit/bbe28496d32f76ca161f5c33787d6ad62267fcc6 --- wagtail/wagtailimages/views/frontend.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailimages/views/frontend.py b/wagtail/wagtailimages/views/frontend.py index 18db74ff1..6dcdbe048 100644 --- a/wagtail/wagtailimages/views/frontend.py +++ b/wagtail/wagtailimages/views/frontend.py @@ -1,6 +1,7 @@ +from wsgiref.util import FileWrapper + from django.shortcuts import get_object_or_404 from django.http import HttpResponse -from django.core.servers.basehttp import FileWrapper from django.core.exceptions import PermissionDenied from wagtail.wagtailimages.models import get_image_model, Filter From d0128f21951e89a1e27f3ff6aa88ce8f65221299 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 24 Mar 2015 09:41:06 +0000 Subject: [PATCH 38/48] Update README.rst --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1f6eb8b4e..d5eaccd99 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ Available at `wagtail.readthedocs.org `_ and al Need Support? ~~~~~~~~~~~~~~~ -Ask your questions on our `Google Group `_. +Ask your questions on our `Wagtail support group `_. Compatibility @@ -71,3 +71,5 @@ We suggest you start by checking the `Help develop me! `_. We also welcome `translations `_ for Wagtail's interface. + +We run a separate Wagtail developers group here: https://groups.google.com/forum/#!forum/wagtail-developers please not that this is not for support requests. From ee3115298a0bb351c93899a8c57311c5f305ed30 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 24 Mar 2015 10:54:16 +0000 Subject: [PATCH 39/48] Fix a couple of spacing issues --- wagtail/wagtailembeds/rich_text.py | 1 + wagtail/wagtailimages/rich_text.py | 1 + 2 files changed, 2 insertions(+) diff --git a/wagtail/wagtailembeds/rich_text.py b/wagtail/wagtailembeds/rich_text.py index e099d5dd6..763693c40 100644 --- a/wagtail/wagtailembeds/rich_text.py +++ b/wagtail/wagtailembeds/rich_text.py @@ -1,5 +1,6 @@ from wagtail.wagtailembeds import format + class MediaEmbedHandler(object): """ MediaEmbedHandler will be invoked whenever we encounter an element in HTML content diff --git a/wagtail/wagtailimages/rich_text.py b/wagtail/wagtailimages/rich_text.py index aa2f9edd0..9caaed3bd 100644 --- a/wagtail/wagtailimages/rich_text.py +++ b/wagtail/wagtailimages/rich_text.py @@ -1,6 +1,7 @@ from wagtail.wagtailimages.models import get_image_model from wagtail.wagtailimages.formats import get_image_format + class ImageEmbedHandler(object): """ ImageEmbedHandler will be invoked whenever we encounter an element in HTML content From 59f47411c7d59a3a4d4e9f21d088cceb50fafb8b Mon Sep 17 00:00:00 2001 From: lauantai Date: Tue, 24 Mar 2015 12:15:57 +0100 Subject: [PATCH 40/48] Remove link to now-defunct Google Chrome Frame in warning for From d140bff2f0dcdb2520eecfc57307d4bdeff2c0bc Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 24 Mar 2015 12:53:30 +0000 Subject: [PATCH 41/48] rename TestDocumentLinkHandler as suggested https://github.com/torchbox/wagtail/pull/1093/files#r26976807 --- wagtail/wagtaildocs/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtaildocs/tests.py b/wagtail/wagtaildocs/tests.py index 00a8227e0..863ad8e9c 100644 --- a/wagtail/wagtaildocs/tests.py +++ b/wagtail/wagtaildocs/tests.py @@ -580,7 +580,7 @@ class TestServeView(TestCase): self.assertEqual(response.status_code, 404) -class TestDocumentLinkHandler(TestCase): +class TestDocumentRichTextLinkHandler(TestCase): fixtures = ['wagtail/tests/fixtures/test.json'] def test_get_db_attributes(self): From 2977f46a86366a65325c047def236574eb767843 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 24 Mar 2015 12:57:09 +0000 Subject: [PATCH 42/48] Release note for #1093 --- CHANGELOG.txt | 1 + docs/releases/0.9.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 12b8febd7..41adcf38d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -35,6 +35,7 @@ Changelog * Increased the maximum length of a page slug from 50 to 255 characters * Plain text fields in the page editor now use auto-expanding text areas * Date / time pickers now consistently use times without seconds, to prevent Javascript behaviour glitches when focusing / unfocusing fields + * Added hooks `register_rich_text_embed_handler` and `register_rich_text_link_handler` for customising link / embed handling within rich text fields 0.8.6 (10.03.2015) diff --git a/docs/releases/0.9.rst b/docs/releases/0.9.rst index 1274cf428..31ad530d3 100644 --- a/docs/releases/0.9.rst +++ b/docs/releases/0.9.rst @@ -24,6 +24,7 @@ Core * Added validation to prevent pages being created with only whitespace characters in their title fields * The Page model now records the date/time that a page was first published, as the field ``first_published_at`` * Increased the maximum length of a page slug from 50 to 255 characters + * Added hooks ``register_rich_text_embed_handler`` and ``register_rich_text_link_handler`` for customising link / embed handling within rich text fields Admin From e520a2816a94d71ca1e855f350ba5abd44d5b251 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 24 Mar 2015 13:17:44 +0000 Subject: [PATCH 43/48] reinstate Pillow and django-libsass as setup.py requirements as per https://github.com/torchbox/wagtail/pull/985#issuecomment-84048363 --- setup.py | 2 ++ wagtail/project_template/requirements.txt | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0ecc19863..d2641fc91 100644 --- a/setup.py +++ b/setup.py @@ -30,9 +30,11 @@ PY3 = sys.version_info[0] == 3 install_requires = [ "Django>=1.7.1,<1.8", "django-compressor>=1.4", + "django-libsass>=0.2", "django-modelcluster>=0.5", "django-taggit==0.12.3", "django-treebeard==3.0", + "Pillow>=2.6.1", "beautifulsoup4>=4.3.2", "html5lib==0.999", "Unidecode>=0.04.14", diff --git a/wagtail/project_template/requirements.txt b/wagtail/project_template/requirements.txt index b5dfbfea6..b92cd52df 100644 --- a/wagtail/project_template/requirements.txt +++ b/wagtail/project_template/requirements.txt @@ -1,8 +1,6 @@ # Minimal requirements Django>=1.7.1,<1.8 wagtail==0.8.6 -django-libsass>=0.2 -Pillow>=2.6.1 # Recommended components (require additional setup): # psycopg2==2.5.2 From 40d514964d9baee6310ed1255158218cffc096b0 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 24 Mar 2015 13:56:16 +0000 Subject: [PATCH 44/48] Update docs to reflect the fact that django-libsass / Pillow are not being installed separately --- docs/getting_started/installation.rst | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/docs/getting_started/installation.rst b/docs/getting_started/installation.rst index 579b70bc7..fac423fa2 100644 --- a/docs/getting_started/installation.rst +++ b/docs/getting_started/installation.rst @@ -16,26 +16,9 @@ Whether you just want to try out the demo site, or you're ready to dive in and c Dependencies ============ -Barebones ---------- +Wagtail is based on the Django web framework and various other Python libraries. For the full list of absolutely required libraries, see `setup.py `__. -The basic Wagtail installation is pure Python. No build tools are required on the host machine. -For the full list of absolutely required libraries, see `setup.py `__. - -If you are installing Wagtail differently (e.g. from the Git repository), you must run ``python setup.py install`` from the repository root. The above command will install all Wagtail dependencies. - -Administration UI ------------------ - -.. warning:: - - The administrative interface requires django-libsass and Pillow. The project template bundled with Wagtail includes them (see :doc:`creating_your_project`). You must add the above libraries if you are adding Wagtail to an existing project, unless you will be using it - in a purely framework fashion without visiting wagtailadmin or hooking it up to the urlconf. - - * django-libsass>=0.2 - * Pillow>=2.6.1 - -Both django-libsass and Pillow have native-code components that require further attention: +Most of Wagtail's dependencies are pure Python and will install automatically using ``pip``, but there are a few native-code components that require further attention: * libsass-python (for compiling SASS stylesheets) - requires a C++ compiler and the Python development headers. * Pillow (for image processing) - additionally requires libjpeg and zlib. @@ -78,6 +61,8 @@ The quickest way to install Wagtail is using pip. To get the latest stable versi pip install wagtail +If you are installing Wagtail differently (e.g. from the Git repository), you must run ``python setup.py install`` from the repository root. The above command will install all Wagtail dependencies. + To check that Wagtail can be seen by Python, type ``python`` in your shell then try to import ``wagtail`` from the prompt: .. code-block:: python From d47f608f352776e3191b9a0bc0ed7d78afddfd5f Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 24 Mar 2015 16:29:30 +0000 Subject: [PATCH 45/48] No. --- wagtail/wagtailcore/blocks/struct_block.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/wagtail/wagtailcore/blocks/struct_block.py b/wagtail/wagtailcore/blocks/struct_block.py index f4e37066f..1203c537e 100644 --- a/wagtail/wagtailcore/blocks/struct_block.py +++ b/wagtail/wagtailcore/blocks/struct_block.py @@ -74,8 +74,6 @@ class BaseStructBlock(Block): for child_rendering in child_renderings ]) - - # Can these be rendered with a template? if self.label: return format_html('
    {1}
', self.label, list_items) else: From 8106e884cb0bc932335909fa0147834038351e6a Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 25 Mar 2015 10:19:15 +0000 Subject: [PATCH 46/48] Fixed RuntimeWarning in a test --- wagtail/wagtailcore/tests/test_page_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/tests/test_page_model.py b/wagtail/wagtailcore/tests/test_page_model.py index be549f502..a5d0a125b 100644 --- a/wagtail/wagtailcore/tests/test_page_model.py +++ b/wagtail/wagtailcore/tests/test_page_model.py @@ -463,7 +463,7 @@ class TestCopyPage(TestCase): # Set the created_at of the revision to a time in the past revision = christmas_event.get_latest_revision() - revision.created_at = datetime.datetime(2014, 1, 1) + revision.created_at = datetime.datetime(2014, 1, 1, 0, 0, 0, tzinfo=pytz.utc) revision.save() # Copy it From 3ff98a14c03bbc35cacaf5a42c3e17bee410ca97 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 25 Mar 2015 11:47:27 +0000 Subject: [PATCH 47/48] StructBlock.value_from_datadict should return a StructValue, not dict - fixes #1100 --- wagtail/wagtailcore/blocks/struct_block.py | 2 +- wagtail/wagtailcore/tests/test_blocks.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/blocks/struct_block.py b/wagtail/wagtailcore/blocks/struct_block.py index 1203c537e..2373ef75d 100644 --- a/wagtail/wagtailcore/blocks/struct_block.py +++ b/wagtail/wagtailcore/blocks/struct_block.py @@ -80,7 +80,7 @@ class BaseStructBlock(Block): return format_html('
    {0}
', list_items) def value_from_datadict(self, data, files, prefix): - return dict([ + return StructValue(self, [ (name, block.value_from_datadict(data, files, '%s-%s' % (prefix, name))) for name, block in self.child_blocks.items() ]) diff --git a/wagtail/wagtailcore/tests/test_blocks.py b/wagtail/wagtailcore/tests/test_blocks.py index 3262d5674..458fbb341 100644 --- a/wagtail/wagtailcore/tests/test_blocks.py +++ b/wagtail/wagtailcore/tests/test_blocks.py @@ -438,6 +438,23 @@ class TestStructBlock(unittest.TestCase): self.assertEqual(content, ["Wagtail site"]) + def test_value_from_datadict(self): + block = blocks.StructBlock([ + ('title', blocks.CharBlock()), + ('link', blocks.URLBlock()), + ]) + + struct_val = block.value_from_datadict({ + 'mylink-title': "Torchbox", + 'mylink-link': "http://www.torchbox.com" + }, {}, 'mylink') + + self.assertEqual(struct_val['title'], "Torchbox") + self.assertEqual(struct_val['link'], "http://www.torchbox.com") + self.assertTrue(isinstance(struct_val, blocks.StructValue)) + self.assertTrue(isinstance(struct_val.bound_blocks['link'].block, blocks.URLBlock)) + + class TestListBlock(unittest.TestCase): def test_initialise_with_class(self): From 3e9a81280b179baec281bbe5491b7287ad36c65f Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 25 Mar 2015 12:15:27 +0000 Subject: [PATCH 48/48] Use django-redis in project template --- wagtail/project_template/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/project_template/requirements.txt b/wagtail/project_template/requirements.txt index b92cd52df..2ff3ab199 100644 --- a/wagtail/project_template/requirements.txt +++ b/wagtail/project_template/requirements.txt @@ -7,5 +7,5 @@ wagtail==0.8.6 # elasticsearch==1.1.1 # Recommended components to improve performance in production: -# django-redis-cache==0.13.0 +# django-redis==3.8.2 # django-celery==3.1.10