From a3b7259f98caca8e02b8e95f973207b4f66cfe76 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Thu, 20 Mar 2014 15:58:02 +0200 Subject: [PATCH 001/189] Add a basic skeleton for Wagtail form builder --- wagtail/wagtailforms/__init__.py | 0 wagtail/wagtailforms/models.py | 72 ++++++++++++++++++++++++++++++++ wagtail/wagtailforms/tests.py | 19 +++++++++ 3 files changed, 91 insertions(+) create mode 100644 wagtail/wagtailforms/__init__.py create mode 100644 wagtail/wagtailforms/models.py create mode 100644 wagtail/wagtailforms/tests.py diff --git a/wagtail/wagtailforms/__init__.py b/wagtail/wagtailforms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/wagtailforms/models.py b/wagtail/wagtailforms/models.py new file mode 100644 index 000000000..e4be5a379 --- /dev/null +++ b/wagtail/wagtailforms/models.py @@ -0,0 +1,72 @@ +from django.db import models +from django.shortcuts import render +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailcore.models import Page, Orderable +from wagtail.wagtailadmin.edit_handlers import FieldPanel, InlinePanel + +from modelcluster.fields import ParentalKey + +FORM_FIELD_CHOICES = ( + ('SINGLELINE', _('Single line text')), + ('MULTILINE', _('Multi-line text')), + ('EMAIL', _('Email')), + ('NUMBER', _('Number')), + ('URL', _('URL')), + ('CHECKBOX', _('Checkbox')), + ('CHECKBOXES', _('Checkboxes')), + ('DROPDOWN', _('Drop down')), + ('RADIO', _('Radio buttons')), + ('DATE', _('Date')), + ('DATETIME', _('Date/time')), +) + +class AbstractFormFields(models.Model): + #page = ParentalKey('wagtailforms.AbstractForm', related_name='form_fields') + label = models.CharField(max_length=255) + field_type = models.CharField(max_length=16, choices = FORM_FIELD_CHOICES) + required = models.BooleanField( default=True) + choices = models.CharField(max_length=512, blank=True, help_text='Comma seperated list of choices') + default_value = models.CharField(max_length=255, blank=True) + help_text = models.CharField(max_length=255, blank=True) + + panels = [ + FieldPanel('label'), + FieldPanel('field_type'), + FieldPanel('required'), + FieldPanel('choices'), + FieldPanel('default_value'), + FieldPanel('help_text'), + ] + + class Meta: + abstract = True + + + +class AbstractForm(Page): + is_abstract = True #Don't display me in "Add" + + class Meta: + abstract = True + + def serve(self, request): + # Get fields + form_fields = self.form_fields + + return render(request, self.template, { + 'self': self, + }) + + +class ConcreteFormFields(Orderable, AbstractFormFields): + page = ParentalKey('wagtailforms.ConcreteForm', related_name='form_fields') + + +class ConcreteForm(AbstractForm): + pass + +ConcreteForm.content_panels = [ + FieldPanel('title', classname="full title"), + InlinePanel(ConcreteForm, 'form_fields', label="Form Fields"), +] diff --git a/wagtail/wagtailforms/tests.py b/wagtail/wagtailforms/tests.py new file mode 100644 index 000000000..95cbacbf0 --- /dev/null +++ b/wagtail/wagtailforms/tests.py @@ -0,0 +1,19 @@ +""" +This file demonstrates writing tests using the unittest module. These will pass +when you run "manage.py test". + +Replace this with more appropriate tests for your application. +""" + +import unittest + +from django.test import TestCase + + +@unittest.skip("Need real tests") +class SimpleTest(TestCase): + def test_basic_addition(self): + """ + Tests that 1 + 1 always equals 2. + """ + self.assertEqual(1 + 1, 2) From 92563ff53532eb9480139fdc95186085246e22ba Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Thu, 20 Mar 2014 23:04:44 +0200 Subject: [PATCH 002/189] Add form builder and the basic form workflow also add the implementation of all form field types. The form will save the submitted data to the database (using the FormSubmission) model. --- wagtail/wagtailforms/forms.py | 67 ++++++++++++++++++++++ wagtail/wagtailforms/models.py | 102 +++++++++++++++++++++++++-------- 2 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 wagtail/wagtailforms/forms.py diff --git a/wagtail/wagtailforms/forms.py b/wagtail/wagtailforms/forms.py new file mode 100644 index 000000000..b444895c5 --- /dev/null +++ b/wagtail/wagtailforms/forms.py @@ -0,0 +1,67 @@ +import django.forms +from django.utils.datastructures import SortedDict +from django.utils.text import slugify +from unidecode import unidecode + +class FormBuilder(): + formfields = SortedDict() + def __init__(self, fields): + for field in fields: + options = self.get_options(field) + f = getattr(self, "create_"+field.field_type+"_field" )(field, options) + # unidecode will return an ascii string while slugify wants a unicode string + # on the other hand, slugify returns a safe-string which will be converted + # to a normal str + field_name = str(slugify(unicode(unidecode(field.label)))) + self.formfields[field_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 + + def create_singleline_field(self, field, options): + # TODO: This is a default value - it may need to be changed + options['max_length'] = 255 + return django.forms.CharField(**options) + + def create_multiline_field(self, field, options): + return django.forms.CharField(widget=django.forms.Textarea, **options) + + def create_date_field(self, field, options): + return django.forms.DateField(**options) + + def create_datetime_field(self, field, options): + return django.forms.DateTimeField(**options) + + def create_email_field(self, field, options): + return django.forms.EmailField(**options) + + def create_url_field(self, field, options): + return django.forms.URLField(**options) + + def create_number_field(self, field, options): + return django.forms.DecimalField(**options) + + def create_dropdown_field(self, field, options): + options['choices'] = map(lambda x: (x.strip(),x.strip()), field.choices.split(',')) + return django.forms.ChoiceField(**options) + + def create_radio_field(self, field, options): + options['choices'] = map(lambda x: (x.strip(),x.strip()), field.choices.split(',')) + return django.forms.ChoiceField(widget=django.forms.RadioSelect, **options) + + def create_checkboxes_field(self, field, options): + options['choices'] = map(lambda x: (x.strip(),x.strip()), field.choices.split(',')) + options['initial'] = field.default_value.split(',') + return django.forms.MultipleChoiceField(widget=django.forms.CheckboxSelectMultiple, **options) + + def create_checkbox_field(self, field, options): + return django.forms.BooleanField(**options) + + def get_form_class(self): + return type('WagtailForm', (django.forms.Form,), self.formfields ) + diff --git a/wagtail/wagtailforms/models.py b/wagtail/wagtailforms/models.py index e4be5a379..ff758497c 100644 --- a/wagtail/wagtailforms/models.py +++ b/wagtail/wagtailforms/models.py @@ -1,32 +1,53 @@ +from django.conf import settings from django.db import models from django.shortcuts import render from django.utils.translation import ugettext_lazy as _ +import json +import re + from wagtail.wagtailcore.models import Page, Orderable from wagtail.wagtailadmin.edit_handlers import FieldPanel, InlinePanel from modelcluster.fields import ParentalKey +from .forms import FormBuilder + FORM_FIELD_CHOICES = ( - ('SINGLELINE', _('Single line text')), - ('MULTILINE', _('Multi-line text')), - ('EMAIL', _('Email')), - ('NUMBER', _('Number')), - ('URL', _('URL')), - ('CHECKBOX', _('Checkbox')), - ('CHECKBOXES', _('Checkboxes')), - ('DROPDOWN', _('Drop down')), - ('RADIO', _('Radio buttons')), - ('DATE', _('Date')), - ('DATETIME', _('Date/time')), + ('singleline', _('Single line text')), + ('multiline', _('Multi-line text')), + ('email', _('Email')), + ('number', _('Number')), + ('url', _('URL')), + ('checkbox', _('Checkbox')), + ('checkboxes', _('Checkboxes')), + ('dropdown', _('Drop down')), + ('radio', _('Radio buttons')), + ('date', _('Date')), + ('datetime', _('Date/time')), ) + +HTML_EXTENSION_RE = re.compile(r"(.*)\.html") + + +class FormSubmission(models.Model): + """Data for a Form submission.""" + form_data = models.TextField() + form_page = models.ForeignKey('wagtailcore.Page',related_name='+') + submit_time = models.DateTimeField(auto_now_add=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True) + + def __unicode__(self): + return self.form_data + class AbstractFormFields(models.Model): - #page = ParentalKey('wagtailforms.AbstractForm', related_name='form_fields') - label = models.CharField(max_length=255) + """Database Fields required for building a Django Form field.""" + + label = models.CharField(max_length=255, help_text=_('The label of the form field') ) field_type = models.CharField(max_length=16, choices = FORM_FIELD_CHOICES) - required = models.BooleanField( default=True) - choices = models.CharField(max_length=512, blank=True, help_text='Comma seperated list of choices') + required = models.BooleanField(default=True) + choices = models.CharField(max_length=512, blank=True, help_text=_('Comma seperated list of choices')) default_value = models.CharField(max_length=255, blank=True) help_text = models.CharField(max_length=255, blank=True) @@ -42,31 +63,64 @@ class AbstractFormFields(models.Model): class Meta: abstract = True - - + class AbstractForm(Page): - is_abstract = True #Don't display me in "Add" + """A Form Page. Pages with form should inhert from it""" + form_builder = FormBuilder + is_abstract = True # Don't display me in "Add" + + def __init__(self, *args, **kwargs): + super(Page, self).__init__(*args, **kwargs) + if not hasattr(self, 'landing_page_template'): + template_wo_ext = re.match(HTML_EXTENSION_RE, self.template).group(1) + self.landing_page_template = template_wo_ext + '_landing.html' class Meta: abstract = True - + def serve(self, request): - # Get fields - form_fields = self.form_fields + fb = self.form_builder(self.form_fields.all() ) + form_class = fb.get_form_class() + + if request.method == 'POST': + self.form = form_class(request.POST) + + if self.form.is_valid(): + # remove csrf_token from form.data + form_data = dict( + i for i in self.form.data.items() + if i[0] != 'csrfmiddlewaretoken' + ) + FormSubmission.objects.create( + form_data = json.dumps(form_data), + form_page = self.page_ptr, + user = request.user, + ) + # TODO: Do other things like sending email + # render the landing_page + # TODO: It is much better to redirect to it + return render(request, self.landing_page_template, { + 'self': self, + }) + else: + self.form = form_class() return render(request, self.template, { 'self': self, + 'form': self.form, }) - + + +######## TEST class ConcreteFormFields(Orderable, AbstractFormFields): page = ParentalKey('wagtailforms.ConcreteForm', related_name='form_fields') - class ConcreteForm(AbstractForm): - pass + thank_you = models.CharField(max_length=255) ConcreteForm.content_panels = [ FieldPanel('title', classname="full title"), + FieldPanel('thank_you', classname="full"), InlinePanel(ConcreteForm, 'form_fields', label="Form Fields"), ] From b6aca526dac3b2e55c99cea0b1491e6b387314f2 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Fri, 21 Mar 2014 23:44:44 +0200 Subject: [PATCH 003/189] Add generic send_email task in wagtailadmin.tasks This will be used from the send form data to email. It has been added to the wagtailadmin.tasks module to use celery if it has been defined and configured or just send the email if not (just like the send_notification task). --- wagtail/wagtailadmin/tasks.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/wagtail/wagtailadmin/tasks.py b/wagtail/wagtailadmin/tasks.py index 3d6f3e8f1..d63b17f79 100644 --- a/wagtail/wagtailadmin/tasks.py +++ b/wagtail/wagtailadmin/tasks.py @@ -85,3 +85,16 @@ def send_notification(page_revision_id, notification, excluded_user_id): # Send email send_mail(email_subject, email_content, from_email, email_addresses) + + +@task +def send_email_task(email_subject, email_content, from_email, email_addresses): + if not from_email: + if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'): + from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL + elif hasattr(settings, 'DEFAULT_FROM_EMAIL'): + from_email = settings.DEFAULT_FROM_EMAIL + else: + from_email = 'webmaster@localhost' + + send_mail(email_subject, email_content, from_email, email_addresses) From 9557bc66050ac70a884584596ca0bcb231180ead Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Fri, 21 Mar 2014 23:47:48 +0200 Subject: [PATCH 004/189] Add first version of send form data to email This has been added in a generic way to allow defining form processing backends. Sending the form data to an email is one of these backends - others may include for instance call a web service with the form data (for instance in a customer complains form we'd need to start a customer complains workflow). In any case, if the AbstractForm has a 'form_processing_backend' attribute which should be a class, a new object of that class will be generated and its process method will be called. The process method needs two argumetns: The Page to pass any needed paramaters and the form to actually pass the form data. The form processing backends should inherit from the BaseFormProcessor class (however probably this will be refactored to just use duck-typing since I don't think that a base class offers anything here) and implement the process method. Also, another useful method would be the validate_usage to be called from the Form that uses the backend and actually check that the form defines the correct fields - an example is that for the email processor we need to define an email_to field in the form. The validate_usage would need to raise an ImproperlyConfigured exception if it has not been configured yet, however it has not been yet implemented. --- wagtail/wagtailforms/backends/__init__.py | 0 wagtail/wagtailforms/backends/base.py | 11 ++++++ wagtail/wagtailforms/backends/email.py | 17 +++++++++ wagtail/wagtailforms/forms.py | 4 +-- wagtail/wagtailforms/models.py | 44 +++++++++++++++++++---- 5 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 wagtail/wagtailforms/backends/__init__.py create mode 100644 wagtail/wagtailforms/backends/base.py create mode 100644 wagtail/wagtailforms/backends/email.py diff --git a/wagtail/wagtailforms/backends/__init__.py b/wagtail/wagtailforms/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/wagtailforms/backends/base.py b/wagtail/wagtailforms/backends/base.py new file mode 100644 index 000000000..45d0343ae --- /dev/null +++ b/wagtail/wagtailforms/backends/base.py @@ -0,0 +1,11 @@ +from django.core.exceptions import ImproperlyConfigured + +class BaseFormProcessor(object): + def __init__(self): + pass + + def validate_usage(page): + return True + + def process(self, page, form): + return NotImplemented \ No newline at end of file diff --git a/wagtail/wagtailforms/backends/email.py b/wagtail/wagtailforms/backends/email.py new file mode 100644 index 000000000..ec1677318 --- /dev/null +++ b/wagtail/wagtailforms/backends/email.py @@ -0,0 +1,17 @@ +import datetime + +from django.core.exceptions import ImproperlyConfigured +from .base import BaseFormProcessor +from wagtail.wagtailadmin import tasks + + +class EmailFormProcessor(BaseFormProcessor): + def __init__(self): + pass + + def validate_usage(page): + return True + + def process(self, page, form): + content = ', '.join([ x[1].label +': '+ form.data.get(x[0]) for x in form.fields.items() ]) + tasks.send_email_task.delay("New " + page.title+" form submission at " + str(datetime.datetime.now()) , content, page.email_from, [page.email_to] ) diff --git a/wagtail/wagtailforms/forms.py b/wagtail/wagtailforms/forms.py index b444895c5..6e938822d 100644 --- a/wagtail/wagtailforms/forms.py +++ b/wagtail/wagtailforms/forms.py @@ -55,8 +55,8 @@ class FormBuilder(): return django.forms.ChoiceField(widget=django.forms.RadioSelect, **options) def create_checkboxes_field(self, field, options): - options['choices'] = map(lambda x: (x.strip(),x.strip()), field.choices.split(',')) - options['initial'] = field.default_value.split(',') + options['choices'] = [ (x.strip(), x.strip()) for x in field.choices.split(',')] + options['initial'] = [ x.strip() for x in field.default_value.split(',') ] return django.forms.MultipleChoiceField(widget=django.forms.CheckboxSelectMultiple, **options) def create_checkbox_field(self, field, options): diff --git a/wagtail/wagtailforms/models.py b/wagtail/wagtailforms/models.py index ff758497c..9eed4f61b 100644 --- a/wagtail/wagtailforms/models.py +++ b/wagtail/wagtailforms/models.py @@ -9,6 +9,8 @@ import re from wagtail.wagtailcore.models import Page, Orderable from wagtail.wagtailadmin.edit_handlers import FieldPanel, InlinePanel +from wagtail.wagtailforms.backends.email import EmailFormProcessor + from modelcluster.fields import ParentalKey from .forms import FormBuilder @@ -47,8 +49,8 @@ class AbstractFormFields(models.Model): label = models.CharField(max_length=255, help_text=_('The label of the form field') ) field_type = models.CharField(max_length=16, choices = FORM_FIELD_CHOICES) required = models.BooleanField(default=True) - choices = models.CharField(max_length=512, blank=True, help_text=_('Comma seperated list of choices')) - default_value = models.CharField(max_length=255, blank=True) + choices = models.CharField(max_length=512, blank=True, help_text=_('Comma seperated list of choices. Only applicable in checkboxes, radio and dropdown.')) + default_value = models.CharField(max_length=255, blank=True, help_text=_('Default value. Comma seperated values supported for checkboxes.')) help_text = models.CharField(max_length=255, blank=True) panels = [ @@ -65,12 +67,12 @@ class AbstractFormFields(models.Model): class AbstractForm(Page): - """A Form Page. Pages with form should inhert from it""" + """A Form Page. Pages implementing a form should inhert from it""" form_builder = FormBuilder is_abstract = True # Don't display me in "Add" def __init__(self, *args, **kwargs): - super(Page, self).__init__(*args, **kwargs) + super(AbstractForm, self).__init__(*args, **kwargs) if not hasattr(self, 'landing_page_template'): template_wo_ext = re.match(HTML_EXTENSION_RE, self.template).group(1) self.landing_page_template = template_wo_ext + '_landing.html' @@ -93,10 +95,14 @@ class AbstractForm(Page): ) FormSubmission.objects.create( form_data = json.dumps(form_data), - form_page = self.page_ptr, + form_page = self, user = request.user, ) - # TODO: Do other things like sending email + # If we have a form_processing_backend call its process method + if hasattr(self, 'form_processing_backend'): + form_processor = self.form_processing_backend() + form_processor.process(self, self.form) + # render the landing_page # TODO: It is much better to redirect to it return render(request, self.landing_page_template, { @@ -111,6 +117,17 @@ class AbstractForm(Page): }) +class AbstractEmailForm(AbstractForm): + """A Form Page that sends email. Pages implementing a form that should be send to an email should inhert from it""" + is_abstract = True # Don't display me in "Add" + form_processing_backend = EmailFormProcessor + + email_to = models.CharField(max_length=255, ) + email_from = models.CharField(max_length=255, ) + + class Meta: + abstract = True + ######## TEST class ConcreteFormFields(Orderable, AbstractFormFields): @@ -124,3 +141,18 @@ ConcreteForm.content_panels = [ FieldPanel('thank_you', classname="full"), InlinePanel(ConcreteForm, 'form_fields', label="Form Fields"), ] + +######## +class ConcreteEmailFormFields(Orderable, AbstractFormFields): + page = ParentalKey('wagtailforms.ConcreteEmailForm', related_name='form_fields') + +class ConcreteEmailForm(AbstractEmailForm): + thank_you = models.CharField(max_length=255) + +ConcreteEmailForm.content_panels = [ + FieldPanel('title', classname="full title"), + FieldPanel('thank_you', classname="full"), + FieldPanel('email_from', classname="full"), + FieldPanel('email_to', classname="full"), + InlinePanel(ConcreteEmailForm, 'form_fields', label="Form Fields"), +] From 680119dfef425a6cbc8e44f58c471fcb791f1a18 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Sat, 22 Mar 2014 08:18:53 +0200 Subject: [PATCH 005/189] Make from_address optional in send_email_task --- wagtail/wagtailadmin/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/tasks.py b/wagtail/wagtailadmin/tasks.py index d63b17f79..59779d36b 100644 --- a/wagtail/wagtailadmin/tasks.py +++ b/wagtail/wagtailadmin/tasks.py @@ -88,7 +88,7 @@ def send_notification(page_revision_id, notification, excluded_user_id): @task -def send_email_task(email_subject, email_content, from_email, email_addresses): +def send_email_task(email_subject, email_content, email_addresses, from_email=None): if not from_email: if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'): from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL From 3855da21bf386c821a879339cf55bb2a0094e392 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Sat, 22 Mar 2014 09:40:40 +0200 Subject: [PATCH 006/189] Add metaclass for AbstractForm and validate ... FormEmailProcessor. The metaclass is used to add each non-abstract form in a registry in a similar way as for Pages. It also checks if an form_processing_backend is defined and if it is it will call its validate_usage method. This way, the validate_usage method can throw immediately and ImproperlyConfigured method so the developer would know if he's done something wrong. The validate_usage method of the FormEmailProcessor has been implemented this way. --- wagtail/wagtailforms/backends/email.py | 10 ++++-- wagtail/wagtailforms/models.py | 49 +++++++++++++++++++++----- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/wagtail/wagtailforms/backends/email.py b/wagtail/wagtailforms/backends/email.py index ec1677318..063a2ce65 100644 --- a/wagtail/wagtailforms/backends/email.py +++ b/wagtail/wagtailforms/backends/email.py @@ -9,9 +9,15 @@ class EmailFormProcessor(BaseFormProcessor): def __init__(self): pass + @staticmethod def validate_usage(page): - return True + try: + page._meta.get_field('subject') + page._meta.get_field('to_address') + page._meta.get_field('from_address') + except: + raise ImproperlyConfigured("To use the EmailFormProcessor your Page must define the fields: subject, to_address and from_address.") def process(self, page, form): content = ', '.join([ x[1].label +': '+ form.data.get(x[0]) for x in form.fields.items() ]) - tasks.send_email_task.delay("New " + page.title+" form submission at " + str(datetime.datetime.now()) , content, page.email_from, [page.email_to] ) + tasks.send_email_task.delay(page.subject, content, [page.to_address], page.from_address, ) diff --git a/wagtail/wagtailforms/models.py b/wagtail/wagtailforms/models.py index 9eed4f61b..f64f56df7 100644 --- a/wagtail/wagtailforms/models.py +++ b/wagtail/wagtailforms/models.py @@ -1,4 +1,6 @@ + from django.conf import settings +from django.contrib.contenttypes.models import ContentType from django.db import models from django.shortcuts import render from django.utils.translation import ugettext_lazy as _ @@ -6,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _ import json import re -from wagtail.wagtailcore.models import Page, Orderable +from wagtail.wagtailcore.models import PageBase, Page, Orderable from wagtail.wagtailadmin.edit_handlers import FieldPanel, InlinePanel from wagtail.wagtailforms.backends.email import EmailFormProcessor @@ -66,11 +68,39 @@ class AbstractFormFields(models.Model): abstract = True +FORM_MODEL_CLASSES = [] +_FORM_CONTENT_TYPES = [] + +def get_form_types(): + global _FORM_CONTENT_TYPES + if len(_FORM_CONTENT_TYPES) != len(FORM_MODEL_CLASSES): + _FORM_CONTENT_TYPES = [ + ContentType.objects.get_for_model(cls) for cls in FORM_MODEL_CLASSES + ] + return _FORM_CONTENT_TYPES + + +class FormBase(PageBase): + """Metaclass for Forms""" + def __init__(cls, name, bases, dct): + super(FormBase, cls).__init__(name, bases, dct) + + if not cls.is_abstract: + # register this type in the list of page content types + FORM_MODEL_CLASSES.append(cls) + # Check if form_processing_backend is ok + if hasattr(cls, 'form_processing_backend'): + cls.form_processing_backend.validate_usage(cls) + + class AbstractForm(Page): """A Form Page. Pages implementing a form should inhert from it""" + + __metaclass__ = FormBase + form_builder = FormBuilder is_abstract = True # Don't display me in "Add" - + def __init__(self, *args, **kwargs): super(AbstractForm, self).__init__(*args, **kwargs) if not hasattr(self, 'landing_page_template'): @@ -118,17 +148,18 @@ class AbstractForm(Page): class AbstractEmailForm(AbstractForm): - """A Form Page that sends email. Pages implementing a form that should be send to an email should inhert from it""" + """A Form Page that sends email. Pages implementing a form to be send to an email should inherit from it""" is_abstract = True # Don't display me in "Add" form_processing_backend = EmailFormProcessor - - email_to = models.CharField(max_length=255, ) - email_from = models.CharField(max_length=255, ) - + + to_address = models.CharField(max_length=255, ) + from_address = models.CharField(max_length=255, blank=True) + subject = models.CharField(max_length=255, ) + class Meta: abstract = True - - + + ######## TEST class ConcreteFormFields(Orderable, AbstractFormFields): page = ParentalKey('wagtailforms.ConcreteForm', related_name='form_fields') From 49a1ef30791fd9915fbbbea647bdcee2833d029d Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Sat, 22 Mar 2014 09:45:49 +0200 Subject: [PATCH 007/189] Add a basic views skeleton for wagtailforms --- .../templates/wagtailforms/index.html | 23 ++++ wagtail/wagtailforms/urls.py | 14 ++ wagtail/wagtailforms/views.py | 125 ++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 wagtail/wagtailforms/templates/wagtailforms/index.html create mode 100644 wagtail/wagtailforms/urls.py create mode 100644 wagtail/wagtailforms/views.py diff --git a/wagtail/wagtailforms/templates/wagtailforms/index.html b/wagtail/wagtailforms/templates/wagtailforms/index.html new file mode 100644 index 000000000..b4928e8e3 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/index.html @@ -0,0 +1,23 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% block titletag %}{% trans "Forms" %}{% endblock %} +{% block bodyclass %}menu-forms{% endblock %} +{% block content %} + + {% include "wagtailadmin/shared/header.html" with title="Forms" %} + +
+
    + {% for name, description, content_type in form_types %} +
  • +
    + + {{ name|capfirst }} + + {{ description }} +
    +
  • + {% endfor %} +
