From e4c38302e08e24e4c0abd1f6acb0e33fa5dcb3dd Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Tue, 15 Apr 2014 21:55:24 +0300 Subject: [PATCH 001/139] Add fields for scheduled publishing Also add a clean method to Page to check that expiry date is in the future and that go live date is before expiry date. In order to display the correct error message the views/pages.py view has to be changed to display the error message from clean. Finally add the migration for the new fields. --- wagtail/wagtailadmin/views/pages.py | 7 +- .../0003_fields_for_scheduled_publishing.py | 130 ++++++++++++++++++ wagtail/wagtailcore/models.py | 24 +++- 3 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 wagtail/wagtailcore/migrations/0003_fields_for_scheduled_publishing.py diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 3fb905715..9e71a612f 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -7,7 +7,7 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.contrib.auth.decorators import permission_required from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext as _ from django.views.decorators.vary import vary_on_headers from wagtail.wagtailadmin.edit_handlers import TabbedInterface, ObjectList @@ -199,7 +199,10 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ return redirect('wagtailadmin_explore', page.get_parent().id) else: - messages.error(request, _("The page could not be created due to errors.")) + if form.errors and form.errors.get('__all__'): + messages.error(request, _("The page could not be created: ") + ', '.join(form.errors['__all__'])) + else: + messages.error(request, _("The page could not be created due to errors.")) edit_handler = edit_handler_class(instance=page, form=form) else: form = form_class(instance=page) diff --git a/wagtail/wagtailcore/migrations/0003_fields_for_scheduled_publishing.py b/wagtail/wagtailcore/migrations/0003_fields_for_scheduled_publishing.py new file mode 100644 index 000000000..5e682aa59 --- /dev/null +++ b/wagtail/wagtailcore/migrations/0003_fields_for_scheduled_publishing.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'PageRevision.approved_go_live_datetime' + db.add_column(u'wagtailcore_pagerevision', 'approved_go_live_datetime', + self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), + keep_default=False) + + # Adding field 'Page.go_live_datetime' + db.add_column(u'wagtailcore_page', 'go_live_datetime', + self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), + keep_default=False) + + # Adding field 'Page.expiry_datetime' + db.add_column(u'wagtailcore_page', 'expiry_datetime', + self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), + keep_default=False) + + # Adding field 'Page.expired' + db.add_column(u'wagtailcore_page', 'expired', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'PageRevision.approved_go_live_datetime' + db.delete_column(u'wagtailcore_pagerevision', 'approved_go_live_datetime') + + # Deleting field 'Page.go_live_datetime' + db.delete_column(u'wagtailcore_page', 'go_live_datetime') + + # Deleting field 'Page.expiry_datetime' + db.delete_column(u'wagtailcore_page', 'expiry_datetime') + + # Deleting field 'Page.expired' + db.delete_column(u'wagtailcore_page', 'expired') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'wagtailcore.grouppagepermission': { + 'Meta': {'object_name': 'GroupPagePermission'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': u"orm['wagtailcore.Page']"}), + 'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'}) + }, + u'wagtailcore.page': { + 'Meta': {'object_name': 'Page'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}), + 'depth': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'expiry_datetime': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'go_live_datetime': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['auth.User']"}), + 'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), + 'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) + }, + u'wagtailcore.pagerevision': { + 'Meta': {'object_name': 'PageRevision'}, + 'approved_go_live_datetime': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'content_json': ('django.db.models.fields.TextField', [], {}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': u"orm['wagtailcore.Page']"}), + 'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}) + }, + u'wagtailcore.site': { + 'Meta': {'object_name': 'Site'}, + 'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}), + 'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"}) + } + } + + complete_apps = ['wagtailcore'] \ No newline at end of file diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index d322ab69b..96e670ef9 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -7,10 +7,13 @@ from django.db import models, connection, transaction from django.db.models import get_model, Q from django.http import Http404 from django.core.cache import cache +from django.core.exceptions import ValidationError from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import Group from django.conf import settings from django.template.response import TemplateResponse +from django.utils import timezone +from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ from wagtail.wagtailcore.util import camelcase_to_underscore @@ -234,6 +237,10 @@ class Page(MP_Node, ClusterableModel, 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_datetime = models.DateTimeField(verbose_name=_("Go live date/time"), blank=True, null=True) + expiry_datetime = models.DateTimeField(verbose_name=_("Expiry date/time"), blank=True, null=True) + expired = models.BooleanField(default=False, editable=False) + indexed_fields = { 'title': { 'type': 'string', @@ -320,7 +327,7 @@ class Page(MP_Node, ClusterableModel, Indexed): SET url_path = %s || substring(url_path from %s) WHERE path LIKE %s AND id <> %s """ - cursor.execute(update_statement, + cursor.execute(update_statement, [new_url_path, len(old_url_path) + 1, self.path + '%', self.id]) @property @@ -399,8 +406,8 @@ class Page(MP_Node, ClusterableModel, Indexed): def serve(self, request): return TemplateResponse( - request, - self.get_template(request), + request, + self.get_template(request), self.get_context(request) ) @@ -448,6 +455,16 @@ class Page(MP_Node, ClusterableModel, Indexed): if self.url_path.startswith(root_path): return ('' if current_site.id == id else root_url) + self.url_path[len(root_path) - 1:] + def clean(self): + super(Page, self).clean() + + if self.go_live_datetime and self.expiry_datetime: + if self.go_live_datetime > self.expiry_datetime: + raise ValidationError(ugettext('Go live date/time should be before expiry datetime.')) + + if self.expiry_datetime and self.expiry_datetime < timezone.now(): + raise ValidationError(ugettext('Expiry date/time should be in the future')) + @classmethod def search(cls, query_string, show_unpublished=False, search_title_only=False, extra_filters={}, prefetch_related=[], path=None): # Filters @@ -642,6 +659,7 @@ class PageRevision(models.Model): created_at = models.DateTimeField(auto_now_add=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True) content_json = models.TextField() + approved_go_live_datetime = models.DateTimeField(null=True, blank=True) objects = models.Manager() submitted_revisions = SubmittedRevisionsManager() From a40c71687de01c6ccf2d8c983a14dc41eec82bea Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Wed, 16 Apr 2014 10:27:07 +0300 Subject: [PATCH 002/139] Show clean model errors on edit also --- wagtail/wagtailadmin/views/pages.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 9e71a612f..703d9f8dd 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -279,7 +279,11 @@ def edit(request, page_id): return redirect('wagtailadmin_explore', page.get_parent().id) else: - messages.error(request, _("The page could not be saved due to validation errors")) + if form.errors and form.errors.get('__all__'): + messages.error(request, _("The page could not be saved: ") + ', '.join(form.errors['__all__'])) + else: + messages.error(request, _("The page could not be saved due to validation errors")) + edit_handler = edit_handler_class(instance=page, form=form) errors_debug = ( repr(edit_handler.form.errors) From 1ebe234a7eebe761fdbf7bbbb1bc32480e840a85 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Wed, 16 Apr 2014 01:05:15 +0300 Subject: [PATCH 003/139] Add go_live_datetime handling on views/models The logic for publishing a page exists in the create and edit views of wagtailadmin.views.pages and in the publish methodd of hte PageRevision model. When a page is created and published (create view), if it has a go_live in the future then it will not be live but the revision that will be created will have the approved_go_live set to the corresponding datetime. If the page is just saved or submitted for moderation the normal flow will be followed. When a page is edited and published (edit view): * The approved_go_live_datetime will be cleared for all older revisions of that page. * If the edit has a go_live in the future then the new revision that will be crated will have the approved_go_live set to that datetime. Also the live attribute of the page will be set to False. If the page is edited and not published the normal flow will be followed. When a submitted for moderation page is published (publish method): * If it has a go_live in the future then the live attribute will be set to False, the approved_go_live_datetime of the revision will be set to the go_live_datetime of the page and the approved_go_live_datetime of all other revisions will be cleared. * If it does not have a go_live in the future then the page will be live and the approved_go_live_dattime of all other revisions will be cleard Finally, if a page is unpublished then then approved_go_live_datetime of all revisions of that page will be cleared. --- wagtail/wagtailadmin/views/pages.py | 44 +++++++++++++++++++++++++---- wagtail/wagtailcore/models.py | 32 ++++++++++++++++++--- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 703d9f8dd..8193bdfc4 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -7,6 +7,7 @@ from django.contrib import messages from django.contrib.contenttypes.models import ContentType from django.contrib.auth.decorators import permission_required from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.utils import timezone from django.utils.translation import ugettext as _ from django.views.decorators.vary import vary_on_headers @@ -173,16 +174,31 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ is_publishing = bool(request.POST.get('action-publish')) and parent_page_perms.can_publish_subpage() is_submitting = bool(request.POST.get('action-submit')) + go_live_datetime = form.cleaned_data.get('go_live_datetime') + future_go_live = go_live_datetime and go_live_datetime > timezone.now() + approved_go_live_datetime = None - if is_publishing: + if is_publishing and not future_go_live: page.live = True page.has_unpublished_changes = False + elif is_publishing and future_go_live: + page.live = False + # Set approved_go_live_datetime only if is publishing + # and the future_go_live is actually in future + approved_go_live_datetime = go_live_datetime + page.has_unpublished_changes = False else: page.live = False page.has_unpublished_changes = True parent_page.add_child(page) # assign tree parameters - will cause page to be saved - page.save_revision(user=request.user, submitted_for_moderation=is_submitting) + + # Pass approved_go_live_datetime to save_revision + page.save_revision( + user=request.user, + submitted_for_moderation=is_submitting, + approved_go_live_datetime = approved_go_live_datetime + ) if is_publishing: messages.success(request, _("Page '{0}' published.").format(page.title)) @@ -245,12 +261,24 @@ def edit(request, page_id): if form.is_valid(): is_publishing = bool(request.POST.get('action-publish')) and page_perms.can_publish() is_submitting = bool(request.POST.get('action-submit')) + go_live_datetime = form.cleaned_data.get('go_live_datetime') + future_go_live = go_live_datetime and go_live_datetime > timezone.now() + approved_go_live_datetime = None if is_publishing: - page.live = True page.has_unpublished_changes = False + if future_go_live: + page.live = False + # Set approved_go_live_datetime only if publishing + approved_go_live_datetime = go_live_datetime + else: + page.live = True form.save() - page.revisions.update(submitted_for_moderation=False) + # Clear approved_go_live_datetime for older revisions + page.revisions.update( + submitted_for_moderation=False, + approved_go_live_datetime=None, + ) else: # not publishing the page if page.live: @@ -262,7 +290,11 @@ def edit(request, page_id): page.has_unpublished_changes = True form.save() - page.save_revision(user=request.user, submitted_for_moderation=is_submitting) + page.save_revision( + user=request.user, + submitted_for_moderation=is_submitting, + approved_go_live_datetime = approved_go_live_datetime + ) if is_publishing: messages.success(request, _("Page '{0}' published.").format(page.title)) @@ -443,6 +475,8 @@ def unpublish(request, page_id): parent_id = page.get_parent().id page.live = False page.save() + # Since page is unpublished clear the approved_go_live_datetime of all revisions + page.revisions.update(approved_go_live_datetime=None) messages.success(request, _("Page '{0}' unpublished.").format(page.title)) return redirect('wagtailadmin_explore', parent_id) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 96e670ef9..1ecdd1eb0 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -373,8 +373,13 @@ class Page(MP_Node, ClusterableModel, Indexed): else: raise Http404 - def save_revision(self, user=None, submitted_for_moderation=False): - self.revisions.create(content_json=self.to_json(), user=user, submitted_for_moderation=submitted_for_moderation) + def save_revision(self, user=None, submitted_for_moderation=False, approved_go_live_datetime=None): + self.revisions.create( + content_json=self.to_json(), + user=user, + submitted_for_moderation=submitted_for_moderation, + approved_go_live_datetime=approved_go_live_datetime, + ) def get_latest_revision(self): try: @@ -539,13 +544,20 @@ class Page(MP_Node, ClusterableModel, Indexed): @property def status_string(self): if not self.live: - return "draft" + if self.approved_schedule: + return "scheduled" + else: + return "draft" else: if self.has_unpublished_changes: return "live + draft" else: return "live" + @property + def approved_schedule(self): + return self.revisions.exclude(approved_go_live_datetime__isnull=True).exists() + def has_unpublished_subtree(self): """ An awkwardly-defined flag used in determining whether unprivileged editors have @@ -693,11 +705,23 @@ class PageRevision(models.Model): def publish(self): page = self.as_page_object() - page.live = True + if page.go_live_datetime and page.go_live_datetime > timezone.now(): + # if we have a go_live in the future don't make the page live + page.live = False + # Instead set the approved_go_live_datetime of this revision + self.approved_go_live_datetime = page.go_live_datetime + self.save() + # And clear the the approved_go_live_datetime of any other revisions + page.revisions.exclude(id=self.id).update(approved_go_live_datetime=None) + else: + page.live = True + # If page goes live clear the approved_go_live_datetime of all revisions + page.revisions.update(approved_go_live_datetime=None) page.save() self.submitted_for_moderation = False page.revisions.update(submitted_for_moderation=False) + PAGE_PERMISSION_TYPE_CHOICES = [ ('add', 'Add'), ('edit', 'Edit'), From 89f2d76bfdcd49cb9c132342b83e29fc29a8ac44 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Mon, 21 Apr 2014 18:03:57 +0300 Subject: [PATCH 004/139] Set expired = False when publishing pages both ... in views and models and refactor code a bit. --- wagtail/wagtailadmin/views/pages.py | 18 ++++++++++-------- wagtail/wagtailcore/models.py | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 8193bdfc4..7b181b5fd 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -178,15 +178,16 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ future_go_live = go_live_datetime and go_live_datetime > timezone.now() approved_go_live_datetime = None - if is_publishing and not future_go_live: - page.live = True - page.has_unpublished_changes = False - elif is_publishing and future_go_live: - page.live = False - # Set approved_go_live_datetime only if is publishing - # and the future_go_live is actually in future - approved_go_live_datetime = go_live_datetime + if is_publishing: page.has_unpublished_changes = False + page.expired = False + if future_go_live: + page.live = False + # Set approved_go_live_datetime only if is publishing + # and the future_go_live is actually in future + approved_go_live_datetime = go_live_datetime + else: + page.live = True else: page.live = False page.has_unpublished_changes = True @@ -267,6 +268,7 @@ def edit(request, page_id): if is_publishing: page.has_unpublished_changes = False + page.expired = False if future_go_live: page.live = False # Set approved_go_live_datetime only if publishing diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 1ecdd1eb0..eda1a91ec 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -717,6 +717,7 @@ class PageRevision(models.Model): page.live = True # If page goes live clear the approved_go_live_datetime of all revisions page.revisions.update(approved_go_live_datetime=None) + page.expired = False # When a page is published it can't be expired page.save() self.submitted_for_moderation = False page.revisions.update(submitted_for_moderation=False) From 792b37d9564e3ae32f0acccda9427740a6b4541f Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Mon, 21 Apr 2014 18:08:47 +0300 Subject: [PATCH 005/139] Add "expired" status to pages --- wagtail/wagtailcore/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index eda1a91ec..5348acf84 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -544,7 +544,9 @@ class Page(MP_Node, ClusterableModel, Indexed): @property def status_string(self): if not self.live: - if self.approved_schedule: + if self.expired: + return "expired" + elif self.approved_schedule: return "scheduled" else: return "draft" From 6839a7474ab32873bfffb2b48237d500538ca7c7 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Mon, 21 Apr 2014 21:48:23 +0300 Subject: [PATCH 006/139] Add management command for scheduled pages The publish_scheduled_pages management command does three actions: * Gets live pages which have an expiry_datetime that has passed and set expired = True and live = False * Gets all revisions on the moderation queue which have an expiry_datetime that has passed and remove them from the moderation queue * Gets all revisions that have an approved_go_live_datetime that has passed. For each one of them the publish() method of the revision is called which will perform the required actions for making live this version of the page. Finally, a dryrun parameter has been added to the management command. If this parameter is used then the pages that pass the tests for each of the above lists will be printed. --- .../commands/publish_scheduled_pages.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 wagtail/wagtailcore/management/commands/publish_scheduled_pages.py diff --git a/wagtail/wagtailcore/management/commands/publish_scheduled_pages.py b/wagtail/wagtailcore/management/commands/publish_scheduled_pages.py new file mode 100644 index 000000000..0abf3c744 --- /dev/null +++ b/wagtail/wagtailcore/management/commands/publish_scheduled_pages.py @@ -0,0 +1,109 @@ +import datetime +import json +from optparse import make_option + +from django.core.management.base import BaseCommand +from django.utils import dateparse, timezone +from wagtail.wagtailcore.models import Page, PageRevision + + +def revision_date_expired(r): + expiry_str = json.loads(r.content_json).get('expiry_datetime') + if not expiry_str: + return False + expiry_datetime = dateparse.parse_datetime(expiry_str) + if expiry_datetime < timezone.now(): + return True + else: + return False + + +class Command(BaseCommand): + option_list = BaseCommand.option_list + ( + make_option( + '--dryrun', + action='store_true', + dest='dryrun', + default=False, + help='Dry run -- don\'t change anything.'), + ) + + def handle(self, *args, **options): + dryrun = False + if options['dryrun']: + print "Will do a dry run." + dryrun = True + + # 1. get all expired pages with live = True + expired_pages = Page.objects.filter( + live=True, + expiry_datetime__lt=timezone.now() + ) + if dryrun: + if expired_pages: + print "Expired pages to be deactivated:" + print "Expiry datetime\t\tSlug\t\tName" + print "---------------\t\t----\t\t----" + for ep in expired_pages: + print "{0}\t{1}\t{2}".format( + ep.expiry_datetime.strftime("%Y-%m-%d %H:%M"), + ep.slug, + ep.title + ) + else: + print "No expired pages to be deactivated found." + else: + expired_pages.update(expired=True, live=False) + + # 2. get all page revisions for moderation that have been expired + expired_revs = [ + r for r in PageRevision.objects.filter( + submitted_for_moderation=True + ) if revision_date_expired(r) + ] + if dryrun: + print "---------------------------------" + if expired_revs: + print "Expired revisions to be dropped from moderation queue:" + print "Expiry datetime\t\tSlug\t\tName" + print "---------------\t\t----\t\t----" + for er in expired_revs: + rev_data = json.loads(er.content_json) + print "{0}\t{1}\t{2}".format( + dateparse.parse_datetime( + rev_data.get('expiry_datetime') + ).strftime("%Y-%m-%d %H:%M"), + rev_data.get('slug'), + rev_data.get('title') + ) + else: + print "No expired revision to be dropped from moderation." + else: + for er in expired_revs: + er.submitted_for_moderation = False + er.save() + + # 3. get all revisions that need to be published + revs_for_publishing = PageRevision.objects.filter( + approved_go_live_datetime__lt=timezone.now() + ) + if dryrun: + print "---------------------------------" + if revs_for_publishing: + print "Revisions to be published:" + print "Go live datetime\t\tSlug\t\tName" + print "---------------\t\t\t----\t\t----" + for rp in revs_for_publishing: + rev_data = json.loads(rp.content_json) + print "{0}\t\t{1}\t{2}".format( + rp.approved_go_live_datetime.strftime("%Y-%m-%d %H:%M"), + rev_data.get('slug'), + rev_data.get('title') + ) + else: + print "No pages to go live." + else: + for rp in revs_for_publishing: + # just run publish for the revision -- since the approved go + # live datetime is before now it will make the page live + rp.publish() From 686a9beaa9099ac8f4cca20004f35321e6bdd09e Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Tue, 22 Apr 2014 20:12:20 +0300 Subject: [PATCH 007/139] Add tests for scheduled publishing This does not actually test the management command for scheduled pages. --- wagtail/wagtailadmin/tests.py | 140 ++++++++++++++++++++++++++++++++-- 1 file changed, 134 insertions(+), 6 deletions(-) diff --git a/wagtail/wagtailadmin/tests.py b/wagtail/wagtailadmin/tests.py index 84b00b335..3df92ec22 100644 --- a/wagtail/wagtailadmin/tests.py +++ b/wagtail/wagtailadmin/tests.py @@ -1,8 +1,10 @@ +from datetime import datetime, timedelta +from django.utils import timezone from django.test import TestCase import unittest from wagtail.tests.models import SimplePage, EventPage from wagtail.tests.utils import login -from wagtail.wagtailcore.models import Page +from wagtail.wagtailcore.models import Page, PageRevision from django.core.urlresolvers import reverse @@ -49,7 +51,7 @@ class TestPageSelectTypeLocation(TestCase): response = self.client.get(reverse('wagtailadmin_pages_select_type')) self.assertEqual(response.status_code, 200) - @unittest.expectedFailure # For some reason, this returns a 302... + @unittest.expectedFailure # For some reason, this returns a 302... def test_select_location_testpage(self): response = self.client.get(reverse('wagtailadmin_pages_select_location', args=('tests', 'eventpage'))) self.assertEqual(response.status_code, 200) @@ -100,6 +102,57 @@ class TestPageCreation(TestCase): self.assertIsInstance(page, SimplePage) self.assertFalse(page.live) + def test_create_simplepage_scheduled(self): + go_live_datetime = timezone.now() + timedelta(days=1) + expiry_datetime = timezone.now() + timedelta(days=2) + post_data = { + 'title': "New page!", + 'content': "Some content", + 'slug': 'hello-world', + 'go_live_datetime': str(go_live_datetime), + 'expiry_datetime': str(expiry_datetime), + } + response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data) + + # Should be redirected to explorer page + self.assertEqual(response.status_code, 302) + + # Find the page and check the scheduled times + page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific + self.assertEquals(page.go_live_datetime.date(), go_live_datetime.date()) + self.assertEquals(page.expiry_datetime.date(), expiry_datetime.date()) + self.assertEquals(page.expired, False) + self.assertTrue(page.status_string, "draft") + + # No revisions with approved_go_live_datetime + self.assertFalse(PageRevision.objects.filter(page=page).exclude(approved_go_live_datetime__isnull=True).exists()) + + def test_create_simplepage_scheduled_errored(self): + post_data = { + 'title': "New page!", + 'content': "Some content", + 'slug': 'hello-world', + 'go_live_datetime': str(timezone.now() + timedelta(days=2)), + 'expiry_datetime': str(timezone.now() + timedelta(days=1)), + } + response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data) + + # Should be redirected to explorer page + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context['edit_handler'].form.errors) + + post_data = { + 'title': "New page!", + 'content': "Some content", + 'slug': 'hello-world', + 'expiry_datetime': str(timezone.now() + timedelta(days=-1)), + } + response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data) + + # Should be redirected to explorer page + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context['edit_handler'].form.errors) + def test_create_simplepage_post_publish(self): post_data = { 'title': "New page!", @@ -118,6 +171,34 @@ class TestPageCreation(TestCase): self.assertIsInstance(page, SimplePage) self.assertTrue(page.live) + def test_create_simplepage_post_publish_scheduled(self): + go_live_datetime = timezone.now() + timedelta(days=1) + expiry_datetime = timezone.now() + timedelta(days=2) + post_data = { + 'title': "New page!", + 'content': "Some content", + 'slug': 'hello-world', + 'action-publish': "Publish", + 'go_live_datetime': str(go_live_datetime), + 'expiry_datetime': str(expiry_datetime), + } + response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data) + + # Should be redirected to explorer page + self.assertEqual(response.status_code, 302) + + # Find the page and check it + page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific + self.assertEquals(page.go_live_datetime.date(), go_live_datetime.date()) + self.assertEquals(page.expiry_datetime.date(), expiry_datetime.date()) + self.assertEquals(page.expired, False) + + # A revision with approved_go_live_datetime should exist now + self.assertTrue(PageRevision.objects.filter(page=page).exclude(approved_go_live_datetime__isnull=True).exists()) + # But Page won't be live + self.assertFalse(page.live) + self.assertTrue(page.status_string, "scheduled") + def test_create_simplepage_post_existingslug(self): # This tests the existing slug checking on page save @@ -143,7 +224,7 @@ class TestPageCreation(TestCase): response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', 100000))) self.assertEqual(response.status_code, 404) - @unittest.expectedFailure # FIXME: Crashes! + @unittest.expectedFailure # FIXME: Crashes! def test_create_nonpagetype(self): response = self.client.get(reverse('wagtailadmin_pages_create', args=('wagtailimages', 'image', self.root_page.id))) self.assertEqual(response.status_code, 404) @@ -184,7 +265,7 @@ class TestPageEdit(TestCase): 'slug': 'hello-world', } response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) - + # Should be redirected to explorer page self.assertEqual(response.status_code, 302) @@ -192,6 +273,31 @@ class TestPageEdit(TestCase): child_page_new = SimplePage.objects.get(id=self.child_page.id) self.assertTrue(child_page_new.has_unpublished_changes) + def test_edit_post_scheduled(self): + go_live_datetime = timezone.now() + timedelta(days=1) + expiry_datetime = timezone.now() + timedelta(days=2) + post_data = { + 'title': "I've been edited!", + 'content': "Some content", + 'slug': 'hello-world', + 'go_live_datetime': str(go_live_datetime), + 'expiry_datetime': str(expiry_datetime), + } + response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) + + # Should be redirected to explorer page + self.assertEqual(response.status_code, 302) + + child_page_new = SimplePage.objects.get(id=self.child_page.id) + + # The page will still be live + self.assertTrue(child_page_new.live) + # A revision with approved_go_live_datetime should not exist + self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_datetime__isnull=True).exists()) + # But a revision with go_live_datetime and expiry_datetime in their content json *should* exist + self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(go_live_datetime.date())).exists()) + self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(expiry_datetime.date())).exists()) + def test_edit_post_publish(self): # Tests publish from edit page post_data = { @@ -201,7 +307,7 @@ class TestPageEdit(TestCase): 'action-publish': "Publish", } response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) - + # Should be redirected to explorer page self.assertEqual(response.status_code, 302) @@ -212,6 +318,28 @@ class TestPageEdit(TestCase): # The page shouldn't have "has_unpublished_changes" flag set self.assertFalse(child_page_new.has_unpublished_changes) + def test_edit_post_publish_scheduled(self): + go_live_datetime = timezone.now() + timedelta(days=1) + expiry_datetime = timezone.now() + timedelta(days=2) + post_data = { + 'title': "I've been edited!", + 'content': "Some content", + 'slug': 'hello-world', + 'action-publish': "Publish", + 'go_live_datetime': str(go_live_datetime), + 'expiry_datetime': str(expiry_datetime), + } + response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) + + # Should be redirected to explorer page + self.assertEqual(response.status_code, 302) + + child_page_new = SimplePage.objects.get(id=self.child_page.id) + # The page should not be live anymore + self.assertFalse(child_page_new.live) + # Instead a revision with approved_go_live_datetime should not exist + self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_datetime__isnull=True).exists()) + class TestPageDelete(TestCase): def setUp(self): @@ -232,7 +360,7 @@ class TestPageDelete(TestCase): self.assertEqual(response.status_code, 200) def test_delete_post(self): - post_data = {'hello': 'world'} # For some reason, this test doesn't work without a bit of POST data + post_data = {'hello': 'world'} # For some reason, this test doesn't work without a bit of POST data response = self.client.post(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )), post_data) # Should be redirected to explorer page From 65f093061dc53abf24f9bb30b0be1d6160a4c0f6 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Wed, 23 Apr 2014 11:56:13 +0300 Subject: [PATCH 008/139] Use the plain DateTimeInput widget for ... go_live_datetime and expiry_datetime. This should probably be improved in order to use a javascript datetime picker. --- wagtail/wagtailadmin/edit_handlers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index 245f7ad60..370f2f5bb 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -133,15 +133,18 @@ class LocalizedTimeField(forms.CharField): else: raise ValidationError(_("Please type a valid time") ) - +# For some reason we need to explicitly override the DateTimeField and set +# it to use the DateTimeInput or else it will use the LocalizedDateInput/FriendlyDateInput if hasattr(settings, 'USE_L10N') and settings.USE_L10N==True: FORM_FIELD_OVERRIDES = { models.DateField: {'widget': LocalizedDateInput}, + models.DateTimeField: {'widget': forms.DateTimeInput}, models.TimeField: {'widget': LocalizedTimeInput, 'form_class': LocalizedTimeField}, } else: # Fall back to friendly date/time FORM_FIELD_OVERRIDES = { models.DateField: {'widget': FriendlyDateInput}, + models.DateTimeField: {'widget': forms.DateTimeInput}, models.TimeField: {'widget': FriendlyTimeInput, 'form_class': FriendlyTimeField}, } From 9511b21ea73cf86958c8221eec212bd04c18198d Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Sun, 25 May 2014 22:07:59 +0300 Subject: [PATCH 009/139] Test publish scheduled pages management command --- wagtail/wagtailadmin/tests.py | 154 +++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 3 deletions(-) diff --git a/wagtail/wagtailadmin/tests.py b/wagtail/wagtailadmin/tests.py index 3df92ec22..04bf38f76 100644 --- a/wagtail/wagtailadmin/tests.py +++ b/wagtail/wagtailadmin/tests.py @@ -1,11 +1,13 @@ from datetime import datetime, timedelta +from django.core import management +from django.core.urlresolvers import reverse from django.utils import timezone from django.test import TestCase import unittest +from StringIO import StringIO from wagtail.tests.models import SimplePage, EventPage from wagtail.tests.utils import login from wagtail.wagtailcore.models import Page, PageRevision -from django.core.urlresolvers import reverse class TestHome(TestCase): @@ -127,7 +129,7 @@ class TestPageCreation(TestCase): # No revisions with approved_go_live_datetime self.assertFalse(PageRevision.objects.filter(page=page).exclude(approved_go_live_datetime__isnull=True).exists()) - def test_create_simplepage_scheduled_errored(self): + def test_create_simplepage_scheduled_go_live_before_expiry(self): post_data = { 'title': "New page!", 'content': "Some content", @@ -141,6 +143,7 @@ class TestPageCreation(TestCase): self.assertEqual(response.status_code, 200) self.assertTrue(response.context['edit_handler'].form.errors) + def test_create_simplepage_scheduled_expire_in_the_past(self): post_data = { 'title': "New page!", 'content': "Some content", @@ -337,9 +340,51 @@ class TestPageEdit(TestCase): child_page_new = SimplePage.objects.get(id=self.child_page.id) # The page should not be live anymore self.assertFalse(child_page_new.live) - # Instead a revision with approved_go_live_datetime should not exist + # Instead a revision with approved_go_live_datetime should now exist self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_datetime__isnull=True).exists()) + def test_edit_post_publish_now_an_already_scheduled(self): + # First let's publish a page with a go_live_datetime in the future + go_live_datetime = timezone.now() + timedelta(days=1) + expiry_datetime = timezone.now() + timedelta(days=2) + post_data = { + 'title': "I've been edited!", + 'content': "Some content", + 'slug': 'hello-world', + 'action-publish': "Publish", + 'go_live_datetime': str(go_live_datetime), + 'expiry_datetime': str(expiry_datetime), + } + response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) + + # Should be redirected to explorer page + self.assertEqual(response.status_code, 302) + + child_page_new = SimplePage.objects.get(id=self.child_page.id) + # The page should not be live anymore + self.assertFalse(child_page_new.live) + # Instead a revision with approved_go_live_datetime should now exist + self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_datetime__isnull=True).exists()) + + # Now, let's edit it and publish it right now + go_live_datetime = timezone.now() + post_data = { + 'title': "I've been edited!", + 'content': "Some content", + 'slug': 'hello-world', + 'action-publish': "Publish", + 'go_live_datetime': "", + } + response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) + + # Should be redirected to explorer page + self.assertEqual(response.status_code, 302) + + child_page_new = SimplePage.objects.get(id=self.child_page.id) + # The page should be live now + self.assertTrue(child_page_new.live) + # And a revision with approved_go_live_datetime should not exist + self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_datetime__isnull=True).exists()) class TestPageDelete(TestCase): def setUp(self): @@ -454,3 +499,106 @@ class TestEditorHooks(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, '') self.assertContains(response, '') + +class TestPublishScheduledPages(TestCase): + def setUp(self): + # Find root page + self.root_page = Page.objects.get(id=2) + + def test_go_live_page_will_be_published(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = False + page.go_live_datetime = timezone.now() - timedelta(days=1) + self.root_page.add_child(page) + + page.save_revision( + approved_go_live_datetime = timezone.now() - timedelta(days=1) + ) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + management.call_command('publish_scheduled_pages', verbosity=3, interactive=False) + #management.call_command('publish_scheduled_pages', dryrun=True, verbosity=3, interactive=False) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + + def test_go_live_page_will_be_published(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = False + page.go_live_datetime = timezone.now() - timedelta(days=1) + self.root_page.add_child(page) + + page.save_revision(approved_go_live_datetime = timezone.now() - timedelta(days=1)) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + + def test_future_go_live_page_will_not_be_published(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = False + page.go_live_datetime = timezone.now() + timedelta(days=1) + self.root_page.add_child(page) + page.save_revision(approved_go_live_datetime = timezone.now() - timedelta(days=1)) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + + def test_expired_page_will_be_unpublished(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = True + page.expiry_datetime = timezone.now() - timedelta(days=1) + self.root_page.add_child(page) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(p.expired) + + def test_future_expired_page_will_not_be_unpublished(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = True + page.expiry_datetime = timezone.now() + timedelta(days=1) + self.root_page.add_child(page) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + self.assertFalse(p.expired) + + def test_expired_pages_are_dropped_from_mod_queue(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = False + page.expiry_datetime = timezone.now() - timedelta(days=1) + self.root_page.add_child(page) + page.save_revision(submitted_for_moderation = True) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertFalse(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()) + + From 8dfd0c623f7dfa954e7f5c6307da06213932e4e7 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Thu, 29 May 2014 19:03:19 +0300 Subject: [PATCH 010/139] Add help_text mssage to scheduled publisihing ... datetime fields (go_live_datetime and expiry_datetime) to help users fill them since no datetim control is available. --- wagtail/wagtailcore/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 5bdf1c373..434b174b6 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -235,8 +235,8 @@ class Page(MP_Node, ClusterableModel, 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_datetime = models.DateTimeField(verbose_name=_("Go live date/time"), blank=True, null=True) - expiry_datetime = models.DateTimeField(verbose_name=_("Expiry date/time"), blank=True, null=True) + go_live_datetime = 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) + expiry_datetime = 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) indexed_fields = { From 3fa98ef82ed60a622943e54994b819da21841fee Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 10:21:21 +0100 Subject: [PATCH 011/139] Renamed scheduled publishing fields --- wagtail/wagtailadmin/tests.py | 120 +++++++++--------- wagtail/wagtailadmin/views/pages.py | 34 ++--- .../commands/publish_scheduled_pages.py | 16 +-- .../0003_fields_for_scheduled_publishing.py | 30 ++--- wagtail/wagtailcore/models.py | 32 ++--- 5 files changed, 116 insertions(+), 116 deletions(-) diff --git a/wagtail/wagtailadmin/tests.py b/wagtail/wagtailadmin/tests.py index 3bddc864c..4d6016609 100644 --- a/wagtail/wagtailadmin/tests.py +++ b/wagtail/wagtailadmin/tests.py @@ -78,14 +78,14 @@ class TestPageCreation(TestCase): self.assertFalse(page.live) def test_create_simplepage_scheduled(self): - go_live_datetime = timezone.now() + timedelta(days=1) - expiry_datetime = timezone.now() + timedelta(days=2) + go_live_at = timezone.now() + timedelta(days=1) + expire_at = timezone.now() + timedelta(days=2) post_data = { 'title': "New page!", 'content': "Some content", 'slug': 'hello-world', - 'go_live_datetime': str(go_live_datetime).split('.')[0], - 'expiry_datetime': str(expiry_datetime).split('.')[0], + 'go_live_at': str(go_live_at).split('.')[0], + 'expire_at': str(expire_at).split('.')[0], } response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data) @@ -94,21 +94,21 @@ class TestPageCreation(TestCase): # Find the page and check the scheduled times page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific - self.assertEquals(page.go_live_datetime.date(), go_live_datetime.date()) - self.assertEquals(page.expiry_datetime.date(), expiry_datetime.date()) + self.assertEquals(page.go_live_at.date(), go_live_at.date()) + self.assertEquals(page.expire_at.date(), expire_at.date()) self.assertEquals(page.expired, False) self.assertTrue(page.status_string, "draft") - # No revisions with approved_go_live_datetime - self.assertFalse(PageRevision.objects.filter(page=page).exclude(approved_go_live_datetime__isnull=True).exists()) + # No revisions with approved_go_live_at + self.assertFalse(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists()) def test_create_simplepage_scheduled_go_live_before_expiry(self): post_data = { 'title': "New page!", 'content': "Some content", 'slug': 'hello-world', - 'go_live_datetime': str(timezone.now() + timedelta(days=2)).split('.')[0], - 'expiry_datetime': str(timezone.now() + timedelta(days=1)).split('.')[0], + 'go_live_at': str(timezone.now() + timedelta(days=2)).split('.')[0], + 'expire_at': str(timezone.now() + timedelta(days=1)).split('.')[0], } response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data) @@ -121,7 +121,7 @@ class TestPageCreation(TestCase): 'title': "New page!", 'content': "Some content", 'slug': 'hello-world', - 'expiry_datetime': str(timezone.now() + timedelta(days=-1)).split('.')[0], + 'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0], } response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data) @@ -148,15 +148,15 @@ class TestPageCreation(TestCase): self.assertTrue(page.live) def test_create_simplepage_post_publish_scheduled(self): - go_live_datetime = timezone.now() + timedelta(days=1) - expiry_datetime = timezone.now() + timedelta(days=2) + go_live_at = timezone.now() + timedelta(days=1) + expire_at = timezone.now() + timedelta(days=2) post_data = { 'title': "New page!", 'content': "Some content", 'slug': 'hello-world', 'action-publish': "Publish", - 'go_live_datetime': str(go_live_datetime).split('.')[0], - 'expiry_datetime': str(expiry_datetime).split('.')[0], + 'go_live_at': str(go_live_at).split('.')[0], + 'expire_at': str(expire_at).split('.')[0], } response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data) @@ -165,12 +165,12 @@ class TestPageCreation(TestCase): # Find the page and check it page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific - self.assertEquals(page.go_live_datetime.date(), go_live_datetime.date()) - self.assertEquals(page.expiry_datetime.date(), expiry_datetime.date()) + self.assertEquals(page.go_live_at.date(), go_live_at.date()) + self.assertEquals(page.expire_at.date(), expire_at.date()) self.assertEquals(page.expired, False) - # A revision with approved_go_live_datetime should exist now - self.assertTrue(PageRevision.objects.filter(page=page).exclude(approved_go_live_datetime__isnull=True).exists()) + # A revision with approved_go_live_at should exist now + 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.assertTrue(page.status_string, "scheduled") @@ -250,14 +250,14 @@ class TestPageEdit(TestCase): self.assertTrue(child_page_new.has_unpublished_changes) def test_edit_post_scheduled(self): - go_live_datetime = timezone.now() + timedelta(days=1) - expiry_datetime = timezone.now() + timedelta(days=2) + go_live_at = timezone.now() + timedelta(days=1) + expire_at = timezone.now() + timedelta(days=2) post_data = { 'title': "I've been edited!", 'content': "Some content", 'slug': 'hello-world', - 'go_live_datetime': str(go_live_datetime).split('.')[0], - 'expiry_datetime': str(expiry_datetime).split('.')[0], + 'go_live_at': str(go_live_at).split('.')[0], + 'expire_at': str(expire_at).split('.')[0], } response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) @@ -268,11 +268,11 @@ class TestPageEdit(TestCase): # The page will still be live self.assertTrue(child_page_new.live) - # A revision with approved_go_live_datetime should not exist - self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_datetime__isnull=True).exists()) - # But a revision with go_live_datetime and expiry_datetime in their content json *should* exist - self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(go_live_datetime.date())).exists()) - self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(expiry_datetime.date())).exists()) + # A revision with approved_go_live_at should not exist + self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()) + # But a revision with go_live_at and expire_at in their content json *should* exist + self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(go_live_at.date())).exists()) + self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(expire_at.date())).exists()) def test_edit_post_publish(self): # Tests publish from edit page @@ -295,15 +295,15 @@ class TestPageEdit(TestCase): self.assertFalse(child_page_new.has_unpublished_changes) def test_edit_post_publish_scheduled(self): - go_live_datetime = timezone.now() + timedelta(days=1) - expiry_datetime = timezone.now() + timedelta(days=2) + go_live_at = timezone.now() + timedelta(days=1) + expire_at = timezone.now() + timedelta(days=2) post_data = { 'title': "I've been edited!", 'content': "Some content", 'slug': 'hello-world', 'action-publish': "Publish", - 'go_live_datetime': str(go_live_datetime).split('.')[0], - 'expiry_datetime': str(expiry_datetime).split('.')[0], + 'go_live_at': str(go_live_at).split('.')[0], + 'expire_at': str(expire_at).split('.')[0], } response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) @@ -313,20 +313,20 @@ class TestPageEdit(TestCase): child_page_new = SimplePage.objects.get(id=self.child_page.id) # The page should not be live anymore self.assertFalse(child_page_new.live) - # Instead a revision with approved_go_live_datetime should now exist - self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_datetime__isnull=True).exists()) + # Instead a revision with approved_go_live_at should now exist + self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()) def test_edit_post_publish_now_an_already_scheduled(self): - # First let's publish a page with a go_live_datetime in the future - go_live_datetime = timezone.now() + timedelta(days=1) - expiry_datetime = timezone.now() + timedelta(days=2) + # First let's publish a page with a go_live_at in the future + go_live_at = timezone.now() + timedelta(days=1) + expire_at = timezone.now() + timedelta(days=2) post_data = { 'title': "I've been edited!", 'content': "Some content", 'slug': 'hello-world', 'action-publish': "Publish", - 'go_live_datetime': str(go_live_datetime).split('.')[0], - 'expiry_datetime': str(expiry_datetime).split('.')[0], + 'go_live_at': str(go_live_at).split('.')[0], + 'expire_at': str(expire_at).split('.')[0], } response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) @@ -336,17 +336,17 @@ class TestPageEdit(TestCase): child_page_new = SimplePage.objects.get(id=self.child_page.id) # The page should not be live anymore self.assertFalse(child_page_new.live) - # Instead a revision with approved_go_live_datetime should now exist - self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_datetime__isnull=True).exists()) + # Instead a revision with approved_go_live_at should now exist + self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()) # Now, let's edit it and publish it right now - go_live_datetime = timezone.now() + go_live_at = timezone.now() post_data = { 'title': "I've been edited!", 'content': "Some content", 'slug': 'hello-world', 'action-publish': "Publish", - 'go_live_datetime': "", + 'go_live_at': "", } response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data) @@ -356,8 +356,8 @@ class TestPageEdit(TestCase): child_page_new = SimplePage.objects.get(id=self.child_page.id) # The page should be live now self.assertTrue(child_page_new.live) - # And a revision with approved_go_live_datetime should not exist - self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_datetime__isnull=True).exists()) + # And a revision with approved_go_live_at should not exist + self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()) class TestPageDelete(TestCase): def setUp(self): @@ -483,60 +483,60 @@ class TestPublishScheduledPages(TestCase): page.title = "Hello world!" page.slug = "hello-world" page.live = False - page.go_live_datetime = timezone.now() - timedelta(days=1) + page.go_live_at = timezone.now() - timedelta(days=1) self.root_page.add_child(instance=page) page.save_revision( - approved_go_live_datetime = timezone.now() - timedelta(days=1) + approved_go_live_at = timezone.now() - timedelta(days=1) ) p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) - self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) management.call_command('publish_scheduled_pages', verbosity=3, interactive=False) #management.call_command('publish_scheduled_pages', dryrun=True, verbosity=3, interactive=False) p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) - self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) def test_go_live_page_will_be_published(self): page = SimplePage() page.title = "Hello world!" page.slug = "hello-world" page.live = False - page.go_live_datetime = timezone.now() - timedelta(days=1) + page.go_live_at = timezone.now() - timedelta(days=1) self.root_page.add_child(instance=page) - page.save_revision(approved_go_live_datetime = timezone.now() - timedelta(days=1)) + page.save_revision(approved_go_live_at = timezone.now() - timedelta(days=1)) p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) - self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) management.call_command('publish_scheduled_pages', ) p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) - self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) def test_future_go_live_page_will_not_be_published(self): page = SimplePage() page.title = "Hello world!" page.slug = "hello-world" page.live = False - page.go_live_datetime = timezone.now() + timedelta(days=1) + page.go_live_at = timezone.now() + timedelta(days=1) self.root_page.add_child(instance=page) - page.save_revision(approved_go_live_datetime = timezone.now() - timedelta(days=1)) + page.save_revision(approved_go_live_at = timezone.now() - timedelta(days=1)) p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) - self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) management.call_command('publish_scheduled_pages', ) p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) - self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_datetime__isnull=True).exists()) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) def test_expired_page_will_be_unpublished(self): page = SimplePage() page.title = "Hello world!" page.slug = "hello-world" page.live = True - page.expiry_datetime = timezone.now() - timedelta(days=1) + page.expire_at = timezone.now() - timedelta(days=1) self.root_page.add_child(instance=page) p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) @@ -550,7 +550,7 @@ class TestPublishScheduledPages(TestCase): page.title = "Hello world!" page.slug = "hello-world" page.live = True - page.expiry_datetime = timezone.now() + timedelta(days=1) + page.expire_at = timezone.now() + timedelta(days=1) self.root_page.add_child(instance=page) p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) @@ -564,7 +564,7 @@ class TestPublishScheduledPages(TestCase): page.title = "Hello world!" page.slug = "hello-world" page.live = False - page.expiry_datetime = timezone.now() - timedelta(days=1) + page.expire_at = timezone.now() - timedelta(days=1) self.root_page.add_child(instance=page) page.save_revision(submitted_for_moderation = True) p = Page.objects.get(slug='hello-world') diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 26ab715ec..f4ea960ff 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -133,18 +133,18 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ is_publishing = bool(request.POST.get('action-publish')) and parent_page_perms.can_publish_subpage() is_submitting = bool(request.POST.get('action-submit')) - go_live_datetime = form.cleaned_data.get('go_live_datetime') - future_go_live = go_live_datetime and go_live_datetime > timezone.now() - approved_go_live_datetime = None + go_live_at = form.cleaned_data.get('go_live_at') + future_go_live = go_live_at and go_live_at > timezone.now() + approved_go_live_at = None if is_publishing: page.has_unpublished_changes = False page.expired = False if future_go_live: page.live = False - # Set approved_go_live_datetime only if is publishing + # Set approved_go_live_at only if is publishing # and the future_go_live is actually in future - approved_go_live_datetime = go_live_datetime + approved_go_live_at = go_live_at else: page.live = True else: @@ -153,11 +153,11 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ parent_page.add_child(instance=page) # assign tree parameters - will cause page to be saved - # Pass approved_go_live_datetime to save_revision + # Pass approved_go_live_at to save_revision page.save_revision( user=request.user, submitted_for_moderation=is_submitting, - approved_go_live_datetime = approved_go_live_datetime + approved_go_live_at = approved_go_live_at ) if is_publishing: @@ -222,24 +222,24 @@ def edit(request, page_id): if form.is_valid(): is_publishing = bool(request.POST.get('action-publish')) and page_perms.can_publish() is_submitting = bool(request.POST.get('action-submit')) - go_live_datetime = form.cleaned_data.get('go_live_datetime') - future_go_live = go_live_datetime and go_live_datetime > timezone.now() - approved_go_live_datetime = None + go_live_at = form.cleaned_data.get('go_live_at') + future_go_live = go_live_at and go_live_at > timezone.now() + approved_go_live_at = None if is_publishing: page.has_unpublished_changes = False page.expired = False if future_go_live: page.live = False - # Set approved_go_live_datetime only if publishing - approved_go_live_datetime = go_live_datetime + # Set approved_go_live_at only if publishing + approved_go_live_at = go_live_at else: page.live = True form.save() - # Clear approved_go_live_datetime for older revisions + # Clear approved_go_live_at for older revisions page.revisions.update( submitted_for_moderation=False, - approved_go_live_datetime=None, + approved_go_live_at=None, ) else: # not publishing the page @@ -255,7 +255,7 @@ def edit(request, page_id): page.save_revision( user=request.user, submitted_for_moderation=is_submitting, - approved_go_live_datetime = approved_go_live_datetime + approved_go_live_at = approved_go_live_at ) if is_publishing: @@ -455,8 +455,8 @@ def unpublish(request, page_id): parent_id = page.get_parent().id page.live = False page.save() - # Since page is unpublished clear the approved_go_live_datetime of all revisions - page.revisions.update(approved_go_live_datetime=None) + # Since page is unpublished clear the approved_go_live_at of all revisions + page.revisions.update(approved_go_live_at=None) messages.success(request, _("Page '{0}' unpublished.").format(page.title)) return redirect('wagtailadmin_explore', parent_id) diff --git a/wagtail/wagtailcore/management/commands/publish_scheduled_pages.py b/wagtail/wagtailcore/management/commands/publish_scheduled_pages.py index 0abf3c744..b80ace728 100644 --- a/wagtail/wagtailcore/management/commands/publish_scheduled_pages.py +++ b/wagtail/wagtailcore/management/commands/publish_scheduled_pages.py @@ -8,11 +8,11 @@ from wagtail.wagtailcore.models import Page, PageRevision def revision_date_expired(r): - expiry_str = json.loads(r.content_json).get('expiry_datetime') + expiry_str = json.loads(r.content_json).get('expire_at') if not expiry_str: return False - expiry_datetime = dateparse.parse_datetime(expiry_str) - if expiry_datetime < timezone.now(): + expire_at = dateparse.parse_datetime(expiry_str) + if expire_at < timezone.now(): return True else: return False @@ -37,7 +37,7 @@ class Command(BaseCommand): # 1. get all expired pages with live = True expired_pages = Page.objects.filter( live=True, - expiry_datetime__lt=timezone.now() + expire_at__lt=timezone.now() ) if dryrun: if expired_pages: @@ -46,7 +46,7 @@ class Command(BaseCommand): print "---------------\t\t----\t\t----" for ep in expired_pages: print "{0}\t{1}\t{2}".format( - ep.expiry_datetime.strftime("%Y-%m-%d %H:%M"), + ep.expire_at.strftime("%Y-%m-%d %H:%M"), ep.slug, ep.title ) @@ -71,7 +71,7 @@ class Command(BaseCommand): rev_data = json.loads(er.content_json) print "{0}\t{1}\t{2}".format( dateparse.parse_datetime( - rev_data.get('expiry_datetime') + rev_data.get('expire_at') ).strftime("%Y-%m-%d %H:%M"), rev_data.get('slug'), rev_data.get('title') @@ -85,7 +85,7 @@ class Command(BaseCommand): # 3. get all revisions that need to be published revs_for_publishing = PageRevision.objects.filter( - approved_go_live_datetime__lt=timezone.now() + approved_go_live_at__lt=timezone.now() ) if dryrun: print "---------------------------------" @@ -96,7 +96,7 @@ class Command(BaseCommand): for rp in revs_for_publishing: rev_data = json.loads(rp.content_json) print "{0}\t\t{1}\t{2}".format( - rp.approved_go_live_datetime.strftime("%Y-%m-%d %H:%M"), + rp.approved_go_live_at.strftime("%Y-%m-%d %H:%M"), rev_data.get('slug'), rev_data.get('title') ) diff --git a/wagtail/wagtailcore/migrations/0003_fields_for_scheduled_publishing.py b/wagtail/wagtailcore/migrations/0003_fields_for_scheduled_publishing.py index 5e682aa59..639068c2b 100644 --- a/wagtail/wagtailcore/migrations/0003_fields_for_scheduled_publishing.py +++ b/wagtail/wagtailcore/migrations/0003_fields_for_scheduled_publishing.py @@ -8,18 +8,18 @@ from django.db import models class Migration(SchemaMigration): def forwards(self, orm): - # Adding field 'PageRevision.approved_go_live_datetime' - db.add_column(u'wagtailcore_pagerevision', 'approved_go_live_datetime', + # Adding field 'PageRevision.approved_go_live_at' + db.add_column(u'wagtailcore_pagerevision', 'approved_go_live_at', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), keep_default=False) - # Adding field 'Page.go_live_datetime' - db.add_column(u'wagtailcore_page', 'go_live_datetime', + # Adding field 'Page.go_live_at' + db.add_column(u'wagtailcore_page', 'go_live_at', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), keep_default=False) - # Adding field 'Page.expiry_datetime' - db.add_column(u'wagtailcore_page', 'expiry_datetime', + # Adding field 'Page.expire_at' + db.add_column(u'wagtailcore_page', 'expire_at', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), keep_default=False) @@ -30,14 +30,14 @@ class Migration(SchemaMigration): def backwards(self, orm): - # Deleting field 'PageRevision.approved_go_live_datetime' - db.delete_column(u'wagtailcore_pagerevision', 'approved_go_live_datetime') + # Deleting field 'PageRevision.approved_go_live_at' + db.delete_column(u'wagtailcore_pagerevision', 'approved_go_live_at') - # Deleting field 'Page.go_live_datetime' - db.delete_column(u'wagtailcore_page', 'go_live_datetime') + # Deleting field 'Page.go_live_at' + db.delete_column(u'wagtailcore_page', 'go_live_at') - # Deleting field 'Page.expiry_datetime' - db.delete_column(u'wagtailcore_page', 'expiry_datetime') + # Deleting field 'Page.expire_at' + db.delete_column(u'wagtailcore_page', 'expire_at') # Deleting field 'Page.expired' db.delete_column(u'wagtailcore_page', 'expired') @@ -92,8 +92,8 @@ class Migration(SchemaMigration): 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}), 'depth': ('django.db.models.fields.PositiveIntegerField', [], {}), 'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), - 'expiry_datetime': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), - 'go_live_datetime': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'expire_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), @@ -109,7 +109,7 @@ class Migration(SchemaMigration): }, u'wagtailcore.pagerevision': { 'Meta': {'object_name': 'PageRevision'}, - 'approved_go_live_datetime': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'approved_go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 'content_json': ('django.db.models.fields.TextField', [], {}), 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 434b174b6..3b2b055bd 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -235,8 +235,8 @@ class Page(MP_Node, ClusterableModel, 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_datetime = 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) - expiry_datetime = 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) + 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) indexed_fields = { @@ -382,12 +382,12 @@ class Page(MP_Node, ClusterableModel, Indexed): else: raise Http404 - def save_revision(self, user=None, submitted_for_moderation=False, approved_go_live_datetime=None): + def save_revision(self, user=None, submitted_for_moderation=False, approved_go_live_at=None): self.revisions.create( content_json=self.to_json(), user=user, submitted_for_moderation=submitted_for_moderation, - approved_go_live_datetime=approved_go_live_datetime, + approved_go_live_at=approved_go_live_at, ) def get_latest_revision(self): @@ -472,11 +472,11 @@ class Page(MP_Node, ClusterableModel, Indexed): def clean(self): super(Page, self).clean() - if self.go_live_datetime and self.expiry_datetime: - if self.go_live_datetime > self.expiry_datetime: + if self.go_live_at and self.expire_at: + if self.go_live_at > self.expire_at: raise ValidationError(ugettext('Go live date/time should be before expiry datetime.')) - if self.expiry_datetime and self.expiry_datetime < timezone.now(): + if self.expire_at and self.expire_at < timezone.now(): raise ValidationError(ugettext('Expiry date/time should be in the future')) @classmethod @@ -567,7 +567,7 @@ class Page(MP_Node, ClusterableModel, Indexed): @property def approved_schedule(self): - return self.revisions.exclude(approved_go_live_datetime__isnull=True).exists() + return self.revisions.exclude(approved_go_live_at__isnull=True).exists() def has_unpublished_subtree(self): """ @@ -742,7 +742,7 @@ class PageRevision(models.Model): created_at = models.DateTimeField(auto_now_add=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True) content_json = models.TextField() - approved_go_live_datetime = models.DateTimeField(null=True, blank=True) + approved_go_live_at = models.DateTimeField(null=True, blank=True) objects = models.Manager() submitted_revisions = SubmittedRevisionsManager() @@ -776,18 +776,18 @@ class PageRevision(models.Model): def publish(self): page = self.as_page_object() - if page.go_live_datetime and page.go_live_datetime > timezone.now(): + if page.go_live_at and page.go_live_at > timezone.now(): # if we have a go_live in the future don't make the page live page.live = False - # Instead set the approved_go_live_datetime of this revision - self.approved_go_live_datetime = page.go_live_datetime + # Instead set the approved_go_live_at of this revision + self.approved_go_live_at = page.go_live_at self.save() - # And clear the the approved_go_live_datetime of any other revisions - page.revisions.exclude(id=self.id).update(approved_go_live_datetime=None) + # And clear the the approved_go_live_at of any other revisions + page.revisions.exclude(id=self.id).update(approved_go_live_at=None) else: page.live = True - # If page goes live clear the approved_go_live_datetime of all revisions - page.revisions.update(approved_go_live_datetime=None) + # 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 page.save() self.submitted_for_moderation = False From ad3c62fed520b086ffd6ccf97779bd29af0cbff2 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 10:22:14 +0100 Subject: [PATCH 012/139] Added go_live_at and expire_at to common page configuration --- wagtail/wagtailadmin/edit_handlers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index 370f2f5bb..097b5bb21 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -731,5 +731,7 @@ Page.promote_panels = [ FieldPanel('seo_title'), FieldPanel('show_in_menus'), FieldPanel('search_description'), + FieldPanel('go_live_at'), + FieldPanel('expire_at'), ], ugettext_lazy('Common page configuration')), ] From 97ab613f88ab742c57400f0985d73b62be341635 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 10:28:00 +0100 Subject: [PATCH 013/139] Moved tests for publish pages command into wagtailcore --- wagtail/wagtailadmin/tests.py | 107 +-------------------------------- wagtail/wagtailcore/tests.py | 109 +++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 108 deletions(-) diff --git a/wagtail/wagtailadmin/tests.py b/wagtail/wagtailadmin/tests.py index 4d6016609..c657c1efb 100644 --- a/wagtail/wagtailadmin/tests.py +++ b/wagtail/wagtailadmin/tests.py @@ -1,12 +1,10 @@ -from datetime import datetime, timedelta -from django.core import management +from datetime import timedelta from django.core.urlresolvers import reverse from django.utils import timezone from django.test import TestCase from wagtail.tests.models import SimplePage, EventPage from wagtail.tests.utils import login, unittest from wagtail.wagtailcore.models import Page, PageRevision -from django.core.urlresolvers import reverse class TestHome(TestCase): @@ -472,106 +470,3 @@ class TestEditorHooks(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, '') self.assertContains(response, '') - -class TestPublishScheduledPages(TestCase): - def setUp(self): - # Find root page - self.root_page = Page.objects.get(id=2) - - def test_go_live_page_will_be_published(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = False - page.go_live_at = timezone.now() - timedelta(days=1) - self.root_page.add_child(instance=page) - - page.save_revision( - approved_go_live_at = timezone.now() - timedelta(days=1) - ) - p = Page.objects.get(slug='hello-world') - self.assertFalse(p.live) - self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) - management.call_command('publish_scheduled_pages', verbosity=3, interactive=False) - #management.call_command('publish_scheduled_pages', dryrun=True, verbosity=3, interactive=False) - p = Page.objects.get(slug='hello-world') - self.assertTrue(p.live) - self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) - - def test_go_live_page_will_be_published(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = False - page.go_live_at = timezone.now() - timedelta(days=1) - self.root_page.add_child(instance=page) - - page.save_revision(approved_go_live_at = timezone.now() - timedelta(days=1)) - p = Page.objects.get(slug='hello-world') - self.assertFalse(p.live) - self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) - management.call_command('publish_scheduled_pages', ) - p = Page.objects.get(slug='hello-world') - self.assertTrue(p.live) - self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) - - def test_future_go_live_page_will_not_be_published(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = False - page.go_live_at = timezone.now() + timedelta(days=1) - self.root_page.add_child(instance=page) - page.save_revision(approved_go_live_at = timezone.now() - timedelta(days=1)) - p = Page.objects.get(slug='hello-world') - self.assertFalse(p.live) - self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) - management.call_command('publish_scheduled_pages', ) - p = Page.objects.get(slug='hello-world') - self.assertFalse(p.live) - self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) - - def test_expired_page_will_be_unpublished(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = True - page.expire_at = timezone.now() - timedelta(days=1) - self.root_page.add_child(instance=page) - p = Page.objects.get(slug='hello-world') - self.assertTrue(p.live) - management.call_command('publish_scheduled_pages', ) - p = Page.objects.get(slug='hello-world') - self.assertFalse(p.live) - self.assertTrue(p.expired) - - def test_future_expired_page_will_not_be_unpublished(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = True - page.expire_at = timezone.now() + timedelta(days=1) - self.root_page.add_child(instance=page) - p = Page.objects.get(slug='hello-world') - self.assertTrue(p.live) - management.call_command('publish_scheduled_pages', ) - p = Page.objects.get(slug='hello-world') - self.assertTrue(p.live) - self.assertFalse(p.expired) - - def test_expired_pages_are_dropped_from_mod_queue(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = False - page.expire_at = timezone.now() - timedelta(days=1) - self.root_page.add_child(instance=page) - page.save_revision(submitted_for_moderation = True) - p = Page.objects.get(slug='hello-world') - self.assertFalse(p.live) - self.assertTrue(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()) - management.call_command('publish_scheduled_pages', ) - p = Page.objects.get(slug='hello-world') - self.assertFalse(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()) - - diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index 1b12c1025..3eede0e4a 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -1,9 +1,12 @@ +from datetime import timedelta + from django.test import TestCase, Client from django.http import HttpRequest, Http404 - +from django.utils import timezone from django.contrib.auth.models import User +from django.core import management -from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy +from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy, PageRevision from wagtail.tests.models import EventPage, EventIndex, SimplePage @@ -776,3 +779,105 @@ class TestIssue157(TestCase): # Check url self.assertEqual(homepage.url, '/') + + +class TestPublishScheduledPagesCommand(TestCase): + def setUp(self): + # Find root page + self.root_page = Page.objects.get(id=2) + + def test_go_live_page_will_be_published(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = False + page.go_live_at = timezone.now() - timedelta(days=1) + self.root_page.add_child(instance=page) + + page.save_revision( + approved_go_live_at = timezone.now() - timedelta(days=1) + ) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) + management.call_command('publish_scheduled_pages', verbosity=3, interactive=False) + #management.call_command('publish_scheduled_pages', dryrun=True, verbosity=3, interactive=False) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) + + def test_go_live_page_will_be_published(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = False + page.go_live_at = timezone.now() - timedelta(days=1) + self.root_page.add_child(instance=page) + + page.save_revision(approved_go_live_at = timezone.now() - timedelta(days=1)) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) + + def test_future_go_live_page_will_not_be_published(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = False + page.go_live_at = timezone.now() + timedelta(days=1) + self.root_page.add_child(instance=page) + page.save_revision(approved_go_live_at = timezone.now() - timedelta(days=1)) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) + + def test_expired_page_will_be_unpublished(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = True + page.expire_at = timezone.now() - timedelta(days=1) + self.root_page.add_child(instance=page) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(p.expired) + + def test_future_expired_page_will_not_be_unpublished(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = True + page.expire_at = timezone.now() + timedelta(days=1) + self.root_page.add_child(instance=page) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertTrue(p.live) + self.assertFalse(p.expired) + + def test_expired_pages_are_dropped_from_mod_queue(self): + page = SimplePage() + page.title = "Hello world!" + page.slug = "hello-world" + page.live = False + page.expire_at = timezone.now() - timedelta(days=1) + self.root_page.add_child(instance=page) + page.save_revision(submitted_for_moderation = True) + p = Page.objects.get(slug='hello-world') + self.assertFalse(p.live) + self.assertTrue(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()) + management.call_command('publish_scheduled_pages', ) + p = Page.objects.get(slug='hello-world') + self.assertFalse(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()) From e530531cb9be2c2b398ce374b73294eca27d1eeb Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 10:37:39 +0100 Subject: [PATCH 014/139] Cleanup of scheduled publishing tests --- wagtail/wagtailadmin/tests.py | 11 ++++ wagtail/wagtailcore/tests.py | 109 ++++++++++++++++++++-------------- 2 files changed, 77 insertions(+), 43 deletions(-) diff --git a/wagtail/wagtailadmin/tests.py b/wagtail/wagtailadmin/tests.py index c657c1efb..3e4103137 100644 --- a/wagtail/wagtailadmin/tests.py +++ b/wagtail/wagtailadmin/tests.py @@ -1,7 +1,9 @@ from datetime import timedelta + from django.core.urlresolvers import reverse from django.utils import timezone from django.test import TestCase + from wagtail.tests.models import SimplePage, EventPage from wagtail.tests.utils import login, unittest from wagtail.wagtailcore.models import Page, PageRevision @@ -266,8 +268,10 @@ class TestPageEdit(TestCase): # The page will still be live self.assertTrue(child_page_new.live) + # A revision with approved_go_live_at should not exist self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()) + # But a revision with go_live_at and expire_at in their content json *should* exist self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(go_live_at.date())).exists()) self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(expire_at.date())).exists()) @@ -309,8 +313,10 @@ class TestPageEdit(TestCase): self.assertEqual(response.status_code, 302) child_page_new = SimplePage.objects.get(id=self.child_page.id) + # The page should not be live anymore self.assertFalse(child_page_new.live) + # Instead a revision with approved_go_live_at should now exist self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()) @@ -332,8 +338,10 @@ class TestPageEdit(TestCase): self.assertEqual(response.status_code, 302) child_page_new = SimplePage.objects.get(id=self.child_page.id) + # The page should not be live anymore self.assertFalse(child_page_new.live) + # Instead a revision with approved_go_live_at should now exist self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()) @@ -352,11 +360,14 @@ class TestPageEdit(TestCase): self.assertEqual(response.status_code, 302) child_page_new = SimplePage.objects.get(id=self.child_page.id) + # The page should be live now self.assertTrue(child_page_new.live) + # And a revision with approved_go_live_at should not exist self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists()) + class TestPageDelete(TestCase): def setUp(self): # Find root page diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index 3eede0e4a..32ec3a826 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -787,97 +787,120 @@ class TestPublishScheduledPagesCommand(TestCase): self.root_page = Page.objects.get(id=2) def test_go_live_page_will_be_published(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = False - page.go_live_at = timezone.now() - timedelta(days=1) + page = SimplePage( + title="Hello world!", + slug="hello-world", + live=False, + go_live_at=timezone.now() - timedelta(days=1), + ) self.root_page.add_child(instance=page) - page.save_revision( - approved_go_live_at = timezone.now() - timedelta(days=1) - ) + page.save_revision(approved_go_live_at=timezone.now() - timedelta(days=1)) + p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) - management.call_command('publish_scheduled_pages', verbosity=3, interactive=False) - #management.call_command('publish_scheduled_pages', dryrun=True, verbosity=3, interactive=False) + + management.call_command('publish_scheduled_pages') + p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) def test_go_live_page_will_be_published(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = False - page.go_live_at = timezone.now() - timedelta(days=1) + page = SimplePage( + title="Hello world!", + slug="hello-world", + live=False, + go_live_at=timezone.now() - timedelta(days=1), + ) self.root_page.add_child(instance=page) - page.save_revision(approved_go_live_at = timezone.now() - timedelta(days=1)) + page.save_revision(approved_go_live_at=timezone.now() - timedelta(days=1)) + p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) - management.call_command('publish_scheduled_pages', ) + + management.call_command('publish_scheduled_pages') + p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) def test_future_go_live_page_will_not_be_published(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = False - page.go_live_at = timezone.now() + timedelta(days=1) + page = SimplePage( + title="Hello world!", + slug="hello-world", + live=False, + go_live_at=timezone.now() + timedelta(days=1), + ) self.root_page.add_child(instance=page) - page.save_revision(approved_go_live_at = timezone.now() - timedelta(days=1)) + + page.save_revision(approved_go_live_at=timezone.now() - timedelta(days=1)) + p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) - management.call_command('publish_scheduled_pages', ) + + management.call_command('publish_scheduled_pages') + p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists()) def test_expired_page_will_be_unpublished(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = True - page.expire_at = timezone.now() - timedelta(days=1) + page = SimplePage( + title="Hello world!", + slug="hello-world", + live=True, + expire_at=timezone.now() - timedelta(days=1), + ) self.root_page.add_child(instance=page) + p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) - management.call_command('publish_scheduled_pages', ) + + management.call_command('publish_scheduled_pages') + p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) self.assertTrue(p.expired) def test_future_expired_page_will_not_be_unpublished(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = True - page.expire_at = timezone.now() + timedelta(days=1) + page = SimplePage( + title="Hello world!", + slug="hello-world", + live=True, + expire_at=timezone.now() + timedelta(days=1), + ) self.root_page.add_child(instance=page) + p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) - management.call_command('publish_scheduled_pages', ) + + management.call_command('publish_scheduled_pages') + p = Page.objects.get(slug='hello-world') self.assertTrue(p.live) self.assertFalse(p.expired) def test_expired_pages_are_dropped_from_mod_queue(self): - page = SimplePage() - page.title = "Hello world!" - page.slug = "hello-world" - page.live = False - page.expire_at = timezone.now() - timedelta(days=1) + page = SimplePage( + title="Hello world!", + slug="hello-world", + live=False, + expire_at=timezone.now() - timedelta(days=1), + ) self.root_page.add_child(instance=page) - page.save_revision(submitted_for_moderation = True) + + page.save_revision(submitted_for_moderation=True) + p = Page.objects.get(slug='hello-world') self.assertFalse(p.live) self.assertTrue(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()) - management.call_command('publish_scheduled_pages', ) + + management.call_command('publish_scheduled_pages') + p = Page.objects.get(slug='hello-world') self.assertFalse(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists()) From c7e6c429543c883bec025cf41f0f477a0ce5a67e Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 12:56:15 +0100 Subject: [PATCH 015/139] A couple of minor coding style fixes --- wagtail/wagtailadmin/views/pages.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index f4ea960ff..55040366c 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -157,7 +157,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ page.save_revision( user=request.user, submitted_for_moderation=is_submitting, - approved_go_live_at = approved_go_live_at + approved_go_live_at=approved_go_live_at ) if is_publishing: @@ -255,7 +255,7 @@ def edit(request, page_id): page.save_revision( user=request.user, submitted_for_moderation=is_submitting, - approved_go_live_at = approved_go_live_at + approved_go_live_at=approved_go_live_at ) if is_publishing: From 2a84d7ef1dbbc1bb7b93c0bc49b214598c2d8f1d Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 11:57:48 +0100 Subject: [PATCH 016/139] A few improvements to the wagtailforms submission tests --- wagtail/tests/fixtures/test.json | 3 +++ wagtail/wagtailforms/tests.py | 28 +++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index 11b82d9f1..541450211 100644 --- a/wagtail/tests/fixtures/test.json +++ b/wagtail/tests/fixtures/test.json @@ -183,6 +183,9 @@ "pk": 8, "model": "tests.formpage", "fields": { + "to_address": "to@email.com", + "from_address": "from@email.com", + "subject": "The subject" } }, diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py index 8ec2ee5c3..459dbbea4 100644 --- a/wagtail/wagtailforms/tests.py +++ b/wagtail/wagtailforms/tests.py @@ -1,32 +1,50 @@ from django.test import TestCase +from django.core import mail from wagtail.wagtailcore.models import Page from wagtail.wagtailforms.models import FormSubmission + class TestFormSubmission(TestCase): fixtures = ['test.json'] def test_get_form(self): response = self.client.get('/contact-us/') + + # Check response self.assertContains(response, """""") - self.assertNotContains(response, "Thank you for your feedback") + self.assertTemplateUsed(response, 'tests/form_page.html') + self.assertTemplateNotUsed(response, 'tests/form_page_landing.html') def test_post_invalid_form(self): response = self.client.post('/contact-us/', { 'your-email': 'bob', 'your-message': 'hello world' }) - self.assertNotContains(response, "Thank you for your feedback") + + # Check response self.assertContains(response, "Enter a valid email address.") + self.assertTemplateUsed(response, 'tests/form_page.html') + self.assertTemplateNotUsed(response, 'tests/form_page_landing.html') def test_post_valid_form(self): response = self.client.post('/contact-us/', { 'your-email': 'bob@example.com', 'your-message': 'hello world' }) - self.assertNotContains(response, "Your email") - self.assertContains(response, "Thank you for your feedback") + # 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') + + # Check that an email was sent + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, "The subject") + self.assertTrue("Your message: hello world" in mail.outbox[0].body) + self.assertEqual(mail.outbox[0].to, ['to@email.com']) + self.assertEqual(mail.outbox[0].from_email, 'from@email.com') + + # Check that form submission was saved correctly form_page = Page.objects.get(url_path='/home/contact-us/') - self.assertTrue(FormSubmission.objects.filter(page=form_page, form_data__contains='hello world').exists()) From 7cf899bdbc8835a05a7896fd4eaaa2452d0ddbd8 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 17:03:33 +0100 Subject: [PATCH 017/139] Added test for FormBuilder class --- wagtail/wagtailforms/tests.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py index 459dbbea4..ac18c7fa7 100644 --- a/wagtail/wagtailforms/tests.py +++ b/wagtail/wagtailforms/tests.py @@ -1,8 +1,10 @@ from django.test import TestCase from django.core import mail +from django import forms from wagtail.wagtailcore.models import Page from wagtail.wagtailforms.models import FormSubmission +from wagtail.wagtailforms.forms import FormBuilder class TestFormSubmission(TestCase): @@ -48,6 +50,26 @@ class TestFormSubmission(TestCase): self.assertTrue(FormSubmission.objects.filter(page=form_page, form_data__contains='hello world').exists()) +class TestFormBuilder(TestCase): + fixtures = ['test.json'] + + def setUp(self): + self.form_page = Page.objects.get(url_path='/home/contact-us/').specific + self.fb = FormBuilder(self.form_page.form_fields.all()) + + def test_fields(self): + """ + This tests that all fields were added to the form with the correct types + """ + form_class = self.fb.get_form_class() + + self.assertTrue('your-email' in form_class.base_fields.keys()) + self.assertTrue('your-message' in form_class.base_fields.keys()) + + self.assertIsInstance(form_class.base_fields['your-email'], forms.EmailField) + self.assertIsInstance(form_class.base_fields['your-message'], forms.CharField) + + class TestFormsBackend(TestCase): fixtures = ['test.json'] From 97a981a7660154027df1da4f8a2cc314e701d037 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 17:12:08 +0100 Subject: [PATCH 018/139] Cleaned up FormBuilder class --- wagtail/wagtailforms/forms.py | 53 +++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/wagtail/wagtailforms/forms.py b/wagtail/wagtailforms/forms.py index f17023a77..aaf16eb79 100644 --- a/wagtail/wagtailforms/forms.py +++ b/wagtail/wagtailforms/forms.py @@ -8,22 +8,9 @@ class BaseForm(django.forms.Form): return super(BaseForm, self).__init__(*args, **kwargs) -class FormBuilder(): - formfields = SortedDict() - +class FormBuilder(object): def __init__(self, fields): - for field in fields: - options = self.get_options(field) - f = getattr(self, "create_"+field.field_type+"_field")(field, options) - self.formfields[field.clean_name] = f - - def get_options(self, field): - options = {} - options['label'] = field.label - options['help_text'] = field.help_text - options['required'] = field.required - options['initial'] = field.default_value - return options + self.fields = fields def create_singleline_field(self, field, options): # TODO: This is a default value - it may need to be changed @@ -72,6 +59,42 @@ class FormBuilder(): def create_checkbox_field(self, field, options): return django.forms.BooleanField(**options) + FIELD_TYPES = { + 'singleline': create_singleline_field, + 'multiline': create_multiline_field, + 'date': create_date_field, + 'datetime': create_datetime_field, + 'email': create_email_field, + 'url': create_url_field, + 'number': create_number_field, + 'dropdown': create_dropdown_field, + 'radio': create_radio_field, + 'checkboxes': create_checkboxes_field, + 'checkbox': create_checkbox_field, + } + + @property + def formfields(self): + formfields = SortedDict() + + for field in self.fields: + options = self.get_field_options(field) + + if field.field_type in self.FIELD_TYPES: + formfields[field.clean_name] = self.FIELD_TYPES[field.field_type](self, field, options) + else: + raise Exception("Unrecognised field type: " + form.field_type) + + return formfields + + def get_field_options(self, field): + options = {} + options['label'] = field.label + options['help_text'] = field.help_text + options['required'] = field.required + options['initial'] = field.default_value + return options + def get_form_class(self): return type('WagtailForm', (BaseForm,), self.formfields) From 21ac2e3ab8bb3c52b45b7ae005a5c87a83785def Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 17:27:48 +0100 Subject: [PATCH 019/139] Split wagtailforms landing page into separate view --- wagtail/wagtailforms/models.py | 26 +++++++++++++++++--------- wagtail/wagtailforms/tests.py | 12 +++++++++--- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/wagtail/wagtailforms/models.py b/wagtail/wagtailforms/models.py index 75f0eceee..59941c6ab 100644 --- a/wagtail/wagtailforms/models.py +++ b/wagtail/wagtailforms/models.py @@ -1,5 +1,5 @@ from django.db import models -from django.shortcuts import render +from django.shortcuts import render, redirect from django.utils.translation import ugettext_lazy as _ from django.utils.text import slugify @@ -111,6 +111,7 @@ class AbstractForm(Page): """A Form Page. Pages implementing a form should inhert from it""" form_builder = FormBuilder + landing_page_path = 'done' is_abstract = True # Don't display me in "Add" def __init__(self, *args, **kwargs): @@ -152,11 +153,8 @@ class AbstractForm(Page): form_processor = self.form_processing_backend() form_processor.process(self, form) - # render the landing_page - # TODO: It is much better to redirect to it - return render(request, self.landing_page_template, { - 'self': self, - }) + # Redirect to the landing page + return redirect(self.url + self.landing_page_path + '/') else: form = form_class(**form_params) @@ -165,6 +163,18 @@ class AbstractForm(Page): 'form': form, }) + def serve_landing(self, request): + return render(request, self.landing_page_template, { + 'self': self, + }) + + def route(self, request, path_components): + # Check if this request is for the landing page + if self.live and path_components == [self.landing_page_path]: + return self.serve_landing(request) + + return super(AbstractForm, self).route(request, path_components) + def get_page_modes(self): return [ ('form', 'Form'), @@ -173,9 +183,7 @@ class AbstractForm(Page): def show_as_mode(self, mode): if mode == 'landing': - return render(self.dummy_request(), self.landing_page_template, { - 'self': self, - }) + return self.serve_landing(self.dummy_request()) else: return super(AbstractForm, self).show_as_mode(mode) diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py index ac18c7fa7..2276d10d9 100644 --- a/wagtail/wagtailforms/tests.py +++ b/wagtail/wagtailforms/tests.py @@ -34,9 +34,7 @@ class TestFormSubmission(TestCase): }) # 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') + self.assertEqual(response.status_code, 302) # Check that an email was sent self.assertEqual(len(mail.outbox), 1) @@ -49,6 +47,14 @@ class TestFormSubmission(TestCase): form_page = Page.objects.get(url_path='/home/contact-us/') self.assertTrue(FormSubmission.objects.filter(page=form_page, form_data__contains='hello world').exists()) + def test_get_landing_page(self): + response = self.client.get('/contact-us/done/') + + # 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 6d67749f5a084b22a8ac9eac5894bfdf2e9dbd66 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 4 Jun 2014 15:48:48 +0100 Subject: [PATCH 020/139] Cleanup of TestFormsBackend TestcAse --- wagtail/wagtailforms/tests.py | 38 +++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py index 2276d10d9..019f992d8 100644 --- a/wagtail/wagtailforms/tests.py +++ b/wagtail/wagtailforms/tests.py @@ -79,31 +79,43 @@ class TestFormBuilder(TestCase): class TestFormsBackend(TestCase): fixtures = ['test.json'] - def test_cannot_see_forms_without_permission(self): - form_page = Page.objects.get(url_path='/home/contact-us/') + def setUp(self): + self.client.login(username='siteeditor', password='password') + self.form_page = Page.objects.get(url_path='/home/contact-us/') + def test_cannot_see_forms_without_permission(self): + # Login with as a user without permission to see forms self.client.login(username='eventeditor', password='password') + response = self.client.get('/admin/forms/') - self.assertFalse(form_page in response.context['form_pages']) + + # Check that the user cannot see the form page + self.assertFalse(self.form_page in response.context['form_pages']) def test_can_see_forms_with_permission(self): - form_page = Page.objects.get(url_path='/home/contact-us/') - - self.client.login(username='siteeditor', password='password') response = self.client.get('/admin/forms/') - self.assertTrue(form_page in response.context['form_pages']) - def test_can_get_submissions(self): - form_page = Page.objects.get(url_path='/home/contact-us/') + # Check that the user can see the form page + self.assertTrue(self.form_page in response.context['form_pages']) - self.client.login(username='siteeditor', password='password') + def test_list_submissions(self): + response = self.client.get('/admin/forms/submissions/%d/' % self.form_page.id) - response = self.client.get('/admin/forms/submissions/%d/' % form_page.id) + # Check response + self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context['data_rows']), 2) - response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014' % form_page.id) + def test_list_submissions_filtered(self): + response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014' % self.form_page.id) + + # Check response + self.assertEqual(response.status_code, 200) self.assertEqual(len(response.context['data_rows']), 1) - response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014&action=CSV' % form_page.id) + def test_list_submissions_csv_export(self): + response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014&action=CSV' % self.form_page.id) + + # Check response + self.assertEqual(response.status_code, 200) data_line = response.content.split("\n")[1] self.assertTrue('new@example.com' in data_line) From 7e7885b2b514856de0b53c513f2450201be573a6 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 4 Jun 2014 15:55:25 +0100 Subject: [PATCH 021/139] Added tests for wagtailforms page modes --- wagtail/wagtailforms/tests.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py index 019f992d8..ab65f909b 100644 --- a/wagtail/wagtailforms/tests.py +++ b/wagtail/wagtailforms/tests.py @@ -56,6 +56,29 @@ class TestFormSubmission(TestCase): self.assertTemplateUsed(response, 'tests/form_page_landing.html') +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.show_as_mode('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.show_as_mode('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 24df26c8c338562e64dad8440709175d99f488fe Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 4 Jun 2014 16:25:05 +0100 Subject: [PATCH 022/139] More wagtailforms tests --- wagtail/wagtailforms/tests.py | 127 +++++++++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py index ab65f909b..5145eae3f 100644 --- a/wagtail/wagtailforms/tests.py +++ b/wagtail/wagtailforms/tests.py @@ -1,3 +1,5 @@ +import json + from django.test import TestCase from django.core import mail from django import forms @@ -5,6 +7,7 @@ from django import forms from wagtail.wagtailcore.models import Page from wagtail.wagtailforms.models import FormSubmission from wagtail.wagtailforms.forms import FormBuilder +from wagtail.tests.models import FormPage class TestFormSubmission(TestCase): @@ -99,13 +102,74 @@ class TestFormBuilder(TestCase): self.assertIsInstance(form_class.base_fields['your-message'], forms.CharField) -class TestFormsBackend(TestCase): +class TestFormsIndex(TestCase): fixtures = ['test.json'] def setUp(self): self.client.login(username='siteeditor', password='password') self.form_page = Page.objects.get(url_path='/home/contact-us/') + def make_form_pages(self): + """ + This makes 100 form pages and adds them as children to 'contact-us' + This is used to test pagination on the forms index + """ + for i in range(100): + self.form_page.add_child(instance=FormPage( + title="Form " + str(i), + slug='form-' + str(i), + live=True + )) + + def test_forms_index(self): + response = self.client.get('/admin/forms/') + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index.html') + + def test_forms_index_pagination(self): + # Create some more form pages to make pagination kick in + self.make_form_pages() + + # Get page two + response = self.client.get('/admin/forms/', {'p': 2}) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index.html') + + # Check that we got the correct page + self.assertEqual(response.context['form_pages'].number, 2) + + def test_forms_index_pagination_invalid(self): + # Create some more form pages to make pagination kick in + self.make_form_pages() + + # Get page two + response = self.client.get('/admin/forms/', {'p': 'Hello world!'}) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index.html') + + # Check that it got page one + self.assertEqual(response.context['form_pages'].number, 1) + + def test_forms_index_pagination_out_of_range(self): + # Create some more form pages to make pagination kick in + self.make_form_pages() + + # Get page two + response = self.client.get('/admin/forms/', {'p': 99999}) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index.html') + + # Check that it got the last page + self.assertEqual(response.context['form_pages'].number, response.context['form_pages'].paginator.num_pages) + def test_cannot_see_forms_without_permission(self): # Login with as a user without permission to see forms self.client.login(username='eventeditor', password='password') @@ -121,20 +185,79 @@ class TestFormsBackend(TestCase): # Check that the user can see the form page self.assertTrue(self.form_page in response.context['form_pages']) + +class TestFormsSubmissions(TestCase): + fixtures = ['test.json'] + + def setUp(self): + self.client.login(username='siteeditor', password='password') + self.form_page = Page.objects.get(url_path='/home/contact-us/') + + def make_list_submissions(self): + """ + This makes 100 submissions to test pagination on the forms submissions page + """ + for i in range(100): + submission = FormSubmission( + page=self.form_page, + form_data=json.dumps({ + 'hello': 'world' + }) + ) + submission.save() + def test_list_submissions(self): response = self.client.get('/admin/forms/submissions/%d/' % self.form_page.id) # Check response self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html') self.assertEqual(len(response.context['data_rows']), 2) - def test_list_submissions_filtered(self): + def test_list_submissions_filtering(self): response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014' % self.form_page.id) # Check response self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html') self.assertEqual(len(response.context['data_rows']), 1) + def test_list_submissions_pagination(self): + self.make_list_submissions() + + response = self.client.get('/admin/forms/submissions/%d/' % self.form_page.id, {'p': 2}) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html') + + # Check that we got the correct page + self.assertEqual(response.context['submissions'].number, 2) + + def test_list_submissions_pagination_invalid(self): + self.make_list_submissions() + + response = self.client.get('/admin/forms/submissions/%d/' % self.form_page.id, {'p': 'Hello World!'}) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html') + + # Check that we got page one + self.assertEqual(response.context['submissions'].number, 1) + + def test_list_submissions_pagination_out_of_range(self): + self.make_list_submissions() + + response = self.client.get('/admin/forms/submissions/%d/' % self.form_page.id, {'p': 99999}) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailforms/index_submissions.html') + + # Check that we got the last page + self.assertEqual(response.context['submissions'].number, response.context['submissions'].paginator.num_pages) + def test_list_submissions_csv_export(self): response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014&action=CSV' % self.form_page.id) From 2b4b19a6414d2a20654baf540fb89df907622de0 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 4 Jun 2014 16:38:44 +0100 Subject: [PATCH 023/139] Use reversed URLs in wagtailforms tests --- wagtail/wagtailforms/tests.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py index 5145eae3f..24bace8c5 100644 --- a/wagtail/wagtailforms/tests.py +++ b/wagtail/wagtailforms/tests.py @@ -3,6 +3,7 @@ import json from django.test import TestCase from django.core import mail from django import forms +from django.core.urlresolvers import reverse from wagtail.wagtailcore.models import Page from wagtail.wagtailforms.models import FormSubmission @@ -122,7 +123,7 @@ class TestFormsIndex(TestCase): )) def test_forms_index(self): - response = self.client.get('/admin/forms/') + response = self.client.get(reverse('wagtailforms_index')) # Check response self.assertEqual(response.status_code, 200) @@ -133,7 +134,7 @@ class TestFormsIndex(TestCase): self.make_form_pages() # Get page two - response = self.client.get('/admin/forms/', {'p': 2}) + response = self.client.get(reverse('wagtailforms_index'), {'p': 2}) # Check response self.assertEqual(response.status_code, 200) @@ -147,7 +148,7 @@ class TestFormsIndex(TestCase): self.make_form_pages() # Get page two - response = self.client.get('/admin/forms/', {'p': 'Hello world!'}) + response = self.client.get(reverse('wagtailforms_index'), {'p': 'Hello world!'}) # Check response self.assertEqual(response.status_code, 200) @@ -161,7 +162,7 @@ class TestFormsIndex(TestCase): self.make_form_pages() # Get page two - response = self.client.get('/admin/forms/', {'p': 99999}) + response = self.client.get(reverse('wagtailforms_index'), {'p': 99999}) # Check response self.assertEqual(response.status_code, 200) @@ -174,13 +175,13 @@ class TestFormsIndex(TestCase): # Login with as a user without permission to see forms self.client.login(username='eventeditor', password='password') - response = self.client.get('/admin/forms/') + response = self.client.get(reverse('wagtailforms_index')) # Check that the user cannot see the form page self.assertFalse(self.form_page in response.context['form_pages']) def test_can_see_forms_with_permission(self): - response = self.client.get('/admin/forms/') + response = self.client.get(reverse('wagtailforms_index')) # Check that the user can see the form page self.assertTrue(self.form_page in response.context['form_pages']) @@ -207,7 +208,7 @@ class TestFormsSubmissions(TestCase): submission.save() def test_list_submissions(self): - response = self.client.get('/admin/forms/submissions/%d/' % self.form_page.id) + response = self.client.get(reverse('wagtailforms_list_submissions', args=(self.form_page.id, ))) # Check response self.assertEqual(response.status_code, 200) @@ -215,7 +216,7 @@ class TestFormsSubmissions(TestCase): self.assertEqual(len(response.context['data_rows']), 2) def test_list_submissions_filtering(self): - response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014' % self.form_page.id) + response = self.client.get(reverse('wagtailforms_list_submissions', args=(self.form_page.id, )), {'date_from': '01/01/2014'}) # Check response self.assertEqual(response.status_code, 200) @@ -225,7 +226,7 @@ class TestFormsSubmissions(TestCase): def test_list_submissions_pagination(self): self.make_list_submissions() - response = self.client.get('/admin/forms/submissions/%d/' % self.form_page.id, {'p': 2}) + response = self.client.get(reverse('wagtailforms_list_submissions', args=(self.form_page.id, )), {'p': 2}) # Check response self.assertEqual(response.status_code, 200) @@ -237,7 +238,7 @@ class TestFormsSubmissions(TestCase): def test_list_submissions_pagination_invalid(self): self.make_list_submissions() - response = self.client.get('/admin/forms/submissions/%d/' % self.form_page.id, {'p': 'Hello World!'}) + response = self.client.get(reverse('wagtailforms_list_submissions', args=(self.form_page.id, )), {'p': 'Hello World!'}) # Check response self.assertEqual(response.status_code, 200) @@ -249,7 +250,7 @@ class TestFormsSubmissions(TestCase): def test_list_submissions_pagination_out_of_range(self): self.make_list_submissions() - response = self.client.get('/admin/forms/submissions/%d/' % self.form_page.id, {'p': 99999}) + response = self.client.get(reverse('wagtailforms_list_submissions', args=(self.form_page.id, )), {'p': 99999}) # Check response self.assertEqual(response.status_code, 200) @@ -259,7 +260,7 @@ class TestFormsSubmissions(TestCase): self.assertEqual(response.context['submissions'].number, response.context['submissions'].paginator.num_pages) def test_list_submissions_csv_export(self): - response = self.client.get('/admin/forms/submissions/%d/?date_from=01%%2F01%%2F2014&action=CSV' % self.form_page.id) + response = self.client.get(reverse('wagtailforms_list_submissions', args=(self.form_page.id, )), {'date_from': '01/01/2014', 'action': 'CSV'}) # Check response self.assertEqual(response.status_code, 200) From 1683287ee7b5c0780024ecb055513762f8828c34 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 11:15:12 +0100 Subject: [PATCH 024/139] Change wagtailforms SelectDateForm to use DateTimeFields This is because DateFields are not timezone aware which causes warnings to be raised by django. The widgets are still set to "DateInput" so the look and behaviour should not change --- wagtail/wagtailforms/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailforms/forms.py b/wagtail/wagtailforms/forms.py index aaf16eb79..4f874133a 100644 --- a/wagtail/wagtailforms/forms.py +++ b/wagtail/wagtailforms/forms.py @@ -100,11 +100,11 @@ class FormBuilder(object): class SelectDateForm(django.forms.Form): - date_from = django.forms.DateField( + date_from = django.forms.DateTimeField( required=False, widget=django.forms.DateInput(attrs={'placeholder': 'Date from'}) ) - date_to = django.forms.DateField( + date_to = django.forms.DateTimeField( required=False, widget=django.forms.DateInput(attrs={'placeholder': 'Date to'}) ) From 004b90d9cf54532a0f1d30f25001872c790ae814 Mon Sep 17 00:00:00 2001 From: Ben Margolis Date: Fri, 6 Jun 2014 10:03:32 -0700 Subject: [PATCH 025/139] send init_new_page signal when creating a new page in the admin --- wagtail/wagtailadmin/signals.py | 3 +++ wagtail/wagtailadmin/views/pages.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 wagtail/wagtailadmin/signals.py diff --git a/wagtail/wagtailadmin/signals.py b/wagtail/wagtailadmin/signals.py new file mode 100644 index 000000000..53a135584 --- /dev/null +++ b/wagtail/wagtailadmin/signals.py @@ -0,0 +1,3 @@ +from django.dispatch import Signal + +init_new_page = Signal(providing_args=["page","parent"]) \ No newline at end of file diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 69e6b7f4f..f86306ae8 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -10,7 +10,7 @@ from django.views.decorators.vary import vary_on_headers from wagtail.wagtailadmin.edit_handlers import TabbedInterface, ObjectList from wagtail.wagtailadmin.forms import SearchForm -from wagtail.wagtailadmin import tasks, hooks +from wagtail.wagtailadmin import tasks, hooks, signals from wagtail.wagtailcore.models import Page, PageRevision, get_page_types @@ -124,6 +124,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ # return redirect('wagtailadmin_pages_select_type') page = page_class(owner=request.user) + signals.init_new_page.send(sender=create,page=page,parent=parent_page) edit_handler_class = get_page_edit_handler(page_class) form_class = edit_handler_class.get_form_class(page_class) From de678ac8ba5312952ec90a4e89ef57cbf216d196 Mon Sep 17 00:00:00 2001 From: Robert Clark Date: Fri, 6 Jun 2014 14:40:05 -0400 Subject: [PATCH 026/139] add next and previous published siblings to Page model --- wagtail/tests/fixtures/test.json | 30 ++++++++++++++++++- wagtail/wagtailcore/models.py | 15 ++++++++++ .../wagtailcore/tests/test_page_queryset.py | 18 +++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index 11b82d9f1..8e316ffd8 100644 --- a/wagtail/tests/fixtures/test.json +++ b/wagtail/tests/fixtures/test.json @@ -39,7 +39,7 @@ "model": "wagtailcore.page", "fields": { "title": "Events", - "numchild": 3, + "numchild": 4, "show_in_menus": true, "live": true, "depth": 3, @@ -186,6 +186,34 @@ } }, +{ + "pk": 9, + "model": "wagtailcore.page", + "fields": { + "title": "Ameristralia Day", + "numchild": 0, + "show_in_menus": true, + "live": true, + "depth": 4, + "content_type": ["tests", "eventpage"], + "path": "0001000100010004", + "url_path": "/home/events/final-event/", + "slug": "final-event", + "owner": 3 + } +}, +{ + "pk": 9, + "model": "tests.eventpage", + "fields": { + "date_from": "2015-04-22", + "audience": "public", + "location": "Ameristralia", + "body": "

come celebrate the independence of Ameristralia

", + "cost": "Free" + } +}, + { "pk": 1, "model": "tests.formfield", diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 30ffc8a77..3b90e9c0e 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -649,6 +649,21 @@ class Page(MP_Node, ClusterableModel, Indexed): def get_siblings(self, inclusive=True): return Page.objects.sibling_of(self, inclusive) + def get_next_published_sibling(self): + next_sibling = self.get_next_sibling() + + while next_sibling and not next_sibling.live: + next_sibling = next_sibling.get_next_sibling() + + return next_sibling + + def get_prev_published_sibling(self): + prev_sibling = self.get_prev_sibling() + + while prev_sibling and not prev_sibling.live: + prev_sibling = prev_sibling.get_prev_sibling() + + return prev_sibling def get_navigation_menu_items(): # Get all pages that appear in the navigation menu: ones which have children, diff --git a/wagtail/wagtailcore/tests/test_page_queryset.py b/wagtail/wagtailcore/tests/test_page_queryset.py index 06f2c3e21..3db3bd5f8 100644 --- a/wagtail/wagtailcore/tests/test_page_queryset.py +++ b/wagtail/wagtailcore/tests/test_page_queryset.py @@ -254,3 +254,21 @@ class TestPageQuerySet(TestCase): # Check that the homepage is in the results homepage = Page.objects.get(url_path='/home/') self.assertTrue(pages.filter(id=homepage.id).exists()) + + def test_published_next(self): + events_index = Page.objects.get(url_path='/home/events/') + current_page = Page.objects.descendant_of(events_index).live().first() + + # All pages must be live + while current_page: + self.assertTrue(current_page.live) + current_page = current_page.get_next_published_sibling() + + def test_published_prev(self): + events_index = Page.objects.get(url_path='/home/events/') + current_page = Page.objects.descendant_of(events_index).live().last() + + # All pages must be live + while current_page: + self.assertTrue(current_page.live) + current_page = current_page.get_prev_published_sibling() From 9b17154cdf5c3770c7f5c44140952c21f759db27 Mon Sep 17 00:00:00 2001 From: Jeffrey Hearn Date: Thu, 5 Jun 2014 21:46:24 -0400 Subject: [PATCH 027/139] Added wagtail and django settings doc more settings stuffs settings stuff settings stuff so many things omg is it really two AM More docs --- docs/building_your_site/djangodevelopers.rst | 17 + .../building_your_site/frontenddevelopers.rst | 3 +- docs/editing_api.rst | 4 +- .../new_pages/inserting_videos.rst | 5 +- docs/form_builder.rst | 3 + docs/images/screen_wagtail_redirects.png | Bin 0 -> 19260 bytes docs/index.rst | 1 + docs/settings.rst | 615 ++++++++++++++++++ docs/wagtail_search.rst | 3 + 9 files changed, 647 insertions(+), 4 deletions(-) create mode 100644 docs/images/screen_wagtail_redirects.png create mode 100644 docs/settings.rst diff --git a/docs/building_your_site/djangodevelopers.rst b/docs/building_your_site/djangodevelopers.rst index b4899d70f..46e24b08f 100644 --- a/docs/building_your_site/djangodevelopers.rst +++ b/docs/building_your_site/djangodevelopers.rst @@ -1,9 +1,15 @@ For Django developers ===================== +.. contents:: Contents + :local: + .. note:: This documentation is currently being written. +Overview +~~~~~~~~ + Wagtail requires a little careful setup to define the types of content that you want to present through your website. The basic unit of content in Wagtail is the ``Page``, and all of your page-level content will inherit basic webpage-related properties from it. But for the most part, you will be defining content yourself, through the construction of Django models using Wagtail's ``Page`` as a base. Wagtail organizes content created from your models in a tree, which can have any structure and combination of model objects in it. Wagtail doesn't prescribe ways to organize and interrelate your content, but here we've sketched out some strategies for organizing your models. @@ -269,6 +275,7 @@ not_type(self, model): return self.get_query_set().not_type(model) +.. _wagtail_site_admin: Site ~~~~ @@ -278,3 +285,13 @@ Django's built-in admin interface provides the way to map a "site" (hostname or Access this by going to ``/django-admin/`` and then "Home › Wagtailcore › Sites." To try out a development site, add a single site with the hostname ``localhost`` at port ``8000`` and map it to one of the pieces of content you have created. Wagtail's developers plan to move the site settings into the Wagtail admin interface. + + +.. _redirects: + +Redirects +~~~~~~~~~ + +Wagtail provides a simple interface for creating arbitrary redirects to and from any URL. + +.. image:: ../images/screen_wagtail_redirects.png diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index e5f583397..83dbb9e6c 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -1,7 +1,8 @@ For Front End developers ======================== -.. contents:: +.. contents:: Contents + :local: ======================== Overview diff --git a/docs/editing_api.rst b/docs/editing_api.rst index c06370026..60698a74a 100644 --- a/docs/editing_api.rst +++ b/docs/editing_api.rst @@ -346,9 +346,9 @@ The ``RelatedLink`` class is a vanilla Django abstract model. The ``BookPageRela For another example of using model clusters, see :ref:`tagging` -For more on ``django-modelcluster``, visit `the django-modelcluster github project page`_ ). +For more on ``django-modelcluster``, visit `the django-modelcluster github project page`_. -.. _the django-modelcluster github page: https://github.com/torchbox/django-modelcluster +.. _the django-modelcluster github project page: https://github.com/torchbox/django-modelcluster .. _extending_wysiwyg: diff --git a/docs/editor_manual/new_pages/inserting_videos.rst b/docs/editor_manual/new_pages/inserting_videos.rst index 038f3e427..1b32c0d1c 100644 --- a/docs/editor_manual/new_pages/inserting_videos.rst +++ b/docs/editor_manual/new_pages/inserting_videos.rst @@ -1,3 +1,6 @@ + +.. _inserting_videos: + Inserting videos into body content ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -9,4 +12,4 @@ As well as inserting videos into a carousel, Wagtail's rich text fields allow yo .. image:: ../../images/screen21_video_in_editor.png -* A placeholder with the name of the video and a screenshot will be inserted into the text area. Clicking the X in the top corner will remove the video. \ No newline at end of file +* A placeholder with the name of the video and a screenshot will be inserted into the text area. Clicking the X in the top corner will remove the video. diff --git a/docs/form_builder.rst b/docs/form_builder.rst index 9aa220e19..a141cc376 100644 --- a/docs/form_builder.rst +++ b/docs/form_builder.rst @@ -1,3 +1,6 @@ + +.. _form_builder: + Form builder ============ diff --git a/docs/images/screen_wagtail_redirects.png b/docs/images/screen_wagtail_redirects.png new file mode 100644 index 0000000000000000000000000000000000000000..516bdd046551da4edeed93935626850ed0899ca8 GIT binary patch literal 19260 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 Date: Mon, 9 Jun 2014 10:04:33 -0400 Subject: [PATCH 028/139] made published siblings a single query --- wagtail/wagtailcore/models.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 3b90e9c0e..1864f5c88 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -650,20 +650,10 @@ class Page(MP_Node, ClusterableModel, Indexed): return Page.objects.sibling_of(self, inclusive) def get_next_published_sibling(self): - next_sibling = self.get_next_sibling() - - while next_sibling and not next_sibling.live: - next_sibling = next_sibling.get_next_sibling() - - return next_sibling + return self.get_siblings().live().filter(path__gt=self.path).order_by('path').first() def get_prev_published_sibling(self): - prev_sibling = self.get_prev_sibling() - - while prev_sibling and not prev_sibling.live: - prev_sibling = prev_sibling.get_prev_sibling() - - return prev_sibling + return self.get_siblings().live().filter(path__lt=self.path).order_by('-path').first() def get_navigation_menu_items(): # Get all pages that appear in the navigation menu: ones which have children, From 85793d3dbb3f5687efe9659ef7d0ced3c0f7e832 Mon Sep 17 00:00:00 2001 From: Jeffrey Hearn Date: Mon, 9 Jun 2014 11:47:12 -0400 Subject: [PATCH 029/139] Settings doc changes based on feedback --- docs/settings.rst | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index c6e9a4fc0..018af5d01 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -25,7 +25,7 @@ From your app directory, you can safely remove ``admin.py`` and ``views.py``, si .. _Django Settings: https://docs.djangoproject.com/en/dev/topics/settings/ -.. _Django URL Dispatcher:https://docs.djangoproject.com/en/dev/topics/http/urls/ +.. _Django URL Dispatcher: https://docs.djangoproject.com/en/dev/topics/http/urls/ What follows is a settings reference which skips many boilerplate Django settings. If you just want to get your Wagtail install up quickly without fussing with settings at the moment, see :ref:`complete_example_config`. @@ -161,7 +161,7 @@ Authentication .. code-block:: python - LOGIN_URL = 'wagtail.wagtailadmin.views.account.login' + LOGIN_URL = 'wagtailadmin_login' LOGIN_REDIRECT_URL = 'wagtailadmin_home' These settings variables set the Django authentication system to redirect to the Wagtail admin login. If you plan to use the Django authentication module to log in non-privileged users, you should set these variables to your own login views. See `Django User Authentication`_. @@ -243,7 +243,7 @@ Email Notifications WAGTAILADMIN_NOTIFICATION_FROM_EMAIL = 'wagtail@myhost.io' -Wagtail sends email notifications when content is submitted for moderation, and when the content is accepted or rejected. This setting lets you pick which email address these automatic notifications will come from. +Wagtail sends email notifications when content is submitted for moderation, and when the content is accepted or rejected. This setting lets you pick which email address these automatic notifications will come from. If omitted, Django will fall back to using the ``DEFAULT_FROM_EMAIL`` variable if set, and ``webmaster@localhost`` if not. Other Django Settings Used by Wagtail @@ -271,18 +271,6 @@ For information on what these settings do, see `Django Settings`_. .. _Django Settings: https://docs.djangoproject.com/en/dev/ref/settings/ -URL Configuration (urls.py) -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There's one more setting that must be included in ``settings.py``: - -.. code-block:: python - - ROOT_URLCONF = 'myproject.urls' - -This setting bootstraps your project's URL patterns and views into the Django server. The root urlconf will include URL patterns for Wagtail and any URLs for your own app functionality (developed external to Wagtail). - - Search Signal Handlers ---------------------- @@ -292,7 +280,7 @@ Search Signal Handlers wagtailsearch_register_signal_handlers() -This loads Wagtail's search signal handlers, which need to be loaded very early in the Django life cycle. While not technically a urlconf, this is a convenient place to load them. +This loads Wagtail's search signal handlers, which need to be loaded very early in the Django life cycle. While not technically a urlconf, this is a convenient place to load them. Calling this function registers signal handlers to watch for when indexed models get saved or deleted. This allows wagtailsearch to update ElasticSearch automatically. URL Patterns @@ -437,7 +425,7 @@ settings.py ) # Make this unique, and don't share it with anybody. - SECRET_KEY = 'wq21wtjo3@d_qfjvd-#td!%7gfy2updj2z+nev^k$iy%=m4_tr' + SECRET_KEY = 'change-me' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( @@ -505,7 +493,7 @@ settings.py ) # Auth settings - LOGIN_URL = 'django.contrib.auth.views.login' + LOGIN_URL = 'wagtailadmin_login' LOGIN_REDIRECT_URL = 'wagtailadmin_home' # A sample logging configuration. The only tangible logging From e6008ccaee626d44138c8d58a7ec667af1a5b84b Mon Sep 17 00:00:00 2001 From: Ben Margolis Date: Mon, 9 Jun 2014 11:24:19 -0700 Subject: [PATCH 030/139] fix kwarg spacing --- wagtail/wagtailadmin/views/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index f86306ae8..5f0da8d00 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -124,7 +124,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ # return redirect('wagtailadmin_pages_select_type') page = page_class(owner=request.user) - signals.init_new_page.send(sender=create,page=page,parent=parent_page) + signals.init_new_page.send(sender=create, page=page, parent=parent_page) edit_handler_class = get_page_edit_handler(page_class) form_class = edit_handler_class.get_form_class(page_class) From a3b4bcb558df5ec28c343e9cb2eca04c539a7635 Mon Sep 17 00:00:00 2001 From: Ben Margolis Date: Tue, 10 Jun 2014 04:15:35 -0700 Subject: [PATCH 031/139] only send signal outside if not POSTing new page --- wagtail/wagtailadmin/views/pages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 5f0da8d00..b7a85dced 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -124,7 +124,6 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ # return redirect('wagtailadmin_pages_select_type') page = page_class(owner=request.user) - signals.init_new_page.send(sender=create, page=page, parent=parent_page) edit_handler_class = get_page_edit_handler(page_class) form_class = edit_handler_class.get_form_class(page_class) @@ -173,6 +172,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ messages.error(request, _("The page could not be created due to errors.")) edit_handler = edit_handler_class(instance=page, form=form) else: + signals.init_new_page.send(sender=create, page=page, parent=parent_page) form = form_class(instance=page) edit_handler = edit_handler_class(instance=page, form=form) From 4788144c86b1dbdc5e230add8407652acefd0316 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 17 Jun 2014 15:09:10 +0100 Subject: [PATCH 032/139] first bash at customisable tabs like in PR #210 --- wagtail/wagtailadmin/edit_handlers.py | 3 +- .../edit_handlers/tabbed_interface.html | 4 +- wagtail/wagtailadmin/views/pages.py | 40 +++++++++++++++++-- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index 245f7ad60..ecf692025 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -447,10 +447,11 @@ class BaseObjectList(BaseCompositeEditHandler): template = "wagtailadmin/edit_handlers/object_list.html" -def ObjectList(children, heading=""): +def ObjectList(children, heading="", classes=None): return type('_ObjectList', (BaseObjectList,), { 'children': children, 'heading': heading, + 'classes': classes }) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/tabbed_interface.html b/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/tabbed_interface.html index 5bbccb03b..813e40f53 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/tabbed_interface.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/tabbed_interface.html @@ -1,12 +1,12 @@