+
+{% endblock %} diff --git a/wagtail/wagtailforms/urls.py b/wagtail/wagtailforms/urls.py new file mode 100644 index 000000000..7de8b003b --- /dev/null +++ b/wagtail/wagtailforms/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls import patterns, url + + +urlpatterns = patterns( + 'wagtail.wagtailforms.views', + url(r'^$', 'index', name='wagtailforms_index'), + + #url(r'^choose/$', 'chooser.choose', name='wagtailsnippets_choose_generic'), + #url(r'^choose/(\w+)/(\w+)/$', 'chooser.choose', name='wagtailsnippets_choose'), + + #url(r'^(\w+)/(\w+)/$', 'snippets.list', name='wagtailsnippets_list'), + #url(r'^(\w+)/(\w+)/new/$', 'snippets.create', name='wagtailsnippets_create'), + #url(r'^(\w+)/(\w+)/(\d+)/$', 'snippets.edit', name='wagtailsnippets_edit'), +) diff --git a/wagtail/wagtailforms/views.py b/wagtail/wagtailforms/views.py new file mode 100644 index 000000000..1814cd814 --- /dev/null +++ b/wagtail/wagtailforms/views.py @@ -0,0 +1,125 @@ +from django.http import Http404 +from django.shortcuts import get_object_or_404, render, redirect +from django.utils.encoding import force_text +from django.utils.text import capfirst +from django.contrib.contenttypes.models import ContentType +from django.contrib import messages +from django.contrib.auth.decorators import permission_required +from django.core.exceptions import PermissionDenied +from django.utils.translation import ugettext as _ + +from wagtail.wagtailadmin.edit_handlers import ObjectList, extract_panel_definitions_from_model_class + + +@permission_required('wagtailadmin.access_admin') +def index(request): + + + return render(request, 'wagtailforms/index.html', { + #'snippet_types': snippet_types, + }) + +""" +@permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view +def list(request, content_type_app_name, content_type_model_name): + content_type = get_content_type_from_url_params(content_type_app_name, content_type_model_name) + if not user_can_edit_snippet_type(request.user, content_type): + raise PermissionDenied + + model = content_type.model_class() + snippet_type_name, snippet_type_name_plural = get_snippet_type_name(content_type) + + items = model.objects.all() + + return render(request, 'wagtailsnippets/snippets/type_index.html', { + 'content_type': content_type, + 'snippet_type_name': snippet_type_name, + 'snippet_type_name_plural': snippet_type_name_plural, + 'items': items, + }) + + +@permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view +def create(request, content_type_app_name, content_type_model_name): + content_type = get_content_type_from_url_params(content_type_app_name, content_type_model_name) + if not user_can_edit_snippet_type(request.user, content_type): + raise PermissionDenied + + model = content_type.model_class() + snippet_type_name = get_snippet_type_name(content_type)[0] + + instance = model() + edit_handler_class = get_snippet_edit_handler(model) + form_class = edit_handler_class.get_form_class(model) + + if request.POST: + form = form_class(request.POST, request.FILES, instance=instance) + + if form.is_valid(): + form.save() + + messages.success( + request, + _("{snippet_type} '{instance}' created.").format( + snippet_type=capfirst(get_snippet_type_name(content_type)[0]), + instance=instance + ) + ) + return redirect('wagtailsnippets_list', content_type.app_label, content_type.model) + else: + messages.error(request, _("The snippet could not be created due to errors.")) + edit_handler = edit_handler_class(instance=instance, form=form) + else: + form = form_class(instance=instance) + edit_handler = edit_handler_class(instance=instance, form=form) + + return render(request, 'wagtailsnippets/snippets/create.html', { + 'content_type': content_type, + 'snippet_type_name': snippet_type_name, + 'edit_handler': edit_handler, + }) + + +@permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view +def edit(request, content_type_app_name, content_type_model_name, id): + content_type = get_content_type_from_url_params(content_type_app_name, content_type_model_name) + if not user_can_edit_snippet_type(request.user, content_type): + raise PermissionDenied + + model = content_type.model_class() + snippet_type_name = get_snippet_type_name(content_type)[0] + + instance = get_object_or_404(model, id=id) + edit_handler_class = get_snippet_edit_handler(model) + form_class = edit_handler_class.get_form_class(model) + + if request.POST: + form = form_class(request.POST, request.FILES, instance=instance) + + if form.is_valid(): + form.save() + + messages.success( + request, + _("{snippet_type} '{instance}' updated.").format( + snippet_type=capfirst(snippet_type_name), + instance=instance + ) + ) + return redirect('wagtailsnippets_list', content_type.app_label, content_type.model) + else: + messages.error(request, _("The snippet could not be saved due to errors.")) + edit_handler = edit_handler_class(instance=instance, form=form) + else: + form = form_class(instance=instance) + edit_handler = edit_handler_class(instance=instance, form=form) + + return render(request, 'wagtailsnippets/snippets/edit.html', { + 'content_type': content_type, + 'snippet_type_name': snippet_type_name, + 'instance': instance, + 'edit_handler': edit_handler, + }) + + +""" \ No newline at end of file From e3cb37ca72e43c2b544a9f1bef10942f47ce154a Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Sat, 22 Mar 2014 13:38:37 +0200 Subject: [PATCH 008/189] Add basic form submission views Add two urls: index which lists all different forms that have been defined and submissions which for a specific form will list all its submissions. The index lists each Page seperately (so if we have two different instances of a form they will be listed seperately here), so the submissions view will need the app_label, model_name and id to find out all the submissions of a specific page. --- .../templates/wagtailforms/form_index.html | 25 +++++++++ .../templates/wagtailforms/index.html | 14 ++--- .../templates/wagtailforms/list.html | 28 ++++++++++ wagtail/wagtailforms/urls.py | 7 +-- wagtail/wagtailforms/views.py | 53 ++++++++++++------- 5 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 wagtail/wagtailforms/templates/wagtailforms/form_index.html create mode 100644 wagtail/wagtailforms/templates/wagtailforms/list.html diff --git a/wagtail/wagtailforms/templates/wagtailforms/form_index.html b/wagtail/wagtailforms/templates/wagtailforms/form_index.html new file mode 100644 index 000000000..143542367 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/form_index.html @@ -0,0 +1,25 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% block titletag %}{% blocktrans with form_title=form_page.title|capfirst %}Submissions of {{ form_title }}{% endblocktrans %}{% endblock %} +{% block bodyclass %}menu-snippets{% endblock %} +{% block content %} + +
+
+
+

+ {% blocktrans with form_title=form_page.title|capfirst %}Submissions of {{ form_title }}{% endblocktrans %} +

+
+ +
+
+
+
+ {% if submissions %} + {% include "wagtailforms/list.html" %} + {% else %} +

{% blocktrans with title=form_page.title %}No submissions of the '{{ title }}' form.{% endblocktrans %}

+ {% endif %} +
+{% endblock %} diff --git a/wagtail/wagtailforms/templates/wagtailforms/index.html b/wagtail/wagtailforms/templates/wagtailforms/index.html index b4928e8e3..d99fc8d98 100644 --- a/wagtail/wagtailforms/templates/wagtailforms/index.html +++ b/wagtail/wagtailforms/templates/wagtailforms/index.html @@ -3,18 +3,20 @@ {% block titletag %}{% trans "Forms" %}{% endblock %} {% block bodyclass %}menu-forms{% endblock %} {% block content %} - - {% include "wagtailadmin/shared/header.html" with title="Forms" %} + {% trans "Forms" as forms_str %} + {% trans "Please select a form to view its submissions" as select_form_str %} + {% include "wagtailadmin/shared/header.html" with title=forms_str subtitle=select_form_str %}
    - {% for name, description, content_type in form_types %} + {% for fp in form_pages %}
  • - - {{ name|capfirst }} + + {{ fp|capfirst }} - {{ description }} + + {{ fp.content_type.name |capfirst }} ({{ fp.content_type.app_label }}.{{ fp.content_type.model }})
  • {% endfor %} diff --git a/wagtail/wagtailforms/templates/wagtailforms/list.html b/wagtail/wagtailforms/templates/wagtailforms/list.html new file mode 100644 index 000000000..243846615 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/list.html @@ -0,0 +1,28 @@ +{% load i18n %} + + + + + + + + + + + + + {% for submission in submissions %} + + + + + + {% endfor %} + +
    {% trans "Submission Date" %}{% trans "User" %}{% trans "Data" %}
    + {{ submission.submit_time }} + + {{ submission.user }} + + {{ submission.form_data }} +
    \ No newline at end of file diff --git a/wagtail/wagtailforms/urls.py b/wagtail/wagtailforms/urls.py index 7de8b003b..afcaaa5a6 100644 --- a/wagtail/wagtailforms/urls.py +++ b/wagtail/wagtailforms/urls.py @@ -4,11 +4,6 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'wagtail.wagtailforms.views', url(r'^$', 'index', name='wagtailforms_index'), + url(r'^submissions/(\w+)/(\w+)/(\d+)/$', 'list_submissions', name='wagtailforms_list_submissions'), - #url(r'^choose/$', 'chooser.choose', name='wagtailsnippets_choose_generic'), - #url(r'^choose/(\w+)/(\w+)/$', 'chooser.choose', name='wagtailsnippets_choose'), - - #url(r'^(\w+)/(\w+)/$', 'snippets.list', name='wagtailsnippets_list'), - #url(r'^(\w+)/(\w+)/new/$', 'snippets.create', name='wagtailsnippets_create'), - #url(r'^(\w+)/(\w+)/(\d+)/$', 'snippets.edit', name='wagtailsnippets_edit'), ) diff --git a/wagtail/wagtailforms/views.py b/wagtail/wagtailforms/views.py index 1814cd814..a263acaf0 100644 --- a/wagtail/wagtailforms/views.py +++ b/wagtail/wagtailforms/views.py @@ -9,35 +9,48 @@ from django.core.exceptions import PermissionDenied from django.utils.translation import ugettext as _ from wagtail.wagtailadmin.edit_handlers import ObjectList, extract_panel_definitions_from_model_class +from wagtail.wagtailcore.models import Page +from wagtail.wagtailforms.models import FormSubmission, get_form_types + + +def get_form_type_from_url_params(app_name, model_name): + """ + Retrieve a form type from an app_name / model_name combo. + Throw Http404 if not a valid form type + """ + try: + content_type = ContentType.objects.get_by_natural_key(app_name, model_name) + except ContentType.DoesNotExist: + raise Http404 + if content_type not in get_form_types(): + raise Http404 + + return content_type @permission_required('wagtailadmin.access_admin') def index(request): - + form_types = get_form_types() + form_pages = Page.objects.filter(content_type__in=form_types) return render(request, 'wagtailforms/index.html', { - #'snippet_types': snippet_types, + 'form_pages': form_pages, + }) + +@permission_required('wagtailadmin.access_admin') +def list_submissions(request, app_label, model, id): + + model = get_form_type_from_url_params(app_label, model).model_class() + form_page = get_object_or_404(model, id=id) + + submissions = FormSubmission.objects.filter(form_page=form_page) + + return render(request, 'wagtailforms/form_index.html', { + 'form_page': form_page, + 'submissions': submissions, }) """ -@permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view -def list(request, content_type_app_name, content_type_model_name): - content_type = get_content_type_from_url_params(content_type_app_name, content_type_model_name) - if not user_can_edit_snippet_type(request.user, content_type): - raise PermissionDenied - - model = content_type.model_class() - snippet_type_name, snippet_type_name_plural = get_snippet_type_name(content_type) - - items = model.objects.all() - - return render(request, 'wagtailsnippets/snippets/type_index.html', { - 'content_type': content_type, - 'snippet_type_name': snippet_type_name, - 'snippet_type_name_plural': snippet_type_name_plural, - 'items': items, - }) - @permission_required('wagtailadmin.access_admin') # further permissions are enforced within the view def create(request, content_type_app_name, content_type_model_name): From 2a4c371f51b5a53d9fdbab0faa2c07c80fb3921e Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Sat, 22 Mar 2014 18:23:16 +0200 Subject: [PATCH 009/189] Change pagination_nav.html to work without linkurl pagination_nav.html received a linkurl parameter which has to be a url name that get resolved to generate the link for next/previous. The thing is that if the url to be resolved had parameters, these wouldn't be passed and an exception would be thrown when trying to resolve it. A quick and dirty fix is to just use {% url linkurl as link_to_use %} so link_to_use will be empty and the currrent page url will be used. --- .../templates/wagtailadmin/shared/pagination_nav.html | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/shared/pagination_nav.html b/wagtail/wagtailadmin/templates/wagtailadmin/shared/pagination_nav.html index 39eb88a15..847b971e0 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/shared/pagination_nav.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/shared/pagination_nav.html @@ -1,4 +1,5 @@ {% load i18n %} +{% url linkurl as url_to_use %}
- -
{% if submissions %} - {% include "wagtailforms/list.html" %} - {% include "wagtailadmin/shared/pagination_nav.html" with items=submissions is_searching=False linkurl='-' %}{# Here we pass an invalid non-empty URL name as linkurl to generate pagination links of the form "?p=123" with the URL path omitted #} + {% include "wagtailforms/list_submissions.html" %} + + {% include "wagtailadmin/shared/pagination_nav.html" with items=submissions is_searching=False linkurl='-' %} + {# Here we pass an invalid non-empty URL name as linkurl to generate pagination links with the URL path omitted #} {% else %}

{% blocktrans with title=form_page.title %}No submissions of the '{{ title }}' form.{% endblocktrans %}

{% endif %} diff --git a/wagtail/wagtailforms/templates/wagtailforms/list_forms.html b/wagtail/wagtailforms/templates/wagtailforms/list_forms.html new file mode 100644 index 000000000..b1958f5b2 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/list_forms.html @@ -0,0 +1,23 @@ +{% load i18n %} + + + + + + + + + + + {% for fp in form_pages %} + + + + + {% endfor %} + +
{% trans "Title" %}{% trans "Origin" %}
+

{{ fp|capfirst }}

+
+ {{ fp.content_type.name |capfirst }} ({{ fp.content_type.app_label }}.{{ fp.content_type.model }}) +
\ No newline at end of file diff --git a/wagtail/wagtailforms/templates/wagtailforms/list.html b/wagtail/wagtailforms/templates/wagtailforms/list_submissions.html similarity index 90% rename from wagtail/wagtailforms/templates/wagtailforms/list.html rename to wagtail/wagtailforms/templates/wagtailforms/list_submissions.html index 4dea4a61d..0ead37bde 100644 --- a/wagtail/wagtailforms/templates/wagtailforms/list.html +++ b/wagtail/wagtailforms/templates/wagtailforms/list_submissions.html @@ -2,9 +2,9 @@ - + - + {% for heading in data_headings %} diff --git a/wagtail/wagtailforms/templates/wagtailforms/results_forms.html b/wagtail/wagtailforms/templates/wagtailforms/results_forms.html new file mode 100644 index 000000000..6cf198553 --- /dev/null +++ b/wagtail/wagtailforms/templates/wagtailforms/results_forms.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% if form_pages %} + {% include "wagtailforms/list_forms.html" %} + + {% include "wagtailadmin/shared/pagination_nav.html" with items=form_pages linkurl="wagtailforms_index" %} +{% else %} +

{% trans "No form pages have been created." %}

+{% endif %} diff --git a/wagtail/wagtailforms/views.py b/wagtail/wagtailforms/views.py index f5ce0b07a..aa3d525df 100644 --- a/wagtail/wagtailforms/views.py +++ b/wagtail/wagtailforms/views.py @@ -14,8 +14,19 @@ from wagtail.wagtailforms.forms import SelectDateForm @permission_required('wagtailadmin.access_admin') def index(request): + p = request.GET.get("p", 1) + form_pages = get_forms_for_user(request.user) + paginator = Paginator(form_pages, 20) + + try: + form_pages = paginator.page(p) + except PageNotAnInteger: + form_pages = paginator.page(1) + except EmptyPage: + form_pages = paginator.page(paginator.num_pages) + return render(request, 'wagtailforms/index.html', { 'form_pages': form_pages, }) @@ -68,7 +79,7 @@ def list_submissions(request, page_id): return response p = request.GET.get('p', 1) - paginator = Paginator(submissions, 20) + paginator = Paginator(submissions, 1) try: submissions = paginator.page(p) @@ -84,7 +95,7 @@ def list_submissions(request, page_id): data_row = [s.submit_time] + [form_data.get(name) for name, label in data_fields] data_rows.append(data_row) - return render(request, 'wagtailforms/form_index.html', { + return render(request, 'wagtailforms/index_submissions.html', { 'form_page': form_page, 'select_date_form': select_date_form, 'submissions': submissions, From aa41a5e8a9a11e1c7babbcf547b0ea590e414e87 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 13 May 2014 12:45:49 +0100 Subject: [PATCH 067/189] updating header form style --- .../templates/wagtailforms/index.html | 2 +- .../wagtailforms/index_submissions.html | 47 ++++++++++--------- wagtail/wagtailforms/views.py | 2 +- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/wagtail/wagtailforms/templates/wagtailforms/index.html b/wagtail/wagtailforms/templates/wagtailforms/index.html index e542e596d..c3a831501 100644 --- a/wagtail/wagtailforms/templates/wagtailforms/index.html +++ b/wagtail/wagtailforms/templates/wagtailforms/index.html @@ -4,7 +4,7 @@ {% block bodyclass %}menu-forms{% endblock %} {% block content %} {% trans "Forms" as forms_str %} - {% trans "Data" as select_form_str %} + {% trans "Pages" as select_form_str %} {% include "wagtailadmin/shared/header.html" with title=forms_str subtitle=select_form_str %}
diff --git a/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html b/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html index 26ed4a7bd..7bba352fb 100644 --- a/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html +++ b/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html @@ -30,27 +30,32 @@ {% endblock %} {% block content %}
-
-
-

- {% blocktrans with form_title=form_page.title|capfirst %}Submissions of {{ form_title }}{% endblocktrans %} -

-
-
-
    - {% for field in select_date_form %} -
    - {% include "wagtailadmin/shared/field_as_li.html" with field=field %} -
    - {% endfor %} -
    -
  • -
  • + +
    +
    +
    +

    + {% blocktrans with form_title=form_page.title|capfirst %}Form data {{ form_title }}{% endblocktrans %} +

    - -
- -
+ +
+
+ +
+ +
{% if submissions %} @@ -59,7 +64,7 @@ {% include "wagtailadmin/shared/pagination_nav.html" with items=submissions is_searching=False linkurl='-' %} {# Here we pass an invalid non-empty URL name as linkurl to generate pagination links with the URL path omitted #} {% else %} -

{% blocktrans with title=form_page.title %}No submissions of the '{{ title }}' form.{% endblocktrans %}

+

{% blocktrans with title=form_page.title %}There have been no submissions of the '{{ title }}' form.{% endblocktrans %}

{% endif %}
{% endblock %} diff --git a/wagtail/wagtailforms/views.py b/wagtail/wagtailforms/views.py index aa3d525df..2174642c6 100644 --- a/wagtail/wagtailforms/views.py +++ b/wagtail/wagtailforms/views.py @@ -79,7 +79,7 @@ def list_submissions(request, page_id): return response p = request.GET.get('p', 1) - paginator = Paginator(submissions, 1) + paginator = Paginator(submissions, 20) try: submissions = paginator.page(p) From 6f5203c6fdd052ca8b274246c261c0e5b6437593 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 13 May 2014 15:51:28 +0100 Subject: [PATCH 068/189] Medium-scale changes to fields to allow 'iconfields' and header refactored into own css file --- .../static/wagtailadmin/js/core.js | 2 +- .../wagtailadmin/scss/components/forms.scss | 63 +++++++- .../wagtailadmin/scss/components/header.scss | 153 ++++++++++++++++++ .../wagtailadmin/scss/components/icons.scss | 10 +- .../static/wagtailadmin/scss/core.scss | 129 +-------------- .../wagtailadmin/scss/fonts/wagtail.eot | Bin 12996 -> 13308 bytes .../wagtailadmin/scss/fonts/wagtail.svg | 4 +- .../wagtailadmin/scss/fonts/wagtail.ttf | Bin 12832 -> 13144 bytes .../wagtailadmin/scss/fonts/wagtail.woff | Bin 9164 -> 9356 bytes .../wagtailadmin/shared/field_as_li.html | 10 +- .../templates/wagtailadmin/shared/header.html | 6 +- .../templates/wagtailforms/index.html | 2 +- .../wagtailforms/index_submissions.html | 12 +- wagtail/wagtailforms/wagtail_hooks.py | 2 +- 14 files changed, 237 insertions(+), 156 deletions(-) create mode 100644 wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/core.js b/wagtail/wagtailadmin/static/wagtailadmin/js/core.js index e5895e34f..380c3d179 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/core.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/core.js @@ -128,7 +128,7 @@ $(function(){ $(window.headerSearch.termInput).trigger('focus'); function search () { - var workingClasses = "working icon icon-spinner"; + var workingClasses = "icon-spinner"; $(window.headerSearch.termInput).parent().addClass(workingClasses); search_next_index++; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss index 2425458ee..fa35e6226 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/forms.scss @@ -356,12 +356,6 @@ button.icon{ color:$color-grey-2; } -/* permanently show checkbox/radio help as they have no focus state */ -.boolean_field .help, .radio .help{ - opacity:1; -} - - fieldset:hover > .help, .field.focused + .help, .field:focus + .help, @@ -391,7 +385,58 @@ li.focused > .help{ background-color:$color-input-error-bg; } + +/* Layouts for particular kinds of of fields */ + +/* permanently show checkbox/radio help as they have no focus state */ +.boolean_field .help, .radio .help{ + opacity:1; +} +.iconfield { + position:relative; + + input:not([type=radio]), input:not([type=checkbox]), input:not([type=submit]), input:not([type=button]){ + padding-left:2.5em; + } + + &:before, &:after{ + font-family:wagtail; + position:absolute; + top:0.4em; + font-size:1.4em; + color:$color-grey-3; + } + &:before{ + left:0.5em; + } + &:after{ + right:0.5em; + } + + /* special case for search spinners */ + &.icon-spinner:after{ + color:$color-teal; + opacity:0.8; + font-size:20px; + width:20px; + height:20px; + line-height:23px; + text-align:center; + top:0.3em; + + } +} + + /* field sizing */ + +.field-small{ + input, textarea, select, .richtext, .tagit{ + @include border-radius(3px); + padding:0.4em 1em; + } +} + .field{ &.col1, &.col2, @@ -568,7 +613,8 @@ ul.tagit li.tagit-choice-editable{ } -/* search bars (search integrated into header area) */ +/* +// search bars (search integrated into header area) .search-bar{ margin-top:-2em; padding-top:1em; @@ -666,12 +712,13 @@ ul.tagit li.tagit-choice-editable{ } } -/* mozilla specific hack */ +// mozilla specific hack @-moz-document url-prefix() { .search-bar .fields .field:after{ line-height:20px; } } +*/ /* Transitions */ fieldset, input, textarea, select{ diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss new file mode 100644 index 000000000..1c7f7435a --- /dev/null +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/header.scss @@ -0,0 +1,153 @@ +header{ + padding-top:1em; + padding-bottom:1em; + background-color: $color-header-bg; + margin-bottom:2em; + color:white; + + h1, h2{ + margin:0; + color:white; + } + + h1{ + padding:0.2em 0; + + &.icon:before{ + width:1em; + display:none; + margin-right:0.4em; + font-size:1.5em; + } + } + + .col{ + float:left; + margin-right:2em; + } + .left{ + float:left; + + .hasform &:first-child{ + padding-bottom:0.5em; + float:none; + } + } + .right{ + text-align:right; + float:right; + } + + /* For case where content below header should merge with it */ + &.merged{ + margin-bottom:0; + } + &.tab-merged, &.no-border{ + border:0; + } + &.merged.no-border{ + padding-bottom:0; + } + &.no-v-padding{ + padding-top:0; + padding-bottom:0; + } + /* + &.hasform h1{ + margin-top:0.2em; + } + */ + .button{ + background-color:$color-teal-darker; + &:hover{ + background-color:$color-teal-dark; + } + } + + /* necessary on mobile only to make way for hamburger menu */ + &.nice-padding{ + padding-left:4em; + } + + label{ + @include visuallyhidden(); + } + + input[type=text], select{ + border-width:0; + + &:focus{ + background-color:white; + } + } + + .fields{ + margin-top:-0.5em; + + .field{ + padding:0; + } + } +} + +/* mozilla specific hack */ +@-moz-document url-prefix() { + .search-form .fields .field:after{ + line-height:20px; + } +} + +.page-explorer header{ + margin-bottom:0; + padding-bottom:0em; +} + + +@media screen and (min-width: $breakpoint-mobile){ + header{ + padding-top:1.5em; + padding-bottom:1.5em; + + &.nice-padding{ + @include nice-padding(); + } + + .left{ + float:left; + margin-right:0; + + &:first-child{ + padding-bottom:0; + float:left; + } + } + .second{ + clear:none; + + .right, .left{ + float:right; + } + } + + h1.icon:before{ + display:inline-block; + } + + .col3{ + @include column(3); + } + .col3.addbutton{ + width:auto; + } + .col6{ + @include column(6); + } + .col9{ + @include column(9); + } + .breadcrumb{ + margin-left:-($desktop-nice-padding); + margin-right:-($desktop-nice-padding); + } + } +} \ No newline at end of file diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss index 9a1351d9a..4de3aa131 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/components/icons.scss @@ -231,14 +231,20 @@ .icon-collapse-up:before{ content:"6"; } +.icon-date:before{ + content:"7"; +} +.icon-success:before{ + content:"9"; +} .icon-help:before{ content:"?"; } .icon-warning:before{ content:"!"; } -.icon-success:before{ - content:"9"; +.icon-form:before{ + content:"$"; } .icon.text-replace{ diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss index 8ffaa450a..d2e9e4759 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss @@ -11,6 +11,7 @@ @import "components/listing.scss"; @import "components/messages.scss"; @import "components/formatters.scss"; +@import "components/header.scss"; @import "fonts.scss"; @@ -385,88 +386,6 @@ body.explorer-open { } } -header{ - padding-top:1em; - padding-bottom:1em; - background-color: $color-header-bg; - margin-bottom:2em; - color:white; - - h1, h2{ - margin:0; - color:white; - } - - h1{ - padding:0.2em 0; - - &.icon:before{ - width:1em; - display:none; - margin-right:0.4em; - font-size:1.5em; - } - } - - .col{ - float:left; - margin-right:2em; - } - .left{ - float:left; - - .hasform &:first-child{ - padding-bottom:0.5em; - float:none; - } - } - .search-bar input{ - @include border-radius(3px); - width:auto; - border-width:0; - } - .right{ - text-align:right; - float:right; - } - - /* For case where content below header should merge with it */ - &.merged{ - margin-bottom:0; - } - &.tab-merged, &.no-border{ - border:0; - } - &.merged.no-border{ - padding-bottom:0; - } - &.no-v-padding{ - padding-top:0; - padding-bottom:0; - } - /* - &.hasform h1{ - margin-top:0.2em; - } - */ - .button{ - background-color:$color-teal-darker; - &:hover{ - background-color:$color-teal-dark; - } - } - /* necessary on mobile only to make way for hamburger menu */ - &.nice-padding{ - padding-left:4em; - } -} - -.page-explorer header{ - margin-bottom:0; - padding-bottom:0em; -} - - footer{ @include row(); @include border-radius(3px 3px 0 0); @@ -832,52 +751,6 @@ footer, .logo{ } } - header{ - padding-top:1.5em; - padding-bottom:1.5em; - - &.nice-padding{ - @include nice-padding(); - } - - .left{ - float:left; - margin-right:0; - - &:first-child{ - padding-bottom:0; - float:left; - } - } - .second{ - clear:none; - - .right, .left{ - float:right; - } - } - - h1.icon:before{ - display:inline-block; - } - - .col3{ - @include column(3); - } - .col3.addbutton{ - width:auto; - } - .col6{ - @include column(6); - } - .col9{ - @include column(9); - } - .breadcrumb{ - margin-left:-($desktop-nice-padding); - margin-right:-($desktop-nice-padding); - } - } footer{ width:80%; margin-left:50px; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.eot b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.eot index f55be6906a2bfcc05ed4e43273c1ebe331ee1914..6ac63514abbec26479e6a6f827ba9212dd270687 100644 GIT binary patch delta 890 zcmZ8fOH30{6g{t12dgYDL)-R&_rV-G10J48m+J>(gaL^P3s0- zp*RZ@e_^9>;X)~i3nd{@;)bXz;=%#)Okhx0$4PF{TND&r3(dEDaKQ- zb0{@=(epBK5a3<_2g1Y2#08vqc3ZDtLdGrFb5!(%5@%AQ=M%%P$KsS2CI8w;Vj_*} ztd8P@A$Tbt zUD&q3#GRz{9BZ=mupnIeXSSIsL7$Om63uVy0KsevtdnOPAb`U=(ms?CVssJ_LJL}P z2m_d+%?&az$;7&lFfjqugxd<+X58px&<;}uaFler2P)#!N~6Lybm;@uz3h;lwWj!P zGwqjXSK}>OYg^Y%u9Bhxx1hh_rkwn`f7<^&z~_$Jk`4N&=JDLqrYOs}j2d&{D%4{? znxW7E?KEpIjsXpNuczn8aLMK2#v#7Io7SNwG}uo%ftMshHoD{* zqYWD@z~l3I2DLdYSmd=bNh%|z2l%~|6;71G3+!g1ml-4O5>>H^k+2|tZ&+Ad%uln) z{8ctdK3zn7~WTqHB6c+d!*S4@OW5 z^&o^fd9R>1B|Z2D)Pf)s6uqb@>jNJbFTIE*{${$MEWgKm-?zV+Z)g8h*Ijd605Q=t zvwYg~qRryf{#$P!0D=RsPL)qqFb5Mzer~=PNWVWF-UkFG0QRlwEY#a=1c`e`+Ky`N z`jwkMzyAREL*RJVjO$!RX>wH#Fssq|!Td!pJtpDZ+WaNwZ0_7)5>nvKtg}?d3Ohu+ zP3zd4GwYTMFFygJYlPjd&o3;7L?}WVDSj1gzD@%EIktNXr+c*;MkzrLyyt`gUBL}0;X_-JntY0jg;VM z#3ImWZ);A?0J?PUMTP1(GO&@O33-gkncxvtl=p(QUM=y{c*f2Z@?((Qd_U1cd`*7j zOT6z)-*W4s;T~({N7GC`8(EYq$)oXuxaOalBZNnOZy+@3XFN>4 zx^ds{4eY@azc&egEe{Bn*>YJ7h3( - + + + diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.ttf b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.ttf index a401ae2ddd45ba74cf37715cee04a925d9c13915..2006feefbf5c293024c0dcc039aa196fcb4aad67 100644 GIT binary patch delta 967 zcmZ8fOH31C5dQw{_CYI^)@^;T$QG&;G`*5}1j;)`(v!pPcj{??e+`@rrIP9pF1>n0xpmT5DwWKuyv%V_tQQ1R znTecBv@H@|AUvBM8&p#t#^cnWj_}=#n#~;g#vH|m%-BMkBbzZ#{1Y>_!jC00wh_OETTn5A z39>gqd#Z(%B5hEsnk1unw(H$=hQ%h$FCT>q;4G*M2LduDu0b G?)eMGS+v&x delta 698 zcmYLGO=uHA6#jO1vN1`m9$GCLA=N6V&?bv*k~We?V(LMuP*=pC%F?EZlB9tKjUG%v zDcFOcg9mROD(FpVkD?bTcq#~jpdv&IS}$ID5hQ-IS#*|p^Zm@enfKGyq#M z4+E!jxihCK^BBh{hpFcJL@f0}RQOrBj$kN|?*XqG9 z<}dQ+TEKr`f!4d0T1|As)rCc0XvhS3PJy(2n{tS_Gy%rfoKI2q;Gt;m)v|189<|R)rz3 z%5^(GwFl_M5RypaJZ`f1wi0!f3>;SpQ}ng|xSq-Zbhxn#Ij$oZL;`VUNMcyN(f4bk zazihvhWr~NgNb-LIShH#*iC!ESdl4F7u_ej7nWa&{K4gi{YNy}+h3FG(F2XWxaQqj zz>jAh_929~9yY13tBOQ`N+-{0W1YCi!v?a5dRS0@hEzK4hpC-UO@?v zi!obE$Y$&iW2ZGK%T`3o$a_{}Y$toghhkTB_a^G(9=A%x4U94h^7b>iy;_@G{jGNV If3u+d7u{ieXaE2J diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.woff b/wagtail/wagtailadmin/static/wagtailadmin/scss/fonts/wagtail.woff index fbefc159a6096afaf3cae10fbf91a86aba458210..072c20a1661ce2b507bdde0c196bb483c2f20f94 100644 GIT binary patch delta 9324 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 RIa`UsMInPfnvMa${s&%0s&N1S delta 9109 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 MuJv=#F(9=60A-IkQUCw| diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/shared/field_as_li.html b/wagtail/wagtailadmin/templates/wagtailadmin/shared/field_as_li.html index 131449915..e40e6dbda 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/shared/field_as_li.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/shared/field_as_li.html @@ -1,10 +1,12 @@ {% load wagtailadmin_tags %} -
  • +
  • {{ field.label_tag }} - {% block form_field %} - {{ field }} - {% endblock %} +
    + {% block form_field %} + {{ field }} + {% endblock %} +
    {% if field.errors %} diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/shared/header.html b/wagtail/wagtailadmin/templates/wagtailadmin/shared/header.html index 3ea5d4d10..59340d2d8 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/shared/header.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/shared/header.html @@ -5,12 +5,12 @@

    {{ title }} {{ subtitle }}

    {% if search_url %} - +
      {% for field in search_form %} - {% include "wagtailadmin/shared/field_as_li.html" with field=field %} + {% include "wagtailadmin/shared/field_as_li.html" with field=field input_classes="field-small iconfield icon-search" %} {% endfor %} - +
    {% endif %} diff --git a/wagtail/wagtailforms/templates/wagtailforms/index.html b/wagtail/wagtailforms/templates/wagtailforms/index.html index c3a831501..c5ccc1aa4 100644 --- a/wagtail/wagtailforms/templates/wagtailforms/index.html +++ b/wagtail/wagtailforms/templates/wagtailforms/index.html @@ -5,7 +5,7 @@ {% block content %} {% trans "Forms" as forms_str %} {% trans "Pages" as select_form_str %} - {% include "wagtailadmin/shared/header.html" with title=forms_str subtitle=select_form_str %} + {% include "wagtailadmin/shared/header.html" with title=forms_str subtitle=select_form_str icon="form" %}
    diff --git a/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html b/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html index 7bba352fb..fa4bfdb13 100644 --- a/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html +++ b/wagtail/wagtailforms/templates/wagtailforms/index_submissions.html @@ -38,15 +38,13 @@ {% blocktrans with form_title=form_page.title|capfirst %}Form data {{ form_title }}{% endblocktrans %}
    -
  • {% trans "Submission Date" %}{{ heading }}
    @@ -23,3 +24,4 @@ {% endfor %}
    +
    From 7d88d285cd7891ffb43fa9a74b55755eaf7aa70b Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 20 May 2014 10:41:08 +0100 Subject: [PATCH 080/189] updated login styles to match new 'iconfield' implementation --- .../wagtailadmin/scss/layouts/login.scss | 38 ++++++++++--------- .../templates/wagtailadmin/login.html | 12 ++++-- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/login.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/login.scss index 4abf3ae20..3e158e51f 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/login.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/layouts/login.scss @@ -82,7 +82,7 @@ form{ .field{ padding:0; } - .field.icon:before{ + .iconfield:before{ display:none; } @@ -168,25 +168,29 @@ form{ font-size:4em; } - .field.icon:before{ - display:inline-block; - position: absolute; - color:$color-grey-4; - border: 2px solid $color-grey-4; - border-radius: 100%; - width: 1em; - padding: 0.3em; - left: $desktop-nice-padding; - margin-top: -1em; - top: 50%; - font-size:1.5em; - } - .full{ margin:0px (-$desktop-nice-padding); - input{ - padding-left:($desktop-nice-padding + 50px); + .iconfield{ + &:before{ + display:inline-block; + position: absolute; + color:$color-grey-4; + border: 2px solid $color-grey-4; + border-radius: 100%; + width: 1em; + padding: 0.3em; + left: $desktop-nice-padding; + margin-top: -1.1rem; + top: 50%; + font-size:1.3rem; + } + + input{ + padding-left:($desktop-nice-padding + 50px); + } } + + } } \ No newline at end of file diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/login.html b/wagtail/wagtailadmin/templates/wagtailadmin/login.html index 9cb28ed86..012dd32f8 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/login.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/login.html @@ -28,15 +28,19 @@ diff --git a/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py b/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py index 311163ca3..2d85c355e 100644 --- a/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py +++ b/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py @@ -26,17 +26,6 @@ def explorer_subnav(nodes): } -@register.assignment_tag -def get_wagtailadmin_tab_urls(): - resolver = urlresolvers.get_resolver(None) - return [ - (key, value[2].get("title", key)) - for key, value - in resolver.reverse_dict.items() - if isinstance(key, basestring) and key.startswith('wagtailadmin_tab_') - ] - - @register.inclusion_tag('wagtailadmin/shared/main_nav.html', takes_context=True) def main_nav(context): menu_items = [ From 280bbff9d92e83530aa8c702fbf149f7b41e4cfd Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 27 May 2014 14:37:11 +0100 Subject: [PATCH 114/189] Added search and redirects links to menu using hooks --- wagtail/wagtailredirects/wagtail_hooks.py | 13 +++++++++++++ wagtail/wagtailsearch/wagtail_hooks.py | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/wagtail/wagtailredirects/wagtail_hooks.py b/wagtail/wagtailredirects/wagtail_hooks.py index 22ae97320..4dbe1a028 100644 --- a/wagtail/wagtailredirects/wagtail_hooks.py +++ b/wagtail/wagtailredirects/wagtail_hooks.py @@ -1,11 +1,24 @@ +from django.core import urlresolvers from django.conf.urls import include, url +from django.utils.translation import ugettext_lazy as _ from wagtail.wagtailadmin import hooks from wagtail.wagtailredirects import urls +from wagtail.wagtailadmin.menu import MenuItem + def register_admin_urls(): return [ url(r'^redirects/', include(urls)), ] hooks.register('register_admin_urls', register_admin_urls) + + +def construct_main_menu(request, menu_items): + # TEMPORARY: Only show if the user is a superuser + if request.user.is_superuser: + menu_items.append( + MenuItem(_('Redirects'), urlresolvers.reverse('wagtailredirects_index'), classnames='icon icon-redirect', order=800) + ) +hooks.register('construct_main_menu', construct_main_menu) diff --git a/wagtail/wagtailsearch/wagtail_hooks.py b/wagtail/wagtailsearch/wagtail_hooks.py index b8bbd2c06..1a656c0ef 100644 --- a/wagtail/wagtailsearch/wagtail_hooks.py +++ b/wagtail/wagtailsearch/wagtail_hooks.py @@ -1,11 +1,24 @@ +from django.core import urlresolvers from django.conf.urls import include, url +from django.utils.translation import ugettext_lazy as _ from wagtail.wagtailadmin import hooks from wagtail.wagtailsearch.urls import admin as admin_urls +from wagtail.wagtailadmin.menu import MenuItem + def register_admin_urls(): return [ url(r'^search/', include(admin_urls)), ] hooks.register('register_admin_urls', register_admin_urls) + + +def construct_main_menu(request, menu_items): + # TEMPORARY: Only show if the user is a superuser + if request.user.is_superuser: + menu_items.append( + MenuItem(_('Editors picks'), urlresolvers.reverse('wagtailsearch_editorspicks_index'), classnames='icon icon-pick', order=900) + ) +hooks.register('construct_main_menu', construct_main_menu) From 9bbe3f1c191e712e81b53c019cdb2535ee249f82 Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Tue, 27 May 2014 16:54:06 +0100 Subject: [PATCH 115/189] Installation comment to address wagtail issue 220. --- scripts/install/debian.sh | 2 ++ scripts/install/ubuntu.sh | 2 ++ 2 files changed, 4 insertions(+) diff --git a/scripts/install/debian.sh b/scripts/install/debian.sh index d29d5acdc..ee08191ac 100644 --- a/scripts/install/debian.sh +++ b/scripts/install/debian.sh @@ -1,5 +1,7 @@ # Production-configured Wagtail installation. # BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE! +# For a non-dummy email backend configure Django's EMAIL_BACKEND +# in settings/production.py post-installation. # Tested on Debian 7.0. # Tom Dyson and Neal Todd diff --git a/scripts/install/ubuntu.sh b/scripts/install/ubuntu.sh index c713cefa9..299e67a8d 100644 --- a/scripts/install/ubuntu.sh +++ b/scripts/install/ubuntu.sh @@ -1,5 +1,7 @@ # Production-configured Wagtail installation. # BUT, SECURE SERVICES/ACCOUNT FOR FULL PRODUCTION USE! +# For a non-dummy email backend configure Django's EMAIL_BACKEND +# in settings/production.py post-installation. # Tested on Ubuntu 13.04 and 13.10. # Tom Dyson and Neal Todd From 9ec2c902b5f277f2701f837e27ece23df854bc65 Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Tue, 27 May 2014 17:06:37 +0100 Subject: [PATCH 116/189] Fix #175. Change to image chooser: 'Upload' tab now retains focus if submit action returns a form error. --- .../templates/wagtailimages/chooser/chooser.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html b/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html index 03b4554c8..1fcadb3f9 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html +++ b/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html @@ -5,13 +5,13 @@ {% if uploadform %} {% endif %}
    -
    {% if uploadform %} -
    +
    {% csrf_token %}
      From 6314c75ca6013eca502eca02fb9d0cadb66ba1b0 Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Wed, 28 May 2014 11:08:13 +0100 Subject: [PATCH 117/189] Fix #268: search input now appears on image chooser after form validation error --- wagtail/wagtailimages/views/chooser.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailimages/views/chooser.py b/wagtail/wagtailimages/views/chooser.py index 510174c4c..221a75581 100644 --- a/wagtail/wagtailimages/views/chooser.py +++ b/wagtail/wagtailimages/views/chooser.py @@ -113,6 +113,8 @@ def chooser_upload(request): Image = get_image_model() ImageForm = get_image_form() + searchform = SearchForm() + if request.POST: image = Image(uploaded_by_user=request.user) form = ImageForm(request.POST, request.FILES, instance=image) @@ -138,7 +140,7 @@ def chooser_upload(request): return render_modal_workflow( request, 'wagtailimages/chooser/chooser.html', 'wagtailimages/chooser/chooser.js', - {'images': images, 'uploadform': form} + {'images': images, 'uploadform': form, 'searchform': searchform} ) From 580e9130ad22ac01515902f06eb7efe5600c29a3 Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Wed, 28 May 2014 12:05:35 +0100 Subject: [PATCH 118/189] Ref issue 177, Elasticsearch timeout on Travis: Support passing configuration arguments to Elasticsearch. Also documenting Elasticsearch setup in Wagtail. --- docs/wagtail_search.rst | 27 +++++++++++++++++++ runtests.py | 2 ++ .../wagtailsearch/backends/elasticsearch.py | 20 ++++++++++---- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/docs/wagtail_search.rst b/docs/wagtail_search.rst index 8fa263801..c03f74fcf 100644 --- a/docs/wagtail_search.rst +++ b/docs/wagtail_search.rst @@ -220,6 +220,30 @@ The default DB search backend uses Django's ``__icontains`` filter. Elasticsearch Backend ````````````````````` +Prerequisites are the Elasticsearch service itself and, via pip, the `elasticutils`_ and `pyelasticsearch`_ packages: + +.. code-block:: guess + + pip install elasticutils pyelasticsearch + +NB: The dependency on pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_. + +The backend is configured in settings: + +.. code-block:: python + + WAGTAILSEARCH_BACKENDS = { + 'default': { + 'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch', + 'URL': ['http://localhost:9200'], + 'INDEX': 'wagtail', + 'TIMEOUT': 5, + 'FORCE_NEW': False, + } + } + +Other than `BACKEND` the other keys are optional and default to the values shown. In addition any other keys are passed directly to the Elasticsearch constructor as keyword arguments (e.g. `'max_retries': 1`). + If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly: - Sign up for an account at `dashboard.searchly.com/users/sign\_up`_ @@ -229,6 +253,9 @@ If you prefer not to run an Elasticsearch server in development or production, t your local settings - Run ``./manage.py update_index`` +.. _elasticutuils: http://elasticutils.readthedocs.org +.. _pyelasticsearch: http://pyelasticsearch.readthedocs.org +.. _elasticsearch-py: http://elasticsearch-py.readthedocs.org .. _Searchly: http://www.searchly.com/ .. _dashboard.searchly.com/users/sign\_up: https://dashboard.searchly.com/users/sign_up diff --git a/runtests.py b/runtests.py index 1577d73b5..e3942646a 100755 --- a/runtests.py +++ b/runtests.py @@ -26,6 +26,8 @@ if not settings.configured: if has_elasticsearch: WAGTAILSEARCH_BACKENDS['elasticsearch'] = { 'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch', + 'TIMEOUT': 10, + 'max_retries': 1, } settings.configure( diff --git a/wagtail/wagtailsearch/backends/elasticsearch.py b/wagtail/wagtailsearch/backends/elasticsearch.py index b308add0b..d2b58d564 100644 --- a/wagtail/wagtailsearch/backends/elasticsearch.py +++ b/wagtail/wagtailsearch/backends/elasticsearch.py @@ -1,5 +1,4 @@ from django.db import models -from django.conf import settings from elasticutils import get_es, S @@ -58,12 +57,23 @@ class ElasticSearch(BaseSearch): super(ElasticSearch, self).__init__(params) # Get settings - self.es_urls = params.get('URLS', ['http://localhost:9200']) - self.es_index = params.get('INDEX', 'wagtail') + self.es_urls = params.pop('URLS', ['http://localhost:9200']) + self.es_index = params.pop('INDEX', 'wagtail') + self.es_timeout = params.pop('TIMEOUT', 5) + self.es_force_new = params.pop('FORCE_NEW', False) # Get ElasticSearch interface - self.es = get_es(urls=self.es_urls) - self.s = S().es(urls=self.es_urls).indexes(self.es_index) + # Any remaining params are passed into the ElasticSearch constructor + self.es = get_es( + urls=self.es_urls, + timeout=self.es_timeout, + force_new=self.es_force_new, + **params) + self.s = S().es( + urls=self.es_urls, + timeout=self.es_timeout, + force_new=self.es_force_new, + **params).indexes(self.es_index) def reset_index(self): # Delete old index From 0c2163734cbcdf94d67aab5efe88d0881ea4b6a4 Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Wed, 28 May 2014 12:11:57 +0100 Subject: [PATCH 119/189] Elasticsearch documentation tweak --- docs/wagtail_search.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/wagtail_search.rst b/docs/wagtail_search.rst index c03f74fcf..250a3bcb3 100644 --- a/docs/wagtail_search.rst +++ b/docs/wagtail_search.rst @@ -242,7 +242,7 @@ The backend is configured in settings: } } -Other than `BACKEND` the other keys are optional and default to the values shown. In addition any other keys are passed directly to the Elasticsearch constructor as keyword arguments (e.g. `'max_retries': 1`). +Other than `BACKEND` the other keys are optional and default to the values shown. ``FORCE_NEW`` is used by elasticutils. In addition, any other keys are passed directly to the Elasticsearch constructor as keyword arguments (e.g. ``'max_retries': 1``). If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly: @@ -253,7 +253,7 @@ If you prefer not to run an Elasticsearch server in development or production, t your local settings - Run ``./manage.py update_index`` -.. _elasticutuils: http://elasticutils.readthedocs.org +.. _elasticututils: http://elasticutils.readthedocs.org .. _pyelasticsearch: http://pyelasticsearch.readthedocs.org .. _elasticsearch-py: http://elasticsearch-py.readthedocs.org .. _Searchly: http://www.searchly.com/ From 700fcac8133bc1e8ee28305e917942e2f9318a8d Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Wed, 28 May 2014 12:14:30 +0100 Subject: [PATCH 120/189] Elasticsearch documentation typo fix --- docs/wagtail_search.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/wagtail_search.rst b/docs/wagtail_search.rst index 250a3bcb3..3c432f5d6 100644 --- a/docs/wagtail_search.rst +++ b/docs/wagtail_search.rst @@ -253,7 +253,7 @@ If you prefer not to run an Elasticsearch server in development or production, t your local settings - Run ``./manage.py update_index`` -.. _elasticututils: http://elasticutils.readthedocs.org +.. _elasticutils: http://elasticutils.readthedocs.org .. _pyelasticsearch: http://pyelasticsearch.readthedocs.org .. _elasticsearch-py: http://elasticsearch-py.readthedocs.org .. _Searchly: http://www.searchly.com/ From c5effdc641f69f40fb8f986133399860565539e0 Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Wed, 28 May 2014 12:51:19 +0100 Subject: [PATCH 121/189] Elasticsearch documentation update --- docs/wagtail_search.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/wagtail_search.rst b/docs/wagtail_search.rst index 3c432f5d6..ae9c73dca 100644 --- a/docs/wagtail_search.rst +++ b/docs/wagtail_search.rst @@ -242,15 +242,14 @@ The backend is configured in settings: } } -Other than `BACKEND` the other keys are optional and default to the values shown. ``FORCE_NEW`` is used by elasticutils. In addition, any other keys are passed directly to the Elasticsearch constructor as keyword arguments (e.g. ``'max_retries': 1``). +Other than ``BACKEND`` the keys are optional and default to the values shown. ``FORCE_NEW`` is used by elasticutils. In addition, any other keys are passed directly to the Elasticsearch constructor as case-sensitive keyword arguments (e.g. ``'max_retries': 1``). If you prefer not to run an Elasticsearch server in development or production, there are many hosted services available, including `Searchly`_, who offer a free account suitable for testing and development. To use Searchly: - Sign up for an account at `dashboard.searchly.com/users/sign\_up`_ - Use your Searchly dashboard to create a new index, e.g. 'wagtaildemo' - Note the connection URL from your Searchly dashboard -- Update ``WAGTAILSEARCH_ES_URLS`` and ``WAGTAILSEARCH_ES_INDEX`` in - your local settings +- Configure ``URL`` and ``INDEX`` in the Elasticsearch entry in ``WAGTAILSEARCH_BACKENDS`` - Run ``./manage.py update_index`` .. _elasticutils: http://elasticutils.readthedocs.org From 07733c83ec9079926f3c81f255765b150c42b1fc Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Wed, 28 May 2014 12:58:10 +0100 Subject: [PATCH 122/189] Elasticsearch documentation typo fix --- docs/wagtail_search.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/wagtail_search.rst b/docs/wagtail_search.rst index ae9c73dca..0e260c062 100644 --- a/docs/wagtail_search.rst +++ b/docs/wagtail_search.rst @@ -235,7 +235,7 @@ The backend is configured in settings: WAGTAILSEARCH_BACKENDS = { 'default': { 'BACKEND': 'wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch', - 'URL': ['http://localhost:9200'], + 'URLS': ['http://localhost:9200'], 'INDEX': 'wagtail', 'TIMEOUT': 5, 'FORCE_NEW': False, @@ -249,7 +249,7 @@ If you prefer not to run an Elasticsearch server in development or production, t - Sign up for an account at `dashboard.searchly.com/users/sign\_up`_ - Use your Searchly dashboard to create a new index, e.g. 'wagtaildemo' - Note the connection URL from your Searchly dashboard -- Configure ``URL`` and ``INDEX`` in the Elasticsearch entry in ``WAGTAILSEARCH_BACKENDS`` +- Configure ``URLS`` and ``INDEX`` in the Elasticsearch entry in ``WAGTAILSEARCH_BACKENDS`` - Run ``./manage.py update_index`` .. _elasticutils: http://elasticutils.readthedocs.org From 9e294cb030194ce7f595da20eb1f97c6e89fc6a2 Mon Sep 17 00:00:00 2001 From: Tom Dyson Date: Wed, 28 May 2014 13:22:32 +0100 Subject: [PATCH 123/189] Documentation tweaks --- docs/building_your_site/djangodevelopers.rst | 44 +++++++++----------- docs/building_your_site/index.rst | 6 +-- docs/wagtail_search.rst | 27 ++++++------ 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/docs/building_your_site/djangodevelopers.rst b/docs/building_your_site/djangodevelopers.rst index 5632a419b..b4899d70f 100644 --- a/docs/building_your_site/djangodevelopers.rst +++ b/docs/building_your_site/djangodevelopers.rst @@ -187,33 +187,29 @@ In addition to the model fields provided, ``Page`` has many properties and metho Properties: -specific -url -full_url -relative_url -has_unpublished_changes -status_string -subpage_types -indexed_fields - +* specific +* url +* full_url +* relative_url +* has_unpublished_changes +* status_string +* subpage_types +* indexed_fields Methods: -route -serve -get_context -get_template -is_navigable -get_other_siblings -get_ancestors -get_descendants -get_siblings -search -get_page_modes -show_as_mode - - - +* route +* serve +* get_context +* get_template +* is_navigable +* get_other_siblings +* get_ancestors +* get_descendants +* get_siblings +* search +* get_page_modes +* show_as_mode Page Queryset Methods diff --git a/docs/building_your_site/index.rst b/docs/building_your_site/index.rst index fb336e18b..da5cf4f2d 100644 --- a/docs/building_your_site/index.rst +++ b/docs/building_your_site/index.rst @@ -2,11 +2,7 @@ Building your site ================== .. note:: - Documentation currently incomplete and in draft status - -Serafeim Papastefanos has written a comprehensive tutorial on creating a site from scratch in Wagtail; for the time being, this is our recommended resource: - -`spapas.github.io/2014/02/13/wagtail-tutorial/ `_ + This documentation is currently incomplete. .. toctree:: :maxdepth: 3 diff --git a/docs/wagtail_search.rst b/docs/wagtail_search.rst index 0e260c062..642a5e741 100644 --- a/docs/wagtail_search.rst +++ b/docs/wagtail_search.rst @@ -1,14 +1,14 @@ Search ====== -Wagtail provides a very comprehensive, extensible, and flexible search interface. In addition, it provides ways to promote search results through "Editor's Picks." Wagtail also collects simple statistics on queries made through the search interface. +Wagtail provides a comprehensive and extensible search interface. In addition, it provides ways to promote search results through "Editor's Picks." Wagtail also collects simple statistics on queries made through the search interface. Default Page Search ------------------- -Wagtail provides a default frontend search interface which indexes the ``title`` field common to all ``Page``-derived models. Lets take a look at all the components of the search interface. +Wagtail provides a default frontend search interface which indexes the ``title`` field common to all ``Page``-derived models. Let's take a look at all the components of the search interface. -The most basic search functionality just needs a search box which submits a request. Since this will be reused throughout the site, lets put it in ``mysite/includes/search_box.html`` and then use ``{% include ... %}`` to weave it into templates: +The most basic search functionality just needs a search box which submits a request. Since this will be reused throughout the site, let's put it in ``mysite/includes/search_box.html`` and then use ``{% include ... %}`` to weave it into templates: .. code-block:: django @@ -17,15 +17,15 @@ The most basic search functionality just needs a search box which submits a requ -The form is submitted to the url of the ``wagtailsearch_search`` view, with the search terms variable ``q``. The view will use its own (very) basic search results template. +The form is submitted to the url of the ``wagtailsearch_search`` view, with the search terms variable ``q``. The view will use its own basic search results template. -Lets use our own template for the results, though. First, in your project's ``settings.py``, define a path to your template: +Let's use our own template for the results, though. First, in your project's ``settings.py``, define a path to your template: .. code-block:: python WAGTAILSEARCH_RESULTS_TEMPLATE = 'mysite/search_results.html' -Next, lets look at the template itself: +Next, let's look at the template itself: .. code-block:: django @@ -82,7 +82,7 @@ Editor's Picks are a way of explicitly linking relevant content to search terms, ``editors_pick.description`` The description entered when choosing the pick, perhaps explaining why the page is relevant to the search terms. -Putting this all together, a block of your search results template displaying Editor's Picks might look like this: +Putting this all together, a block of your search results template displaying editor's Picks might look like this: .. code-block:: django @@ -106,10 +106,10 @@ Putting this all together, a block of your search results template displaying Ed {% endif %} {% endwith %} -Asyncronous Search with JSON and AJAX -------------------------------------- +Asynchronous Search with JSON and AJAX +-------------------------------------- -Wagtail's provides JSON search results when queries are made to the ``wagtailsearch_suggest`` view. To take advantage of it, we need a way to make that URL available to a static script. Instead of hard-coding it, lets set a global variable in our ``base.html``: +Wagtail provides JSON search results when queries are made to the ``wagtailsearch_suggest`` view. To take advantage of it, we need a way to make that URL available to a static script. Instead of hard-coding it, let's set a global variable in our ``base.html``: .. code-block:: django @@ -117,7 +117,7 @@ Wagtail's provides JSON search results when queries are made to the ``wagtailsea var wagtailJSONSearchURL = "{% url 'wagtailsearch_suggest' %}"; -Lets also add a simple interface for the search with a ```` element to gather search terms and a ``
      `` to display the results: +Now add a simple interface for the search with a ```` element to gather search terms and a ``
      `` to display the results: .. code-block:: html @@ -130,7 +130,7 @@ Lets also add a simple interface for the search with a ```` element to ga Finally, we'll use JQuery to make the asynchronous requests and handle the interactivity: .. code-block:: guess - + $(function() { // cache the elements @@ -226,7 +226,8 @@ Prerequisites are the Elasticsearch service itself and, via pip, the `elasticuti pip install elasticutils pyelasticsearch -NB: The dependency on pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_. +.. note:: + The dependency on pyelasticsearch is scheduled to be replaced by a dependency on `elasticsearch-py`_. The backend is configured in settings: From 0393128509558d1dff525acbc63db7b5b86e3d8f Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 28 May 2014 15:41:40 +0100 Subject: [PATCH 124/189] update changelog / contributors for #267 --- CHANGELOG.txt | 1 + CONTRIBUTORS.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8b69d346a..27db208fb 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -28,6 +28,7 @@ Changelog * Fix: Moving or changing a site root page no longer causes URLs for subpages to change to 'None' * Fix: Eliminated raw SQL queries from wagtailcore / wagtailadmin, to ensure cross-database compatibility * Fix: Snippets menu item is hidden for administrators if no snippet types are defined + * Fix: 'Upload' tab in image chooser now retains focus if submit action returns a form error. 0.2 (11.03.2014) ~~~~~~~~~~~~~~~~ diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 3549e8407..2edc1c079 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -26,6 +26,7 @@ Contributors * Ben Emery * David Smith * Ben Margolis +* Tom Talbot Translators =========== From 545723cd8b69b18043a8637681fe30f97c8bbb09 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 28 May 2014 15:44:50 +0100 Subject: [PATCH 125/189] changelog entry for #269 --- CHANGELOG.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 27db208fb..2d6a9ca53 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -29,6 +29,7 @@ Changelog * Fix: Eliminated raw SQL queries from wagtailcore / wagtailadmin, to ensure cross-database compatibility * Fix: Snippets menu item is hidden for administrators if no snippet types are defined * Fix: 'Upload' tab in image chooser now retains focus if submit action returns a form error. + * Fix: Search input now appears on image chooser after form validation error. 0.2 (11.03.2014) ~~~~~~~~~~~~~~~~ From c7731ad02047b43a7267b01abac9081de1be5d28 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 28 May 2014 15:56:01 +0100 Subject: [PATCH 126/189] Additional contributors / translators --- CHANGELOG.txt | 3 ++- CONTRIBUTORS.rst | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2d6a9ca53..9016e93b3 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -9,6 +9,7 @@ Changelog * Added custom Query set for Pages with some handy methods for querying pages * Added 'wagtailforms' module for creating form pages on a site, and handling form submissions * Editor's guide documentation + * Expanded developer documentation * Editor interface now outputs form media CSS / JS, to support custom widgets with assets * Migrations and user management now correctly handle custom AUTH_USER_MODEL settings * Added 'slugurl' template tag to output the URL of a page with a given slug @@ -16,7 +17,7 @@ Changelog * Added 'insert_editor_css' and 'insert_editor_js' hooks for passing in custom CSS / JS to the editor interface * Made JPEG compression level configurable through the IMAGE_COMPRESSION_QUALITY setting, and increased default to 85 * Added document_served signal which gets fired when a document is downloaded - * Added translation for Portuguese Brazil + * Added translations for Portuguese Brazil and Traditional Chinese (Taiwan). * Made compatible with Python 2.6 * 'richtext' template filter now wraps output in
      , to assist in styling * Embeds now save author_name and provider_name if set by oEmbed provider diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index 2edc1c079..d7222307d 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -27,6 +27,7 @@ Contributors * David Smith * Ben Margolis * Tom Talbot +* Jeffrey Hearn Translators =========== @@ -34,7 +35,7 @@ Translators * Basque: Unai Zalakain * Bulgarian: Lyuboslav Petrov * Catalan: David Llop -* Chinese: Lihan Li +* Chinese: Lihan Li, tulpar008, wwj718 * French: Sylvain Fankhauser * Galician: fooflare * German: Karl Sander, Johannes Spielmann @@ -44,3 +45,4 @@ Translators * Portuguese Brazil: Gilson Filho * Romanian: Dan Braghis * Spanish: Unai Zalakain, fooflare +* Traditional Chinese (Taiwan): wdv4758h From 7bd8c160227d61295f0a22b0f399ebdcbfc2a36e Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 28 May 2014 15:59:58 +0100 Subject: [PATCH 127/189] change deprecated mimetype args to HttpResponse to content_type --- wagtail/wagtailadmin/modal_workflow.py | 2 +- wagtail/wagtailadmin/views/tags.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailadmin/modal_workflow.py b/wagtail/wagtailadmin/modal_workflow.py index a4d86f501..998dfdec3 100644 --- a/wagtail/wagtailadmin/modal_workflow.py +++ b/wagtail/wagtailadmin/modal_workflow.py @@ -23,4 +23,4 @@ def render_modal_workflow(request, html_template, js_template, template_vars={}) response_text = "{%s}" % ','.join(response_keyvars) - return HttpResponse(response_text, mimetype="text/javascript") + return HttpResponse(response_text, content_type="text/javascript") diff --git a/wagtail/wagtailadmin/views/tags.py b/wagtail/wagtailadmin/views/tags.py index 31281d958..bef3043ae 100644 --- a/wagtail/wagtailadmin/views/tags.py +++ b/wagtail/wagtailadmin/views/tags.py @@ -16,4 +16,4 @@ def autocomplete(request): response = json.dumps([tag.name for tag in tags]) - return HttpResponse(response, mimetype='text/javascript') + return HttpResponse(response, content_type='text/javascript') From 3042d6ef6a2938f190c13870232199df95d4b516 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 28 May 2014 16:27:53 +0100 Subject: [PATCH 128/189] set USE_TZ=True in runtests to handle fixtures correctly on sqlite --- runtests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/runtests.py b/runtests.py index e3942646a..6fd37ebeb 100755 --- a/runtests.py +++ b/runtests.py @@ -42,6 +42,7 @@ if not settings.configured: STATIC_URL='/static/', STATIC_ROOT=STATIC_ROOT, MEDIA_ROOT=MEDIA_ROOT, + USE_TZ=True, STATICFILES_FINDERS=( 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'compressor.finders.CompressorFinder', From 2cbf128eb5066743930e40b450d30a2901f759c0 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 28 May 2014 16:28:21 +0100 Subject: [PATCH 129/189] prepare for 0.3 release --- CHANGELOG.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9016e93b3..0a6024bd1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,7 +1,7 @@ Changelog ========= -0.3 (xx.xx.20xx) +0.3 (28.05.2014) ~~~~~~~~~~~~~~~~ * Added toolbar to allow logged-in users to add and edit pages from the site front-end * Support for alternative image processing backends such as Wand, via the WAGTAILIMAGES_BACKENDS setting diff --git a/setup.py b/setup.py index 8a85f618c..4f5b87c12 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ except ImportError: setup( name='wagtail', - version='0.2', + version='0.3', description='A Django content management system focused on flexibility and user experience', author='Matthew Westcott', author_email='matthew.westcott@torchbox.com', From f25c44851e510efe055b2ec5f56755419e328621 Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Thu, 29 May 2014 09:47:21 +0100 Subject: [PATCH 130/189] Recache version badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index eddbb85fc..d967dcb1f 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ .. image:: https://coveralls.io/repos/torchbox/wagtail/badge.png?branch=master :target: https://coveralls.io/r/torchbox/wagtail?branch=master -.. image:: https://pypip.in/v/wagtail/badge.png?asdf +.. image:: https://pypip.in/v/wagtail/badge.png?zxcv :target: https://crate.io/packages/wagtail/ Wagtail CMS From 24320332b0a55f8f06a06a7e92075b1e89f35bd5 Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Thu, 29 May 2014 15:02:56 +0100 Subject: [PATCH 131/189] Fix #94. Panels are now hidden on page load if they are marked as deleted after a form validation failure. --- .../static/wagtailadmin/js/page-editor.js | 11 +++++++++++ wagtail/wagtailadmin/views/pages.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js b/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js index cbff2c8af..4e8e1b02d 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js @@ -181,6 +181,17 @@ function InlinePanel(opts) { self.updateMoveButtonDisabledStates(); }); } + + /* Hide container on page load if it is marked as deleted. Remove the error + message so that it doesn't count towards the number of errors on the tab at the + top of the page. */ + if ( $('#' + deleteInputId).val() === "1" ) { + $('#' + childId).hide(0, function() { + self.updateMoveButtonDisabledStates(); + self.setHasContent(); + }); + $('#' + childId).find(".error-message").remove(); + } }; self.formsUl = $('#' + opts.formsetPrefix + '-FORMS'); diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 308f6fdd4..809497d83 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -5,7 +5,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 From d1bad6fadf4cc67d48c16ca50cc252afa3a87d5d Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 11:18:09 +0100 Subject: [PATCH 132/189] Split up wagtailadmin tests --- wagtail/wagtailadmin/tests/__init__.py | 0 .../{tests.py => tests/test_pages_views.py} | 28 ---------------- wagtail/wagtailadmin/tests/tests.py | 33 +++++++++++++++++++ 3 files changed, 33 insertions(+), 28 deletions(-) create mode 100644 wagtail/wagtailadmin/tests/__init__.py rename wagtail/wagtailadmin/{tests.py => tests/test_pages_views.py} (89%) create mode 100644 wagtail/wagtailadmin/tests/tests.py diff --git a/wagtail/wagtailadmin/tests/__init__.py b/wagtail/wagtailadmin/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/wagtailadmin/tests.py b/wagtail/wagtailadmin/tests/test_pages_views.py similarity index 89% rename from wagtail/wagtailadmin/tests.py rename to wagtail/wagtailadmin/tests/test_pages_views.py index c6bf6c8e6..1c546b5ad 100644 --- a/wagtail/wagtailadmin/tests.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -5,16 +5,6 @@ from wagtail.wagtailcore.models import Page from django.core.urlresolvers import reverse -class TestHome(TestCase): - def setUp(self): - # Login - login(self.client) - - def test_status_code(self): - response = self.client.get(reverse('wagtailadmin_home')) - self.assertEqual(response.status_code, 200) - - class TestPageExplorer(TestCase): def setUp(self): # Find root page @@ -281,21 +271,3 @@ class TestPageMove(TestCase): def test_page_set_page_position(self): response = self.client.get(reverse('wagtailadmin_pages_set_page_position', args=(self.test_page.id, ))) self.assertEqual(response.status_code, 200) - - -class TestEditorHooks(TestCase): - def setUp(self): - self.homepage = Page.objects.get(id=2) - login(self.client) - - def test_editor_css_and_js_hooks_on_add(self): - response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.homepage.id))) - self.assertEqual(response.status_code, 200) - self.assertContains(response, '') - self.assertContains(response, '') - - def test_editor_css_and_js_hooks_on_edit(self): - response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.homepage.id, ))) - self.assertEqual(response.status_code, 200) - self.assertContains(response, '') - self.assertContains(response, '') diff --git a/wagtail/wagtailadmin/tests/tests.py b/wagtail/wagtailadmin/tests/tests.py new file mode 100644 index 000000000..3e5db6204 --- /dev/null +++ b/wagtail/wagtailadmin/tests/tests.py @@ -0,0 +1,33 @@ +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 +from django.core.urlresolvers import reverse + + +class TestHome(TestCase): + def setUp(self): + # Login + login(self.client) + + def test_status_code(self): + response = self.client.get(reverse('wagtailadmin_home')) + self.assertEqual(response.status_code, 200) + + +class TestEditorHooks(TestCase): + def setUp(self): + self.homepage = Page.objects.get(id=2) + login(self.client) + + def test_editor_css_and_js_hooks_on_add(self): + response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.homepage.id))) + self.assertEqual(response.status_code, 200) + self.assertContains(response, '') + self.assertContains(response, '') + + def test_editor_css_and_js_hooks_on_edit(self): + response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.homepage.id, ))) + self.assertEqual(response.status_code, 200) + self.assertContains(response, '') + self.assertContains(response, '') From 90a08fffe5f39a2675208ce33ba52b9f73d07082 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 12:43:28 +0100 Subject: [PATCH 133/189] Added wagtailadmin account management tests These test the following: - Users can login and logout - Logged in users are redirected to dashboard (not implemented yet #25) - Account section works and users can change their password - The password reset functionality works --- .../tests/test_account_management.py | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 wagtail/wagtailadmin/tests/test_account_management.py diff --git a/wagtail/wagtailadmin/tests/test_account_management.py b/wagtail/wagtailadmin/tests/test_account_management.py new file mode 100644 index 000000000..95d54d7d9 --- /dev/null +++ b/wagtail/wagtailadmin/tests/test_account_management.py @@ -0,0 +1,320 @@ +from django.test import TestCase +from wagtail.tests.utils import login, unittest +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.core import mail + + +class TestAuthentication(TestCase): + """ + This tests that users can login and logout of the admin interface + """ + def setUp(self): + login(self.client) + + def test_login_view(self): + """ + This tests that the login view responds with a login page + """ + # Logout so we can test the login view + self.client.logout() + + # Get login page + response = self.client.get(reverse('wagtailadmin_login')) + + # Check that the user recieved a login page + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/login.html') + + def test_login_view_post(self): + """ + This posts user credentials to the login view and checks that + the user was logged in successfully + """ + # Logout so we can test the login view + self.client.logout() + + # Post credentials to the login page + post_data = { + 'username': 'test', + 'password': 'password', + } + response = self.client.post(reverse('wagtailadmin_login'), post_data) + + # Check that the user was redirected to the dashboard + self.assertEqual(response.status_code, 302) + + # Check that the user was logged in + self.assertTrue('_auth_user_id' in self.client.session) + self.assertEqual(self.client.session['_auth_user_id'], User.objects.get(username='test').id) + + @unittest.expectedFailure # See: https://github.com/torchbox/wagtail/issues/25 + def test_already_logged_in_redirect(self): + """ + This tests that a user who is already logged in is automatically + redirected to the admin dashboard if they try to access the login + page + """ + # Get login page + response = self.client.get(reverse('wagtailadmin_login')) + + # Check that the user was redirected to the dashboard + self.assertEqual(response.status_code, 302) + + def test_logout(self): + """ + This tests that the user can logout + """ + # Get logout page page + response = self.client.get(reverse('wagtailadmin_logout')) + + # Check that the user was redirected to the login page + self.assertEqual(response.status_code, 302) + + # Check that the user was logged out + self.assertFalse('_auth_user_id' in self.client.session) + + +class TestAccountSection(TestCase): + """ + This tests that the accounts section is working + """ + def setUp(self): + login(self.client) + + def test_account_view(self): + """ + This tests that the login view responds with a login page + """ + # Get account page + response = self.client.get(reverse('wagtailadmin_account')) + + # Check that the user recieved an account page + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/account/account.html') + + def test_change_password_view(self): + """ + This tests that the change password view responds with a change password page + """ + # Get change password page + response = self.client.get(reverse('wagtailadmin_account_change_password')) + + # Check that the user recieved a change password page + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/account/change_password.html') + + def test_change_password_view_post(self): + """ + This posts a new password to the change password view and checks + that the users password was changed + """ + # Post new password to change password page + post_data = { + 'new_password1': 'newpassword', + 'new_password2': 'newpassword', + } + response = self.client.post(reverse('wagtailadmin_account_change_password'), post_data) + + # Check that the user was redirected + self.assertEqual(response.status_code, 302) + + # Check that the password was changed + self.assertTrue(User.objects.get(username='test').check_password('newpassword')) + + def test_change_password_view_post_password_mismatch(self): + """ + This posts a two passwords that don't match to the password change + view and checks that a validation error was raised + """ + # Post new password to change password page + post_data = { + 'new_password1': 'newpassword', + 'new_password2': 'badpassword', + } + response = self.client.post(reverse('wagtailadmin_account_change_password'), post_data) + + # Check that the user wasn't redirected + self.assertEqual(response.status_code, 200) + + # Check that a validation error was raised + self.assertTrue('new_password2' in response.context['form'].errors.keys()) + self.assertTrue("The two password fields didn't match." in response.context['form'].errors['new_password2']) + + # Check that the password was not changed + self.assertTrue(User.objects.get(username='test').check_password('password')) + + +class TestPasswordReset(TestCase): + """ + This tests that the password reset is working + """ + def setUp(self): + # Create a user + User.objects.create_superuser(username='test', email='test@email.com', password='password') + + def test_password_reset_view(self): + """ + This tests that the password reset view returns a password reset page + """ + # Get password reset page + response = self.client.get(reverse('password_reset')) + + # Check that the user recieved a password reset page + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/account/password_reset/form.html') + + def test_password_reset_view_post(self): + """ + This posts an email address to the password reset view and + checks that a password reset email was sent + """ + # Post email address to password reset view + post_data = { + 'email': 'test@email.com', + } + response = self.client.post(reverse('password_reset'), post_data) + + # Check that the user was redirected + self.assertEqual(response.status_code, 302) + + # Check that a password reset email was sent to the user + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, ['test@email.com']) + self.assertEqual(mail.outbox[0].subject, "Password reset") + + def test_password_reset_view_post_unknown_email(self): + """ + This posts an unknown email address to the password reset view and + checks that the password reset form raises a validation error + """ + post_data = { + 'email': 'unknown@email.com', + } + response = self.client.post(reverse('password_reset'), post_data) + + # Check that the user wasn't redirected + self.assertEqual(response.status_code, 200) + + # Check that a validation error was raised + self.assertTrue('__all__' in response.context['form'].errors.keys()) + self.assertTrue("This email address is not recognised." in response.context['form'].errors['__all__']) + + # Check that an email was not sent + self.assertEqual(len(mail.outbox), 0) + + def test_password_reset_view_post_invalid_email(self): + """ + This posts an incalid email address to the password reset view and + checks that the password reset form raises a validation error + """ + post_data = { + 'email': 'Hello world!', + } + response = self.client.post(reverse('password_reset'), post_data) + + # Check that the user wasn't redirected + self.assertEqual(response.status_code, 200) + + # Check that a validation error was raised + self.assertTrue('email' in response.context['form'].errors.keys()) + self.assertTrue("Enter a valid email address." in response.context['form'].errors['email']) + + # Check that an email was not sent + self.assertEqual(len(mail.outbox), 0) + + def setup_password_reset_confirm_tests(self): + from django.utils.encoding import force_bytes + from django.utils.http import urlsafe_base64_encode + + # Get user + self.user = User.objects.get(username='test') + + # Generate a password reset token + self.password_reset_token = PasswordResetTokenGenerator().make_token(self.user) + + # Generate a password reset uid + self.password_reset_uid = urlsafe_base64_encode(force_bytes(self.user.pk)) + + # Create url_args + self.url_kwargs = dict(uidb64=self.password_reset_uid, token=self.password_reset_token) + + def test_password_reset_confirm_view(self): + """ + This tests that the password reset confirm view returns a password reset confirm page + """ + self.setup_password_reset_confirm_tests() + + # Get password reset confirm page + response = self.client.get(reverse('password_reset_confirm', kwargs=self.url_kwargs)) + + # Check that the user recieved a password confirm done page + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/account/password_reset/confirm.html') + + def test_password_reset_confirm_view_post(self): + """ + This posts a new password to the password reset confirm view and checks + that the users password was changed + """ + self.setup_password_reset_confirm_tests() + + # Post new password to change password page + post_data = { + 'new_password1': 'newpassword', + 'new_password2': 'newpassword', + } + response = self.client.post(reverse('password_reset_confirm', kwargs=self.url_kwargs), post_data) + + # Check that the user was redirected + self.assertEqual(response.status_code, 302) + + # Check that the password was changed + self.assertTrue(User.objects.get(username='test').check_password('newpassword')) + + def test_password_reset_confirm_view_post_password_mismatch(self): + """ + This posts a two passwords that don't match to the password reset + confirm view and checks that a validation error was raised + """ + self.setup_password_reset_confirm_tests() + + # Post new password to change password page + post_data = { + 'new_password1': 'newpassword', + 'new_password2': 'badpassword', + } + response = self.client.post(reverse('password_reset_confirm', kwargs=self.url_kwargs), post_data) + + # Check that the user wasn't redirected + self.assertEqual(response.status_code, 200) + + # Check that a validation error was raised + self.assertTrue('new_password2' in response.context['form'].errors.keys()) + self.assertTrue("The two password fields didn't match." in response.context['form'].errors['new_password2']) + + # Check that the password was not changed + self.assertTrue(User.objects.get(username='test').check_password('password')) + + def test_password_reset_done_view(self): + """ + This tests that the password reset done view returns a password reset done page + """ + # Get password reset done page + response = self.client.get(reverse('password_reset_done')) + + # Check that the user recieved a password reset done page + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/account/password_reset/done.html') + + def test_password_reset_complete_view(self): + """ + This tests that the password reset complete view returns a password reset complete page + """ + # Get password reset complete page + response = self.client.get(reverse('password_reset_complete')) + + # Check that the user recieved a password reset complete page + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/account/password_reset/complete.html') From 526cfba68285f89a85319aa4e627773a01ce482c Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 14:13:43 +0100 Subject: [PATCH 134/189] Added tests for unpublish view --- wagtail/tests/utils.py | 6 +- .../wagtailadmin/tests/test_pages_views.py | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/wagtail/tests/utils.py b/wagtail/tests/utils.py index 7b6557a78..4774fc4da 100644 --- a/wagtail/tests/utils.py +++ b/wagtail/tests/utils.py @@ -1,4 +1,4 @@ -from django.contrib.auth.models import User +from django.contrib.auth.models import User, Permission # We need to make sure that we're using the same unittest library that Django uses internally # Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors @@ -14,7 +14,9 @@ except ImportError: def login(client): # Create a user - User.objects.create_superuser(username='test', email='test@email.com', password='password') + user = User.objects.create_superuser(username='test', email='test@email.com', password='password') # Login client.login(username='test', password='password') + + return user diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 1c546b5ad..75d727f22 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -3,6 +3,7 @@ from wagtail.tests.models import SimplePage, EventPage from wagtail.tests.utils import login, unittest from wagtail.wagtailcore.models import Page from django.core.urlresolvers import reverse +from django.contrib.auth.models import Permission class TestPageExplorer(TestCase): @@ -271,3 +272,70 @@ class TestPageMove(TestCase): def test_page_set_page_position(self): response = self.client.get(reverse('wagtailadmin_pages_set_page_position', args=(self.test_page.id, ))) self.assertEqual(response.status_code, 200) + + +class TestPageUnpublish(TestCase): + def setUp(self): + self.user = login(self.client) + + # Create a page to unpublish + root_page = Page.objects.get(id=2) + self.page = SimplePage( + title="Hello world!", + slug='hello-world', + live=True, + ) + root_page.add_child(instance=self.page) + + def test_unpublish_view(self): + """ + This tests that the unpublish view responds with an unpublish confirm page + """ + # Get unpublish page + response = self.client.get(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, ))) + + # Check that the user recieved an unpublish confirm page + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/pages/confirm_unpublish.html') + + def test_unpublish_view_invalid_page_id(self): + """ + This tests that the unpublish view returns an error if the page id is invalid + """ + # Get unpublish page + response = self.client.get(reverse('wagtailadmin_pages_unpublish', args=(12345, ))) + + # Check that the user recieved a 404 response + self.assertEqual(response.status_code, 404) + + def test_unpublish_view_bad_permissions(self): + """ + This tests that the unpublish view doesn't allow users without unpublish permissions + """ + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin') + ) + self.user.save() + + # Get unpublish page + response = self.client.get(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, ))) + + # Check that the user recieved a 403 response + self.assertEqual(response.status_code, 403) + + def test_unpublish_view_post(self): + """ + This posts to the unpublish view and checks that the page was unpublished + """ + # Post to the unpublish page + response = self.client.post(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, )), { + 'foo': "Must post something or the view won't see this as a POST request", + }) + + # Check that the user was redirected + self.assertEqual(response.status_code, 302) + + # Check that the page was unpublished + self.assertFalse(SimplePage.objects.get(id=self.page.id).live) From d85612dac88e38b96bc5a1001f0e5aa78990b943 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 14:22:10 +0100 Subject: [PATCH 135/189] Added permission checks to page views --- .../wagtailadmin/tests/test_pages_views.py | 88 +++++++++++++++++-- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 75d727f22..eaba42f7b 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -33,12 +33,26 @@ class TestPageCreation(TestCase): self.root_page = Page.objects.get(id=2) # Login - login(self.client) + self.user = login(self.client) def test_add_subpage(self): response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(self.root_page.id, ))) self.assertEqual(response.status_code, 200) + def test_add_subpage_bad_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin') + ) + self.user.save() + + # Get add subpage page + response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(self.root_page.id, ))) + + # Check that the user recieved a 403 response + self.assertEqual(response.status_code, 403) + def test_add_subpage_nonexistantparent(self): response = self.client.get(reverse('wagtailadmin_pages_add_subpage', args=(100000, ))) self.assertEqual(response.status_code, 404) @@ -47,6 +61,20 @@ class TestPageCreation(TestCase): response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id))) self.assertEqual(response.status_code, 200) + def test_create_simplepage_bad_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin') + ) + self.user.save() + + # Get page + response = self.client.get(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id, ))) + + # Check that the user recieved a 403 response + self.assertEqual(response.status_code, 403) + def test_create_simplepage_post(self): post_data = { 'title': "New page!", @@ -133,14 +161,28 @@ class TestPageEdit(TestCase): self.root_page.add_child(instance=self.event_page) # Login - login(self.client) + self.user = login(self.client) - def test_edit_page(self): + def test_page_edit(self): # Tests that the edit page loads response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.event_page.id, ))) self.assertEqual(response.status_code, 200) - def test_edit_post(self): + def test_page_edit_bad_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin') + ) + self.user.save() + + # Get edit page + response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, ))) + + # Check that the user recieved a 403 response + self.assertEqual(response.status_code, 403) + + def test_page_edit_post(self): # Tests simple editing post_data = { 'title': "I've been edited!", @@ -156,7 +198,7 @@ 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_publish(self): + def test_page_edit_post_publish(self): # Tests publish from edit page post_data = { 'title': "I've been edited!", @@ -189,13 +231,27 @@ class TestPageDelete(TestCase): self.root_page.add_child(instance=self.child_page) # Login - login(self.client) + self.user = login(self.client) - def test_delete(self): + def test_page_delete(self): response = self.client.get(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, ))) self.assertEqual(response.status_code, 200) - def test_delete_post(self): + def test_page_delete_bad_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin') + ) + self.user.save() + + # Get delete page + response = self.client.get(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, ))) + + # Check that the user recieved a 403 response + self.assertEqual(response.status_code, 403) + + def test_page_delete_post(self): 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) @@ -259,12 +315,26 @@ class TestPageMove(TestCase): self.section_a.add_child(instance=self.test_page) # Login - login(self.client) + self.user = login(self.client) def test_page_move(self): response = self.client.get(reverse('wagtailadmin_pages_move', args=(self.test_page.id, ))) self.assertEqual(response.status_code, 200) + def test_page_move_bad_permissions(self): + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin') + ) + self.user.save() + + # Get move page + response = self.client.get(reverse('wagtailadmin_pages_move', args=(self.test_page.id, ))) + + # Check that the user recieved a 403 response + self.assertEqual(response.status_code, 403) + def test_page_move_confirm(self): response = self.client.get(reverse('wagtailadmin_pages_move_confirm', args=(self.test_page.id, self.section_b.id))) self.assertEqual(response.status_code, 200) From dcca37264b1eef7e8954e3c6275aecedcad93621 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 14:40:03 +0100 Subject: [PATCH 136/189] Added tests for approve/reject moderation views --- .../wagtailadmin/tests/test_pages_views.py | 71 ++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index eaba42f7b..1c3abf555 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -1,9 +1,10 @@ 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 +from wagtail.wagtailcore.models import Page, PageRevision from django.core.urlresolvers import reverse -from django.contrib.auth.models import Permission +from django.contrib.auth.models import User, Permission +from django.core import mail class TestPageExplorer(TestCase): @@ -409,3 +410,69 @@ class TestPageUnpublish(TestCase): # Check that the page was unpublished self.assertFalse(SimplePage.objects.get(id=self.page.id).live) + + +class TestApproveRejectModeration(TestCase): + def setUp(self): + self.submitter = User.objects.create_superuser( + username='submitter', + email='submitter@email.com', + password='password', + ) + + self.user = login(self.client) + + # Create a page and submit it for moderation + root_page = Page.objects.get(id=2) + self.page = SimplePage( + title="Hello world!", + slug='hello-world', + live=False, + ) + root_page.add_child(instance=self.page) + + self.page.save_revision(user=self.submitter, submitted_for_moderation=True) + self.revision = self.page.get_latest_revision() + + def test_approve_moderation_view(self): + """ + This posts to the approve moderation view and checks that the page was approved + """ + # Post + response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )), { + 'foo': "Must post something or the view won't see this as a POST request", + }) + + # Check that the user was redirected + self.assertEqual(response.status_code, 302) + + # Page must be live + self.assertTrue(Page.objects.get(id=self.page.id).live) + + # Submitter must recieve an approved email + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, ['submitter@email.com']) + self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been approved') + + def test_reject_moderation_view(self): + """ + This posts to the reject moderation view and checks that the page was rejected + """ + # Post + response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(self.revision.id, )), { + 'foo': "Must post something or the view won't see this as a POST request", + }) + + # Check that the user was redirected + self.assertEqual(response.status_code, 302) + + # Page must not be live + self.assertFalse(Page.objects.get(id=self.page.id).live) + + # Revision must no longer be submitted for moderation + self.assertFalse(PageRevision.objects.get(id=self.revision.id).submitted_for_moderation) + + # Submitter must recieve a rejected email + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, ['submitter@email.com']) + self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been rejected') From 046f17e64f4bb2688b8efb224e9686775700b8f7 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 14:44:51 +0100 Subject: [PATCH 137/189] Added error tests for approve/moderation views --- .../wagtailadmin/tests/test_pages_views.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 1c3abf555..23dae25d8 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -454,6 +454,37 @@ class TestApproveRejectModeration(TestCase): self.assertEqual(mail.outbox[0].to, ['submitter@email.com']) self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been approved') + def test_approve_moderation_view_bad_revision_id(self): + """ + This tests that the approve moderation view handles invalid revision ids correctly + """ + # Post + response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(12345, )), { + 'foo': "Must post something or the view won't see this as a POST request", + }) + + # Check that the user recieved a 404 response + self.assertEqual(response.status_code, 404) + + def test_approve_moderation_view_bad_permissions(self): + """ + This tests that the approve moderation view doesn't allow users without moderation permissions + """ + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin') + ) + self.user.save() + + # Post + response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )), { + 'foo': "Must post something or the view won't see this as a POST request", + }) + + # Check that the user recieved a 403 response + self.assertEqual(response.status_code, 403) + def test_reject_moderation_view(self): """ This posts to the reject moderation view and checks that the page was rejected @@ -476,3 +507,34 @@ class TestApproveRejectModeration(TestCase): self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].to, ['submitter@email.com']) self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been rejected') + + def test_reject_moderation_view_bad_revision_id(self): + """ + This tests that the reject moderation view handles invalid revision ids correctly + """ + # Post + response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(12345, )), { + 'foo': "Must post something or the view won't see this as a POST request", + }) + + # Check that the user recieved a 404 response + self.assertEqual(response.status_code, 404) + + def test_reject_moderation_view_bad_permissions(self): + """ + This tests that the reject moderation view doesn't allow users without moderation permissions + """ + # Remove privileges from user + self.user.is_superuser = False + self.user.user_permissions.add( + Permission.objects.get(content_type__app_label='wagtailadmin', codename='access_admin') + ) + self.user.save() + + # Post + response = self.client.post(reverse('wagtailadmin_pages_reject_moderation', args=(self.revision.id, )), { + 'foo': "Must post something or the view won't see this as a POST request", + }) + + # Check that the user recieved a 403 response + self.assertEqual(response.status_code, 403) From 87838f887996e53a15130af60276a50c0bf67f95 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 15:01:37 +0100 Subject: [PATCH 138/189] Improvements to search view tests --- .../wagtailadmin/tests/test_pages_views.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 23dae25d8..bc1a99e89 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -268,25 +268,36 @@ class TestPageSearch(TestCase): # Login login(self.client) - def get(self, params={}): - return self.client.get(reverse('wagtailadmin_pages_search'), params) + def get(self, params=None, **extra): + return self.client.get(reverse('wagtailadmin_pages_search'), params or {}, **extra) - def test_status_code(self): - self.assertEqual(self.get().status_code, 200) + def test_view(self): + response = self.get() + self.assertTemplateUsed(response, 'wagtailadmin/pages/search.html') + self.assertEqual(response.status_code, 200) def test_search(self): response = self.get({'q': "Hello"}) self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/pages/search.html') + self.assertEqual(response.context['query_string'], "Hello") + + def test_ajax(self): + response = self.get({'q': "Hello"}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + self.assertTemplateNotUsed(response, 'wagtailadmin/pages/search.html') + self.assertTemplateUsed(response, 'wagtailadmin/pages/search_results.html') self.assertEqual(response.context['query_string'], "Hello") def test_pagination(self): pages = ['0', '1', '-1', '9999', 'Not a page'] for page in pages: - response = self.get({'p': page}) + response = self.get({'q': "Hello", 'p': page}) self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/pages/search.html') def test_root_can_appear_in_search_results(self): - response = self.client.get('/admin/pages/search/?q=roo') + response = self.get({'q': "roo"}) self.assertEqual(response.status_code, 200) # 'pages' list in the response should contain root results = response.context['pages'] From 4e260fb9a192ba523bf17ebe214b2bcc8846892e Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Fri, 30 May 2014 15:08:12 +0100 Subject: [PATCH 139/189] Unit tests for snippets views --- wagtail/tests/models.py | 18 +++ wagtail/wagtailsnippets/tests.py | 152 +++++++++++++++++++--- wagtail/wagtailsnippets/views/snippets.py | 6 +- 3 files changed, 157 insertions(+), 19 deletions(-) diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index 844fb836f..bbf91d278 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -7,6 +7,7 @@ from wagtail.wagtailadmin.edit_handlers import FieldPanel, MultiFieldPanel, Inli from wagtail.wagtailimages.edit_handlers import ImageChooserPanel from wagtail.wagtaildocs.edit_handlers import DocumentChooserPanel from wagtail.wagtailforms.models import AbstractEmailForm, AbstractFormField +from wagtail.wagtailsnippets.models import register_snippet EVENT_AUDIENCE_CHOICES = ( @@ -252,3 +253,20 @@ FormPage.content_panels = [ FieldPanel('subject', classname="full"), ], "Email") ] + + +# Snippets + +class Advert(models.Model): + url = models.URLField(null=True, blank=True) + text = models.CharField(max_length=255) + + panels = [ + FieldPanel('url'), + FieldPanel('text'), + ] + + def __unicode__(self): + return self.text + +register_snippet(Advert) diff --git a/wagtail/wagtailsnippets/tests.py b/wagtail/wagtailsnippets/tests.py index 782fe9087..fd283f7f5 100644 --- a/wagtail/wagtailsnippets/tests.py +++ b/wagtail/wagtailsnippets/tests.py @@ -1,19 +1,139 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from wagtail.tests.utils import unittest - from django.test import TestCase +from django.core.urlresolvers import reverse +from django.contrib.auth.models import User + +from wagtail.tests.utils import login, unittest +from wagtail.tests.models import Advert -@unittest.skip("Need real tests") -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) +class TestSnippetIndexView(TestCase): + def setUp(self): + login(self.client) + + def get(self, params={}): + return self.client.get(reverse('wagtailsnippets_index'), params) + + def test_status_code(self): + self.assertEqual(self.get().status_code, 200) + + def test_displays_snippet(self): + self.assertContains(self.get(), "Adverts") + + +class TestSnippetListView(TestCase): + def setUp(self): + login(self.client) + + def get(self, params={}): + return self.client.get(reverse('wagtailsnippets_list', + args=('tests', 'advert')), + params) + + def test_status_code(self): + self.assertEqual(self.get().status_code, 200) + + def test_displays_add_button(self): + self.assertContains(self.get(), "Add advert") + + +class TestSnippetCreateView(TestCase): + def setUp(self): + login(self.client) + + def get(self, params={}): + return self.client.get(reverse('wagtailsnippets_create', + args=('tests', 'advert')), + params) + + def post(self, post_data={}): + return self.client.post(reverse('wagtailsnippets_create', + args=('tests', 'advert')), + post_data) + + def test_status_code(self): + self.assertEqual(self.get().status_code, 200) + + def test_create_invalid(self): + response = self.post(post_data={'foo': 'bar'}) + self.assertContains(response, "The snippet could not be created due to errors.") + self.assertContains(response, "This field is required.") + + def test_create(self): + response = self.post(post_data={'text': 'test_advert', + 'url': 'http://www.example.com/'}) + self.assertEqual(response.status_code, 302) + + snippets = Advert.objects.filter(text='test_advert') + self.assertEqual(snippets.count(), 1) + self.assertEqual(snippets.first().url, 'http://www.example.com/') + + +class TestSnippetEditView(TestCase): + def setUp(self): + self.test_snippet = Advert() + self.test_snippet.text = 'test_advert' + self.test_snippet.url = 'http://www.example.com/' + self.test_snippet.save() + + login(self.client) + + def get(self, params={}): + return self.client.get(reverse('wagtailsnippets_edit', + args=('tests', 'advert', self.test_snippet.id)), + params) + + def post(self, post_data={}): + return self.client.post(reverse('wagtailsnippets_edit', + args=('tests', 'advert', self.test_snippet.id)), + post_data) + + def test_status_code(self): + self.assertEqual(self.get().status_code, 200) + + def test_non_existant_model(self): + response = self.client.get(reverse('wagtailsnippets_edit', + args=('tests', 'foo', self.test_snippet.id))) + self.assertEqual(response.status_code, 404) + + def test_nonexistant_id(self): + response = self.client.get(reverse('wagtailsnippets_edit', + args=('tests', 'advert', 999999))) + self.assertEqual(response.status_code, 404) + + def test_edit_invalid(self): + response = self.post(post_data={'foo': 'bar'}) + self.assertContains(response, "The snippet could not be saved due to errors.") + self.assertContains(response, "This field is required.") + + def test_edit(self): + response = self.post(post_data={'text': 'edited_test_advert', + 'url': 'http://www.example.com/edited'}) + self.assertEqual(response.status_code, 302) + + snippets = Advert.objects.filter(text='edited_test_advert') + self.assertEqual(snippets.count(), 1) + self.assertEqual(snippets.first().url, 'http://www.example.com/edited') + + +class TestSnippetDelete(TestCase): + def setUp(self): + self.test_snippet = Advert() + self.test_snippet.text = 'test_advert' + self.test_snippet.url = 'http://www.example.com/' + self.test_snippet.save() + + login(self.client) + + def test_delete_get(self): + response = self.client.get(reverse('wagtailsnippets_delete', args=('tests', 'advert', self.test_snippet.id, ))) + self.assertEqual(response.status_code, 200) + + def test_delete_post(self): + post_data = {'foo': 'bar'} # For some reason, this test doesn't work without a bit of POST data + response = self.client.post(reverse('wagtailsnippets_delete', args=('tests', 'advert', self.test_snippet.id, )), post_data) + + # Should be redirected to explorer page + self.assertEqual(response.status_code, 302) + + # Check that the page is gone + self.assertEqual(Advert.objects.filter(text='test_advert').count(), 0) diff --git a/wagtail/wagtailsnippets/views/snippets.py b/wagtail/wagtailsnippets/views/snippets.py index 763362750..ed0e310cb 100644 --- a/wagtail/wagtailsnippets/views/snippets.py +++ b/wagtail/wagtailsnippets/views/snippets.py @@ -125,7 +125,7 @@ def create(request, content_type_app_name, content_type_model_name): messages.success( request, _("{snippet_type} '{instance}' created.").format( - snippet_type=capfirst(get_snippet_type_name(content_type)[0]), + snippet_type=capfirst(get_snippet_type_name(content_type)[0]), instance=instance ) ) @@ -166,7 +166,7 @@ def edit(request, content_type_app_name, content_type_model_name, id): messages.success( request, _("{snippet_type} '{instance}' updated.").format( - snippet_type=capfirst(snippet_type_name), + snippet_type=capfirst(snippet_type_name), instance=instance ) ) @@ -202,7 +202,7 @@ def delete(request, content_type_app_name, content_type_model_name, id): messages.success( request, _("{snippet_type} '{instance}' deleted.").format( - snippet_type=capfirst(snippet_type_name), + snippet_type=capfirst(snippet_type_name), instance=instance ) ) From 890072aa5288e9a42e9f651d49580b2d3405301d Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 15:21:26 +0100 Subject: [PATCH 140/189] Added tests for fixtree command --- .../management/commands/fixtree.py | 22 ++++----- wagtail/wagtailcore/tests.py | 45 +++++++++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/wagtail/wagtailcore/management/commands/fixtree.py b/wagtail/wagtailcore/management/commands/fixtree.py index 4bf989d1e..7fbed1e5b 100644 --- a/wagtail/wagtailcore/management/commands/fixtree.py +++ b/wagtail/wagtailcore/management/commands/fixtree.py @@ -12,15 +12,15 @@ class Command(NoArgsCommand): try: page.specific except ObjectDoesNotExist: - print "Page %d (%s) is missing a subclass record; deleting." % (page.id, page.title) + self.stdout.write("Page %d (%s) is missing a subclass record; deleting." % (page.id, page.title)) problems_found = True page.delete() (_, _, _, bad_depth, bad_numchild) = Page.find_problems() if bad_depth: - print "Incorrect depth value found for pages: %r" % bad_depth + self.stdout.write("Incorrect depth value found for pages: %r" % bad_depth) if bad_numchild: - print "Incorrect numchild value found for pages: %r" % bad_numchild + self.stdout.write("Incorrect numchild value found for pages: %r" % bad_numchild) if bad_depth or bad_numchild: Page.fix_tree(destructive=False) @@ -28,20 +28,20 @@ class Command(NoArgsCommand): remaining_problems = Page.find_problems() if any(remaining_problems): - print "Remaining problems (cannot fix automatically):" + self.stdout.write("Remaining problems (cannot fix automatically):") (bad_alpha, bad_path, orphans, bad_depth, bad_numchild) = remaining_problems if bad_alpha: - print "Invalid characters found in path for pages: %r" % bad_alpha + self.stdout.write("Invalid characters found in path for pages: %r" % bad_alpha) if bad_path: - print "Invalid path length found for pages: %r" % bad_path + self.stdout.write("Invalid path length found for pages: %r" % bad_path) if orphans: - print "Orphaned pages found: %r" % orphans + self.stdout.write("Orphaned pages found: %r" % orphans) if bad_depth: - print "Incorrect depth value found for pages: %r" % bad_depth + self.stdout.write("Incorrect depth value found for pages: %r" % bad_depth) if bad_numchild: - print "Incorrect numchild value found for pages: %r" % bad_numchild + self.stdout.write("Incorrect numchild value found for pages: %r" % bad_numchild) elif problems_found: - print "All problems fixed." + self.stdout.write("All problems fixed.") else: - print "No problems found." + self.stdout.write("No problems found.") diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index 1b12c1025..ce00002f6 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -1,5 +1,7 @@ from django.test import TestCase, Client from django.http import HttpRequest, Http404 +from django.core import management +from StringIO import StringIO from django.contrib.auth.models import User @@ -776,3 +778,46 @@ class TestIssue157(TestCase): # Check url self.assertEqual(homepage.url, '/') + + +class TestFixTreeCommand(TestCase): + fixtures = ['test.json'] + + def run_command(self): + management.call_command('fixtree', interactive=False, stdout=StringIO()) + + def test_fixes_numchild(self): + # Get homepage and save old value + homepage = Page.objects.get(url_path='/home/') + old_numchild = homepage.numchild + + # Break it + homepage.numchild = 12345 + homepage.save() + + # Check that its broken + self.assertEqual(Page.objects.get(url_path='/home/').numchild, 12345) + + # Call command + self.run_command() + + # Check if its fixed + self.assertEqual(Page.objects.get(url_path='/home/').numchild, old_numchild) + + def test_fixes_depth(self): + # Get homepage and save old value + homepage = Page.objects.get(url_path='/home/') + old_depth = homepage.depth + + # Break it + homepage.depth = 12345 + homepage.save() + + # Check that its broken + self.assertEqual(Page.objects.get(url_path='/home/').depth, 12345) + + # Call command + self.run_command() + + # Check if its fixed + self.assertEqual(Page.objects.get(url_path='/home/').depth, old_depth) From 9b0e56dc90eed67cca795c0b76b38b844a81ead5 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 15:43:34 +0100 Subject: [PATCH 141/189] Fixed invalid numchild values in test fixtures --- wagtail/tests/fixtures/test.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index 625c5618b..11b82d9f1 100644 --- a/wagtail/tests/fixtures/test.json +++ b/wagtail/tests/fixtures/test.json @@ -39,7 +39,7 @@ "model": "wagtailcore.page", "fields": { "title": "Events", - "numchild": 2, + "numchild": 3, "show_in_menus": true, "live": true, "depth": 3, @@ -62,7 +62,7 @@ "model": "wagtailcore.page", "fields": { "title": "Christmas", - "numchild": 1, + "numchild": 0, "show_in_menus": true, "live": true, "depth": 4, @@ -90,7 +90,7 @@ "model": "wagtailcore.page", "fields": { "title": "Tentative Unpublished Event", - "numchild": 1, + "numchild": 0, "show_in_menus": true, "live": false, "depth": 4, @@ -118,7 +118,7 @@ "model": "wagtailcore.page", "fields": { "title": "Someone Else's Event", - "numchild": 1, + "numchild": 0, "show_in_menus": true, "live": false, "depth": 4, From e9ce499d5a40287b82b1caea955a861d5241066e Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 15:43:54 +0100 Subject: [PATCH 142/189] Added test for move_pages command --- .../management/commands/move_pages.py | 4 ++-- wagtail/wagtailcore/tests.py | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailcore/management/commands/move_pages.py b/wagtail/wagtailcore/management/commands/move_pages.py index 344604f06..86ea4016d 100644 --- a/wagtail/wagtailcore/management/commands/move_pages.py +++ b/wagtail/wagtailcore/management/commands/move_pages.py @@ -15,8 +15,8 @@ class Command(BaseCommand): pages = from_page.get_children() # Move the pages - print 'Moving ' + str(len(pages)) + ' pages from "' + from_page.title + '" to "' + to_page.title + '"' + self.stdout.write('Moving ' + str(len(pages)) + ' pages from "' + from_page.title + '" to "' + to_page.title + '"') for page in pages: page.move(to_page, pos='last-child') - print 'Done' + self.stdout.write('Done') diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index ce00002f6..8cbd7be9a 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -821,3 +821,23 @@ class TestFixTreeCommand(TestCase): # Check if its fixed self.assertEqual(Page.objects.get(url_path='/home/').depth, old_depth) + + +class TestMovePagesCommand(TestCase): + fixtures = ['test.json'] + + def run_command(self, from_, to): + management.call_command('move_pages', str(from_), str(to), interactive=False, stdout=StringIO()) + + def test_move_pages(self): + # Get pages + events_index = Page.objects.get(url_path='/home/events/') + about_us = Page.objects.get(url_path='/home/about-us/') + page_ids = events_index.get_children().values_list('id', flat=True) + + # Move all events into "about us" + self.run_command(events_index.id, about_us.id) + + # Check that all pages moved + for page_id in page_ids: + self.assertEqual(Page.objects.get(id=page_id).get_parent(), about_us) From 86969e9df3a4e5416e14c8a0a67a1d047d9ba782 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 15:49:21 +0100 Subject: [PATCH 143/189] Added test for replace_text command --- .../management/commands/replace_text.py | 2 +- wagtail/wagtailcore/tests.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/management/commands/replace_text.py b/wagtail/wagtailcore/management/commands/replace_text.py index 23788d370..d654a61d6 100644 --- a/wagtail/wagtailcore/management/commands/replace_text.py +++ b/wagtail/wagtailcore/management/commands/replace_text.py @@ -24,7 +24,7 @@ class Command(BaseCommand): revision.save(update_fields=['content_json']) for content_type in get_page_types(): - print "scanning %s" % content_type.name + self.stdout.write("scanning %s" % content_type.name) page_class = content_type.model_class() try: diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index 8cbd7be9a..df66c6b93 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -841,3 +841,20 @@ class TestMovePagesCommand(TestCase): # Check that all pages moved for page_id in page_ids: self.assertEqual(Page.objects.get(id=page_id).get_parent(), about_us) + + +class TestReplaceTextCommand(TestCase): + fixtures = ['test.json'] + + def run_command(self, from_text, to_text): + management.call_command('replace_text', from_text, to_text, interactive=False, stdout=StringIO()) + + def test_replace_text(self): + # Check that the christmas page is definitely about christmas + self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Christmas") + + # Make it about easter + self.run_command("Christmas", "Easter") + + # Check that its now about easter + self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter") From 2a92db9e5af1e2e9df0ecc4184f1be74668c4c27 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 15:55:04 +0100 Subject: [PATCH 144/189] Removed unneeded import --- wagtail/tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/tests/utils.py b/wagtail/tests/utils.py index 4774fc4da..6590e6dcc 100644 --- a/wagtail/tests/utils.py +++ b/wagtail/tests/utils.py @@ -1,4 +1,4 @@ -from django.contrib.auth.models import User, Permission +from django.contrib.auth.models import User # We need to make sure that we're using the same unittest library that Django uses internally # Otherwise, we get issues with the "SkipTest" and "ExpectedFailure" exceptions being recognised as errors From d057b1774f2cb159f6caa06ebf91817dea6ace92 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 16:07:23 +0100 Subject: [PATCH 145/189] Added tests for submitting pages to moderation --- .../wagtailadmin/tests/test_pages_views.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index bc1a99e89..9cad6ecdc 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -111,6 +111,36 @@ class TestPageCreation(TestCase): self.assertIsInstance(page, SimplePage) self.assertTrue(page.live) + def test_create_simplepage_post_submit(self): + # Create a moderator user for testing email + moderator = User.objects.create_superuser('moderator', 'moderator@email.com', 'password') + + # Submit + post_data = { + 'title': "New page!", + 'content': "Some content", + 'slug': 'hello-world', + 'action-submit': "Submit", + } + 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.assertEqual(page.title, post_data['title']) + self.assertIsInstance(page, SimplePage) + self.assertFalse(page.live) + + # The latest revision for the page should now be in moderation + self.assertTrue(page.get_latest_revision().submitted_for_moderation) + + # Check that the moderator got an email + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, ['moderator@email.com']) + self.assertEqual(mail.outbox[0].subject, 'The page "New page!" has been submitted for moderation') + def test_create_simplepage_post_existingslug(self): # This tests the existing slug checking on page save @@ -219,6 +249,34 @@ class TestPageEdit(TestCase): # The page shouldn't have "has_unpublished_changes" flag set self.assertFalse(child_page_new.has_unpublished_changes) + def test_page_edit_post_submit(self): + # Create a moderator user for testing email + moderator = User.objects.create_superuser('moderator', 'moderator@email.com', 'password') + + # Tests submitting from edit page + post_data = { + 'title': "I've been edited!", + 'content': "Some content", + 'slug': 'hello-world', + 'action-submit': "Submit", + } + 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) + + # The page should have "has_unpublished_changes" flag set + child_page_new = SimplePage.objects.get(id=self.child_page.id) + self.assertTrue(child_page_new.has_unpublished_changes) + + # The latest revision for the page should now be in moderation + self.assertTrue(child_page_new.get_latest_revision().submitted_for_moderation) + + # Check that the moderator got an email + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, ['moderator@email.com']) + self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been submitted for moderation') # Note: should this be "I've been edited!"? + class TestPageDelete(TestCase): def setUp(self): From c4399636dd726dd09acc026b5fbf77fe69c84969 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 16:16:57 +0100 Subject: [PATCH 146/189] Added test for preview on edit --- wagtail/wagtailadmin/tests/test_pages_views.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 9cad6ecdc..23ece10bb 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -277,6 +277,21 @@ class TestPageEdit(TestCase): self.assertEqual(mail.outbox[0].to, ['moderator@email.com']) self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been submitted for moderation') # Note: should this be "I've been edited!"? + def test_preview_on_edit(self): + # Tests submitting from edit page + post_data = { + 'title': "I've been edited!", + 'content': "Some content", + 'slug': 'hello-world', + 'action-submit': "Submit", + } + response = self.client.post(reverse('wagtailadmin_pages_preview_on_edit', args=(self.child_page.id, )), post_data) + + # Check the response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'tests/simple_page.html') + self.assertContains(response, "I've been edited!") + class TestPageDelete(TestCase): def setUp(self): From 73242ad02b4b6354de69cb33f949ee2cdeee50c5 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 16:19:55 +0100 Subject: [PATCH 147/189] Added test for preview on create --- wagtail/wagtailadmin/tests/test_pages_views.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 23ece10bb..a2531e63b 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -171,6 +171,20 @@ class TestPageCreation(TestCase): response = self.client.get(reverse('wagtailadmin_pages_create', args=('wagtailimages', 'image', self.root_page.id))) self.assertEqual(response.status_code, 404) + def test_preview_on_create(self): + post_data = { + 'title': "New page!", + 'content': "Some content", + 'slug': 'hello-world', + 'action-submit': "Submit", + } + response = self.client.post(reverse('wagtailadmin_pages_preview_on_create', args=('tests', 'simplepage', self.root_page.id)), post_data) + + # Check the response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'tests/simple_page.html') + self.assertContains(response, "New page!") + class TestPageEdit(TestCase): def setUp(self): @@ -278,7 +292,6 @@ class TestPageEdit(TestCase): self.assertEqual(mail.outbox[0].subject, 'The page "Hello world!" has been submitted for moderation') # Note: should this be "I've been edited!"? def test_preview_on_edit(self): - # Tests submitting from edit page post_data = { 'title': "I've been edited!", 'content': "Some content", From c4107067096f62a0ee11f8a0ebecac356a7dfbc1 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 16:24:45 +0100 Subject: [PATCH 148/189] Added test for content_type_use --- wagtail/wagtailadmin/tests/test_pages_views.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index a2531e63b..5bcb0594f 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -635,3 +635,19 @@ class TestApproveRejectModeration(TestCase): # Check that the user recieved a 403 response self.assertEqual(response.status_code, 403) + + +class TestContentTypeUse(TestCase): + fixtures = ['test.json'] + + def setUp(self): + self.user = login(self.client) + + def test_content_type_use(self): + # Get use of event page + response = self.client.get(reverse('wagtailadmin_pages_type_use', args=('tests', 'eventpage'))) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailadmin/pages/content_type_use.html') + self.assertContains(response, "Christmas") From 488acc6afa86aba35ecf37b6c1babd8c44390633 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 16:36:56 +0100 Subject: [PATCH 149/189] Added test for preview for moderation --- wagtail/wagtailadmin/tests/test_pages_views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 5bcb0594f..ccaa67dae 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -636,6 +636,14 @@ class TestApproveRejectModeration(TestCase): # Check that the user recieved a 403 response self.assertEqual(response.status_code, 403) + def test_preview_for_moderation(self): + response = self.client.get(reverse('wagtailadmin_pages_preview_for_moderation', args=(self.revision.id, ))) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'tests/simple_page.html') + self.assertContains(response, "Hello world!") + class TestContentTypeUse(TestCase): fixtures = ['test.json'] From 370442d13dbb7c309784c861c7841d317709194b Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 16:38:32 +0100 Subject: [PATCH 150/189] Added missing template --- wagtail/tests/templates/tests/simple_page.html | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 wagtail/tests/templates/tests/simple_page.html diff --git a/wagtail/tests/templates/tests/simple_page.html b/wagtail/tests/templates/tests/simple_page.html new file mode 100644 index 000000000..3413b3aa2 --- /dev/null +++ b/wagtail/tests/templates/tests/simple_page.html @@ -0,0 +1,11 @@ +{% load pageurl %} + + + + {{ self.title }} + + +

      {{ self.title }}

      +

      Simple page

      + + From 85ef536f0566bdb9610bd993d4ef6bdb1a54b3a6 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 30 May 2014 16:45:27 +0100 Subject: [PATCH 151/189] Added test for send email task --- wagtail/wagtailadmin/tests/tests.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/wagtail/wagtailadmin/tests/tests.py b/wagtail/wagtailadmin/tests/tests.py index 3e5db6204..12aa28a0b 100644 --- a/wagtail/wagtailadmin/tests/tests.py +++ b/wagtail/wagtailadmin/tests/tests.py @@ -2,7 +2,9 @@ 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 +from wagtail.wagtailadmin.tasks import send_email_task from django.core.urlresolvers import reverse +from django.core import mail class TestHome(TestCase): @@ -31,3 +33,14 @@ class TestEditorHooks(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, '') self.assertContains(response, '') + + +class TestSendEmailTask(TestCase): + def test_send_email(self): + send_email_task("Test subject", "Test content", ["nobody@email.com"], "test@email.com") + + # Check that the email was sent + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].subject, "Test subject") + self.assertEqual(mail.outbox[0].body, "Test content") + self.assertEqual(mail.outbox[0].to, ["nobody@email.com"]) From 4887bb7cf7df9d47153bc79be6eb22d44feb3e36 Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Fri, 30 May 2014 17:13:07 +0100 Subject: [PATCH 152/189] Added tests for snippets edit_handlers.py --- wagtail/wagtailsnippets/tests.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/wagtail/wagtailsnippets/tests.py b/wagtail/wagtailsnippets/tests.py index fd283f7f5..727e5e52d 100644 --- a/wagtail/wagtailsnippets/tests.py +++ b/wagtail/wagtailsnippets/tests.py @@ -5,6 +5,8 @@ from django.contrib.auth.models import User from wagtail.tests.utils import login, unittest from wagtail.tests.models import Advert +from wagtail.wagtailsnippets.views.snippets import get_content_type_from_url_params, get_snippet_edit_handler +from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel class TestSnippetIndexView(TestCase): def setUp(self): @@ -137,3 +139,32 @@ class TestSnippetDelete(TestCase): # Check that the page is gone self.assertEqual(Advert.objects.filter(text='test_advert').count(), 0) + + +class TestSnippetChooserPanel(TestCase): + def setUp(self): + content_type = get_content_type_from_url_params('tests', + 'advert') + + test_snippet = Advert() + test_snippet.text = 'test_advert' + test_snippet.url = 'http://www.example.com/' + test_snippet.save() + + edit_handler_class = get_snippet_edit_handler(Advert) + form_class = edit_handler_class.get_form_class(Advert) + form = form_class(instance=test_snippet) + + self.snippet_chooser_panel_class = SnippetChooserPanel('text', content_type) + self.snippet_chooser_panel = self.snippet_chooser_panel_class(instance=test_snippet, + form=form) + + def test_create_snippet_chooser_panel_class(self): + self.assertEqual(self.snippet_chooser_panel_class.__name__, '_SnippetChooserPanel') + + def test_render_as_field(self): + self.assertTrue('test_advert' in self.snippet_chooser_panel.render_as_field()) + + def test_render_js(self): + self.assertTrue("createSnippetChooser(fixPrefix('id_text'), 'contenttypes/contenttype');" + in self.snippet_chooser_panel.render_js()) From 9ccdcc604e2b81ccb7506f241e502808cd7b7096 Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Fri, 30 May 2014 17:24:37 +0100 Subject: [PATCH 153/189] Recache coverage badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d967dcb1f..3f29a679b 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ .. image:: https://travis-ci.org/torchbox/wagtail.png?branch=master :target: https://travis-ci.org/torchbox/wagtail -.. image:: https://coveralls.io/repos/torchbox/wagtail/badge.png?branch=master +.. image:: https://coveralls.io/repos/torchbox/wagtail/badge.png?branch=master&zxcv :target: https://coveralls.io/r/torchbox/wagtail?branch=master .. image:: https://pypip.in/v/wagtail/badge.png?zxcv From 337f5d414648aa544fa9000a19a6d8d9241e03d4 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 2 Jun 2014 12:42:59 +0100 Subject: [PATCH 154/189] unittest2 no longer an 'essential' requirement for developers --- requirements-dev.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 008d24d2e..d35d6b087 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,3 @@ -# Requirements essential for developing wagtail (not needed to run it) - -unittest2==0.5.1 - # For coverage and PEP8 linting coverage==3.7.1 flake8==2.1.0 From 3f9cb2da6fcd614c3206631d6132d4286b6f1fbc Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 2 Jun 2014 13:27:39 +0100 Subject: [PATCH 155/189] Made login view redirect already logged in users to dashboard. Fixes #25 --- .../account/password_reset/complete.html | 2 +- .../templates/wagtailadmin/login.html | 2 +- .../tests/test_account_management.py | 1 - wagtail/wagtailadmin/urls.py | 10 ++------- wagtail/wagtailadmin/views/account.py | 22 ++++++++++++++++++- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html index c0cf872e2..3dc8272b0 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html @@ -13,6 +13,6 @@ {% block furniture %}

      {% trans "Password change successful" %}

      -

      {% trans "Login" %}

      +

      {% trans "Login" %}

      {% endblock %} \ No newline at end of file diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/login.html b/wagtail/wagtailadmin/templates/wagtailadmin/login.html index 012dd32f8..22682500f 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/login.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/login.html @@ -20,7 +20,7 @@
      {% endif %} -
      + {% csrf_token %}

      {% trans "Sign in to Wagtail" %}

      diff --git a/wagtail/wagtailadmin/tests/test_account_management.py b/wagtail/wagtailadmin/tests/test_account_management.py index 95d54d7d9..63c70779b 100644 --- a/wagtail/wagtailadmin/tests/test_account_management.py +++ b/wagtail/wagtailadmin/tests/test_account_management.py @@ -49,7 +49,6 @@ class TestAuthentication(TestCase): self.assertTrue('_auth_user_id' in self.client.session) self.assertEqual(self.client.session['_auth_user_id'], User.objects.get(username='test').id) - @unittest.expectedFailure # See: https://github.com/torchbox/wagtail/issues/25 def test_already_logged_in_redirect(self): """ This tests that a user who is already logged in is automatically diff --git a/wagtail/wagtailadmin/urls.py b/wagtail/wagtailadmin/urls.py index 8fbf2f6e7..806240c5c 100644 --- a/wagtail/wagtailadmin/urls.py +++ b/wagtail/wagtailadmin/urls.py @@ -5,15 +5,8 @@ from wagtail.wagtailadmin.forms import LoginForm, PasswordResetForm from wagtail.wagtailadmin.views import account, chooser, home, pages, tags, userbar from wagtail.wagtailadmin import hooks -urlpatterns = [ - url( - r'^login/$', 'django.contrib.auth.views.login', { - 'template_name': 'wagtailadmin/login.html', - 'authentication_form': LoginForm, - 'extra_context': {'show_password_reset': getattr(settings, 'WAGTAIL_PASSWORD_MANAGEMENT_ENABLED', True)}, - }, name='wagtailadmin_login' - ), +urlpatterns = [ # Password reset url( r'^password_reset/$', 'django.contrib.auth.views.password_reset', { @@ -81,6 +74,7 @@ urlpatterns += [ url(r'^tag-autocomplete/$', tags.autocomplete, name='wagtailadmin_tag_autocomplete'), + url(r'^login/$', account.login, name='wagtailadmin_login'), url(r'^account/$', account.account, name='wagtailadmin_account'), url(r'^account/change_password/$', account.change_password, name='wagtailadmin_account_change_password'), url(r'^logout/$', account.logout, name='wagtailadmin_logout'), diff --git a/wagtail/wagtailadmin/views/account.py b/wagtail/wagtailadmin/views/account.py index 8479ea6b0..c5e461f55 100644 --- a/wagtail/wagtailadmin/views/account.py +++ b/wagtail/wagtailadmin/views/account.py @@ -3,8 +3,13 @@ from django.shortcuts import render, redirect from django.contrib import messages from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.decorators import permission_required -from django.contrib.auth.views import logout as auth_logout +from django.contrib.auth.views import logout as auth_logout, login as auth_login from django.utils.translation import ugettext as _ +from django.views.decorators.debug import sensitive_post_parameters +from django.views.decorators.cache import never_cache + +from wagtail.wagtailadmin import forms + @permission_required('wagtailadmin.access_admin') def account(request): @@ -37,6 +42,21 @@ def change_password(request): }) +@sensitive_post_parameters() +@never_cache +def login(request): + if request.user.is_authenticated(): + return redirect('wagtailadmin_home') + else: + return auth_login(request, + template_name='wagtailadmin/login.html', + authentication_form=forms.LoginForm, + extra_context={ + 'show_password_reset': getattr(settings, 'WAGTAIL_PASSWORD_MANAGEMENT_ENABLED', True), + }, + ) + + def logout(request): response = auth_logout(request, next_page = 'wagtailadmin_login') From 13c1af3d970346e809e37fe74ed2101d6d95512a Mon Sep 17 00:00:00 2001 From: Tom Dyson Date: Mon, 2 Jun 2014 13:49:10 +0100 Subject: [PATCH 156/189] Don't be coy about our test coverage --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3f29a679b..62b3c5cd5 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ Wagtail is a Django content management system built originally for the `Royal Co * Support for tree-based content organisation * Optional preview->submit->approve workflow * Fast out of the box. `Varnish `_-friendly if you need it -* Tests! But not enough; we're working hard to improve this +* Excellent test coverage Find out more at `wagtail.io `_. From 3fb8dc03e57b4457e1ff95b0f0549648018f4e62 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Mon, 2 Jun 2014 15:54:26 +0100 Subject: [PATCH 157/189] updates to FE docs --- .../building_your_site/frontenddevelopers.rst | 118 +++++++++++++----- 1 file changed, 84 insertions(+), 34 deletions(-) diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index d39bd8c33..8febd2923 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -4,51 +4,77 @@ For Front End developers .. note:: This documentation is currently being written. +.. contents:: + ======================== Overview ======================== +This page is aimed at non-Django-literate Front End developers. + Wagtail uses Django's templating language. For developers new to Django, start with Django's own template documentation: https://docs.djangoproject.com/en/dev/topics/templates/ Python programmers new to Django/Wagtail may prefer more technical documentation: https://docs.djangoproject.com/en/dev/ref/templates/api/ +You should be familiar with Django templating basics before continuing with this documentation. + ========================== -Displaying Pages +Templates ========================== -Template Location -~~~~~~~~~~~~~~~~~ +Every type of page or "content type" in Wagtail is defined in "The Model": a file called ``models.py``. The name of each "page model" is prefixing with ``class``. e.g If your site has a blog, you might have a ``BlogPage`` page model and another called ``BlogPageListing``. The names of the models are up to the developer. -For each of your ``Page``-derived models, Wagtail will look for a template in the following location, relative to your project root:: +For each type of page in ``models.py``, Wagtail assumes an HTML template file exists of (almost) the same name. The Front End developer may need to create these templates themselves. - project/ - app/ +To find a suitable template, Wagtail converts CamelCase names to underscore_case. So for a ``BlogPage``, a template file ``blog_page.html`` will be expected. The name of the template file can be overridden per model if necessary. + +Template files are assumed to exist here:: + + name_of_project/ + name_of_app/ templates/ - app/ - blog_index_page.html + name_of_app/ + blog_page.html models.py -Class names are converted from camel case to underscores. For example, the template for model class ``BlogIndexPage`` would be assumed to be ``blog_index_page.html``. For more information, see the Django documentation for the `application directories template loader`_. + +For more information, see the Django documentation for the `application directories template loader`_. .. _application directories template loader: https://docs.djangoproject.com/en/dev/ref/templates/api/ -Self -~~~~ +Page content +~~~~~~~~~~~~ -By default, the context passed to a model's template consists of two properties: ``self`` and ``request``. ``self`` is the model object being displayed. ``request`` is the normal Django request object. So, to include the title of a ``Page``, use ``{{ self.title }}``. +The data/content entered into each page is accessed/output through Django's ``{{ double-brace }}`` notation. Each field from the model must be accessed by prefixing ``self.``. e.g the page title ``{{ self.title }}`` or another field ``{{ self.author }}``. -======================== -Static files (css, js, images) -======================== +Additionally ``request.`` is available and contains Django's request object. +============== +Static assets +============== -Images -~~~~~~ +Static files e.g CSS, JS and images are typically stored here:: + + name_of_project/ + name_of_app/ + static/ + name_of_app/ + css/ + js/ + images/ + models.py -Images uploaded to Wagtail go into the image library and from there are added to pages via the :doc:`page editor interface `. +(The names "css", "js" etc aren't important, only their position within the tree.) + +Any file within the static folder should be inserted into your HTML using the ``{% static %}`` tag. More about it: :ref:`static_tag`. + +User images +~~~~~~~~~~~ + +Images uploaded to Wagtail by its users go into the image library and from there are added to pages via the :doc:`page editor interface `. Unlike other CMS, adding images to a page does not involve choosing a "version" of the image to use. Wagtail has no predefined image "formats" or "sizes". Instead the template developer defines image manipulation to occur *on the fly* when the image is requested, via a special syntax within the template. @@ -73,16 +99,21 @@ The syntax for displaying/manipulating an image is thus:: {% image [image] [method]-[dimension(s)] %} -For example:: +For example: + +.. code-block:: django + + {% load image %} + ... {% image self.photo width-400 %} {% image self.photo fill-80x80 %} -The ``image`` is the Django object refering to the image. If your page model defined a field called "photo" then ``image`` would probably be ``self.photo``. The ``method`` defines which resizing algorithm to use and ``dimension(s)`` provides height and/or width values (as ``[width|height]`` or ``[width]x[height]``) to refine that algorithm. +In the above syntax ``[image]`` is the Django object refering to the image. If your page model defined a field called "photo" then ``[image]`` would probably be ``self.photo``. The ``[method]`` defines which resizing algorithm to use and ``[dimension(s)]`` provides height and/or width values (as ``[width|height]`` or ``[width]x[height]``) to refine that algorithm. -Note that a space separates ``image`` and ``method``, but not ``method`` and ``dimensions``: a hyphen between ``method`` and ``dimensions`` is mandatory. Multiple dimensions must be separated by an ``x``. +Note that a space separates ``[image]`` and ``[method]``, but not ``[method]`` and ``[dimensions]``: a hyphen between ``[method]`` and ``[dimensions]`` is mandatory. Multiple dimensions must be separated by an ``x``. The available ``method`` s are: @@ -123,53 +154,63 @@ The available ``method`` s are: .. Note:: Wagtail *does not allow deforming or stretching images*. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions. - -To request the "original" version of an image, it is suggested you rely on the lack of upscaling support by requesting an image much larger than it's maximum dimensions. e.g to insert an image who's dimensions are uncertain/unknown, at it's maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide. +.. Note:: + Wagtail does not make the "original" version of an image explicitly available. To request it, it's suggested you rely on the lack of upscaling by requesting an image much larger than it's maximum dimensions. e.g to insert an image who's dimensions are uncertain/unknown at it's maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide. .. _rich-text-filter: + Rich text (filter) ~~~~~~~~~~~~~~~~~~ -This filter is required for use with any ``RichTextField``. It will expand internal shorthand references to embeds and links made in the Wagtail editor into fully-baked HTML ready for display. **Note that the template tag loaded differs from the name of the filter.** +This filter is required for use with any field that generates raw HTML e.g ``RichTextField``. It will expand internal shorthand references to embeds and links made in the Wagtail editor into fully-baked HTML ready for display. .. code-block:: django {% load rich_text %} ... - {{ body|richtext }} + {{ self.body|richtext }} + +.. Note:: + Note that the template tag loaded differs from the name of the filter. Internal links (tag) ~~~~~~~~~~~~~~~~~~~~ **pageurl** -Takes a ``Page``-derived object and returns its 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. +Takes a Page object and returns a relative URL (``/foo/bar/``) if within the same site as the current page, or absolute (``http://example.com/foo/bar/``) if not. .. code-block:: django {% load pageurl %} ... - + **slugurl** -Takes a ``slug`` string and returns the URL for the ``Page``-derived object with that slug. Like ``pageurl``, will try to provide a relative link if possible, but will default to an absolute link if on a different site. +Takes any ``slug`` as defined in a page's "Promote" tab and returns the URL for the matching Page. Like ``pageurl``, will try to provide a relative link if possible, but will default to an absolute link if on a different site. This is most useful when creating shared page furniture e.g top level navigation or site-wide links. .. code-block:: django {% load slugurl %} ... - - + +.. _static_tag: Static files (tag) -~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~ +Used to load anything from your static files directory. Use of this tag avoids rewriting all static paths if hosting arrangements change, as they might between local and a live environments. -Misc -~~~~~~~~~~ +.. code-block:: django + + {% load static %} + ... + My image + +Notice that the full path name is not required and the path snippet you enter only need begin with the parent app's directory name. @@ -177,10 +218,19 @@ Misc Wagtail User Bar ======================== -This tag provides a Wagtail icon and flyout menu on the top-right of a page for a logged-in user with editing capabilities, with the option of editing the current Page-derived object or adding a new sibling object. +This tag provides a contextual flyout menu on the top-right of a page for logged-in users. The menu gives editors the ability to edit the current page or add another at the same level. Moderators are also given the ability to accept or reject a page previewed as part of content moderation. .. code-block:: django {% load wagtailuserbar %} ... {% wagtailuserbar %} + +By default the User Bar appears in the top right of the browser window, flush with the edge. If this conflicts with your design it can be moved with a css rule in your own CSS files e.g to move it down from the top: + +.. code-block:: css + + #wagtail-userbar{ + top:200px + } + From 4c8ae31b6f459d68707c2a9081735124612ce3c9 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Mon, 2 Jun 2014 15:56:16 +0100 Subject: [PATCH 158/189] removed under construction notice. I consider it complete-ish. Otherwise it'll never be removed --- docs/building_your_site/frontenddevelopers.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index 8febd2923..6232f5383 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -1,9 +1,6 @@ For Front End developers ======================== -.. note:: - This documentation is currently being written. - .. contents:: ======================== From e7e63e605e725ce8a21d564411dbb0ab5bf8b892 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Mon, 2 Jun 2014 16:15:49 +0100 Subject: [PATCH 159/189] added a note about the image tag 'as' syntax --- .../building_your_site/frontenddevelopers.rst | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index 6232f5383..78c8cf78e 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -71,11 +71,11 @@ Any file within the static folder should be inserted into your HTML using the `` User images ~~~~~~~~~~~ -Images uploaded to Wagtail by its users go into the image library and from there are added to pages via the :doc:`page editor interface `. +Images uploaded to Wagtail by its users (as opposed to a developer's static files, above) go into the image library and from there are added to pages via the :doc:`page editor interface `. Unlike other CMS, adding images to a page does not involve choosing a "version" of the image to use. Wagtail has no predefined image "formats" or "sizes". Instead the template developer defines image manipulation to occur *on the fly* when the image is requested, via a special syntax within the template. -Images from the library **must** be requested using this syntax, but images in your codebase can be added via conventional means e.g ``img`` tags. Only images from the library can be manipulated on the fly. +Images from the library **must** be requested using this syntax, but a developer's static images can be added via conventional means e.g ``img`` tags. Only images from the library can be manipulated on the fly. Read more about the image manipulation syntax here :ref:`image_tag`. @@ -92,7 +92,9 @@ In addition to Django's standard tags and filters, Wagtail provides some of it's Images (tag) ~~~~~~~~~~~~ -The syntax for displaying/manipulating an image is thus:: +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:: {% image [image] [method]-[dimension(s)] %} @@ -154,6 +156,24 @@ The available ``method`` s are: .. Note:: Wagtail does not make the "original" version of an image explicitly available. To request it, it's suggested you rely on the lack of upscaling by requesting an image much larger than it's maximum dimensions. e.g to insert an image who's dimensions are uncertain/unknown at it's maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide. + +.. _image_tag_alt: + +More control over the ``img`` tag +--------------------------------- + +In some cases greater control over the ``img`` tag is required, for example to add a custom ``class``. Rather than generating the ``img`` element for you, Wagtail can assign the relevant data to another object using Django's ``as`` syntax: + +.. code-block:: django + + {% load image %} + ... + {% image self.photo width-400 as tmp_photo %} + + {{ tmp_photo.alt }} + + .. _rich-text-filter: Rich text (filter) From 94f14ab12a191db6d188c88f3c6947f974bf4011 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 2 Jun 2014 16:17:40 +0100 Subject: [PATCH 160/189] Added changelog entry for #280 --- CHANGELOG.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0a6024bd1..d2914194b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,8 @@ Changelog ========= + * When logged in user visits login page, they are now redirected to the dashboard + 0.3 (28.05.2014) ~~~~~~~~~~~~~~~~ * Added toolbar to allow logged-in users to add and edit pages from the site front-end From 09f1300f89a121852ee05fa8a82a5a75d2247008 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Mon, 2 Jun 2014 16:26:46 +0100 Subject: [PATCH 161/189] wording tweak --- docs/building_your_site/frontenddevelopers.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index 78c8cf78e..3d3117b84 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -21,11 +21,11 @@ You should be familiar with Django templating basics before continuing with this Templates ========================== -Every type of page or "content type" in Wagtail is defined in "The Model": a file called ``models.py``. The name of each "page model" is prefixing with ``class``. e.g If your site has a blog, you might have a ``BlogPage`` page model and another called ``BlogPageListing``. The names of the models are up to the developer. +Every type of page or "content type" in Wagtail is defined as a "model" in a file called ``models.py``. If your site has a blog, you might have a ``BlogPage`` model and another called ``BlogPageListing``. The names of the models are up to the Django developer. -For each type of page in ``models.py``, Wagtail assumes an HTML template file exists of (almost) the same name. The Front End developer may need to create these templates themselves. +For each page model in ``models.py``, Wagtail assumes an HTML template file exists of (almost) the same name. The Front End developer may need to create these templates themselves by refering to ``models.py`` to infer template names from the models defined therein. -To find a suitable template, Wagtail converts CamelCase names to underscore_case. So for a ``BlogPage``, a template file ``blog_page.html`` will be expected. The name of the template file can be overridden per model if necessary. +To find a suitable template, Wagtail converts CamelCase names to underscore_case. So for a ``BlogPage``, a template ``blog_page.html`` will be expected. The name of the template file can be overridden per model if necessary. Template files are assumed to exist here:: From 1207342f203e12a92809cf7ba748e4dbe9c6e242 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Mon, 2 Jun 2014 16:34:45 +0100 Subject: [PATCH 162/189] wording tweak --- docs/building_your_site/frontenddevelopers.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index 3d3117b84..d009a1af8 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -179,7 +179,9 @@ In some cases greater control over the ``img`` tag is required, for example to a Rich text (filter) ~~~~~~~~~~~~~~~~~~ -This filter is required for use with any field that generates raw HTML e.g ``RichTextField``. It will expand internal shorthand references to embeds and links made in the Wagtail editor into fully-baked HTML ready for display. +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. .. code-block:: django @@ -193,7 +195,8 @@ This filter is required for use with any field that generates raw HTML e.g ``Ric Internal links (tag) ~~~~~~~~~~~~~~~~~~~~ -**pageurl** +pageurl +-------- Takes a Page object and returns a relative URL (``/foo/bar/``) if within the same site as the current page, or absolute (``http://example.com/foo/bar/``) if not. @@ -203,7 +206,8 @@ Takes a Page object and returns a relative URL (``/foo/bar/``) if within the sam ... -**slugurl** +slugurl +-------- Takes any ``slug`` as defined in a page's "Promote" tab and returns the URL for the matching Page. Like ``pageurl``, will try to provide a relative link if possible, but will default to an absolute link if on a different site. This is most useful when creating shared page furniture e.g top level navigation or site-wide links. From 12b81cb8570db762c9891bcd3380e8115e5555ed Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 2 Jun 2014 17:06:18 +0100 Subject: [PATCH 163/189] Added page pagination --- .../templates/wagtailadmin/pages/list.html | 22 ++++++++++++++++++- wagtail/wagtailadmin/views/pages.py | 10 +++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html index 7078834ae..0c8f16467 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html @@ -229,4 +229,24 @@

      {% trans "No pages have been created." %}{% if parent_page and parent_page_perms.can_add_subpage %} {% blocktrans %}Why not add one?{% endblocktrans %}{% endif %} {% endif %} - \ No newline at end of file + + +{% if parent_page and pages and pages.paginator %} +

      +{% endif %} \ No newline at end of file diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 308f6fdd4..64c612a7b 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -33,6 +33,16 @@ def index(request, parent_page_id=None): else: ordering = 'title' + # Pagination + p = request.GET.get('p', 1) + paginator = Paginator(pages, 50) + try: + pages = paginator.page(p) + except PageNotAnInteger: + pages = paginator.page(1) + except EmptyPage: + pages = paginator.page(paginator.num_pages) + return render(request, 'wagtailadmin/pages/index.html', { 'parent_page': parent_page, 'ordering': ordering, From 8b1db37b4474f32802d372f7f42ec6fda0476d31 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 2 Jun 2014 17:10:11 +0100 Subject: [PATCH 164/189] Keep ordering when paginating pages --- wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html index 0c8f16467..7258c5429 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html @@ -239,12 +239,12 @@ From 414b3684b98da784c8b486ffc081f360921ebe1a Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 2 Jun 2014 17:15:28 +0100 Subject: [PATCH 165/189] Don't paginate if the user is reordering --- wagtail/wagtailadmin/views/pages.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 64c612a7b..b5779a6ad 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -34,14 +34,15 @@ def index(request, parent_page_id=None): ordering = 'title' # Pagination - p = request.GET.get('p', 1) - paginator = Paginator(pages, 50) - try: - pages = paginator.page(p) - except PageNotAnInteger: - pages = paginator.page(1) - except EmptyPage: - pages = paginator.page(paginator.num_pages) + if ordering != 'ord': + p = request.GET.get('p', 1) + paginator = Paginator(pages, 50) + try: + pages = paginator.page(p) + except PageNotAnInteger: + pages = paginator.page(1) + except EmptyPage: + pages = paginator.page(paginator.num_pages) return render(request, 'wagtailadmin/pages/index.html', { 'parent_page': parent_page, From 83ecd86937459da76f94795fe6de95476845e50a Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 09:37:01 +0100 Subject: [PATCH 166/189] Fixed unit test --- wagtail/wagtailadmin/tests/test_pages_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index ccaa67dae..14e57a35e 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -25,7 +25,7 @@ class TestPageExplorer(TestCase): response = self.client.get(reverse('wagtailadmin_explore', args=(self.root_page.id, ))) self.assertEqual(response.status_code, 200) self.assertEqual(self.root_page, response.context['parent_page']) - self.assertTrue(response.context['pages'].filter(id=self.child_page.id).exists()) + self.assertTrue(response.context['pages'].paginator.object_list.filter(id=self.child_page.id).exists()) class TestPageCreation(TestCase): From 6107fa0bcf5c4d899d53635f780d857fae71d689 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 3 Jun 2014 09:43:10 +0100 Subject: [PATCH 167/189] Removed unittest2 from .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cc1cb74e8..e23794d4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ services: install: - python setup.py install - pip install psycopg2 pyelasticsearch elasticutils==0.8.2 wand - - pip install coveralls unittest2 + - pip install coveralls # Pre-test configuration before_script: - psql -c 'create database wagtaildemo;' -U postgres From c7b997bd02e06f02362de098958b6a1b9c0023f5 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 3 Jun 2014 11:50:38 +0100 Subject: [PATCH 168/189] When previewing a page creation, populate url_path. This ensures that Page.dummy_request can infer a sensible hostname rather than falling back on example.com, which fails when ALLOWED_HOSTS is enforced. --- wagtail/wagtailadmin/views/pages.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 308f6fdd4..17717dbba 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -349,6 +349,10 @@ def preview_on_create(request, content_type_app_name, content_type_model_name, p if form.is_valid(): form.save(commit=False) + # ensure that our unsaved page instance has a suitable url set + parent_page = get_object_or_404(Page, id=parent_page_id).specific + page.set_url_path(parent_page) + # This view will generally be invoked as an AJAX request; as such, in the case of # an error Django will return a plaintext response. This isn't what we want, since # we will be writing the response back to an HTML page regardless of success or From e24e68fceda6d483e71c548d3c30d1f056981110 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 3 Jun 2014 12:59:49 +0100 Subject: [PATCH 169/189] Update CHANGELOG.txt --- CHANGELOG.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d2914194b..68a21731b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -2,6 +2,7 @@ Changelog ========= * When logged in user visits login page, they are now redirected to the dashboard + * Fix: Deleting an item from an InlinePanel, then generating a validation error on saving, no longer causes the deleted item to confusingly reappear with an error of it's own. 0.3 (28.05.2014) ~~~~~~~~~~~~~~~~ From c052214880fccff239c14466ee5d67ac118e5488 Mon Sep 17 00:00:00 2001 From: Tom Dyson Date: Tue, 3 Jun 2014 13:30:45 +0100 Subject: [PATCH 170/189] Grammar fixes in FE docs --- docs/building_your_site/frontenddevelopers.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/building_your_site/frontenddevelopers.rst b/docs/building_your_site/frontenddevelopers.rst index d009a1af8..e5f583397 100644 --- a/docs/building_your_site/frontenddevelopers.rst +++ b/docs/building_your_site/frontenddevelopers.rst @@ -7,8 +7,6 @@ For Front End developers Overview ======================== -This page is aimed at non-Django-literate Front End developers. - Wagtail uses Django's templating language. For developers new to Django, start with Django's own template documentation: https://docs.djangoproject.com/en/dev/topics/templates/ @@ -75,7 +73,7 @@ Images uploaded to Wagtail by its users (as opposed to a developer's static file Unlike other CMS, adding images to a page does not involve choosing a "version" of the image to use. Wagtail has no predefined image "formats" or "sizes". Instead the template developer defines image manipulation to occur *on the fly* when the image is requested, via a special syntax within the template. -Images from the library **must** be requested using this syntax, but a developer's static images can be added via conventional means e.g ``img`` tags. Only images from the library can be manipulated on the fly. +Images from the library must be requested using this syntax, but a developer's static images can be added via conventional means e.g ``img`` tags. Only images from the library can be manipulated on the fly. Read more about the image manipulation syntax here :ref:`image_tag`. @@ -84,7 +82,7 @@ Read more about the image manipulation syntax here :ref:`image_tag`. Template tags & filters ======================== -In addition to Django's standard tags and filters, Wagtail provides some of it's own, which can be ``load``-ed `as you would any other `_ +In addition to Django's standard tags and filters, Wagtail provides some of its own, which can be ``load``-ed `as you would any other `_ .. _image_tag: @@ -146,15 +144,15 @@ The available ``method`` s are: Resize and **crop** to fill the **exact** dimensions. - This can be particularly useful for websites requiring square thumbnails of arbitrary images. e.g A landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have it's height reduced to 200, then it's width (ordinarily 400) cropped to 200. + This can be particularly useful for websites requiring square thumbnails of arbitrary images. For example, a landscape image of width 2000, height 1000, treated with ``fill`` dimensions ``200x200`` would have its height reduced to 200, then its width (ordinarily 400) cropped to 200. **The crop always aligns on the centre of the image.** .. Note:: - Wagtail *does not allow deforming or stretching images*. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions. + Wagtail does not allow deforming or stretching images. Image dimension ratios will always be kept. Wagtail also *does not support upscaling*. Small images forced to appear at larger sizes will "max out" at their their native dimensions. .. Note:: - Wagtail does not make the "original" version of an image explicitly available. To request it, it's suggested you rely on the lack of upscaling by requesting an image much larger than it's maximum dimensions. e.g to insert an image who's dimensions are uncertain/unknown at it's maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide. + Wagtail does not make the "original" version of an image explicitly available. To request it, you could rely on the lack of upscaling by requesting an image larger than its maximum dimensions. e.g to insert an image whose dimensions are unknown at its maximum size, try: ``{% image self.image width-10000 %}``. This assumes the image is unlikely to be larger than 10000px wide. .. _image_tag_alt: From f3f7b466b13944de4cd43cc9f0a19ceb8d0e2552 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 3 Jun 2014 14:15:18 +0100 Subject: [PATCH 171/189] When generating dummy requests for pages with no routable URL, fall back on a hostname from ALLOWED_HOSTS and finally localhost --- wagtail/wagtailcore/models.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index f72e6f6f9..30ffc8a77 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -581,7 +581,12 @@ class Page(MP_Node, ClusterableModel, Indexed): path = url_info.path port = url_info.port or 80 else: - hostname = 'example.com' + # Cannot determine a URL to this page - cobble one together based on + # whatever we find in ALLOWED_HOSTS + try: + hostname = settings.ALLOWED_HOSTS[0] + except IndexError: + hostname = 'localhost' path = '/' port = 80 From 2d7a276b831aebf5c56a61eaec13d08f41ea807e Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 3 Jun 2014 14:33:21 +0100 Subject: [PATCH 172/189] Changelog entries for dummy_request fixes --- CHANGELOG.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0a6024bd1..1e8d9ac93 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,11 @@ Changelog ========= +0.3.1 (xx.xx.20xx) +~~~~~~~~~~~~~~~~~~ + * Fix: When constructing dummy requests for pages with no routable URL, fall back on a hostname from ALLOWED_HOSTS and finally 'localhost', to avoid 'Invalid HTTP_HOST header' errors on preview when DEBUG=False. + * Fix: Ensure that url_path is populated when previewing a newly created page, to avoid unnecessarily taking the above fallback. + 0.3 (28.05.2014) ~~~~~~~~~~~~~~~~ * Added toolbar to allow logged-in users to add and edit pages from the site front-end From a5072c768df546641311a0085aed6e6ae8debb57 Mon Sep 17 00:00:00 2001 From: Tom Talbot Date: Thu, 29 May 2014 15:02:56 +0100 Subject: [PATCH 173/189] Fix #94. Panels are now hidden on page load if they are marked as deleted after a form validation failure. --- .../static/wagtailadmin/js/page-editor.js | 11 +++++++++++ wagtail/wagtailadmin/views/pages.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js b/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js index cbff2c8af..4e8e1b02d 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/page-editor.js @@ -181,6 +181,17 @@ function InlinePanel(opts) { self.updateMoveButtonDisabledStates(); }); } + + /* Hide container on page load if it is marked as deleted. Remove the error + message so that it doesn't count towards the number of errors on the tab at the + top of the page. */ + if ( $('#' + deleteInputId).val() === "1" ) { + $('#' + childId).hide(0, function() { + self.updateMoveButtonDisabledStates(); + self.setHasContent(); + }); + $('#' + childId).find(".error-message").remove(); + } }; self.formsUl = $('#' + opts.formsetPrefix + '-FORMS'); diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 17717dbba..9bf6dbf44 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -5,7 +5,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 From e449fcc8610d157df51dc1bd67174f696f53b6d5 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 3 Jun 2014 14:47:05 +0100 Subject: [PATCH 174/189] Changelog entry for a5072c7 --- CHANGELOG.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1e8d9ac93..10fe8132d 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -5,6 +5,7 @@ Changelog ~~~~~~~~~~~~~~~~~~ * Fix: When constructing dummy requests for pages with no routable URL, fall back on a hostname from ALLOWED_HOSTS and finally 'localhost', to avoid 'Invalid HTTP_HOST header' errors on preview when DEBUG=False. * Fix: Ensure that url_path is populated when previewing a newly created page, to avoid unnecessarily taking the above fallback. + * Fix: Deleting an item from an InlinePanel, then generating a validation error on saving, no longer causes the deleted item to confusingly reappear with an error of its own. 0.3 (28.05.2014) ~~~~~~~~~~~~~~~~ From 649cbf95c4af4e0edc86888cccbdb86b5f21c6ba Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 3 Jun 2014 14:54:37 +0100 Subject: [PATCH 175/189] version bump for 0.3.1 --- CHANGELOG.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 10fe8132d..4910fa4bb 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,7 +1,7 @@ Changelog ========= -0.3.1 (xx.xx.20xx) +0.3.1 (03.06.2014) ~~~~~~~~~~~~~~~~~~ * Fix: When constructing dummy requests for pages with no routable URL, fall back on a hostname from ALLOWED_HOSTS and finally 'localhost', to avoid 'Invalid HTTP_HOST header' errors on preview when DEBUG=False. * Fix: Ensure that url_path is populated when previewing a newly created page, to avoid unnecessarily taking the above fallback. diff --git a/setup.py b/setup.py index 4f5b87c12..7b26817cd 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ except ImportError: setup( name='wagtail', - version='0.3', + version='0.3.1', description='A Django content management system focused on flexibility and user experience', author='Matthew Westcott', author_email='matthew.westcott@torchbox.com', From a885c7a56cdc401c063cfe2b6d35e02114f81e26 Mon Sep 17 00:00:00 2001 From: Jeffrey Hearn Date: Mon, 2 Jun 2014 13:47:30 -0400 Subject: [PATCH 176/189] Edit API docs expanded with hooks, modelcluster, and InlinePanel usage --- docs/editing_api.rst | 270 ++++++++++++++++++++++++++++++++++++++--- docs/model_recipes.rst | 2 + 2 files changed, 256 insertions(+), 16 deletions(-) diff --git a/docs/editing_api.rst b/docs/editing_api.rst index 99e859437..c06370026 100644 --- a/docs/editing_api.rst +++ b/docs/editing_api.rst @@ -211,7 +211,7 @@ You can explicitly link ``Page``-derived models together using the ``Page`` mode Snippets -------- -Snippets are not subclasses, so you must include the model class directly. A chooser is provided which takes the field name snippet class. +Snippets are vanilla Django models you create yourself without a Wagtail-provided base class. So using them as a field in a page requires specifying your own ``appname.modelname``. A chooser, ``SnippetChooserPanel``, is provided which takes the field name and snippet class. .. code-block:: python @@ -248,6 +248,12 @@ Full-Width Input Use ``classname="full"`` to make a field (input element) stretch the full width of the Wagtail page editor. This will not work if the field is encapsulated in a ``MultiFieldPanel``, which places its child fields into a formset. +Titles +------ + +Use ``classname="title"`` to make Page's built-in title field stand out with more vertical padding. + + Required Fields --------------- @@ -264,19 +270,11 @@ Without a panel definition, a default form field (without label) will be used to .. _Django model field reference (editable): https://docs.djangoproject.com/en/dev/ref/models/fields/#editable - - - - - - - - - - MultiFieldPanel ~~~~~~~~~~~~~~~ +The ``MultiFieldPanel`` groups a list of child fields into a fieldset, which can also be collapsed into a heading bar to save space. + .. code-block:: python BOOK_FIELD_COLLECTION = [ @@ -294,8 +292,7 @@ MultiFieldPanel # ... ] - - +By default, ``MultiFieldPanel`` s are expanded and not collapsible. Adding the classname ``collapsible`` will enable the collapse control. Adding both ``collapsible`` and ``collapsed`` to the classname parameter will load the editor page with the ``MultiFieldPanel`` collapsed under its heading. .. _inline_panels: @@ -303,7 +300,55 @@ MultiFieldPanel Inline Panels and Model Clusters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``django-modelcluster`` module allows for streamlined relation of extra models to a Wagtail page. +The ``django-modelcluster`` module allows for streamlined relation of extra models to a Wagtail page. For instance, you can create objects related through a ``ForeignKey`` relationship on the fly and save them to a draft revision of a ``Page`` object. Normally, your related objects "cluster" would need to be created beforehand (or asynchronously) before linking them to a Page. + +Let's look at the example of adding related links to a ``Page``-derived model. We want to be able to add as many as we like, assign an order, and do all of this without leaving the page editing screen. + +.. code-block:: python + + from wagtail.wagtailcore.models import Orderable, Page + from modelcluster.fields import ParentalKey + + # The abstract model for related links, complete with panels + class RelatedLink(models.Model): + title = models.CharField(max_length=255) + link_external = models.URLField("External link", blank=True) + + panels = [ + FieldPanel('title'), + FieldPanel('link_external'), + ] + + class Meta: + abstract = True + + # The real model which combines the abstract model, an + # Orderable helper class, and what amounts to a ForeignKey link + # to the model we want to add related links to (BookPage) + class BookPageRelatedLinks(Orderable, RelatedLink): + page = ParentalKey('demo.BookPage', related_name='related_links') + + class BookPage( Page ): + # ... + + BookPage.content_panels = [ + # ... + InlinePanel( BookPage, 'related_links', label="Related Links" ), + ] + +The ``RelatedLink`` class is a vanilla Django abstract model. The ``BookPageRelatedLinks`` model extends it with capability for being ordered in the Wagtail interface via the ``Orderable`` class as well as adding a ``page`` property which links the model to the ``BookPage`` model we're adding the related links objects to. Finally, in the panel definitions for ``BookPage``, we'll add an ``InlinePanel`` to provide an interface for it all. Let's look again at the parameters that ``InlinePanel`` accepts: + +.. code-block:: python + + InlinePanel( base_model, relation_name, panels=None, label='', help_text='' ) + +``base_model`` is the model you're extending with the cluster. The ``relation_name`` is the ``related_name`` label given to the cluster's ``ParentalKey`` relation. You can add the ``panels`` manually or make them part of the cluster model. Finally, ``label`` and ``help_text`` provide a heading and caption, respectively, for the Wagtail editor. + +For another example of using model clusters, see :ref:`tagging` + +For more on ``django-modelcluster``, visit `the django-modelcluster github project page`_ ). + +.. _the django-modelcluster github page: https://github.com/torchbox/django-modelcluster .. _extending_wysiwyg: @@ -311,12 +356,205 @@ The ``django-modelcluster`` module allows for streamlined relation of extra mode Extending the WYSIWYG Editor (hallo.js) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Adding hallo.js plugins: -https://github.com/torchbox/wagtail/commit/1ecc215759142e6cafdacb185bbfd3f8e9cd3185 +To inject javascript into the Wagtail page editor, see the :ref:`insert_editor_js` hook. Once you have the hook in place and your hallo.js plugin loads into the Wagtail page editor, use the following Javascript to register the plugin with hallo.js. + +.. code-block:: javascript + + registerHalloPlugin(name, opts); + +hallo.js plugin names are prefixed with the ``"IKS."`` namespace, but the ``name`` you pass into ``registerHalloPlugin()`` should be without the prefix. ``opts`` is an object passed into the plugin. + +For information on developing custom hallo.js plugins, see the project's page: https://github.com/bergie/hallo Edit Handler API ~~~~~~~~~~~~~~~~ +Hooks +----- + +On loading, Wagtail will search for any app with the file ``wagtail_hooks.py`` and execute the contents. This provides a way to register your own functions to execute at certain points in Wagtail's execution, such as when a ``Page`` object is saved or when the main menu is constructed. + +Registering functions with a Wagtail hook follows the following pattern: + +.. code-block:: python + + from wagtail.wagtailadmin import hooks + + hooks.register('hook', function) + +Where ``'hook'`` is one of the following hook strings and ``function`` is a function you've defined to handle the hook. + +.. _construct_wagtail_edit_bird: + +``construct_wagtail_edit_bird`` + Add or remove items from the wagtail userbar. Add, edit, and moderation tools are provided by default. The callable passed into the hook must take the ``request`` object and a list of menu objects, ``items``. The menu item objects must have a ``render`` method which can take a ``request`` object and return the HTML string representing the menu item. See the userbar templates and menu item classes for more information. + + .. code-block:: python + + from wagtail.wagtailadmin import hooks + + class UserbarPuppyLinkItem(object): + def render(self, request): + return '
    • Puppies!
    • ' + + def add_puppy_link_item(request, items): + return items.append( UserbarPuppyLinkItem() ) + + hooks.register('construct_wagtail_edit_bird', add_puppy_link_item) + +.. _construct_homepage_panels: + +``construct_homepage_panels`` + Add or remove panels from the Wagtail admin homepage. The callable passed into this hook should take a ``request`` object and a list of ``panels``, objects which have a ``render()`` method returning a string. The objects also have an ``order`` property, an integer used for ordering the panels. The default panels use integers between ``100`` and ``300``. + + .. code-block:: python + + from django.utils.safestring import mark_safe + + from wagtail.wagtailadmin import hooks + + class WelcomePanel(object): + order = 50 + + def render(self): + return mark_safe(""" +
      +

      No, but seriously -- welcome to the admin homepage.

      +
      + """) + + def add_another_welcome_panel(request, panels): + return panels.append( WelcomePanel() ) + + hooks.register('construct_homepage_panels', add_another_welcome_panel) + +.. _after_create_page: + +``after_create_page`` + Do something with a ``Page`` object after it has been saved to the database (as a published page or a revision). The callable passed to this hook should take a ``request`` object and a ``page`` object. The function does not have to return anything, but if an object with a ``status_code`` property is returned, Wagtail will use it as a response object. By default, Wagtail will instead redirect to the Explorer page for the new page's parent. + + .. code-block:: python + + from django.http import HttpResponse + + from wagtail.wagtailadmin import hooks + + def do_after_page_create(request, page): + return HttpResponse("Congrats on making content!", content_type="text/plain") + hooks.register('after_create_page', do_after_page_create) + +.. _after_edit_page: + +``after_edit_page`` + Do something with a ``Page`` object after it has been updated. Uses the same behavior as ``after_create_page``. + +.. _after_delete_page: + +``after_delete_page`` + Do something after a ``Page`` object is deleted. Uses the same behavior as ``after_create_page``. + +.. _register_admin_urls: + +``register_admin_urls`` + Register additional admin page URLs. The callable fed into this hook should return a list of Django URL patterns which define the structure of the pages and endpoints of your extension to the Wagtail admin. For more about vanilla Django URLconfs and views, see `url dispatcher`_. + + .. _url dispatcher: https://docs.djangoproject.com/en/dev/topics/http/urls/ + + .. code-block:: python + + from django.http import HttpResponse + from django.conf.urls import url + + from wagtail.wagtailadmin import hooks + + def admin_view( request ): + return HttpResponse( \ + "I have approximate knowledge of many things!", \ + content_type="text/plain") + + def urlconf_time(): + return [ + url(r'^how_did_you_almost_know_my_name/$', admin_view, name='frank' ), + ] + hooks.register('register_admin_urls', urlconf_time) + +.. _construct_main_menu: + +``construct_main_menu`` + Add, remove, or alter ``MenuItem`` objects from the Wagtail admin menu. The callable passed to this hook must take a ``request`` object and a list of ``menu_items``; it must return a list of menu items. New items can be constructed from the ``MenuItem`` class by passing in: a ``label`` which will be the text in the menu item, the URL of the admin page you want the menu item to link to (usually by calling ``reverse()`` on the admin view you've set up), CSS class ``name`` applied to the wrapping ``
    • `` of the menu item as ``"menu-{name}"``, CSS ``classnames`` which are used to give the link an icon, and an ``order`` integer which determine's the item's place in the menu. + + .. code-block:: python + + from django.core.urlresolvers import reverse + + from wagtail.wagtailadmin import hooks + from wagtail.wagtailadmin.menu import MenuItem + + def construct_main_menu(request, menu_items): + menu_items.append( + MenuItem( 'Frank', reverse('frank'), classnames='icon icon-folder-inverse', order=10000) + ) + hooks.register('construct_main_menu', construct_main_menu) + + +.. _insert_editor_js: + +``insert_editor_js`` + Add additional Javascript files or code snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string. + + .. code-block:: python + + from django.utils.html import format_html, format_html_join + from django.conf import settings + + from wagtail.wagtailadmin import hooks + + def editor_js(): + js_files = [ + 'demo/js/hallo-plugins/hallo-demo-plugin.js', + ] + js_includes = format_html_join('\n', '', + ((settings.STATIC_URL, filename) for filename in js_files) + ) + return js_includes + format_html( + """ + + """ + ) + hooks.register('insert_editor_js', editor_js) + +.. _insert_editor_css: + +``insert_editor_css`` + Add additional CSS or SCSS files or snippets to the page editor. Output must be compatible with ``compress``, as local static includes or string. + + .. code-block:: python + + from django.utils.html import format_html + from django.conf import settings + + from wagtail.wagtailadmin import hooks + + def editor_css(): + return format_html('') + hooks.register('insert_editor_css', editor_css) + + +Content Index Pages (CRUD) +-------------------------- + + +Custom Choosers +--------------- + + +Tests +----- diff --git a/docs/model_recipes.rst b/docs/model_recipes.rst index fe8a89e32..6a4abed47 100644 --- a/docs/model_recipes.rst +++ b/docs/model_recipes.rst @@ -118,6 +118,8 @@ Will return:: tauntaun kennel bed and breakfast +.. _tagging: + Tagging ------- From 399f271ca7a9a2352cb8f4e01c03ad70e7cbff7d Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 4 Jun 2014 13:27:06 +0100 Subject: [PATCH 177/189] Fixed missing return statement in embed frontend renderer --- wagtail/wagtailembeds/format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailembeds/format.py b/wagtail/wagtailembeds/format.py index 453c73641..1654be989 100644 --- a/wagtail/wagtailembeds/format.py +++ b/wagtail/wagtailembeds/format.py @@ -17,7 +17,7 @@ def embed_to_frontend_html(url): ratio = "0" # Render template - render_to_string('wagtailembeds/embed_frontend.html', { + return render_to_string('wagtailembeds/embed_frontend.html', { 'embed': embed, 'ratio': ratio, }) From 6a4df387e3e55e6d710dc38c6b63be086576e338 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 4 Jun 2014 13:35:58 +0100 Subject: [PATCH 178/189] Mark embed code as safe in frontend template --- .../wagtailembeds/templates/wagtailembeds/embed_frontend.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailembeds/templates/wagtailembeds/embed_frontend.html b/wagtail/wagtailembeds/templates/wagtailembeds/embed_frontend.html index feb209311..b97dceb83 100644 --- a/wagtail/wagtailembeds/templates/wagtailembeds/embed_frontend.html +++ b/wagtail/wagtailembeds/templates/wagtailembeds/embed_frontend.html @@ -1,3 +1,3 @@
      - {{ embed.html }} + {{ embed.html|safe }}
      From 43e0ce721fcbca7a70d41f4bd92d859cacb296ad Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 4 Jun 2014 14:43:10 +0100 Subject: [PATCH 179/189] Revert "Made login view redirect already logged in users to dashboard." This reverts commit 3f9cb2da6fcd614c3206631d6132d4286b6f1fbc. --- .../account/password_reset/complete.html | 2 +- .../templates/wagtailadmin/login.html | 2 +- .../tests/test_account_management.py | 1 + wagtail/wagtailadmin/urls.py | 10 +++++++-- wagtail/wagtailadmin/views/account.py | 22 +------------------ 5 files changed, 12 insertions(+), 25 deletions(-) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html index 3dc8272b0..c0cf872e2 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/account/password_reset/complete.html @@ -13,6 +13,6 @@ {% block furniture %}

      {% trans "Password change successful" %}

      -

      {% trans "Login" %}

      +

      {% trans "Login" %}

      {% endblock %} \ No newline at end of file diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/login.html b/wagtail/wagtailadmin/templates/wagtailadmin/login.html index 22682500f..012dd32f8 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/login.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/login.html @@ -20,7 +20,7 @@
    • {% endif %} - + {% csrf_token %}

      {% trans "Sign in to Wagtail" %}

      diff --git a/wagtail/wagtailadmin/tests/test_account_management.py b/wagtail/wagtailadmin/tests/test_account_management.py index 63c70779b..95d54d7d9 100644 --- a/wagtail/wagtailadmin/tests/test_account_management.py +++ b/wagtail/wagtailadmin/tests/test_account_management.py @@ -49,6 +49,7 @@ class TestAuthentication(TestCase): self.assertTrue('_auth_user_id' in self.client.session) self.assertEqual(self.client.session['_auth_user_id'], User.objects.get(username='test').id) + @unittest.expectedFailure # See: https://github.com/torchbox/wagtail/issues/25 def test_already_logged_in_redirect(self): """ This tests that a user who is already logged in is automatically diff --git a/wagtail/wagtailadmin/urls.py b/wagtail/wagtailadmin/urls.py index 806240c5c..8fbf2f6e7 100644 --- a/wagtail/wagtailadmin/urls.py +++ b/wagtail/wagtailadmin/urls.py @@ -5,8 +5,15 @@ from wagtail.wagtailadmin.forms import LoginForm, PasswordResetForm from wagtail.wagtailadmin.views import account, chooser, home, pages, tags, userbar from wagtail.wagtailadmin import hooks - urlpatterns = [ + url( + r'^login/$', 'django.contrib.auth.views.login', { + 'template_name': 'wagtailadmin/login.html', + 'authentication_form': LoginForm, + 'extra_context': {'show_password_reset': getattr(settings, 'WAGTAIL_PASSWORD_MANAGEMENT_ENABLED', True)}, + }, name='wagtailadmin_login' + ), + # Password reset url( r'^password_reset/$', 'django.contrib.auth.views.password_reset', { @@ -74,7 +81,6 @@ urlpatterns += [ url(r'^tag-autocomplete/$', tags.autocomplete, name='wagtailadmin_tag_autocomplete'), - url(r'^login/$', account.login, name='wagtailadmin_login'), url(r'^account/$', account.account, name='wagtailadmin_account'), url(r'^account/change_password/$', account.change_password, name='wagtailadmin_account_change_password'), url(r'^logout/$', account.logout, name='wagtailadmin_logout'), diff --git a/wagtail/wagtailadmin/views/account.py b/wagtail/wagtailadmin/views/account.py index c5e461f55..8479ea6b0 100644 --- a/wagtail/wagtailadmin/views/account.py +++ b/wagtail/wagtailadmin/views/account.py @@ -3,13 +3,8 @@ from django.shortcuts import render, redirect from django.contrib import messages from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.decorators import permission_required -from django.contrib.auth.views import logout as auth_logout, login as auth_login +from django.contrib.auth.views import logout as auth_logout from django.utils.translation import ugettext as _ -from django.views.decorators.debug import sensitive_post_parameters -from django.views.decorators.cache import never_cache - -from wagtail.wagtailadmin import forms - @permission_required('wagtailadmin.access_admin') def account(request): @@ -42,21 +37,6 @@ def change_password(request): }) -@sensitive_post_parameters() -@never_cache -def login(request): - if request.user.is_authenticated(): - return redirect('wagtailadmin_home') - else: - return auth_login(request, - template_name='wagtailadmin/login.html', - authentication_form=forms.LoginForm, - extra_context={ - 'show_password_reset': getattr(settings, 'WAGTAIL_PASSWORD_MANAGEMENT_ENABLED', True), - }, - ) - - def logout(request): response = auth_logout(request, next_page = 'wagtailadmin_login') From a104c5eb168866cef3b47b59013933172b740730 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Wed, 4 Jun 2014 14:44:14 +0100 Subject: [PATCH 180/189] revert changelog entry --- CHANGELOG.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 9a5a5669f..b708cfd21 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -3,7 +3,6 @@ Changelog 0.4 (xx.xx.20xx) ~~~~~~~~~~~~~~~~~~ - * When logged in user visits login page, they are now redirected to the dashboard 0.3.1 (03.06.2014) ~~~~~~~~~~~~~~~~~~ From 3b372ccbac4343f46d4cade8459045788bcf47b4 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 4 Jun 2014 17:08:12 +0100 Subject: [PATCH 181/189] Splitup wagtailcore tests wagtailcore/tests.py is getting a bit big so this commit splits them up into separate files test_management_commands.py test_page_model.py test_page_permissions.py test_page_queryset.py tests.py --- wagtail/wagtailcore/tests.py | 860 ------------------ wagtail/wagtailcore/tests/__init__.py | 0 .../tests/test_management_commands.py | 89 ++ wagtail/wagtailcore/tests/test_page_model.py | 226 +++++ .../tests/test_page_permissions.py | 226 +++++ .../wagtailcore/tests/test_page_queryset.py | 256 ++++++ wagtail/wagtailcore/tests/tests.py | 99 ++ 7 files changed, 896 insertions(+), 860 deletions(-) delete mode 100644 wagtail/wagtailcore/tests.py create mode 100644 wagtail/wagtailcore/tests/__init__.py create mode 100644 wagtail/wagtailcore/tests/test_management_commands.py create mode 100644 wagtail/wagtailcore/tests/test_page_model.py create mode 100644 wagtail/wagtailcore/tests/test_page_permissions.py create mode 100644 wagtail/wagtailcore/tests/test_page_queryset.py create mode 100644 wagtail/wagtailcore/tests/tests.py diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py deleted file mode 100644 index df66c6b93..000000000 --- a/wagtail/wagtailcore/tests.py +++ /dev/null @@ -1,860 +0,0 @@ -from django.test import TestCase, Client -from django.http import HttpRequest, Http404 -from django.core import management -from StringIO import StringIO - -from django.contrib.auth.models import User - -from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy -from wagtail.tests.models import EventPage, EventIndex, SimplePage - - -class TestRouting(TestCase): - fixtures = ['test.json'] - - def test_find_site_for_request(self): - default_site = Site.objects.get(is_default_site=True) - events_page = Page.objects.get(url_path='/home/events/') - events_site = Site.objects.create(hostname='events.example.com', root_page=events_page) - - # requests without a Host: header should be directed to the default site - request = HttpRequest() - request.path = '/' - self.assertEqual(Site.find_for_request(request), default_site) - - # requests with a known Host: header should be directed to the specific site - request = HttpRequest() - request.path = '/' - request.META['HTTP_HOST'] = 'events.example.com' - self.assertEqual(Site.find_for_request(request), events_site) - - # requests with an unrecognised Host: header should be directed to the default site - request = HttpRequest() - request.path = '/' - request.META['HTTP_HOST'] = 'unknown.example.com' - self.assertEqual(Site.find_for_request(request), default_site) - - def test_urls(self): - default_site = Site.objects.get(is_default_site=True) - homepage = Page.objects.get(url_path='/home/') - christmas_page = Page.objects.get(url_path='/home/events/christmas/') - - # Basic installation only has one site configured, so page.url will return local URLs - self.assertEqual(homepage.full_url, 'http://localhost/') - self.assertEqual(homepage.url, '/') - self.assertEqual(homepage.relative_url(default_site), '/') - - self.assertEqual(christmas_page.full_url, 'http://localhost/events/christmas/') - self.assertEqual(christmas_page.url, '/events/christmas/') - self.assertEqual(christmas_page.relative_url(default_site), '/events/christmas/') - - def test_urls_with_multiple_sites(self): - events_page = Page.objects.get(url_path='/home/events/') - events_site = Site.objects.create(hostname='events.example.com', root_page=events_page) - - default_site = Site.objects.get(is_default_site=True) - homepage = Page.objects.get(url_path='/home/') - christmas_page = Page.objects.get(url_path='/home/events/christmas/') - - # with multiple sites, page.url will return full URLs to ensure that - # they work across sites - self.assertEqual(homepage.full_url, 'http://localhost/') - self.assertEqual(homepage.url, 'http://localhost/') - self.assertEqual(homepage.relative_url(default_site), '/') - self.assertEqual(homepage.relative_url(events_site), 'http://localhost/') - - self.assertEqual(christmas_page.full_url, 'http://events.example.com/christmas/') - self.assertEqual(christmas_page.url, 'http://events.example.com/christmas/') - self.assertEqual(christmas_page.relative_url(default_site), 'http://events.example.com/christmas/') - self.assertEqual(christmas_page.relative_url(events_site), '/christmas/') - - def test_request_routing(self): - homepage = Page.objects.get(url_path='/home/') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - - request = HttpRequest() - request.path = '/events/christmas/' - response = homepage.route(request, ['events', 'christmas']) - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context_data['self'], christmas_page) - used_template = response.resolve_template(response.template_name) - self.assertEqual(used_template.name, 'tests/event_page.html') - - def test_route_to_unknown_page_returns_404(self): - homepage = Page.objects.get(url_path='/home/') - - request = HttpRequest() - request.path = '/events/quinquagesima/' - with self.assertRaises(Http404): - homepage.route(request, ['events', 'quinquagesima']) - - def test_route_to_unpublished_page_returns_404(self): - homepage = Page.objects.get(url_path='/home/') - - request = HttpRequest() - request.path = '/events/tentative-unpublished-event/' - with self.assertRaises(Http404): - homepage.route(request, ['events', 'tentative-unpublished-event']) - - -class TestServeView(TestCase): - fixtures = ['test.json'] - - def setUp(self): - # Explicitly clear the cache of site root paths. Normally this would be kept - # in sync by the Site.save logic, but this is bypassed when the database is - # rolled back between tests using transactions. - from django.core.cache import cache - cache.delete('wagtail_site_root_paths') - - def test_serve(self): - response = self.client.get('/events/christmas/') - - self.assertEqual(response.status_code, 200) - self.assertEqual(response.templates[0].name, 'tests/event_page.html') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - self.assertEqual(response.context['self'], christmas_page) - - self.assertContains(response, '

      Christmas

      ') - self.assertContains(response, '

      Event

      ') - - def test_serve_unknown_page_returns_404(self): - response = self.client.get('/events/quinquagesima/') - self.assertEqual(response.status_code, 404) - - def test_serve_unpublished_page_returns_404(self): - response = self.client.get('/events/tentative-unpublished-event/') - self.assertEqual(response.status_code, 404) - - def test_serve_with_multiple_sites(self): - events_page = Page.objects.get(url_path='/home/events/') - Site.objects.create(hostname='events.example.com', root_page=events_page) - - response = self.client.get('/christmas/', HTTP_HOST='events.example.com') - self.assertEqual(response.status_code, 200) - self.assertEqual(response.templates[0].name, 'tests/event_page.html') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - self.assertEqual(response.context['self'], christmas_page) - - self.assertContains(response, '

      Christmas

      ') - self.assertContains(response, '

      Event

      ') - - # same request to the default host should return a 404 - c = Client() - response = c.get('/christmas/', HTTP_HOST='localhost') - self.assertEqual(response.status_code, 404) - - def test_serve_with_custom_context(self): - response = self.client.get('/events/') - self.assertEqual(response.status_code, 200) - - # should render the whole page - self.assertContains(response, '

      Events

      ') - - # response should contain data from the custom 'events' context variable - self.assertContains(response, 'Christmas') - - def test_ajax_response(self): - response = self.client.get('/events/', HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # should only render the content of includes/event_listing.html, not the whole page - self.assertNotContains(response, '

      Events

      ') - self.assertContains(response, 'Christmas') - - -class TestStaticSitePaths(TestCase): - def setUp(self): - self.root_page = Page.objects.get(id=1) - - # For simple tests - self.home_page = self.root_page.add_child(instance=SimplePage(title="Homepage", slug="home")) - self.about_page = self.home_page.add_child(instance=SimplePage(title="About us", slug="about")) - self.contact_page = self.home_page.add_child(instance=SimplePage(title="Contact", slug="contact")) - - # For custom tests - self.event_index = self.root_page.add_child(instance=EventIndex(title="Events", slug="events")) - for i in range(20): - self.event_index.add_child(instance=EventPage(title="Event " + str(i), slug="event" + str(i))) - - def test_local_static_site_paths(self): - paths = list(self.about_page.get_static_site_paths()) - - self.assertEqual(paths, ['/']) - - def test_child_static_site_paths(self): - paths = list(self.home_page.get_static_site_paths()) - - self.assertEqual(paths, ['/', '/about/', '/contact/']) - - def test_custom_static_site_paths(self): - paths = list(self.event_index.get_static_site_paths()) - - # Event index path - expected_paths = ['/'] - - # One path for each page of results - expected_paths.extend(['/' + str(i + 1) + '/' for i in range(5)]) - - # One path for each event page - expected_paths.extend(['/event' + str(i) + '/' for i in range(20)]) - - paths.sort() - expected_paths.sort() - self.assertEqual(paths, expected_paths) - - -class TestPageUrlTags(TestCase): - fixtures = ['test.json'] - - def test_pageurl_tag(self): - response = self.client.get('/events/') - self.assertEqual(response.status_code, 200) - 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') - - -class TestPagePermission(TestCase): - fixtures = ['test.json'] - - def test_nonpublisher_page_permissions(self): - event_editor = User.objects.get(username='eventeditor') - homepage = Page.objects.get(url_path='/home/') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') - someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') - - homepage_perms = homepage.permissions_for_user(event_editor) - christmas_page_perms = christmas_page.permissions_for_user(event_editor) - unpub_perms = unpublished_event_page.permissions_for_user(event_editor) - someone_elses_event_perms = someone_elses_event_page.permissions_for_user(event_editor) - - self.assertFalse(homepage_perms.can_add_subpage()) - self.assertTrue(christmas_page_perms.can_add_subpage()) - self.assertTrue(unpub_perms.can_add_subpage()) - self.assertTrue(someone_elses_event_perms.can_add_subpage()) - - self.assertFalse(homepage_perms.can_edit()) - self.assertTrue(christmas_page_perms.can_edit()) - self.assertTrue(unpub_perms.can_edit()) - self.assertFalse(someone_elses_event_perms.can_edit()) # basic 'add' permission doesn't allow editing pages owned by someone else - - self.assertFalse(homepage_perms.can_delete()) - self.assertFalse(christmas_page_perms.can_delete()) # cannot delete because it is published - self.assertTrue(unpub_perms.can_delete()) - self.assertFalse(someone_elses_event_perms.can_delete()) - - self.assertFalse(homepage_perms.can_publish()) - self.assertFalse(christmas_page_perms.can_publish()) - self.assertFalse(unpub_perms.can_publish()) - - self.assertFalse(homepage_perms.can_unpublish()) - self.assertFalse(christmas_page_perms.can_unpublish()) - self.assertFalse(unpub_perms.can_unpublish()) - - self.assertFalse(homepage_perms.can_publish_subpage()) - self.assertFalse(christmas_page_perms.can_publish_subpage()) - self.assertFalse(unpub_perms.can_publish_subpage()) - - self.assertFalse(homepage_perms.can_reorder_children()) - self.assertFalse(christmas_page_perms.can_reorder_children()) - self.assertFalse(unpub_perms.can_reorder_children()) - - self.assertFalse(homepage_perms.can_move()) - self.assertFalse(christmas_page_perms.can_move()) # cannot move because this would involve unpublishing from its current location - self.assertTrue(unpub_perms.can_move()) - self.assertFalse(someone_elses_event_perms.can_move()) - - self.assertFalse(christmas_page_perms.can_move_to(unpublished_event_page)) # cannot move because this would involve unpublishing from its current location - self.assertTrue(unpub_perms.can_move_to(christmas_page)) - self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination - self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself - - - def test_publisher_page_permissions(self): - event_moderator = User.objects.get(username='eventmoderator') - homepage = Page.objects.get(url_path='/home/') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') - - homepage_perms = homepage.permissions_for_user(event_moderator) - christmas_page_perms = christmas_page.permissions_for_user(event_moderator) - unpub_perms = unpublished_event_page.permissions_for_user(event_moderator) - - self.assertFalse(homepage_perms.can_add_subpage()) - self.assertTrue(christmas_page_perms.can_add_subpage()) - self.assertTrue(unpub_perms.can_add_subpage()) - - self.assertFalse(homepage_perms.can_edit()) - self.assertTrue(christmas_page_perms.can_edit()) - self.assertTrue(unpub_perms.can_edit()) - - self.assertFalse(homepage_perms.can_delete()) - self.assertTrue(christmas_page_perms.can_delete()) # cannot delete because it is published - self.assertTrue(unpub_perms.can_delete()) - - self.assertFalse(homepage_perms.can_publish()) - self.assertTrue(christmas_page_perms.can_publish()) - self.assertTrue(unpub_perms.can_publish()) - - self.assertFalse(homepage_perms.can_unpublish()) - self.assertTrue(christmas_page_perms.can_unpublish()) - self.assertFalse(unpub_perms.can_unpublish()) # cannot unpublish a page that isn't published - - self.assertFalse(homepage_perms.can_publish_subpage()) - self.assertTrue(christmas_page_perms.can_publish_subpage()) - self.assertTrue(unpub_perms.can_publish_subpage()) - - self.assertFalse(homepage_perms.can_reorder_children()) - self.assertTrue(christmas_page_perms.can_reorder_children()) - self.assertTrue(unpub_perms.can_reorder_children()) - - self.assertFalse(homepage_perms.can_move()) - self.assertTrue(christmas_page_perms.can_move()) - self.assertTrue(unpub_perms.can_move()) - - self.assertTrue(christmas_page_perms.can_move_to(unpublished_event_page)) - self.assertTrue(unpub_perms.can_move_to(christmas_page)) - self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination - self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself - - def test_inactive_user_has_no_permissions(self): - user = User.objects.get(username='inactiveuser') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') - - christmas_page_perms = christmas_page.permissions_for_user(user) - unpub_perms = unpublished_event_page.permissions_for_user(user) - - self.assertFalse(unpub_perms.can_add_subpage()) - self.assertFalse(unpub_perms.can_edit()) - self.assertFalse(unpub_perms.can_delete()) - self.assertFalse(unpub_perms.can_publish()) - self.assertFalse(christmas_page_perms.can_unpublish()) - self.assertFalse(unpub_perms.can_publish_subpage()) - self.assertFalse(unpub_perms.can_reorder_children()) - self.assertFalse(unpub_perms.can_move()) - self.assertFalse(unpub_perms.can_move_to(christmas_page)) - - def test_superuser_has_full_permissions(self): - user = User.objects.get(username='superuser') - homepage = Page.objects.get(url_path='/home/') - root = Page.objects.get(url_path='/') - unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') - - homepage_perms = homepage.permissions_for_user(user) - root_perms = root.permissions_for_user(user) - unpub_perms = unpublished_event_page.permissions_for_user(user) - - self.assertTrue(homepage_perms.can_add_subpage()) - self.assertTrue(root_perms.can_add_subpage()) - - self.assertTrue(homepage_perms.can_edit()) - self.assertFalse(root_perms.can_edit()) # root is not a real editable page, even to superusers - - self.assertTrue(homepage_perms.can_delete()) - self.assertFalse(root_perms.can_delete()) - - self.assertTrue(homepage_perms.can_publish()) - self.assertFalse(root_perms.can_publish()) - - self.assertTrue(homepage_perms.can_unpublish()) - self.assertFalse(root_perms.can_unpublish()) - self.assertFalse(unpub_perms.can_unpublish()) - - self.assertTrue(homepage_perms.can_publish_subpage()) - self.assertTrue(root_perms.can_publish_subpage()) - - self.assertTrue(homepage_perms.can_reorder_children()) - self.assertTrue(root_perms.can_reorder_children()) - - self.assertTrue(homepage_perms.can_move()) - self.assertFalse(root_perms.can_move()) - - self.assertTrue(homepage_perms.can_move_to(root)) - self.assertFalse(homepage_perms.can_move_to(unpublished_event_page)) - - def test_editable_pages_for_user_with_add_permission(self): - event_editor = User.objects.get(username='eventeditor') - homepage = Page.objects.get(url_path='/home/') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') - someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') - - editable_pages = UserPagePermissionsProxy(event_editor).editable_pages() - - self.assertFalse(editable_pages.filter(id=homepage.id).exists()) - self.assertTrue(editable_pages.filter(id=christmas_page.id).exists()) - self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists()) - self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists()) - - def test_editable_pages_for_user_with_edit_permission(self): - event_moderator = User.objects.get(username='eventmoderator') - homepage = Page.objects.get(url_path='/home/') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') - someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') - - editable_pages = UserPagePermissionsProxy(event_moderator).editable_pages() - - self.assertFalse(editable_pages.filter(id=homepage.id).exists()) - self.assertTrue(editable_pages.filter(id=christmas_page.id).exists()) - self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists()) - self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists()) - - def test_editable_pages_for_inactive_user(self): - user = User.objects.get(username='inactiveuser') - homepage = Page.objects.get(url_path='/home/') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') - someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') - - editable_pages = UserPagePermissionsProxy(user).editable_pages() - - self.assertFalse(editable_pages.filter(id=homepage.id).exists()) - self.assertFalse(editable_pages.filter(id=christmas_page.id).exists()) - self.assertFalse(editable_pages.filter(id=unpublished_event_page.id).exists()) - self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists()) - - def test_editable_pages_for_superuser(self): - user = User.objects.get(username='superuser') - homepage = Page.objects.get(url_path='/home/') - christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') - unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') - someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') - - editable_pages = UserPagePermissionsProxy(user).editable_pages() - - self.assertTrue(editable_pages.filter(id=homepage.id).exists()) - self.assertTrue(editable_pages.filter(id=christmas_page.id).exists()) - self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists()) - self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists()) - - -class TestPageQuerySet(TestCase): - fixtures = ['test.json'] - - def test_live(self): - pages = Page.objects.live() - - # All pages must be live - for page in pages: - self.assertTrue(page.live) - - # 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_not_live(self): - pages = Page.objects.not_live() - - # All pages must not be live - for page in pages: - self.assertFalse(page.live) - - # Check that "someone elses event" is in the results - event = Page.objects.get(url_path='/home/events/someone-elses-event/') - self.assertTrue(pages.filter(id=event.id).exists()) - - def test_page(self): - homepage = Page.objects.get(url_path='/home/') - pages = Page.objects.page(homepage) - - # Should only select the homepage - self.assertEqual(pages.count(), 1) - self.assertEqual(pages.first(), homepage) - - def test_not_page(self): - homepage = Page.objects.get(url_path='/home/') - pages = Page.objects.not_page(homepage) - - # Should select everything except for the homepage - self.assertEqual(pages.count(), Page.objects.all().count() - 1) - for page in pages: - self.assertNotEqual(page, homepage) - - def test_descendant_of(self): - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.descendant_of(events_index) - - # Check that all pages descend from events index - for page in pages: - self.assertTrue(page.get_ancestors().filter(id=events_index.id).exists()) - - def test_descendant_of_inclusive(self): - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.descendant_of(events_index, inclusive=True) - - # Check that all pages descend from events index, includes event index - for page in pages: - self.assertTrue(page == events_index or page.get_ancestors().filter(id=events_index.id).exists()) - - # Check that event index was included - self.assertTrue(pages.filter(id=events_index.id).exists()) - - def test_not_descendant_of(self): - homepage = Page.objects.get(url_path='/home/') - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.not_descendant_of(events_index) - - # Check that no pages descend from events_index - for page in pages: - self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists()) - - # As this is not inclusive, events index should be in the results - self.assertTrue(pages.filter(id=events_index.id).exists()) - - def test_not_descendant_of_inclusive(self): - homepage = Page.objects.get(url_path='/home/') - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.not_descendant_of(events_index, inclusive=True) - - # Check that all pages descend from homepage but not events index - for page in pages: - self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists()) - - # As this is inclusive, events index should not be in the results - self.assertFalse(pages.filter(id=events_index.id).exists()) - - def test_child_of(self): - homepage = Page.objects.get(url_path='/home/') - pages = Page.objects.child_of(homepage) - - # Check that all pages are children of homepage - for page in pages: - self.assertEqual(page.get_parent(), homepage) - - def test_not_child_of(self): - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.not_child_of(events_index) - - # Check that all pages are not children of events_index - for page in pages: - self.assertNotEqual(page.get_parent(), events_index) - - def test_ancestor_of(self): - root_page = Page.objects.get(id=1) - homepage = Page.objects.get(url_path='/home/') - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.ancestor_of(events_index) - - self.assertEqual(pages.count(), 2) - self.assertEqual(pages[0], root_page) - self.assertEqual(pages[1], homepage) - - def test_ancestor_of_inclusive(self): - root_page = Page.objects.get(id=1) - homepage = Page.objects.get(url_path='/home/') - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.ancestor_of(events_index, inclusive=True) - - self.assertEqual(pages.count(), 3) - self.assertEqual(pages[0], root_page) - self.assertEqual(pages[1], homepage) - self.assertEqual(pages[2], events_index) - - def test_not_ancestor_of(self): - root_page = Page.objects.get(id=1) - homepage = Page.objects.get(url_path='/home/') - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.not_ancestor_of(events_index) - - # Test that none of the ancestors are in pages - for page in pages: - self.assertNotEqual(page, root_page) - self.assertNotEqual(page, homepage) - - # Test that events index is in pages - self.assertTrue(pages.filter(id=events_index.id).exists()) - - def test_not_ancestor_of_inclusive(self): - root_page = Page.objects.get(id=1) - homepage = Page.objects.get(url_path='/home/') - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.not_ancestor_of(events_index, inclusive=True) - - # Test that none of the ancestors or the events_index are in pages - for page in pages: - self.assertNotEqual(page, root_page) - self.assertNotEqual(page, homepage) - self.assertNotEqual(page, events_index) - - def test_parent_of(self): - homepage = Page.objects.get(url_path='/home/') - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.parent_of(events_index) - - # Pages must only contain homepage - self.assertEqual(pages.count(), 1) - self.assertEqual(pages[0], homepage) - - def test_not_parent_of(self): - homepage = Page.objects.get(url_path='/home/') - events_index = Page.objects.get(url_path='/home/events/') - pages = Page.objects.not_parent_of(events_index) - - # Pages must not contain homepage - for page in pages: - self.assertNotEqual(page, homepage) - - # Test that events index is in pages - self.assertTrue(pages.filter(id=events_index.id).exists()) - - def test_sibling_of(self): - events_index = Page.objects.get(url_path='/home/events/') - event = Page.objects.get(url_path='/home/events/christmas/') - pages = Page.objects.sibling_of(event) - - # Check that all pages are children of events_index - for page in pages: - self.assertEqual(page.get_parent(), events_index) - - # Check that the event is not included - self.assertFalse(pages.filter(id=event.id).exists()) - - def test_sibling_of_inclusive(self): - events_index = Page.objects.get(url_path='/home/events/') - event = Page.objects.get(url_path='/home/events/christmas/') - pages = Page.objects.sibling_of(event, inclusive=True) - - # Check that all pages are children of events_index - for page in pages: - self.assertEqual(page.get_parent(), events_index) - - # Check that the event is included - self.assertTrue(pages.filter(id=event.id).exists()) - - def test_not_sibling_of(self): - events_index = Page.objects.get(url_path='/home/events/') - event = Page.objects.get(url_path='/home/events/christmas/') - pages = Page.objects.not_sibling_of(event) - - # Check that all pages are not children of events_index - for page in pages: - if page != event: - self.assertNotEqual(page.get_parent(), events_index) - - # Check that the event is included - self.assertTrue(pages.filter(id=event.id).exists()) - - # Test that events index is in pages - self.assertTrue(pages.filter(id=events_index.id).exists()) - - def test_not_sibling_of_inclusive(self): - events_index = Page.objects.get(url_path='/home/events/') - event = Page.objects.get(url_path='/home/events/christmas/') - pages = Page.objects.not_sibling_of(event, inclusive=True) - - # Check that all pages are not children of events_index - for page in pages: - self.assertNotEqual(page.get_parent(), events_index) - - # Check that the event is not included - self.assertFalse(pages.filter(id=event.id).exists()) - - # Test that events index is in pages - self.assertTrue(pages.filter(id=events_index.id).exists()) - - def test_type(self): - pages = Page.objects.type(EventPage) - - # Check that all objects are EventPages - for page in pages: - self.assertIsInstance(page.specific, EventPage) - - # Check that "someone elses event" is in the results - event = Page.objects.get(url_path='/home/events/someone-elses-event/') - self.assertTrue(pages.filter(id=event.id).exists()) - - def test_not_type(self): - pages = Page.objects.not_type(EventPage) - - # Check that no objects are EventPages - for page in pages: - self.assertNotIsInstance(page.specific, EventPage) - - # Check that the homepage is in the results - homepage = Page.objects.get(url_path='/home/') - self.assertTrue(pages.filter(id=homepage.id).exists()) - - -class TestMovePage(TestCase): - fixtures = ['test.json'] - - def test_move_page(self): - about_us_page = SimplePage.objects.get(url_path='/home/about-us/') - events_index = EventIndex.objects.get(url_path='/home/events/') - - events_index.move(about_us_page, pos='last-child') - - # re-fetch events index to confirm that db fields have been updated - events_index = EventIndex.objects.get(id=events_index.id) - self.assertEqual(events_index.url_path, '/home/about-us/events/') - self.assertEqual(events_index.depth, 4) - self.assertEqual(events_index.get_parent().id, about_us_page.id) - - # children of events_index should also have been updated - christmas = events_index.get_children().get(slug='christmas') - self.assertEqual(christmas.depth, 5) - self.assertEqual(christmas.url_path, '/home/about-us/events/christmas/') - - -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. - - 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 - """ - - fixtures = ['test.json'] - - def test_issue7(self): - # Get homepage, root page and site - root_page = Page.objects.get(id=1) - homepage = Page.objects.get(url_path='/home/') - default_site = Site.objects.get(is_default_site=True) - - # Create a new homepage under current homepage - new_homepage = SimplePage(title="New Homepage", slug="new-homepage") - homepage.add_child(instance=new_homepage) - - # Set new homepage as the site root page - default_site.root_page = new_homepage - default_site.save() - - # Warm up the cache by getting the url - _ = homepage.url - - # Move new homepage to root - new_homepage.move(root_page, pos='last-child') - - # Get fresh instance of new_homepage - new_homepage = Page.objects.get(id=new_homepage.id) - - # Check url - self.assertEqual(new_homepage.url, '/') - - -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. - - 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 - """ - - fixtures = ['test.json'] - - def test_issue157(self): - # Get homepage - homepage = Page.objects.get(url_path='/home/') - - # Warm up the cache by getting the url - _ = homepage.url - - # Change homepage title and slug - homepage.title = "New home" - homepage.slug = "new-home" - homepage.save() - - # Get fresh instance of homepage - homepage = Page.objects.get(id=homepage.id) - - # Check url - self.assertEqual(homepage.url, '/') - - -class TestFixTreeCommand(TestCase): - fixtures = ['test.json'] - - def run_command(self): - management.call_command('fixtree', interactive=False, stdout=StringIO()) - - def test_fixes_numchild(self): - # Get homepage and save old value - homepage = Page.objects.get(url_path='/home/') - old_numchild = homepage.numchild - - # Break it - homepage.numchild = 12345 - homepage.save() - - # Check that its broken - self.assertEqual(Page.objects.get(url_path='/home/').numchild, 12345) - - # Call command - self.run_command() - - # Check if its fixed - self.assertEqual(Page.objects.get(url_path='/home/').numchild, old_numchild) - - def test_fixes_depth(self): - # Get homepage and save old value - homepage = Page.objects.get(url_path='/home/') - old_depth = homepage.depth - - # Break it - homepage.depth = 12345 - homepage.save() - - # Check that its broken - self.assertEqual(Page.objects.get(url_path='/home/').depth, 12345) - - # Call command - self.run_command() - - # Check if its fixed - self.assertEqual(Page.objects.get(url_path='/home/').depth, old_depth) - - -class TestMovePagesCommand(TestCase): - fixtures = ['test.json'] - - def run_command(self, from_, to): - management.call_command('move_pages', str(from_), str(to), interactive=False, stdout=StringIO()) - - def test_move_pages(self): - # Get pages - events_index = Page.objects.get(url_path='/home/events/') - about_us = Page.objects.get(url_path='/home/about-us/') - page_ids = events_index.get_children().values_list('id', flat=True) - - # Move all events into "about us" - self.run_command(events_index.id, about_us.id) - - # Check that all pages moved - for page_id in page_ids: - self.assertEqual(Page.objects.get(id=page_id).get_parent(), about_us) - - -class TestReplaceTextCommand(TestCase): - fixtures = ['test.json'] - - def run_command(self, from_text, to_text): - management.call_command('replace_text', from_text, to_text, interactive=False, stdout=StringIO()) - - def test_replace_text(self): - # Check that the christmas page is definitely about christmas - self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Christmas") - - # Make it about easter - self.run_command("Christmas", "Easter") - - # Check that its now about easter - self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter") diff --git a/wagtail/wagtailcore/tests/__init__.py b/wagtail/wagtailcore/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/wagtailcore/tests/test_management_commands.py b/wagtail/wagtailcore/tests/test_management_commands.py new file mode 100644 index 000000000..787b808b5 --- /dev/null +++ b/wagtail/wagtailcore/tests/test_management_commands.py @@ -0,0 +1,89 @@ +from StringIO import StringIO + +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 + + +class TestFixTreeCommand(TestCase): + fixtures = ['test.json'] + + def run_command(self): + management.call_command('fixtree', interactive=False, stdout=StringIO()) + + def test_fixes_numchild(self): + # Get homepage and save old value + homepage = Page.objects.get(url_path='/home/') + old_numchild = homepage.numchild + + # Break it + homepage.numchild = 12345 + homepage.save() + + # Check that its broken + self.assertEqual(Page.objects.get(url_path='/home/').numchild, 12345) + + # Call command + self.run_command() + + # Check if its fixed + self.assertEqual(Page.objects.get(url_path='/home/').numchild, old_numchild) + + def test_fixes_depth(self): + # Get homepage and save old value + homepage = Page.objects.get(url_path='/home/') + old_depth = homepage.depth + + # Break it + homepage.depth = 12345 + homepage.save() + + # Check that its broken + self.assertEqual(Page.objects.get(url_path='/home/').depth, 12345) + + # Call command + self.run_command() + + # Check if its fixed + self.assertEqual(Page.objects.get(url_path='/home/').depth, old_depth) + + +class TestMovePagesCommand(TestCase): + fixtures = ['test.json'] + + def run_command(self, from_, to): + management.call_command('move_pages', str(from_), str(to), interactive=False, stdout=StringIO()) + + def test_move_pages(self): + # Get pages + events_index = Page.objects.get(url_path='/home/events/') + about_us = Page.objects.get(url_path='/home/about-us/') + page_ids = events_index.get_children().values_list('id', flat=True) + + # Move all events into "about us" + self.run_command(events_index.id, about_us.id) + + # Check that all pages moved + for page_id in page_ids: + self.assertEqual(Page.objects.get(id=page_id).get_parent(), about_us) + + +class TestReplaceTextCommand(TestCase): + fixtures = ['test.json'] + + def run_command(self, from_text, to_text): + management.call_command('replace_text', from_text, to_text, interactive=False, stdout=StringIO()) + + def test_replace_text(self): + # Check that the christmas page is definitely about christmas + self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Christmas") + + # Make it about easter + self.run_command("Christmas", "Easter") + + # Check that its now about easter + self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter") diff --git a/wagtail/wagtailcore/tests/test_page_model.py b/wagtail/wagtailcore/tests/test_page_model.py new file mode 100644 index 000000000..1bae57952 --- /dev/null +++ b/wagtail/wagtailcore/tests/test_page_model.py @@ -0,0 +1,226 @@ +from StringIO import StringIO + +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 + + +class TestRouting(TestCase): + fixtures = ['test.json'] + + def test_find_site_for_request(self): + default_site = Site.objects.get(is_default_site=True) + events_page = Page.objects.get(url_path='/home/events/') + events_site = Site.objects.create(hostname='events.example.com', root_page=events_page) + + # requests without a Host: header should be directed to the default site + request = HttpRequest() + request.path = '/' + self.assertEqual(Site.find_for_request(request), default_site) + + # requests with a known Host: header should be directed to the specific site + request = HttpRequest() + request.path = '/' + request.META['HTTP_HOST'] = 'events.example.com' + self.assertEqual(Site.find_for_request(request), events_site) + + # requests with an unrecognised Host: header should be directed to the default site + request = HttpRequest() + request.path = '/' + request.META['HTTP_HOST'] = 'unknown.example.com' + self.assertEqual(Site.find_for_request(request), default_site) + + def test_urls(self): + default_site = Site.objects.get(is_default_site=True) + homepage = Page.objects.get(url_path='/home/') + christmas_page = Page.objects.get(url_path='/home/events/christmas/') + + # Basic installation only has one site configured, so page.url will return local URLs + self.assertEqual(homepage.full_url, 'http://localhost/') + self.assertEqual(homepage.url, '/') + self.assertEqual(homepage.relative_url(default_site), '/') + + self.assertEqual(christmas_page.full_url, 'http://localhost/events/christmas/') + self.assertEqual(christmas_page.url, '/events/christmas/') + self.assertEqual(christmas_page.relative_url(default_site), '/events/christmas/') + + def test_urls_with_multiple_sites(self): + events_page = Page.objects.get(url_path='/home/events/') + events_site = Site.objects.create(hostname='events.example.com', root_page=events_page) + + default_site = Site.objects.get(is_default_site=True) + homepage = Page.objects.get(url_path='/home/') + christmas_page = Page.objects.get(url_path='/home/events/christmas/') + + # with multiple sites, page.url will return full URLs to ensure that + # they work across sites + self.assertEqual(homepage.full_url, 'http://localhost/') + self.assertEqual(homepage.url, 'http://localhost/') + self.assertEqual(homepage.relative_url(default_site), '/') + self.assertEqual(homepage.relative_url(events_site), 'http://localhost/') + + self.assertEqual(christmas_page.full_url, 'http://events.example.com/christmas/') + self.assertEqual(christmas_page.url, 'http://events.example.com/christmas/') + self.assertEqual(christmas_page.relative_url(default_site), 'http://events.example.com/christmas/') + self.assertEqual(christmas_page.relative_url(events_site), '/christmas/') + + def test_request_routing(self): + homepage = Page.objects.get(url_path='/home/') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + + request = HttpRequest() + request.path = '/events/christmas/' + response = homepage.route(request, ['events', 'christmas']) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context_data['self'], christmas_page) + used_template = response.resolve_template(response.template_name) + self.assertEqual(used_template.name, 'tests/event_page.html') + + def test_route_to_unknown_page_returns_404(self): + homepage = Page.objects.get(url_path='/home/') + + request = HttpRequest() + request.path = '/events/quinquagesima/' + with self.assertRaises(Http404): + homepage.route(request, ['events', 'quinquagesima']) + + def test_route_to_unpublished_page_returns_404(self): + homepage = Page.objects.get(url_path='/home/') + + request = HttpRequest() + request.path = '/events/tentative-unpublished-event/' + with self.assertRaises(Http404): + homepage.route(request, ['events', 'tentative-unpublished-event']) + + +class TestServeView(TestCase): + fixtures = ['test.json'] + + def setUp(self): + # Explicitly clear the cache of site root paths. Normally this would be kept + # in sync by the Site.save logic, but this is bypassed when the database is + # rolled back between tests using transactions. + from django.core.cache import cache + cache.delete('wagtail_site_root_paths') + + def test_serve(self): + response = self.client.get('/events/christmas/') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.templates[0].name, 'tests/event_page.html') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + self.assertEqual(response.context['self'], christmas_page) + + self.assertContains(response, '

      Christmas

      ') + self.assertContains(response, '

      Event

      ') + + def test_serve_unknown_page_returns_404(self): + response = self.client.get('/events/quinquagesima/') + self.assertEqual(response.status_code, 404) + + def test_serve_unpublished_page_returns_404(self): + response = self.client.get('/events/tentative-unpublished-event/') + self.assertEqual(response.status_code, 404) + + def test_serve_with_multiple_sites(self): + events_page = Page.objects.get(url_path='/home/events/') + Site.objects.create(hostname='events.example.com', root_page=events_page) + + response = self.client.get('/christmas/', HTTP_HOST='events.example.com') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.templates[0].name, 'tests/event_page.html') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + self.assertEqual(response.context['self'], christmas_page) + + self.assertContains(response, '

      Christmas

      ') + self.assertContains(response, '

      Event

      ') + + # same request to the default host should return a 404 + c = Client() + response = c.get('/christmas/', HTTP_HOST='localhost') + self.assertEqual(response.status_code, 404) + + def test_serve_with_custom_context(self): + response = self.client.get('/events/') + self.assertEqual(response.status_code, 200) + + # should render the whole page + self.assertContains(response, '

      Events

      ') + + # response should contain data from the custom 'events' context variable + self.assertContains(response, 'Christmas') + + def test_ajax_response(self): + response = self.client.get('/events/', HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + + # should only render the content of includes/event_listing.html, not the whole page + self.assertNotContains(response, '

      Events

      ') + self.assertContains(response, 'Christmas') + + +class TestStaticSitePaths(TestCase): + def setUp(self): + self.root_page = Page.objects.get(id=1) + + # For simple tests + self.home_page = self.root_page.add_child(instance=SimplePage(title="Homepage", slug="home")) + self.about_page = self.home_page.add_child(instance=SimplePage(title="About us", slug="about")) + self.contact_page = self.home_page.add_child(instance=SimplePage(title="Contact", slug="contact")) + + # For custom tests + self.event_index = self.root_page.add_child(instance=EventIndex(title="Events", slug="events")) + for i in range(20): + self.event_index.add_child(instance=EventPage(title="Event " + str(i), slug="event" + str(i))) + + def test_local_static_site_paths(self): + paths = list(self.about_page.get_static_site_paths()) + + self.assertEqual(paths, ['/']) + + def test_child_static_site_paths(self): + paths = list(self.home_page.get_static_site_paths()) + + self.assertEqual(paths, ['/', '/about/', '/contact/']) + + def test_custom_static_site_paths(self): + paths = list(self.event_index.get_static_site_paths()) + + # Event index path + expected_paths = ['/'] + + # One path for each page of results + expected_paths.extend(['/' + str(i + 1) + '/' for i in range(5)]) + + # One path for each event page + expected_paths.extend(['/event' + str(i) + '/' for i in range(20)]) + + paths.sort() + expected_paths.sort() + self.assertEqual(paths, expected_paths) + + +class TestMovePage(TestCase): + fixtures = ['test.json'] + + def test_move_page(self): + about_us_page = SimplePage.objects.get(url_path='/home/about-us/') + events_index = EventIndex.objects.get(url_path='/home/events/') + + events_index.move(about_us_page, pos='last-child') + + # re-fetch events index to confirm that db fields have been updated + events_index = EventIndex.objects.get(id=events_index.id) + self.assertEqual(events_index.url_path, '/home/about-us/events/') + self.assertEqual(events_index.depth, 4) + self.assertEqual(events_index.get_parent().id, about_us_page.id) + + # children of events_index should also have been updated + christmas = events_index.get_children().get(slug='christmas') + self.assertEqual(christmas.depth, 5) + self.assertEqual(christmas.url_path, '/home/about-us/events/christmas/') diff --git a/wagtail/wagtailcore/tests/test_page_permissions.py b/wagtail/wagtailcore/tests/test_page_permissions.py new file mode 100644 index 000000000..dbe35e39b --- /dev/null +++ b/wagtail/wagtailcore/tests/test_page_permissions.py @@ -0,0 +1,226 @@ +from StringIO import StringIO + +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 + + +class TestPagePermission(TestCase): + fixtures = ['test.json'] + + def test_nonpublisher_page_permissions(self): + event_editor = User.objects.get(username='eventeditor') + homepage = Page.objects.get(url_path='/home/') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') + someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') + + homepage_perms = homepage.permissions_for_user(event_editor) + christmas_page_perms = christmas_page.permissions_for_user(event_editor) + unpub_perms = unpublished_event_page.permissions_for_user(event_editor) + someone_elses_event_perms = someone_elses_event_page.permissions_for_user(event_editor) + + self.assertFalse(homepage_perms.can_add_subpage()) + self.assertTrue(christmas_page_perms.can_add_subpage()) + self.assertTrue(unpub_perms.can_add_subpage()) + self.assertTrue(someone_elses_event_perms.can_add_subpage()) + + self.assertFalse(homepage_perms.can_edit()) + self.assertTrue(christmas_page_perms.can_edit()) + self.assertTrue(unpub_perms.can_edit()) + self.assertFalse(someone_elses_event_perms.can_edit()) # basic 'add' permission doesn't allow editing pages owned by someone else + + self.assertFalse(homepage_perms.can_delete()) + self.assertFalse(christmas_page_perms.can_delete()) # cannot delete because it is published + self.assertTrue(unpub_perms.can_delete()) + self.assertFalse(someone_elses_event_perms.can_delete()) + + self.assertFalse(homepage_perms.can_publish()) + self.assertFalse(christmas_page_perms.can_publish()) + self.assertFalse(unpub_perms.can_publish()) + + self.assertFalse(homepage_perms.can_unpublish()) + self.assertFalse(christmas_page_perms.can_unpublish()) + self.assertFalse(unpub_perms.can_unpublish()) + + self.assertFalse(homepage_perms.can_publish_subpage()) + self.assertFalse(christmas_page_perms.can_publish_subpage()) + self.assertFalse(unpub_perms.can_publish_subpage()) + + self.assertFalse(homepage_perms.can_reorder_children()) + self.assertFalse(christmas_page_perms.can_reorder_children()) + self.assertFalse(unpub_perms.can_reorder_children()) + + self.assertFalse(homepage_perms.can_move()) + self.assertFalse(christmas_page_perms.can_move()) # cannot move because this would involve unpublishing from its current location + self.assertTrue(unpub_perms.can_move()) + self.assertFalse(someone_elses_event_perms.can_move()) + + self.assertFalse(christmas_page_perms.can_move_to(unpublished_event_page)) # cannot move because this would involve unpublishing from its current location + self.assertTrue(unpub_perms.can_move_to(christmas_page)) + self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination + self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself + + + def test_publisher_page_permissions(self): + event_moderator = User.objects.get(username='eventmoderator') + homepage = Page.objects.get(url_path='/home/') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') + + homepage_perms = homepage.permissions_for_user(event_moderator) + christmas_page_perms = christmas_page.permissions_for_user(event_moderator) + unpub_perms = unpublished_event_page.permissions_for_user(event_moderator) + + self.assertFalse(homepage_perms.can_add_subpage()) + self.assertTrue(christmas_page_perms.can_add_subpage()) + self.assertTrue(unpub_perms.can_add_subpage()) + + self.assertFalse(homepage_perms.can_edit()) + self.assertTrue(christmas_page_perms.can_edit()) + self.assertTrue(unpub_perms.can_edit()) + + self.assertFalse(homepage_perms.can_delete()) + self.assertTrue(christmas_page_perms.can_delete()) # cannot delete because it is published + self.assertTrue(unpub_perms.can_delete()) + + self.assertFalse(homepage_perms.can_publish()) + self.assertTrue(christmas_page_perms.can_publish()) + self.assertTrue(unpub_perms.can_publish()) + + self.assertFalse(homepage_perms.can_unpublish()) + self.assertTrue(christmas_page_perms.can_unpublish()) + self.assertFalse(unpub_perms.can_unpublish()) # cannot unpublish a page that isn't published + + self.assertFalse(homepage_perms.can_publish_subpage()) + self.assertTrue(christmas_page_perms.can_publish_subpage()) + self.assertTrue(unpub_perms.can_publish_subpage()) + + self.assertFalse(homepage_perms.can_reorder_children()) + self.assertTrue(christmas_page_perms.can_reorder_children()) + self.assertTrue(unpub_perms.can_reorder_children()) + + self.assertFalse(homepage_perms.can_move()) + self.assertTrue(christmas_page_perms.can_move()) + self.assertTrue(unpub_perms.can_move()) + + self.assertTrue(christmas_page_perms.can_move_to(unpublished_event_page)) + self.assertTrue(unpub_perms.can_move_to(christmas_page)) + self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination + self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself + + def test_inactive_user_has_no_permissions(self): + user = User.objects.get(username='inactiveuser') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') + + christmas_page_perms = christmas_page.permissions_for_user(user) + unpub_perms = unpublished_event_page.permissions_for_user(user) + + self.assertFalse(unpub_perms.can_add_subpage()) + self.assertFalse(unpub_perms.can_edit()) + self.assertFalse(unpub_perms.can_delete()) + self.assertFalse(unpub_perms.can_publish()) + self.assertFalse(christmas_page_perms.can_unpublish()) + self.assertFalse(unpub_perms.can_publish_subpage()) + self.assertFalse(unpub_perms.can_reorder_children()) + self.assertFalse(unpub_perms.can_move()) + self.assertFalse(unpub_perms.can_move_to(christmas_page)) + + def test_superuser_has_full_permissions(self): + user = User.objects.get(username='superuser') + homepage = Page.objects.get(url_path='/home/') + root = Page.objects.get(url_path='/') + unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') + + homepage_perms = homepage.permissions_for_user(user) + root_perms = root.permissions_for_user(user) + unpub_perms = unpublished_event_page.permissions_for_user(user) + + self.assertTrue(homepage_perms.can_add_subpage()) + self.assertTrue(root_perms.can_add_subpage()) + + self.assertTrue(homepage_perms.can_edit()) + self.assertFalse(root_perms.can_edit()) # root is not a real editable page, even to superusers + + self.assertTrue(homepage_perms.can_delete()) + self.assertFalse(root_perms.can_delete()) + + self.assertTrue(homepage_perms.can_publish()) + self.assertFalse(root_perms.can_publish()) + + self.assertTrue(homepage_perms.can_unpublish()) + self.assertFalse(root_perms.can_unpublish()) + self.assertFalse(unpub_perms.can_unpublish()) + + self.assertTrue(homepage_perms.can_publish_subpage()) + self.assertTrue(root_perms.can_publish_subpage()) + + self.assertTrue(homepage_perms.can_reorder_children()) + self.assertTrue(root_perms.can_reorder_children()) + + self.assertTrue(homepage_perms.can_move()) + self.assertFalse(root_perms.can_move()) + + self.assertTrue(homepage_perms.can_move_to(root)) + self.assertFalse(homepage_perms.can_move_to(unpublished_event_page)) + + def test_editable_pages_for_user_with_add_permission(self): + event_editor = User.objects.get(username='eventeditor') + homepage = Page.objects.get(url_path='/home/') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') + someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') + + editable_pages = UserPagePermissionsProxy(event_editor).editable_pages() + + self.assertFalse(editable_pages.filter(id=homepage.id).exists()) + self.assertTrue(editable_pages.filter(id=christmas_page.id).exists()) + self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists()) + self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists()) + + def test_editable_pages_for_user_with_edit_permission(self): + event_moderator = User.objects.get(username='eventmoderator') + homepage = Page.objects.get(url_path='/home/') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') + someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') + + editable_pages = UserPagePermissionsProxy(event_moderator).editable_pages() + + self.assertFalse(editable_pages.filter(id=homepage.id).exists()) + self.assertTrue(editable_pages.filter(id=christmas_page.id).exists()) + self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists()) + self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists()) + + def test_editable_pages_for_inactive_user(self): + user = User.objects.get(username='inactiveuser') + homepage = Page.objects.get(url_path='/home/') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') + someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') + + editable_pages = UserPagePermissionsProxy(user).editable_pages() + + self.assertFalse(editable_pages.filter(id=homepage.id).exists()) + self.assertFalse(editable_pages.filter(id=christmas_page.id).exists()) + self.assertFalse(editable_pages.filter(id=unpublished_event_page.id).exists()) + self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists()) + + def test_editable_pages_for_superuser(self): + user = User.objects.get(username='superuser') + homepage = Page.objects.get(url_path='/home/') + christmas_page = EventPage.objects.get(url_path='/home/events/christmas/') + unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/') + someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/') + + editable_pages = UserPagePermissionsProxy(user).editable_pages() + + self.assertTrue(editable_pages.filter(id=homepage.id).exists()) + self.assertTrue(editable_pages.filter(id=christmas_page.id).exists()) + self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists()) + self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists()) diff --git a/wagtail/wagtailcore/tests/test_page_queryset.py b/wagtail/wagtailcore/tests/test_page_queryset.py new file mode 100644 index 000000000..06f2c3e21 --- /dev/null +++ b/wagtail/wagtailcore/tests/test_page_queryset.py @@ -0,0 +1,256 @@ +from StringIO import StringIO + +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 + + +class TestPageQuerySet(TestCase): + fixtures = ['test.json'] + + def test_live(self): + pages = Page.objects.live() + + # All pages must be live + for page in pages: + self.assertTrue(page.live) + + # 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_not_live(self): + pages = Page.objects.not_live() + + # All pages must not be live + for page in pages: + self.assertFalse(page.live) + + # Check that "someone elses event" is in the results + event = Page.objects.get(url_path='/home/events/someone-elses-event/') + self.assertTrue(pages.filter(id=event.id).exists()) + + def test_page(self): + homepage = Page.objects.get(url_path='/home/') + pages = Page.objects.page(homepage) + + # Should only select the homepage + self.assertEqual(pages.count(), 1) + self.assertEqual(pages.first(), homepage) + + def test_not_page(self): + homepage = Page.objects.get(url_path='/home/') + pages = Page.objects.not_page(homepage) + + # Should select everything except for the homepage + self.assertEqual(pages.count(), Page.objects.all().count() - 1) + for page in pages: + self.assertNotEqual(page, homepage) + + def test_descendant_of(self): + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.descendant_of(events_index) + + # Check that all pages descend from events index + for page in pages: + self.assertTrue(page.get_ancestors().filter(id=events_index.id).exists()) + + def test_descendant_of_inclusive(self): + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.descendant_of(events_index, inclusive=True) + + # Check that all pages descend from events index, includes event index + for page in pages: + self.assertTrue(page == events_index or page.get_ancestors().filter(id=events_index.id).exists()) + + # Check that event index was included + self.assertTrue(pages.filter(id=events_index.id).exists()) + + def test_not_descendant_of(self): + homepage = Page.objects.get(url_path='/home/') + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.not_descendant_of(events_index) + + # Check that no pages descend from events_index + for page in pages: + self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists()) + + # As this is not inclusive, events index should be in the results + self.assertTrue(pages.filter(id=events_index.id).exists()) + + def test_not_descendant_of_inclusive(self): + homepage = Page.objects.get(url_path='/home/') + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.not_descendant_of(events_index, inclusive=True) + + # Check that all pages descend from homepage but not events index + for page in pages: + self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists()) + + # As this is inclusive, events index should not be in the results + self.assertFalse(pages.filter(id=events_index.id).exists()) + + def test_child_of(self): + homepage = Page.objects.get(url_path='/home/') + pages = Page.objects.child_of(homepage) + + # Check that all pages are children of homepage + for page in pages: + self.assertEqual(page.get_parent(), homepage) + + def test_not_child_of(self): + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.not_child_of(events_index) + + # Check that all pages are not children of events_index + for page in pages: + self.assertNotEqual(page.get_parent(), events_index) + + def test_ancestor_of(self): + root_page = Page.objects.get(id=1) + homepage = Page.objects.get(url_path='/home/') + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.ancestor_of(events_index) + + self.assertEqual(pages.count(), 2) + self.assertEqual(pages[0], root_page) + self.assertEqual(pages[1], homepage) + + def test_ancestor_of_inclusive(self): + root_page = Page.objects.get(id=1) + homepage = Page.objects.get(url_path='/home/') + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.ancestor_of(events_index, inclusive=True) + + self.assertEqual(pages.count(), 3) + self.assertEqual(pages[0], root_page) + self.assertEqual(pages[1], homepage) + self.assertEqual(pages[2], events_index) + + def test_not_ancestor_of(self): + root_page = Page.objects.get(id=1) + homepage = Page.objects.get(url_path='/home/') + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.not_ancestor_of(events_index) + + # Test that none of the ancestors are in pages + for page in pages: + self.assertNotEqual(page, root_page) + self.assertNotEqual(page, homepage) + + # Test that events index is in pages + self.assertTrue(pages.filter(id=events_index.id).exists()) + + def test_not_ancestor_of_inclusive(self): + root_page = Page.objects.get(id=1) + homepage = Page.objects.get(url_path='/home/') + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.not_ancestor_of(events_index, inclusive=True) + + # Test that none of the ancestors or the events_index are in pages + for page in pages: + self.assertNotEqual(page, root_page) + self.assertNotEqual(page, homepage) + self.assertNotEqual(page, events_index) + + def test_parent_of(self): + homepage = Page.objects.get(url_path='/home/') + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.parent_of(events_index) + + # Pages must only contain homepage + self.assertEqual(pages.count(), 1) + self.assertEqual(pages[0], homepage) + + def test_not_parent_of(self): + homepage = Page.objects.get(url_path='/home/') + events_index = Page.objects.get(url_path='/home/events/') + pages = Page.objects.not_parent_of(events_index) + + # Pages must not contain homepage + for page in pages: + self.assertNotEqual(page, homepage) + + # Test that events index is in pages + self.assertTrue(pages.filter(id=events_index.id).exists()) + + def test_sibling_of(self): + events_index = Page.objects.get(url_path='/home/events/') + event = Page.objects.get(url_path='/home/events/christmas/') + pages = Page.objects.sibling_of(event) + + # Check that all pages are children of events_index + for page in pages: + self.assertEqual(page.get_parent(), events_index) + + # Check that the event is not included + self.assertFalse(pages.filter(id=event.id).exists()) + + def test_sibling_of_inclusive(self): + events_index = Page.objects.get(url_path='/home/events/') + event = Page.objects.get(url_path='/home/events/christmas/') + pages = Page.objects.sibling_of(event, inclusive=True) + + # Check that all pages are children of events_index + for page in pages: + self.assertEqual(page.get_parent(), events_index) + + # Check that the event is included + self.assertTrue(pages.filter(id=event.id).exists()) + + def test_not_sibling_of(self): + events_index = Page.objects.get(url_path='/home/events/') + event = Page.objects.get(url_path='/home/events/christmas/') + pages = Page.objects.not_sibling_of(event) + + # Check that all pages are not children of events_index + for page in pages: + if page != event: + self.assertNotEqual(page.get_parent(), events_index) + + # Check that the event is included + self.assertTrue(pages.filter(id=event.id).exists()) + + # Test that events index is in pages + self.assertTrue(pages.filter(id=events_index.id).exists()) + + def test_not_sibling_of_inclusive(self): + events_index = Page.objects.get(url_path='/home/events/') + event = Page.objects.get(url_path='/home/events/christmas/') + pages = Page.objects.not_sibling_of(event, inclusive=True) + + # Check that all pages are not children of events_index + for page in pages: + self.assertNotEqual(page.get_parent(), events_index) + + # Check that the event is not included + self.assertFalse(pages.filter(id=event.id).exists()) + + # Test that events index is in pages + self.assertTrue(pages.filter(id=events_index.id).exists()) + + def test_type(self): + pages = Page.objects.type(EventPage) + + # Check that all objects are EventPages + for page in pages: + self.assertIsInstance(page.specific, EventPage) + + # Check that "someone elses event" is in the results + event = Page.objects.get(url_path='/home/events/someone-elses-event/') + self.assertTrue(pages.filter(id=event.id).exists()) + + def test_not_type(self): + pages = Page.objects.not_type(EventPage) + + # Check that no objects are EventPages + for page in pages: + self.assertNotIsInstance(page.specific, EventPage) + + # Check that the homepage is in the results + homepage = Page.objects.get(url_path='/home/') + self.assertTrue(pages.filter(id=homepage.id).exists()) diff --git a/wagtail/wagtailcore/tests/tests.py b/wagtail/wagtailcore/tests/tests.py new file mode 100644 index 000000000..d10474bba --- /dev/null +++ b/wagtail/wagtailcore/tests/tests.py @@ -0,0 +1,99 @@ +from StringIO import StringIO + +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 + + +class TestPageUrlTags(TestCase): + fixtures = ['test.json'] + + def test_pageurl_tag(self): + response = self.client.get('/events/') + self.assertEqual(response.status_code, 200) + 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') + + +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. + + 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 + """ + + fixtures = ['test.json'] + + def test_issue7(self): + # Get homepage, root page and site + root_page = Page.objects.get(id=1) + homepage = Page.objects.get(url_path='/home/') + default_site = Site.objects.get(is_default_site=True) + + # Create a new homepage under current homepage + new_homepage = SimplePage(title="New Homepage", slug="new-homepage") + homepage.add_child(instance=new_homepage) + + # Set new homepage as the site root page + default_site.root_page = new_homepage + default_site.save() + + # Warm up the cache by getting the url + _ = homepage.url + + # Move new homepage to root + new_homepage.move(root_page, pos='last-child') + + # Get fresh instance of new_homepage + new_homepage = Page.objects.get(id=new_homepage.id) + + # Check url + self.assertEqual(new_homepage.url, '/') + + +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. + + 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 + """ + + fixtures = ['test.json'] + + def test_issue157(self): + # Get homepage + homepage = Page.objects.get(url_path='/home/') + + # Warm up the cache by getting the url + _ = homepage.url + + # Change homepage title and slug + homepage.title = "New home" + homepage.slug = "new-home" + homepage.save() + + # Get fresh instance of homepage + homepage = Page.objects.get(id=homepage.id) + + # Check url + self.assertEqual(homepage.url, '/') From ebf1360c0fe0c2d7eddd4a642526ed321449299a Mon Sep 17 00:00:00 2001 From: Ben Delevingne Date: Thu, 5 Jun 2014 15:19:17 +0100 Subject: [PATCH 182/189] Fix indendation of Advert test model --- wagtail/tests/models.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index bbf91d278..5598e88f8 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -255,18 +255,21 @@ FormPage.content_panels = [ ] +# Snippets + # Snippets class Advert(models.Model): - url = models.URLField(null=True, blank=True) - text = models.CharField(max_length=255) + url = models.URLField(null=True, blank=True) + text = models.CharField(max_length=255) - panels = [ - FieldPanel('url'), - FieldPanel('text'), - ] + panels = [ + FieldPanel('url'), + FieldPanel('text'), + ] + + def __unicode__(self): + return self.text - def __unicode__(self): - return self.text register_snippet(Advert) From 1f9fe4bfc1455a0e05cde4384555db315f873ce9 Mon Sep 17 00:00:00 2001 From: Ben Delevingne Date: Thu, 5 Jun 2014 15:21:56 +0100 Subject: [PATCH 183/189] Added two test snippet models definitions --- wagtail/tests/models.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index 5598e88f8..efbce2718 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -273,3 +273,21 @@ class Advert(models.Model): register_snippet(Advert) + + +# AlphaSnippet is not registered here as this is done +# during the tests it are needed for +class AlphaSnippet(models.Model): + text = models.CharField(max_length=255) + + def __unicode__(self): + return self.text + + +# ZuluSnippet is not registered here as this is done +# during the tests it are needed for +class ZuluSnippet(models.Model): + text = models.CharField(max_length=255) + + def __unicode__(self): + return self.text From c200ad50c4363240da14e2d750cba1e59c94b7f1 Mon Sep 17 00:00:00 2001 From: Ben Delevingne Date: Thu, 5 Jun 2014 15:24:29 +0100 Subject: [PATCH 184/189] Test ordering of snippet --- wagtail/wagtailsnippets/tests.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailsnippets/tests.py b/wagtail/wagtailsnippets/tests.py index 727e5e52d..ac5bfa509 100644 --- a/wagtail/wagtailsnippets/tests.py +++ b/wagtail/wagtailsnippets/tests.py @@ -3,7 +3,8 @@ from django.core.urlresolvers import reverse from django.contrib.auth.models import User from wagtail.tests.utils import login, unittest -from wagtail.tests.models import Advert +from wagtail.tests.models import Advert, AlphaSnippet, ZuluSnippet +from wagtail.wagtailsnippets.models import register_snippet, SNIPPET_MODELS from wagtail.wagtailsnippets.views.snippets import get_content_type_from_url_params, get_snippet_edit_handler from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel @@ -168,3 +169,16 @@ class TestSnippetChooserPanel(TestCase): def test_render_js(self): self.assertTrue("createSnippetChooser(fixPrefix('id_text'), 'contenttypes/contenttype');" in self.snippet_chooser_panel.render_js()) + + +class TestSnippetOrdering(TestCase): + def setUp(self): + register_snippet(ZuluSnippet) + register_snippet(AlphaSnippet) + + def test_snippets_ordering(self): + # Ensure AlphaSnippet is before Zulu Snippet + # Cannot check first and last position as other snippets + # may get registered elsewhere during test + self.assertLess(SNIPPET_MODELS.index(AlphaSnippet), + SNIPPET_MODELS.index(ZuluSnippet)) From ea82f1293544b94903ecd8d046bd4a636f68283d Mon Sep 17 00:00:00 2001 From: Ben Delevingne Date: Thu, 5 Jun 2014 15:25:17 +0100 Subject: [PATCH 185/189] Order snippet models during snippet registration --- wagtail/wagtailsnippets/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/wagtail/wagtailsnippets/models.py b/wagtail/wagtailsnippets/models.py index b929f06aa..8af626655 100644 --- a/wagtail/wagtailsnippets/models.py +++ b/wagtail/wagtailsnippets/models.py @@ -19,3 +19,4 @@ def get_snippet_content_types(): def register_snippet(model): if model not in SNIPPET_MODELS: SNIPPET_MODELS.append(model) + SNIPPET_MODELS.sort(key=lambda x: x._meta.verbose_name) From ad6a8682af9853bca937cee8603fa97f082117b1 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Thu, 5 Jun 2014 15:25:23 +0100 Subject: [PATCH 186/189] removing margins from body, which don't play well with jquery position() or offset() --- .../static/wagtailadmin/scss/core.scss | 4 ++-- .../wagtailadmin/scss/panels/rich-text.scss | 21 +++++-------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss index e622680bd..80beb7f5f 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/core.scss @@ -626,7 +626,7 @@ footer, .logo{ padding-right:$desktop-nice-padding; } - body{ + .wrapper{ margin-left:$menu-width; } @@ -645,7 +645,7 @@ footer, .logo{ left:0; height:100%; width:$menu-width; - margin-left: -$menu-width; + margin-left: 0; .inner{ height:100%; diff --git a/wagtail/wagtailadmin/static/wagtailadmin/scss/panels/rich-text.scss b/wagtail/wagtailadmin/static/wagtailadmin/scss/panels/rich-text.scss index 8b26f36ab..c951a0bc5 100644 --- a/wagtail/wagtailadmin/static/wagtailadmin/scss/panels/rich-text.scss +++ b/wagtail/wagtailadmin/static/wagtailadmin/scss/panels/rich-text.scss @@ -3,14 +3,13 @@ .hallotoolbar{ position:absolute; - left:50px; + left:$mobile-nice-padding; z-index:5; margin-top:4em; margin-left:0em; } .hallotoolbar.affixed{ position:fixed; - margin-left:140px; margin-top:0; } .hallotoolbar button{ @@ -148,18 +147,8 @@ } } } - -@media screen and (min-width: $breakpoint-desktop-larger){ - /* .hallotoolbar{ - margin:0 auto; - position:absolute; - left:-$menu-width; - right:0; - z-index:5; - margin-top:3em; +@media screen and (min-width: $breakpoint-mobile){ + .hallotoolbar{ + left:$menu-width + $desktop-nice-padding; } - .hallotoolbar.affixed{ - position:fixed; - margin:0 auto; - }*/ -} + } \ No newline at end of file From 1a19819a193cc70bb7c44b880054013375732a32 Mon Sep 17 00:00:00 2001 From: Ben Delevingne Date: Thu, 5 Jun 2014 15:46:37 +0100 Subject: [PATCH 187/189] Comment changes so they are real in English --- wagtail/tests/models.py | 4 ++-- wagtail/wagtailsnippets/tests.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index efbce2718..f06c41ef9 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -276,7 +276,7 @@ register_snippet(Advert) # AlphaSnippet is not registered here as this is done -# during the tests it are needed for +# during the tests it is needed for class AlphaSnippet(models.Model): text = models.CharField(max_length=255) @@ -285,7 +285,7 @@ class AlphaSnippet(models.Model): # ZuluSnippet is not registered here as this is done -# during the tests it are needed for +# during the tests it is needed for class ZuluSnippet(models.Model): text = models.CharField(max_length=255) diff --git a/wagtail/wagtailsnippets/tests.py b/wagtail/wagtailsnippets/tests.py index ac5bfa509..de5e60b9b 100644 --- a/wagtail/wagtailsnippets/tests.py +++ b/wagtail/wagtailsnippets/tests.py @@ -177,7 +177,7 @@ class TestSnippetOrdering(TestCase): register_snippet(AlphaSnippet) def test_snippets_ordering(self): - # Ensure AlphaSnippet is before Zulu Snippet + # Ensure AlphaSnippet is before ZuluSnippet # Cannot check first and last position as other snippets # may get registered elsewhere during test self.assertLess(SNIPPET_MODELS.index(AlphaSnippet), From eb05073e59e416f974d0b6d952ca47bd6886893b Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 6 Jun 2014 16:54:06 +0100 Subject: [PATCH 188/189] Add comments to test models for clarity --- wagtail/tests/models.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index f06c41ef9..739e462e1 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -275,8 +275,12 @@ class Advert(models.Model): register_snippet(Advert) -# AlphaSnippet is not registered here as this is done -# during the tests it is needed for +# AlphaSnippet and ZuluSnippet are for testing ordering of +# snippets when registering. They are named as such to ensure +# thier orderign is clear. They are registered during testing +# to ensure specific [in]correct register ordering + +# AlphaSnippet is registered during TestSnippetOrdering class AlphaSnippet(models.Model): text = models.CharField(max_length=255) @@ -284,8 +288,7 @@ class AlphaSnippet(models.Model): return self.text -# ZuluSnippet is not registered here as this is done -# during the tests it is needed for +# ZuluSnippet is registered during TestSnippetOrdering class ZuluSnippet(models.Model): text = models.CharField(max_length=255) From 06677ac4448c2f02e185f0fe476010cda397bab6 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 6 Jun 2014 16:54:36 +0100 Subject: [PATCH 189/189] Typo --- wagtail/tests/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/tests/models.py b/wagtail/tests/models.py index 739e462e1..e061a4bfe 100644 --- a/wagtail/tests/models.py +++ b/wagtail/tests/models.py @@ -277,7 +277,7 @@ register_snippet(Advert) # AlphaSnippet and ZuluSnippet are for testing ordering of # snippets when registering. They are named as such to ensure -# thier orderign is clear. They are registered during testing +# thier ordering is clear. They are registered during testing # to ensure specific [in]correct register ordering # AlphaSnippet is registered during TestSnippetOrdering