{% for child in self.children %} -
+
{{ child.render_as_object }}
{% endfor %} diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 69e6b7f4f..dedf3bff3 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -155,6 +155,9 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ page.save_revision(user=request.user, submitted_for_moderation=is_submitting) if is_publishing: + message = mark_safe(render_to_string(self.template, { + 'self': self + })) messages.success(request, _("Page '{0}' published.").format(page.title)) elif is_submitting: messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title)) @@ -527,12 +530,41 @@ def set_page_position(request, page_to_move_id): PAGE_EDIT_HANDLERS = {} +def get_default_panels(page_class): + panels = [] + + try: + panels.append(ObjectList(page_class.content_panels, heading='Content')) + except AttributeError: + pass + + try: + panels.append(ObjectList(page_class.promote_panels, heading='Promote')) + except AttributeError: + pass + + try: + panels.append(ObjectList(page_class.settings_panels, heading='Settings', classes='settings')) + except AttributeError: + pass + + return panels + + +def get_panels(page_class): + try: + return page_class.panels + except AttributeError: + return get_default_panels(page_class) + + +def set_panels(page_class, panels): + page_class.panels = panels + + def get_page_edit_handler(page_class): if page_class not in PAGE_EDIT_HANDLERS: - PAGE_EDIT_HANDLERS[page_class] = TabbedInterface([ - ObjectList(page_class.content_panels, heading='Content'), - ObjectList(page_class.promote_panels, heading='Promote') - ]) + PAGE_EDIT_HANDLERS[page_class] = TabbedInterface(get_panels(page_class)) return PAGE_EDIT_HANDLERS[page_class] From 9efb6a38d75269caaeae71809c3c8b6409deadee Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 17 Jun 2014 16:37:22 +0100 Subject: [PATCH 033/139] styling for scenario where tabs run onto multiple lines --- wagtail/wagtailadmin/edit_handlers.py | 2 +- .../wagtailadmin/scss/components/icons.scss | 1 - .../wagtailadmin/scss/components/tabs.scss | 49 ++++++++++++++----- wagtail/wagtailadmin/views/pages.py | 20 ++++---- 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index ecf692025..d3be76161 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -447,7 +447,7 @@ class BaseObjectList(BaseCompositeEditHandler): template = "wagtailadmin/edit_handlers/object_list.html" -def ObjectList(children, heading="", classes=None): +def ObjectList(children, heading="", classes=""): return type('_ObjectList', (BaseObjectList,), { 'children': children, 'heading': heading, diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss index 4de3aa131..9411cb1a8 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss @@ -96,7 +96,6 @@ .icon-unlocked:before { content: "p"; } - .icon-doc-full-inverse:before { content: "r"; } diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/tabs.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/tabs.scss index b0dec687d..c008ed15e 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/tabs.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/tabs.scss @@ -1,13 +1,15 @@ .tab-nav{ @include clearfix(); padding:0; + background:$color-grey-4; li{ list-style-type:none; - width:48%; + width:50%; float:left; padding:0; position:relative; + margin-bottom:-1px; &:before,&:after{ display:none; @@ -18,13 +20,12 @@ @include box-shadow(inset 0px -2px 3px 0 rgba(0,0,0,0.1)); background-color:$color-grey-4; outline:none; - line-height:3em; text-transform:uppercase; font-weight:700; font-size:1.2em; text-decoration:none; display:block; - padding:0 20px; + padding:0.7em $mobile-nice-padding; color:$color-grey-2; border-top:0.3em solid $color-grey-4; border-bottom:1px solid transparent; @@ -57,6 +58,20 @@ } } + li.settings a{ + padding-left:30px; + padding-right:30px; + + &:before{ + font-family:wagtail; + vertical-align:middle; + text-transform:none; + content:"w"; + margin-right:0.5em; + font-size:1.2em; + } + } + li.active a{ @include box-shadow(none); color:$color-grey-1; @@ -66,7 +81,6 @@ /* For cases where tab-nav should merge with header */ &.merged{ - background-color:$color-header-bg; margin-top:0; } } @@ -82,14 +96,27 @@ } @media screen and (min-width: $breakpoint-mobile){ - .tab-nav li{ - width:auto; - padding:0; - margin-left:0.7em; - } + .tab-nav{ + /* For cases where tab-nav should merge with header */ + &.merged{ + background-color:$color-header-bg; + } - .tab-nav a{ - padding:0 50px; + li{ + width:auto; + padding:0; + margin-left:0.7em; + + &.tab-right{ + float:right; + margin-right:0.7em; + } + } + + a{ + padding-left:$desktop-nice-padding - 10; + padding-right:$desktop-nice-padding - 10; + } } .modal-content .tab-nav li{ diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index dedf3bff3..086c61f0f 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -531,40 +531,40 @@ PAGE_EDIT_HANDLERS = {} def get_default_panels(page_class): - panels = [] + handlers = [] try: - panels.append(ObjectList(page_class.content_panels, heading='Content')) + handlers.append(ObjectList(page_class.content_panels, heading='Content')) except AttributeError: pass try: - panels.append(ObjectList(page_class.promote_panels, heading='Promote')) + handlers.append(ObjectList(page_class.promote_panels, heading='Promote')) except AttributeError: pass try: - panels.append(ObjectList(page_class.settings_panels, heading='Settings', classes='settings')) + handlers.append(ObjectList(page_class.settings_panels, heading='Settings', classes='tab-right settings')) except AttributeError: pass - return panels + return handlers -def get_panels(page_class): +def get_which_page_edit_handler(page_class): try: - return page_class.panels + return page_class.handlers except AttributeError: return get_default_panels(page_class) -def set_panels(page_class, panels): - page_class.panels = panels +def set_page_edit_handler(page_class, handlers): + page_class.handlers = handlers def get_page_edit_handler(page_class): if page_class not in PAGE_EDIT_HANDLERS: - PAGE_EDIT_HANDLERS[page_class] = TabbedInterface(get_panels(page_class)) + PAGE_EDIT_HANDLERS[page_class] = TabbedInterface(get_which_page_edit_handler(page_class)) return PAGE_EDIT_HANDLERS[page_class] From b95d6a0fab18a17a59dc981f7c6213e083c07d6d Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 17 Jun 2014 16:58:12 +0100 Subject: [PATCH 034/139] removed partial work from other branch --- wagtail/wagtailadmin/views/pages.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 086c61f0f..15aa81808 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -155,9 +155,6 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ page.save_revision(user=request.user, submitted_for_moderation=is_submitting) if is_publishing: - message = mark_safe(render_to_string(self.template, { - 'self': self - })) messages.success(request, _("Page '{0}' published.").format(page.title)) elif is_submitting: messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title)) From fb9a876e92c0cbd53a3e30367efcbc05e35b0cdc Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 17 Jun 2014 17:20:22 +0100 Subject: [PATCH 035/139] moved set_page_edit_hanlder to edit_handlders.py. Still not happy it's invoked the right way though --- wagtail/wagtailadmin/edit_handlers.py | 4 ++++ wagtail/wagtailadmin/views/pages.py | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index d3be76161..b7090ff22 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -261,6 +261,10 @@ def extract_panel_definitions_from_model_class(model, exclude=None): return panels +def set_page_edit_handler(page_class, handlers): + page_class.handlers = handlers + + class EditHandler(object): """ Abstract class providing sensible default behaviours for objects implementing diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 15aa81808..67d0e7525 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -555,10 +555,6 @@ def get_which_page_edit_handler(page_class): return get_default_panels(page_class) -def set_page_edit_handler(page_class, handlers): - page_class.handlers = handlers - - def get_page_edit_handler(page_class): if page_class not in PAGE_EDIT_HANDLERS: PAGE_EDIT_HANDLERS[page_class] = TabbedInterface(get_which_page_edit_handler(page_class)) From 553e7062b66bbfaea9d7ba3be5a0d4640269a451 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 17 Jun 2014 17:37:28 +0100 Subject: [PATCH 036/139] added FieldRowPanel from @kaedroho 5dc7a220f8799a7c6871b119724f7e07cb54f4fb --- wagtail/wagtailadmin/edit_handlers.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index 245f7ad60..fb5f4b848 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -454,6 +454,17 @@ def ObjectList(children, heading=""): }) +class BaseFieldRowPanel(BaseCompositeEditHandler): + template = "wagtailadmin/edit_handlers/field_row_panel.html" + + +def FieldRowPanel(children, classname=None): + return type('_FieldRowPanel', (BaseFieldRowPanel,), { + 'children': children, + 'classname': classname, + }) + + class BaseMultiFieldPanel(BaseCompositeEditHandler): template = "wagtailadmin/edit_handlers/multi_field_panel.html" From 68da7f0c2c44d2b8661e6849cde56e29141ef98f Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 17 Jun 2014 17:39:48 +0100 Subject: [PATCH 037/139] corrected commit ref to kaedroho@5dc7a220f8799a7c6871b119724f7e07cb54f4fb --- .../wagtailadmin/edit_handlers/field_row_panel.html | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/field_row_panel.html diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/field_row_panel.html b/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/field_row_panel.html new file mode 100644 index 000000000..e7371f0b9 --- /dev/null +++ b/wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/field_row_panel.html @@ -0,0 +1,7 @@ +
    + {% for child in self.children %} +
  • + {{ child.render_as_field }} +
  • + {% endfor %} +
From 733294cafa45a191478b4dccce4d7f5a22ee98de Mon Sep 17 00:00:00 2001 From: Robert Clark Date: Wed, 18 Jun 2014 11:04:39 -0400 Subject: [PATCH 038/139] return a queryset instead of the first sibling --- wagtail/wagtailcore/models.py | 8 ++++---- wagtail/wagtailcore/tests/test_page_queryset.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index e2b728005..9dcc65f6f 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -645,11 +645,11 @@ class Page(MP_Node, ClusterableModel, Indexed): def get_siblings(self, inclusive=True): return Page.objects.sibling_of(self, inclusive) - def get_next_published_sibling(self): - return self.get_siblings().live().filter(path__gt=self.path).order_by('path').first() + def get_next_siblings(self): + return self.get_siblings().filter(path__gt=self.path).order_by('path') - def get_prev_published_sibling(self): - return self.get_siblings().live().filter(path__lt=self.path).order_by('-path').first() + def get_prev_siblings(self): + return self.get_siblings().filter(path__lt=self.path).order_by('-path') def get_navigation_menu_items(): # Get all pages that appear in the navigation menu: ones which have children, diff --git a/wagtail/wagtailcore/tests/test_page_queryset.py b/wagtail/wagtailcore/tests/test_page_queryset.py index 3db3bd5f8..020d98d15 100644 --- a/wagtail/wagtailcore/tests/test_page_queryset.py +++ b/wagtail/wagtailcore/tests/test_page_queryset.py @@ -262,7 +262,7 @@ class TestPageQuerySet(TestCase): # All pages must be live while current_page: self.assertTrue(current_page.live) - current_page = current_page.get_next_published_sibling() + current_page = current_page.get_next_siblings().live().first() def test_published_prev(self): events_index = Page.objects.get(url_path='/home/events/') @@ -271,4 +271,4 @@ class TestPageQuerySet(TestCase): # All pages must be live while current_page: self.assertTrue(current_page.live) - current_page = current_page.get_prev_published_sibling() + current_page = current_page.get_prev_siblings().live().first() From 9a5954607d4ddc232991e493d61484e3fb2fbdcb Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 19 Jun 2014 10:28:56 +0100 Subject: [PATCH 039/139] Renamed pageurl tags to wagtailcore_tags --- wagtail/wagtailcore/templatetags/pageurl.py | 26 ++++--------------- .../templatetags/wagtailcore_tags.py | 25 ++++++++++++++++++ 2 files changed, 30 insertions(+), 21 deletions(-) create mode 100644 wagtail/wagtailcore/templatetags/wagtailcore_tags.py diff --git a/wagtail/wagtailcore/templatetags/pageurl.py b/wagtail/wagtailcore/templatetags/pageurl.py index d3488d9b4..fe64a302f 100644 --- a/wagtail/wagtailcore/templatetags/pageurl.py +++ b/wagtail/wagtailcore/templatetags/pageurl.py @@ -1,24 +1,8 @@ -from django import template +import warnings -from wagtail.wagtailcore.models import Page - -register = template.Library() +warnings.warn( + "The pageurl tags has been renamed. " + "Use wagtailcore_tags instead.", DeprecationWarning) -@register.simple_tag(takes_context=True) -def pageurl(context, page): - """ - Outputs a page's URL as relative (/foo/bar/) if it's within the same site as the - current page, or absolute (http://example.com/foo/bar/) if not. - """ - return page.relative_url(context['request'].site) - -@register.simple_tag(takes_context=True) -def slugurl(context, slug): - """Returns the URL for the page that has the given slug.""" - page = Page.objects.filter(slug=slug).first() - - if page: - return page.relative_url(context['request'].site) - else: - return None +from wagtail.wagtailcore.templatetags.wagtailcore_tags import register diff --git a/wagtail/wagtailcore/templatetags/wagtailcore_tags.py b/wagtail/wagtailcore/templatetags/wagtailcore_tags.py new file mode 100644 index 000000000..d91c05742 --- /dev/null +++ b/wagtail/wagtailcore/templatetags/wagtailcore_tags.py @@ -0,0 +1,25 @@ +from django import template + +from wagtail.wagtailcore.models import Page + +register = template.Library() + + +@register.simple_tag(takes_context=True) +def pageurl(context, page): + """ + Outputs a page's URL as relative (/foo/bar/) if it's within the same site as the + current page, or absolute (http://example.com/foo/bar/) if not. + """ + return page.relative_url(context['request'].site) + + +@register.simple_tag(takes_context=True) +def slugurl(context, slug): + """Returns the URL for the page that has the given slug.""" + page = Page.objects.filter(slug=slug).first() + + if page: + return page.relative_url(context['request'].site) + else: + return None From 122abc9a8c7a332b623b86e7a5c6d9bdb28ac4b4 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 19 Jun 2014 10:30:09 +0100 Subject: [PATCH 040/139] renamed rich_text tags to wagtailrichtext_tags --- wagtail/wagtailcore/templatetags/rich_text.py | 13 +++++-------- .../templatetags/wagtailrichtext_tags.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 wagtail/wagtailcore/templatetags/wagtailrichtext_tags.py diff --git a/wagtail/wagtailcore/templatetags/rich_text.py b/wagtail/wagtailcore/templatetags/rich_text.py index d3bc64fdd..d7d162645 100644 --- a/wagtail/wagtailcore/templatetags/rich_text.py +++ b/wagtail/wagtailcore/templatetags/rich_text.py @@ -1,11 +1,8 @@ -from django import template -from django.utils.safestring import mark_safe +import warnings -from wagtail.wagtailcore.rich_text import expand_db_html - -register = template.Library() +warnings.warn( + "The rich_text tags has been renamed. " + "Use wagtailrichtext_tags instead.", DeprecationWarning) -@register.filter -def richtext(value): - return mark_safe('
' + expand_db_html(value) + '
') +from wagtail.wagtailcore.templatetags.wagtailrichtext_tags import register diff --git a/wagtail/wagtailcore/templatetags/wagtailrichtext_tags.py b/wagtail/wagtailcore/templatetags/wagtailrichtext_tags.py new file mode 100644 index 000000000..d3bc64fdd --- /dev/null +++ b/wagtail/wagtailcore/templatetags/wagtailrichtext_tags.py @@ -0,0 +1,11 @@ +from django import template +from django.utils.safestring import mark_safe + +from wagtail.wagtailcore.rich_text import expand_db_html + +register = template.Library() + + +@register.filter +def richtext(value): + return mark_safe('
' + expand_db_html(value) + '
') From 65ec6d77e8effdf54aefcb6d5b42f65ba2089436 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 19 Jun 2014 10:36:03 +0100 Subject: [PATCH 041/139] Merged richtext tags into wagtailcore_tags --- wagtail/wagtailcore/templatetags/rich_text.py | 4 ++-- wagtail/wagtailcore/templatetags/wagtailcore_tags.py | 7 +++++++ .../wagtailcore/templatetags/wagtailrichtext_tags.py | 11 ----------- 3 files changed, 9 insertions(+), 13 deletions(-) delete mode 100644 wagtail/wagtailcore/templatetags/wagtailrichtext_tags.py diff --git a/wagtail/wagtailcore/templatetags/rich_text.py b/wagtail/wagtailcore/templatetags/rich_text.py index d7d162645..94c2d176c 100644 --- a/wagtail/wagtailcore/templatetags/rich_text.py +++ b/wagtail/wagtailcore/templatetags/rich_text.py @@ -2,7 +2,7 @@ import warnings warnings.warn( "The rich_text tags has been renamed. " - "Use wagtailrichtext_tags instead.", DeprecationWarning) + "Use wagtailcore_tags instead.", DeprecationWarning) -from wagtail.wagtailcore.templatetags.wagtailrichtext_tags import register +from wagtail.wagtailcore.templatetags.wagtailcore_tags import register diff --git a/wagtail/wagtailcore/templatetags/wagtailcore_tags.py b/wagtail/wagtailcore/templatetags/wagtailcore_tags.py index d91c05742..5e137c7a2 100644 --- a/wagtail/wagtailcore/templatetags/wagtailcore_tags.py +++ b/wagtail/wagtailcore/templatetags/wagtailcore_tags.py @@ -1,6 +1,8 @@ from django import template +from django.utils.safestring import mark_safe from wagtail.wagtailcore.models import Page +from wagtail.wagtailcore.rich_text import expand_db_html register = template.Library() @@ -23,3 +25,8 @@ def slugurl(context, slug): return page.relative_url(context['request'].site) else: return None + + +@register.filter +def richtext(value): + return mark_safe('
' + expand_db_html(value) + '
') diff --git a/wagtail/wagtailcore/templatetags/wagtailrichtext_tags.py b/wagtail/wagtailcore/templatetags/wagtailrichtext_tags.py deleted file mode 100644 index d3bc64fdd..000000000 --- a/wagtail/wagtailcore/templatetags/wagtailrichtext_tags.py +++ /dev/null @@ -1,11 +0,0 @@ -from django import template -from django.utils.safestring import mark_safe - -from wagtail.wagtailcore.rich_text import expand_db_html - -register = template.Library() - - -@register.filter -def richtext(value): - return mark_safe('
' + expand_db_html(value) + '
') From b3e79519329dc9f2b3ebd94405b3e84024dc8353 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 19 Jun 2014 10:39:50 +0100 Subject: [PATCH 042/139] Moved embed_filters to wagtailembeds_tags --- .../templatetags/embed_filters.py | 26 ++++------------- .../templatetags/wagtailembeds_tags.py | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 wagtail/wagtailembeds/templatetags/wagtailembeds_tags.py diff --git a/wagtail/wagtailembeds/templatetags/embed_filters.py b/wagtail/wagtailembeds/templatetags/embed_filters.py index d916c0ffb..25326fec6 100644 --- a/wagtail/wagtailembeds/templatetags/embed_filters.py +++ b/wagtail/wagtailembeds/templatetags/embed_filters.py @@ -1,24 +1,8 @@ -from django import template -from django.utils.safestring import mark_safe +import warnings -from wagtail.wagtailembeds import get_embed +warnings.warn( + "The embed_filters tags has been renamed. " + "Use wagtailembeds_tags instead.", DeprecationWarning) -register = template.Library() - - -@register.filter -def embed(url, max_width=None): - embed = get_embed(url, max_width=max_width) - try: - if embed is not None: - return mark_safe(embed.html) - else: - return '' - except: - return '' - - -@register.filter -def embedly(url, max_width=None): - return embed(url, max_width) +from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import register diff --git a/wagtail/wagtailembeds/templatetags/wagtailembeds_tags.py b/wagtail/wagtailembeds/templatetags/wagtailembeds_tags.py new file mode 100644 index 000000000..aad751c6a --- /dev/null +++ b/wagtail/wagtailembeds/templatetags/wagtailembeds_tags.py @@ -0,0 +1,28 @@ +from django import template +from django.utils.safestring import mark_safe + +from wagtail.wagtailembeds import get_embed + + +register = template.Library() + + +@register.filter +def embed(url, max_width=None): + embed = get_embed(url, max_width=max_width) + try: + if embed is not None: + return mark_safe(embed.html) + else: + return '' + except: + return '' + + +@register.filter +def embedly(url, max_width=None): + warnings.warn( + "The 'embedly' filter has been renamed. " + "Use 'embed' instead.", DeprecationWarning) + + return embed(url, max_width) From 008b3238abfb874e520c7cad0349fc55441aca6f Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 19 Jun 2014 10:41:53 +0100 Subject: [PATCH 043/139] Moved image_tags to wagtailimages_tags --- .../wagtailimages/templatetags/image_tags.py | 71 ++----------------- .../templatetags/wagtailimages_tags.py | 69 ++++++++++++++++++ 2 files changed, 74 insertions(+), 66 deletions(-) create mode 100644 wagtail/wagtailimages/templatetags/wagtailimages_tags.py diff --git a/wagtail/wagtailimages/templatetags/image_tags.py b/wagtail/wagtailimages/templatetags/image_tags.py index e59d9cd14..7eba192d4 100644 --- a/wagtail/wagtailimages/templatetags/image_tags.py +++ b/wagtail/wagtailimages/templatetags/image_tags.py @@ -1,69 +1,8 @@ -from django import template +import warnings -from wagtail.wagtailimages.models import Filter - -register = template.Library() - -# Local cache of filters, avoid hitting the DB -filters = {} - -@register.tag(name="image") -def image(parser, token): - args = token.split_contents() - - if len(args) == 3: - # token is of the form {% image self.photo max-320x200 %} - tag_name, image_var, filter_spec = args - return ImageNode(image_var, filter_spec) - - elif len(args) == 5: - # token is of the form {% image self.photo max-320x200 as img %} - tag_name, image_var, filter_spec, as_token, out_var = args - - if as_token != 'as': - raise template.TemplateSyntaxError("'image' tag should be of the form {%% image self.photo max-320x200 %%} or {%% image self.photo max-320x200 as img %%}") - - return ImageNode(image_var, filter_spec, out_var) - - else: - raise template.TemplateSyntaxError("'image' tag should be of the form {%% image self.photo max-320x200 %%} or {%% image self.photo max-320x200 as img %%}") +warnings.warn( + "The image_tags tags has been renamed. " + "Use wagtailimages_tags instead.", DeprecationWarning) -class ImageNode(template.Node): - def __init__(self, image_var_name, filter_spec, output_var_name=None): - self.image_var = template.Variable(image_var_name) - self.output_var_name = output_var_name - - if filter_spec not in filters: - filters[filter_spec], _ = Filter.objects.get_or_create(spec=filter_spec) - self.filter = filters[filter_spec] - - def render(self, context): - try: - image = self.image_var.resolve(context) - except template.VariableDoesNotExist: - return '' - - if not image: - return '' - - try: - rendition = image.get_rendition(self.filter) - except IOError: - # It's fairly routine for people to pull down remote databases to their - # local dev versions without retrieving the corresponding image files. - # In such a case, we would get an IOError at the point where we try to - # create the resized version of a non-existent image. Since this is a - # bit catastrophic for a missing image, we'll substitute a dummy - # Rendition object so that we just output a broken link instead. - Rendition = image.renditions.model # pick up any custom Image / Rendition classes that may be in use - rendition = Rendition(image=image, width=0, height=0) - rendition.file.name = 'not-found' - - if self.output_var_name: - # return the rendition object in the given variable - context[self.output_var_name] = rendition - return '' - else: - # render the rendition's image tag now - return rendition.img_tag() +from wagtail.wagtailimages.templatetags.wagtailimages_tags import register diff --git a/wagtail/wagtailimages/templatetags/wagtailimages_tags.py b/wagtail/wagtailimages/templatetags/wagtailimages_tags.py new file mode 100644 index 000000000..e59d9cd14 --- /dev/null +++ b/wagtail/wagtailimages/templatetags/wagtailimages_tags.py @@ -0,0 +1,69 @@ +from django import template + +from wagtail.wagtailimages.models import Filter + +register = template.Library() + +# Local cache of filters, avoid hitting the DB +filters = {} + +@register.tag(name="image") +def image(parser, token): + args = token.split_contents() + + if len(args) == 3: + # token is of the form {% image self.photo max-320x200 %} + tag_name, image_var, filter_spec = args + return ImageNode(image_var, filter_spec) + + elif len(args) == 5: + # token is of the form {% image self.photo max-320x200 as img %} + tag_name, image_var, filter_spec, as_token, out_var = args + + if as_token != 'as': + raise template.TemplateSyntaxError("'image' tag should be of the form {%% image self.photo max-320x200 %%} or {%% image self.photo max-320x200 as img %%}") + + return ImageNode(image_var, filter_spec, out_var) + + else: + raise template.TemplateSyntaxError("'image' tag should be of the form {%% image self.photo max-320x200 %%} or {%% image self.photo max-320x200 as img %%}") + + +class ImageNode(template.Node): + def __init__(self, image_var_name, filter_spec, output_var_name=None): + self.image_var = template.Variable(image_var_name) + self.output_var_name = output_var_name + + if filter_spec not in filters: + filters[filter_spec], _ = Filter.objects.get_or_create(spec=filter_spec) + self.filter = filters[filter_spec] + + def render(self, context): + try: + image = self.image_var.resolve(context) + except template.VariableDoesNotExist: + return '' + + if not image: + return '' + + try: + rendition = image.get_rendition(self.filter) + except IOError: + # It's fairly routine for people to pull down remote databases to their + # local dev versions without retrieving the corresponding image files. + # In such a case, we would get an IOError at the point where we try to + # create the resized version of a non-existent image. Since this is a + # bit catastrophic for a missing image, we'll substitute a dummy + # Rendition object so that we just output a broken link instead. + Rendition = image.renditions.model # pick up any custom Image / Rendition classes that may be in use + rendition = Rendition(image=image, width=0, height=0) + rendition.file.name = 'not-found' + + if self.output_var_name: + # return the rendition object in the given variable + context[self.output_var_name] = rendition + return '' + else: + # render the rendition's image tag now + return rendition.img_tag() From f73c7da7728f92cd07702de89cd31544db5fff7f Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 19 Jun 2014 11:17:32 +0100 Subject: [PATCH 044/139] Update docs to import tags from correct tags module --- docs/building_your_site/frontenddevelopers.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index f0054eaab..704708e0c 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -100,7 +100,7 @@ For example: .. code-block:: django - {% load image %} + {% load wagtailimages_tags %} ... {% image self.photo width-400 %} @@ -190,7 +190,7 @@ In some cases greater control over the ``img`` tag is required, for example to a .. code-block:: django - {% load image %} + {% load wagtailimages_tags %} ... {% image self.photo width-400 as tmp_photo %} @@ -209,7 +209,7 @@ Only fields using ``RichTextField`` need this applied in the template. .. code-block:: django - {% load rich_text %} + {% load wagtailcore_tags %} ... {{ self.body|richtext }} @@ -252,7 +252,7 @@ Takes a Page object and returns a relative URL (``/foo/bar/``) if within the sam .. code-block:: django - {% load pageurl %} + {% load wagtailcore_tags %} ... @@ -263,7 +263,7 @@ Takes any ``slug`` as defined in a page's "Promote" tab and returns the URL for .. code-block:: django - {% load slugurl %} + {% load wagtailcore_tags %} ... From 879269ab20e69894c8e947892101f9eba3669471 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Thu, 19 Jun 2014 11:22:58 +0100 Subject: [PATCH 045/139] Added versionchanged notes to docs --- docs/building_your_site/frontenddevelopers.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index 704708e0c..0ef918582 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -90,6 +90,9 @@ In addition to Django's standard tags and filters, Wagtail provides some of its Images (tag) ~~~~~~~~~~~~ +.. versionchanged:: 0.4 + The 'image_tags' tags library was renamed to 'wagtailimages_tags' + The ``image`` tag inserts an XHTML-compatible ``img`` element into the page, setting its ``src``, ``width``, ``height`` and ``alt``. See also :ref:`image_tag_alt`. The syntax for the tag is thus:: @@ -203,6 +206,9 @@ In some cases greater control over the ``img`` tag is required, for example to a Rich text (filter) ~~~~~~~~~~~~~~~~~~ +.. versionchanged:: 0.4 + The 'rich_text' tags library was renamed to 'wagtailcore_tags' + This filter takes a chunk of HTML content and renders it as safe HTML in the page. Importantly it also expands internal shorthand references to embedded images and links made in the Wagtail editor into fully-baked HTML ready for display. Only fields using ``RichTextField`` need this applied in the template. @@ -245,6 +251,9 @@ Wagtail embeds and images are included at their full width, which may overflow t Internal links (tag) ~~~~~~~~~~~~~~~~~~~~ +.. versionchanged:: 0.4 + The 'pageurl' tags library was renamed to 'wagtailcore_tags' + pageurl -------- From 6a33834d2948d69ce75c8ef3413cb9acc093557f Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Thu, 19 Jun 2014 13:30:39 +0100 Subject: [PATCH 046/139] ongoing styling of tabs an publishing fields, inc introduction of salmon colour --- .../templates/wagtailstyleguide/base.html | 13 +++++++--- wagtail/wagtailadmin/edit_handlers.py | 17 ++++++++++-- .../wagtailadmin/scss/components/forms.scss | 8 ++++++ .../wagtailadmin/scss/components/tabs.scss | 5 ++-- .../scss/layouts/page-editor.scss | 26 +++++++++++++------ .../wagtailadmin/scss/layouts/styleguide.scss | 6 +++++ .../static/wagtailadmin/scss/variables.scss | 2 ++ 7 files changed, 61 insertions(+), 16 deletions(-) diff --git a/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html b/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html index e7bd88606..e0d478fe1 100644 --- a/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html +++ b/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html @@ -42,9 +42,10 @@
  • color-teal
  • color-teal-darker
  • color-teal-dark
  • -
  • color-red
  • -
  • color-orange
  • -
  • color-green
  • + +
      +
    • color-salmon
    • +
    • color-salmon-light
    • color-grey-1
    • @@ -54,6 +55,12 @@
    • color-grey-4
    • color-grey-5
    +
      +
    • color-red
    • +
    • color-orange
    • +
    • color-green
    • +
    +
    diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index 175c63225..bfdeaf812 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -602,17 +602,30 @@ def InlinePanel(base_model, relation_name, panels=None, label='', help_text=''): }) +# This allows users to include the publishing panel in their own per-model override +# without having to write these fields out by hand, potentially losing 'classname' +# and therefore the associated styling of the publishing panel +def PublishingPanel(): + return MultiFieldPanel([ + FieldPanel('go_live_at'), + FieldPanel('expire_at'), + ], ugettext_lazy('Scheduled publishing'), classname="publishing") + + # Now that we've defined EditHandlers, we can set up wagtailcore.Page to have some. Page.content_panels = [ FieldPanel('title', classname="full title"), ] + Page.promote_panels = [ MultiFieldPanel([ FieldPanel('slug'), FieldPanel('seo_title'), FieldPanel('show_in_menus'), FieldPanel('search_description'), - FieldPanel('go_live_at'), - FieldPanel('expire_at'), ], ugettext_lazy('Common page configuration')), ] + +Page.settings_panels = [ + PublishingPanel() +] diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss index 5a870fbcf..0327c940a 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss @@ -1,3 +1,9 @@ +/* + These are the generic stylings for forms of any type. + If you're styling something specific to the page editing interface, + it probably ought to go in layouts/page-editor.scss +*/ + form { ul, li{ list-style-type:none; @@ -292,6 +298,7 @@ button.icon{ overflow:hidden; > li{ + @include row(); position:relative; background-color:white; padding:1em 10em 1em 1.5em; /* 10em padding leaves room for controls */ @@ -447,6 +454,7 @@ li.focused > .help{ } } + .fields li{ @include row(); padding-top:0.5em; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/tabs.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/tabs.scss index af0c496b1..dab30d114 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/tabs.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/tabs.scss @@ -2,7 +2,6 @@ @include row(); padding:0; background:$color-grey-4; - overflow:hidden; li{ list-style-type:none; @@ -22,10 +21,10 @@ text-decoration:none; display:block; padding:0.7em; - padding-bottom:1em; - margin-bottom:-0.3em; color:white; border-top:0.3em solid lighten($color-teal-darker, 3%); + max-height:1.2em; + overflow:hidden; &:hover{ color:white; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss index 5e3120c82..c9fe5b54b 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss @@ -25,9 +25,6 @@ } } -.objects{ - background:url("#{$static-root}bg-dark-diag.svg"); -} .object{ position:relative; overflow:hidden; @@ -80,7 +77,7 @@ > h2, &.single-field label{ -webkit-font-smoothing: auto; - background:$color-grey-3; + background:$color-salmon-light; text-transform:uppercase; padding:0.9em 0 0.9em 4em; font-size:0.95em; @@ -92,10 +89,10 @@ left:0; right:0; z-index:1; - text-shadow:1px 1px 1px rgba(255,255,255,0.5); - @include box-shadow(0 0 7px 0 rgba(0,0,0,0.4)); + overflow:hidden; &:before{ + text-shadow:none; font-family:wagtail; text-transform:none; content:"q"; @@ -108,10 +105,11 @@ line-height:1.8em; left:0px; width:1.7em; - opacity:0.15; + color:white; padding:0; margin:0; cursor:pointer; + background-color:$color-salmon; } } @@ -186,6 +184,17 @@ } } + /* special panel for the publishing fields, requires a bit more pizzazz */ + &.publishing{ + h2:before{ + content:"7"; + font-size:2.5em; + line-height:1.4em; + width:1.3em; + } + + } + &.title input, &.title textarea{ font-size:2em; @@ -235,16 +244,17 @@ top:0px; left:0px; width:3.3em; - background-color:$color-teal; padding:0; margin:0 0 0 -20px; cursor:pointer; a{ + @include border-radius(0); font-size: 0em; line-height: 0; overflow: visible; display:block; + background-color:$color-salmon; &:before{ position:relative; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/styleguide.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/styleguide.scss index 38d4281da..09cb627ed 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/styleguide.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/styleguide.scss @@ -68,6 +68,12 @@ section{ .color-grey-5{ background-color:$color-grey-5; } + .color-salmon{ + background-color:$color-salmon; + } + .color-salmon-light{ + background-color:$color-salmon-light; + } } diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss index 0f6567989..9479b8a28 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss @@ -30,6 +30,8 @@ $color-teal-dark: #246060; $color-red: #f7474e; $color-orange:#e9b04d; $color-green: #189370; +$color-salmon: #f37e77; +$color-salmon-light: #fcf2f2; /* darker to lighter */ $color-grey-1: #333333; From d1b321f859e3a9f19e636c9ec4a0af9ac4992b27 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Thu, 19 Jun 2014 13:33:48 +0100 Subject: [PATCH 047/139] made red more distinct from salmon --- wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss index 9479b8a28..e27817175 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/variables.scss @@ -27,7 +27,7 @@ $breakpoint-desktop-larger: 100em; /* 1600px */ $color-teal: #43b1b0; $color-teal-darker: darken($color-teal, 10%); $color-teal-dark: #246060; -$color-red: #f7474e; +$color-red: #cd3238; $color-orange:#e9b04d; $color-green: #189370; $color-salmon: #f37e77; From 62fc679720aeb18c79e9d17fa5e4c6b67b105340 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Thu, 19 Jun 2014 13:47:09 +0100 Subject: [PATCH 048/139] further tweaks to colours and form layout --- .../static/wagtailadmin/scss/components/datetimepicker.scss | 2 +- .../static/wagtailadmin/scss/components/forms.scss | 2 +- .../static/wagtailadmin/scss/layouts/page-editor.scss | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/datetimepicker.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/datetimepicker.scss index 078c93469..4902c2418 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/datetimepicker.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/datetimepicker.scss @@ -246,7 +246,7 @@ .xdsoft_calendar td.xdsoft_default, .xdsoft_calendar td.xdsoft_current, .xdsoft_timepicker .xdsoft_time_box > div > div.xdsoft_current{ - background: $color-orange; + background: $color-salmon; color:#fff; font-weight: 700; } diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss index 0327c940a..1d9b8b7a7 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss @@ -456,7 +456,7 @@ li.focused > .help{ .fields li{ - @include row(); + @include clearfix(); padding-top:0.5em; padding-bottom:1.2em; } diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss index c9fe5b54b..c75f0b6bc 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/page-editor.scss @@ -79,7 +79,7 @@ -webkit-font-smoothing: auto; background:$color-salmon-light; text-transform:uppercase; - padding:0.9em 0 0.9em 4em; + padding:0.9em 0 0.9em 4.1em; font-size:0.95em; margin:0 0 0.2em 0; line-height:1.5em; @@ -188,9 +188,9 @@ &.publishing{ h2:before{ content:"7"; - font-size:2.5em; + font-size:2.4em; line-height:1.4em; - width:1.3em; + width:1.4em; } } From 269dfe62f5517013d54810832a846ac143339b60 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Thu, 19 Jun 2014 16:58:48 +0100 Subject: [PATCH 049/139] refactoring how/where fields get their classes added --- .../templates/wagtailstyleguide/base.html | 1 + wagtail/contrib/wagtailstyleguide/views.py | 1 + wagtail/wagtailadmin/edit_handlers.py | 26 +-- .../wagtailadmin/scss/components/forms.scss | 99 +++++++---- .../wagtailadmin/scss/components/header.scss | 7 - .../wagtailadmin/scss/components/icons.scss | 10 +- .../scss/fonts/wagtail-icomoon.json | 164 ++++++++++-------- .../wagtailadmin/scss/fonts/wagtail.eot | Bin 25504 -> 25648 bytes .../wagtailadmin/scss/fonts/wagtail.svg | 1 + .../wagtailadmin/scss/fonts/wagtail.ttf | Bin 25340 -> 25484 bytes .../wagtailadmin/scss/fonts/wagtail.woff | Bin 15108 -> 15192 bytes .../wagtailadmin/scss/layouts/login.scss | 18 +- .../edit_handlers/chooser_panel.html | 2 +- .../edit_handlers/field_panel_field.html | 24 +-- .../edit_handlers/multi_field_panel.html | 4 +- ...el_object.html => single_field_panel.html} | 2 +- .../templates/wagtailadmin/shared/field.html | 25 +++ .../wagtailadmin/shared/field_as_li.html | 25 +-- .../templates/wagtailadmin/shared/header.html | 2 +- .../wagtailforms/index_submissions.html | 2 +- 20 files changed, 223 insertions(+), 190 deletions(-) rename wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/{field_panel_object.html => single_field_panel.html} (57%) create mode 100644 wagtail/wagtailadmin/templates/wagtailadmin/shared/field.html diff --git a/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html b/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html index e0d478fe1..767f69f7f 100644 --- a/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html +++ b/wagtail/contrib/wagtailstyleguide/templates/wagtailstyleguide/base.html @@ -388,6 +388,7 @@
  • warning
  • success
  • date
  • +
  • time
  • form
  • diff --git a/wagtail/contrib/wagtailstyleguide/views.py b/wagtail/contrib/wagtailstyleguide/views.py index 52c5433a1..9c830073c 100644 --- a/wagtail/contrib/wagtailstyleguide/views.py +++ b/wagtail/contrib/wagtailstyleguide/views.py @@ -23,6 +23,7 @@ class ExampleForm(forms.Form): url = forms.URLField(required=True) email = forms.EmailField(max_length=254) date = forms.DateField() + time = forms.TimeField() select = forms.ChoiceField(choices=CHOICES) boolean = forms.BooleanField(required=False) diff --git a/wagtail/wagtailadmin/edit_handlers.py b/wagtail/wagtailadmin/edit_handlers.py index bfdeaf812..9ded8bb29 100644 --- a/wagtail/wagtailadmin/edit_handlers.py +++ b/wagtail/wagtailadmin/edit_handlers.py @@ -195,9 +195,20 @@ class EditHandler(object): return "" def field_classnames(self): + classname = self.field_type() + "test" + + if self.bound_field.field.required: + classname += " required" + if self.bound_field.errors: + classname += " error" + + return classname + + + def input_classnames(self): """ - Additional classnames to add to the
  • when rendering this within a -
  • {% endblock %} diff --git a/wagtail/wagtailadmin/views/account.py b/wagtail/wagtailadmin/views/account.py index d5ab71bd6..c5cf6ffae 100644 --- a/wagtail/wagtailadmin/views/account.py +++ b/wagtail/wagtailadmin/views/account.py @@ -10,12 +10,17 @@ from django.views.decorators.cache import never_cache from wagtail.wagtailadmin import forms from wagtail.wagtailusers.forms import NotificationPreferencesForm +from wagtail.wagtailcore.models import UserPagePermissionsProxy @permission_required('wagtailadmin.access_admin') def account(request): + user_perms = UserPagePermissionsProxy(request.user) + show_notification_preferences = user_perms.can_edit_pages() or user_perms.can_publish_pages() + return render(request, 'wagtailadmin/account/account.html', { 'show_change_password': getattr(settings, 'WAGTAIL_PASSWORD_MANAGEMENT_ENABLED', True) and request.user.has_usable_password(), + 'show_notification_preferences': show_notification_preferences }) @@ -56,6 +61,11 @@ def notification_preferences(request): else: form = NotificationPreferencesForm(instance=request.user.get_profile()) + # quick-and-dirty catch-all in case the form has been rendered with no + # fields, as the user has no customisable permissions + if not form.fields: + return redirect('wagtailadmin_account') + return render(request, 'wagtailadmin/account/notification_preferences.html', { 'form': form, }) From dd7344d65d13881aa0d923f9fa0cacbaf3db1cfe Mon Sep 17 00:00:00 2001 From: Nick Smith Date: Mon, 23 Jun 2014 10:51:35 +0100 Subject: [PATCH 068/139] Add tests for notification preferences views' being filtered for less privileged users --- .../tests/test_account_management.py | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/tests/test_account_management.py b/wagtail/wagtailadmin/tests/test_account_management.py index 56aa63560..e8f46a25b 100644 --- a/wagtail/wagtailadmin/tests/test_account_management.py +++ b/wagtail/wagtailadmin/tests/test_account_management.py @@ -1,7 +1,7 @@ from django.test import TestCase from wagtail.tests.utils import unittest, WagtailTestUtils from django.core.urlresolvers import reverse -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.core import mail @@ -213,6 +213,62 @@ class TestAccountSection(TestCase, WagtailTestUtils): self.assertTrue(profile.rejected_notifications) +class TestAccountManagementForNonModerator(TestCase, WagtailTestUtils): + """ + Tests of reduced-functionality for editors + """ + def setUp(self): + # Create a non-moderator user + self.submitter = User.objects.create_user('submitter', 'submitter@example.com', 'password') + self.submitter.groups.add(Group.objects.get(name='Editors')) + + self.client.login(username=self.submitter.username, password='password') + + def test_notification_preferences_form_is_reduced_for_non_moderators(self): + """ + This tests that a user without publish permissions is not shown the + notification preference for 'submitted' items + """ + response = self.client.get(reverse('wagtailadmin_account_notification_preferences')) + self.assertIn('approved_notifications', response.context['form'].fields.keys()) + self.assertIn('rejected_notifications', response.context['form'].fields.keys()) + self.assertNotIn('submitted_notifications', response.context['form'].fields.keys()) + + +class TestAccountManagementForAdminOnlyUser(TestCase, WagtailTestUtils): + """ + Tests for users with no edit/publish permissions at all + """ + def setUp(self): + # Create a non-moderator user + admin_only_group = Group.objects.create(name='Admin Only') + admin_only_group.permissions.add(Permission.objects.get(codename='access_admin')) + + self.admin_only_user = User.objects.create_user('admin_only_user', 'admin_only_user@example.com', 'password') + self.admin_only_user.groups.add(admin_only_group) + + self.client.login(username=self.admin_only_user.username, password='password') + + def test_notification_preferences_view_redirects_for_admin_only_users(self): + """ + Test that the user is not shown the notification preferences view but instead + redirected to the account page + """ + response = self.client.get(reverse('wagtailadmin_account_notification_preferences')) + self.assertRedirects(response, reverse('wagtailadmin_account')) + + def test_notification_preferences_link_not_shown_for_admin_only_users(self): + """ + Test that the user is not even shown the link to the notification + preferences view + """ + response = self.client.get(reverse('wagtailadmin_account')) + self.assertEqual(response.context['show_notification_preferences'], False) + self.assertNotContains(response, reverse('wagtailadmin_account_notification_preferences')) + # safety check that checking for absence/presence of urls works + self.assertContains(response, reverse('wagtailadmin_home')) + + class TestPasswordReset(TestCase, WagtailTestUtils): """ This tests that the password reset is working From 64eed9684173c9e1be39883061d595de3bb2a69f Mon Sep 17 00:00:00 2001 From: Robert Clark Date: Mon, 23 Jun 2014 11:40:25 -0400 Subject: [PATCH 069/139] moved tests to page model instead of queryset --- wagtail/wagtailcore/tests/test_page_model.py | 22 +++++++++++++++++++ .../wagtailcore/tests/test_page_queryset.py | 18 --------------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/wagtail/wagtailcore/tests/test_page_model.py b/wagtail/wagtailcore/tests/test_page_model.py index 1bae57952..dfc45d72a 100644 --- a/wagtail/wagtailcore/tests/test_page_model.py +++ b/wagtail/wagtailcore/tests/test_page_model.py @@ -224,3 +224,25 @@ class TestMovePage(TestCase): christmas = events_index.get_children().get(slug='christmas') self.assertEqual(christmas.depth, 5) self.assertEqual(christmas.url_path, '/home/about-us/events/christmas/') + + +class TestPagePagination(TestCase): + fixtures = ['test.json'] + + def test_published_next(self): + events_index = Page.objects.get(url_path='/home/events/') + current_page = Page.objects.descendant_of(events_index).live().first() + + # All pages must be live + while current_page: + self.assertTrue(current_page.live) + current_page = current_page.get_next_siblings().live().first() + + def test_published_prev(self): + events_index = Page.objects.get(url_path='/home/events/') + current_page = Page.objects.descendant_of(events_index).live().last() + + # All pages must be live + while current_page: + self.assertTrue(current_page.live) + current_page = current_page.get_prev_siblings().live().first() diff --git a/wagtail/wagtailcore/tests/test_page_queryset.py b/wagtail/wagtailcore/tests/test_page_queryset.py index 020d98d15..06f2c3e21 100644 --- a/wagtail/wagtailcore/tests/test_page_queryset.py +++ b/wagtail/wagtailcore/tests/test_page_queryset.py @@ -254,21 +254,3 @@ class TestPageQuerySet(TestCase): # Check that the homepage is in the results homepage = Page.objects.get(url_path='/home/') self.assertTrue(pages.filter(id=homepage.id).exists()) - - def test_published_next(self): - events_index = Page.objects.get(url_path='/home/events/') - current_page = Page.objects.descendant_of(events_index).live().first() - - # All pages must be live - while current_page: - self.assertTrue(current_page.live) - current_page = current_page.get_next_siblings().live().first() - - def test_published_prev(self): - events_index = Page.objects.get(url_path='/home/events/') - current_page = Page.objects.descendant_of(events_index).live().last() - - # All pages must be live - while current_page: - self.assertTrue(current_page.live) - current_page = current_page.get_prev_siblings().live().first() From 66dce8df0349fe7bda8a838eed2d3791993ce627 Mon Sep 17 00:00:00 2001 From: Tim Heap Date: Tue, 24 Jun 2014 17:12:10 +1000 Subject: [PATCH 070/139] Make the 'X Pages/Images/Documents' stats links They link to the relevant sections of the admin, allowing for quick jumping around. --- .../static/wagtailadmin/scss/layouts/home.scss | 7 +++++++ .../templates/wagtailadmin/home/site_summary.html | 8 +++++++- wagtail/wagtailadmin/views/home.py | 3 ++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/home.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/home.scss index 40711ef7b..f1e2ee779 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/home.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/home.scss @@ -46,6 +46,13 @@ header{ } } + a{ + position:relative; + display:block; + width:100%; + height:100%; + } + span{ font-family:Bitter, Georgia, serif; display:block; diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/home/site_summary.html b/wagtail/wagtailadmin/templates/wagtailadmin/home/site_summary.html index 8a03b4e04..225568fab 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/home/site_summary.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/home/site_summary.html @@ -3,25 +3,31 @@

    {% trans "Site summary" %}

    - \ No newline at end of file + diff --git a/wagtail/wagtailadmin/views/home.py b/wagtail/wagtailadmin/views/home.py index 24800eefc..77177b652 100644 --- a/wagtail/wagtailadmin/views/home.py +++ b/wagtail/wagtailadmin/views/home.py @@ -7,7 +7,7 @@ from django.template.loader import render_to_string from wagtail.wagtailadmin import hooks from wagtail.wagtailadmin.forms import SearchForm -from wagtail.wagtailcore.models import Page, PageRevision, UserPagePermissionsProxy +from wagtail.wagtailcore.models import Site, Page, PageRevision, UserPagePermissionsProxy from wagtail.wagtaildocs.models import Document @@ -27,6 +27,7 @@ class SiteSummaryPanel(object): 'total_pages': Page.objects.count() - 1, # subtract 1 because the root node is not a real page 'total_images': get_image_model().objects.count(), 'total_docs': Document.objects.count(), + 'root_page': Site.find_for_request(self.request).root_page, 'search_form': SearchForm(), }, RequestContext(self.request)) From bdbcf3cd99d5b44ca275e38da5e6bab0ab181609 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 24 Jun 2014 09:53:41 +0100 Subject: [PATCH 071/139] Updated deprecation warning messages of moved template tags libraries --- wagtail/wagtailcore/templatetags/pageurl.py | 4 ++-- wagtail/wagtailcore/templatetags/rich_text.py | 4 ++-- wagtail/wagtailembeds/templatetags/embed_filters.py | 4 ++-- wagtail/wagtailimages/templatetags/image_tags.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/wagtail/wagtailcore/templatetags/pageurl.py b/wagtail/wagtailcore/templatetags/pageurl.py index fe64a302f..b31756c30 100644 --- a/wagtail/wagtailcore/templatetags/pageurl.py +++ b/wagtail/wagtailcore/templatetags/pageurl.py @@ -1,8 +1,8 @@ import warnings warnings.warn( - "The pageurl tags has been renamed. " - "Use wagtailcore_tags instead.", DeprecationWarning) + "The pageurl tag library has been moved to wagtailcore_tags. " + "Use {% load wagtailcore_tags %} instead.", DeprecationWarning) from wagtail.wagtailcore.templatetags.wagtailcore_tags import register diff --git a/wagtail/wagtailcore/templatetags/rich_text.py b/wagtail/wagtailcore/templatetags/rich_text.py index 94c2d176c..ed8ac6f0f 100644 --- a/wagtail/wagtailcore/templatetags/rich_text.py +++ b/wagtail/wagtailcore/templatetags/rich_text.py @@ -1,8 +1,8 @@ import warnings warnings.warn( - "The rich_text tags has been renamed. " - "Use wagtailcore_tags instead.", DeprecationWarning) + "The rich_text tag library has been moved to wagtailcore_tags. " + "Use {% load wagtailcore_tags %} instead.", DeprecationWarning) from wagtail.wagtailcore.templatetags.wagtailcore_tags import register diff --git a/wagtail/wagtailembeds/templatetags/embed_filters.py b/wagtail/wagtailembeds/templatetags/embed_filters.py index 25326fec6..7d3500e58 100644 --- a/wagtail/wagtailembeds/templatetags/embed_filters.py +++ b/wagtail/wagtailembeds/templatetags/embed_filters.py @@ -1,8 +1,8 @@ import warnings warnings.warn( - "The embed_filters tags has been renamed. " - "Use wagtailembeds_tags instead.", DeprecationWarning) + "The embed_filters tag library has been moved to wagtailcore_tags. " + "Use {% load wagtailembeds_tags %} instead.", DeprecationWarning) from wagtail.wagtailembeds.templatetags.wagtailembeds_tags import register diff --git a/wagtail/wagtailimages/templatetags/image_tags.py b/wagtail/wagtailimages/templatetags/image_tags.py index 7eba192d4..9a96356c9 100644 --- a/wagtail/wagtailimages/templatetags/image_tags.py +++ b/wagtail/wagtailimages/templatetags/image_tags.py @@ -1,8 +1,8 @@ import warnings warnings.warn( - "The image_tags tags has been renamed. " - "Use wagtailimages_tags instead.", DeprecationWarning) + "The image_tags tag library has been moved to wagtailcore_tags. " + "Use {% load wagtailimages_tags %} instead.", DeprecationWarning) from wagtail.wagtailimages.templatetags.wagtailimages_tags import register From 1a986bab0d9cc324c73c37f6ce4639157e342b97 Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Tue, 24 Jun 2014 10:26:44 +0100 Subject: [PATCH 072/139] Add unit tests for rich_text.py --- wagtail/tests/fixtures/test.json | 18 ++ wagtail/wagtailcore/tests/test_rich_text.py | 262 ++++++++++++++++++++ wagtail/wagtailcore/tests/tests.py | 47 ++-- wagtail/wagtailembeds/format.py | 1 - 4 files changed, 303 insertions(+), 25 deletions(-) create mode 100644 wagtail/wagtailcore/tests/test_rich_text.py diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index 11b82d9f1..bd4807e07 100644 --- a/wagtail/tests/fixtures/test.json +++ b/wagtail/tests/fixtures/test.json @@ -418,5 +418,23 @@ "page": 8, "submit_time": "2014-01-01T12:00:00.000Z" } +}, +{ + "pk": 1, + "model": "wagtaildocs.Document", + "fields": { + "title": "test document", + "created_at": "2014-01-01T12:00:00.000Z" + } +}, +{ + "pk": 1, + "model": "wagtailimages.Image", + "fields": { + "title": "test image", + "created_at": "2014-01-01T12:00:00.000Z", + "width": 0, + "height": 0 + } } ] diff --git a/wagtail/wagtailcore/tests/test_rich_text.py b/wagtail/wagtailcore/tests/test_rich_text.py new file mode 100644 index 000000000..9c98c6d3f --- /dev/null +++ b/wagtail/wagtailcore/tests/test_rich_text.py @@ -0,0 +1,262 @@ +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 +) +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'] + + def test_get_db_attributes(self): + soup = BeautifulSoup( + 'foo' + ) + tag = soup.a + result = PageLinkHandler.get_db_attributes(tag) + self.assertEqual(result, + {'id': 'test-id'}) + + def test_expand_db_attributes_page_does_not_exist(self): + result = PageLinkHandler.expand_db_attributes( + {'id': 0}, + False + ) + self.assertEqual(result, '') + + def test_expand_db_attributes_for_editor(self): + result = PageLinkHandler.expand_db_attributes( + {'id': 1}, + True + ) + self.assertEqual(result, + '') + + def test_expand_db_attributes_not_for_editor(self): + result = PageLinkHandler.expand_db_attributes( + {'id': 1}, + False + ) + 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( + '
    foo
    ' + ) + tag = soup.div + self.assertEqual(tag.name, 'div') + DbWhitelister.clean_tag_node(soup, tag) + self.assertEqual(tag.name, 'p') + + def test_clean_tag_node_with_data_embedtype(self): + soup = BeautifulSoup( + '

    foo

    ' + ) + tag = soup.p + DbWhitelister.clean_tag_node(soup, tag) + self.assertEqual(str(tag), + '

    ') + + def test_clean_tag_node_with_data_linktype(self): + soup = BeautifulSoup( + 'foo' + ) + tag = soup.a + DbWhitelister.clean_tag_node(soup, tag) + self.assertEqual(str(tag), 'foo') + + def test_clean_tag_node(self): + soup = BeautifulSoup( + 'foo' + ) + tag = soup.a + DbWhitelister.clean_tag_node(soup, tag) + self.assertEqual(str(tag), 'foo') + + +class TestExtractAttrs(TestCase): + def test_extract_attr(self): + html = 'snowman' + result = extract_attrs(html) + self.assertEqual(result, {'foo': 'bar', 'baz': 'quux'}) + + +class TestExpandDbHtml(TestCase): + def test_expand_db_html_with_linktype(self): + html = 'foo' + result = expand_db_html(html) + self.assertEqual(result, 'foo') + + def test_expand_db_html_no_linktype(self): + html = 'foo' + result = expand_db_html(html) + self.assertEqual(result, 'foo') + + @patch('wagtail.wagtailembeds.embeds.oembed') + def test_expand_db_html_with_embed(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' + } + html = '' + result = expand_db_html(html) + self.assertIn('test html', result) diff --git a/wagtail/wagtailcore/tests/tests.py b/wagtail/wagtailcore/tests/tests.py index d10474bba..d7d9a24f9 100644 --- a/wagtail/wagtailcore/tests/tests.py +++ b/wagtail/wagtailcore/tests/tests.py @@ -1,12 +1,7 @@ -from StringIO import StringIO +from django.test import TestCase -from django.test import TestCase, Client -from django.http import HttpRequest, Http404 -from django.core import management -from django.contrib.auth.models import User - -from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy -from wagtail.tests.models import EventPage, EventIndex, SimplePage +from wagtail.wagtailcore.models import Page, Site +from wagtail.tests.models import SimplePage class TestPageUrlTags(TestCase): @@ -15,25 +10,28 @@ class TestPageUrlTags(TestCase): def test_pageurl_tag(self): response = self.client.get('/events/') self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Christmas') + self.assertContains(response, + 'Christmas') def test_slugurl_tag(self): response = self.client.get('/events/christmas/') self.assertEqual(response.status_code, 200) - self.assertContains(response, 'Back to events index') + self.assertContains(response, + 'Back to events index') class TestIssue7(TestCase): """ - This tests for an issue where if a site root page was moved, all the page - urls in that site would change to None. + This tests for an issue where if a site root page was moved, all + the page urls in that site would change to None. - The issue was caused by the 'wagtail_site_root_paths' cache variable not being - cleared when a site root page was moved. Which left all the child pages - thinking that they are no longer in the site and return None as their url. + The issue was caused by the 'wagtail_site_root_paths' cache + variable not being cleared when a site root page was moved. Which + left all the child pages thinking that they are no longer in the + site and return None as their url. - Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 - Discussion: https://github.com/torchbox/wagtail/issues/7 + Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 Discussion: + https://github.com/torchbox/wagtail/issues/7 """ fixtures = ['test.json'] @@ -67,15 +65,16 @@ class TestIssue7(TestCase): class TestIssue157(TestCase): """ - This tests for an issue where if a site root pages slug was changed, all the page - urls in that site would change to None. + This tests for an issue where if a site root pages slug was + changed, all the page urls in that site would change to None. - The issue was caused by the 'wagtail_site_root_paths' cache variable not being - cleared when a site root page was changed. Which left all the child pages - thinking that they are no longer in the site and return None as their url. + The issue was caused by the 'wagtail_site_root_paths' cache + variable not being cleared when a site root page was changed. + Which left all the child pages thinking that they are no longer in + the site and return None as their url. - Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 - Discussion: https://github.com/torchbox/wagtail/issues/157 + Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 Discussion: + https://github.com/torchbox/wagtail/issues/157 """ fixtures = ['test.json'] diff --git a/wagtail/wagtailembeds/format.py b/wagtail/wagtailembeds/format.py index 1654be989..8a73ff524 100644 --- a/wagtail/wagtailembeds/format.py +++ b/wagtail/wagtailembeds/format.py @@ -1,6 +1,5 @@ from __future__ import division # Use true division -from django.utils.html import escape from django.template.loader import render_to_string from wagtail.wagtailembeds import get_embed From ea14d99d18918353d72613b0e55b8f542d88cd82 Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Tue, 24 Jun 2014 14:56:40 +0100 Subject: [PATCH 073/139] Alter comment formatting in wagtailcore tests.py --- wagtail/wagtailcore/tests/tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wagtail/wagtailcore/tests/tests.py b/wagtail/wagtailcore/tests/tests.py index d7d9a24f9..5fd5aa802 100644 --- a/wagtail/wagtailcore/tests/tests.py +++ b/wagtail/wagtailcore/tests/tests.py @@ -30,8 +30,8 @@ class TestIssue7(TestCase): left all the child pages thinking that they are no longer in the site and return None as their url. - Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 Discussion: - https://github.com/torchbox/wagtail/issues/7 + Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 + Discussion: https://github.com/torchbox/wagtail/issues/7 """ fixtures = ['test.json'] @@ -73,8 +73,8 @@ class TestIssue157(TestCase): Which left all the child pages thinking that they are no longer in the site and return None as their url. - Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 Discussion: - https://github.com/torchbox/wagtail/issues/157 + Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682 + Discussion: https://github.com/torchbox/wagtail/issues/157 """ fixtures = ['test.json'] From 1f645a7ba092c0fbd757ea5e9d2cfcd0b233bb87 Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Tue, 24 Jun 2014 15:38:13 +0100 Subject: [PATCH 074/139] Fix #80 Searching for a non-existent editor's pick now displays the correct not found message. --- wagtail/wagtailsearch/views/editorspicks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wagtail/wagtailsearch/views/editorspicks.py b/wagtail/wagtailsearch/views/editorspicks.py index bb39c02ba..8e1155922 100644 --- a/wagtail/wagtailsearch/views/editorspicks.py +++ b/wagtail/wagtailsearch/views/editorspicks.py @@ -13,6 +13,7 @@ from wagtail.wagtailadmin.forms import SearchForm @permission_required('wagtailadmin.access_admin') @vary_on_headers('X-Requested-With') def index(request): + is_searching = False page = request.GET.get('p', 1) query_string = request.GET.get('q', "") @@ -21,6 +22,7 @@ def index(request): # Search if query_string: queries = queries.filter(query_string__icontains=query_string) + is_searching = True # Pagination paginator = Paginator(queries, 20) @@ -33,11 +35,13 @@ def index(request): if request.is_ajax(): return render(request, "wagtailsearch/editorspicks/results.html", { + 'is_searching': is_searching, 'queries': queries, 'query_string': query_string, }) else: return render(request, 'wagtailsearch/editorspicks/index.html', { + 'is_searching': is_searching, 'queries': queries, 'query_string': query_string, 'search_form': SearchForm(data=dict(q=query_string) if query_string else None, placeholder=_("Search editor's picks")), From 8cc4bdf39efbf5fb70b8daa9b148c88edc630aa6 Mon Sep 17 00:00:00 2001 From: Tim Heap Date: Wed, 25 Jun 2014 08:47:48 +1000 Subject: [PATCH 075/139] Link to the root page, not the site root --- .../wagtailadmin/templates/wagtailadmin/home/site_summary.html | 2 +- wagtail/wagtailadmin/views/home.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/home/site_summary.html b/wagtail/wagtailadmin/templates/wagtailadmin/home/site_summary.html index 225568fab..166c0c912 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/home/site_summary.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/home/site_summary.html @@ -3,7 +3,7 @@

    {% trans "Site summary" %}

    + +
    +
    + + +
    + +
    +