Merge branch 'master' into varnish-cache-invalidation

Conflicts:
	wagtail/wagtailadmin/tests/test_pages_views.py
This commit is contained in:
Karl Hobley 2014-07-02 09:38:51 +01:00
commit d9b22292f2
44 changed files with 1427 additions and 401 deletions

View file

@ -1,5 +1,5 @@
Editing API
Defining models with the Editing API
===========
.. note::
@ -22,18 +22,30 @@ Defining Panels
A "panel" is the basic editing block in Wagtail. Wagtail will automatically pick the appropriate editing widget for most Django field types, you just need to add a panel for each field you want to show in the Wagtail page editor, in the order you want them to appear.
There are three types of panels:
There are four basic types of panels:
``FieldPanel( field_name, classname=None )``
This is the panel used for basic Django field types. ``field_name`` is the name of the class property used in your model definition. ``classname`` is a string of optional CSS classes given to the panel which are used in formatting and scripted interactivity. By default, panels are formatted as inset fields. The CSS class ``full`` can be used to format the panel so it covers the full width of the Wagtail page editor. The CSS class ``title`` can be used to mark a field as the source for auto-generated slug strings.
``MultiFieldPanel( children, heading="", classname=None )``
This panel condenses several ``FieldPanel`` s or choosers, from a list or tuple, under a single ``heading`` string.
``InlinePanel( base_model, relation_name, panels=None, label='', help_text='' )``
``InlinePanel( base_model, relation_name, panels=None, classname=None, label='', help_text='' )``
This panel allows for the creation of a "cluster" of related objects over a join to a separate model, such as a list of related links or slides to an image carousel. This is a very powerful, but tricky feature which will take some space to cover, so we'll skip over it for now. For a full explanation on the usage of ``InlinePanel``, see :ref:`inline_panels`.
Wagtail provides a tabbed interface to help organize panels. ``content_panels`` is the main tab, used for the meat of your model content. The other, ``promote_panels``, is suggested for organizing metadata about the content, such as SEO information and other machine-readable information. Since you're writing the panel definitions, you can organize them however you want.
``FieldRowPanel( children, classname=None)``
This panel is purely aesthetic. It creates a columnar layout in the editing interface, where each of the child Panels appears alongside each others rather than below. Use of FieldRowPanel particularly helps reduce the "snow-blindness" effect of seeing so many fields on the page, for complex models. It also improves the perceived association between fields of a similar nature. For example if you created a model representing an "Event" which had a starting date and ending date, it would be intuitive to find the start and end date on the same "row".
FieldRowPanel should be used in combination with ``col*`` classnames added to each of the child Panels of the FieldRowPanel. The Wagtail editing interface is layed out using a grid system, in which the maximum width of the editor is 12 columns wide. Classes ``col1``-``col12`` can be applied to each child of a FieldRowPanel. The class ``col3`` will ensure that field appears 3 columns wide or a quarter the width. ``col4`` would cause the field to be 4 columns wide, or a third the width.
**(In addition to these four, there are also Chooser Panels, detailed below.)**
Wagtail provides a tabbed interface to help organize panels. Three such tabs are provided:
* ``content_panels`` is the main tab, used for the bulk of your model's fields.
* ``promote_panels`` is suggested for organizing fields regarding the promotion of the page around the site and the internet e.g A field to dictate whether the page should show in site-wide menus, descriptive text that should appear in site search results, SEO friendly titles, OpenGraph meta tag content and other machine-readable information.
* ``settings_panels`` is essentially for non-copy fields. By default it contains the page's scheduled publishing fields. Other suggested fields e.g: a field to switch between one layout/style and another.
Let's look at an example of a panel definition:
@ -55,7 +67,10 @@ Let's look at an example of a panel definition:
ExamplePage.content_panels = [
FieldPanel('title', classname="full title"),
FieldPanel('body', classname="full"),
FieldPanel('date'),
FieldRowPanel([
FieldPanel('start_date', classname="col3"),
FieldPanel('end_date', classname="col3"),
]),
ImageChooserPanel('splash_image'),
DocumentChooserPanel('free_download'),
PageChooserPanel('related_page'),
@ -254,6 +269,12 @@ Titles
Use ``classname="title"`` to make Page's built-in title field stand out with more vertical padding.
Col*
------
Fields within a ``FieldRowPanel`` can have their width dictated in terms of the number of columns it should span. The ``FieldRowPanel`` is always considered to be 12 columns wide regardless of browser size or the nesting of ``FieldRowPanel`` in any other type of panel. Specify a number of columns thus: ``col3``, ``col4``, ``col6`` etc (up to 12). The resulting width with be *relative* to the full width of the ``FieldRowPanel``.
Required Fields
---------------

View file

@ -0,0 +1,59 @@
Management commands
===================
publish_scheduled_pages
-----------------------
**./manage.py publish_scheduled_pages**
This command publishes/unpublishes pages that have had these actions scheduled by an editor.
It is recommended to run this command once an hour.
fixtree
-------
**./manage.py fixtree**
This command scans for errors in your database and attempts to fix any issues it finds.
move_pages
----------
**./manage.py move_pages from to**
This mass moves a bunch of pages from one section of the tree to another.
Options:
- **from**
This is id of the page to move pages from. All descendants of this page will be moved to the destination. After the operation is complete, this page will have no children.
- **to**
This is the id of the page to move pages to.
update_index
------------
**./manage.py update_index**
This command rebuilds the search index from scratch. It is only required when using ElasticSearch.
It is recommended to run this command once a week and at the following times:
- Whenever any pages have been created through a script (eg, import)
- Whenever any changes have been made to models or search configuration
While this command is running, the search may not return any results so avoid running this command at peak times.
search_garbage_collect
----------------------
**./manage.py search_garbage_collect**
Wagtail keeps a log of search queries that are popular on your website. On high traffic websites, this log may get big and sometimes you may want to clean out old search queries.
This command cleans out all search query logs that are more than one week old.

View file

@ -42,9 +42,10 @@
<li class="color-teal">color-teal</li>
<li class="color-teal-darker">color-teal-darker</li>
<li class="color-teal-dark">color-teal-dark</li>
<li class="color-red">color-red</li>
<li class="color-orange">color-orange</li>
<li class="color-green">color-green</li>
</ul>
<ul>
<li class="color-salmon">color-salmon</li>
<li class="color-salmon-light">color-salmon-light</li>
</ul>
<ul>
<li class="color-grey-1">color-grey-1</li>
@ -54,6 +55,12 @@
<li class="color-grey-4">color-grey-4</li>
<li class="color-grey-5">color-grey-5</li>
</ul>
<ul>
<li class="color-red">color-red</li>
<li class="color-orange">color-orange</li>
<li class="color-green">color-green</li>
</ul>
</section>
<section id="typography">
@ -407,6 +414,7 @@
<li class="icon icon-warning">warning</li>
<li class="icon icon-success">success</li>
<li class="icon icon-date">date</li>
<li class="icon icon-time">time</li>
<li class="icon icon-form">form</li>
</ul>

View file

@ -23,6 +23,7 @@ class ExampleForm(forms.Form):
url = forms.URLField(required=True)
email = forms.EmailField(max_length=254)
date = forms.DateField()
time = forms.TimeField()
select = forms.ChoiceField(choices=CHOICES)
boolean = forms.BooleanField(required=False)

View file

@ -140,6 +140,10 @@ def extract_panel_definitions_from_model_class(model, exclude=None):
return panels
def set_page_edit_handler(page_class, handlers):
page_class.handlers = handlers
class EditHandler(object):
"""
Abstract class providing sensible default behaviours for objects implementing
@ -183,19 +187,21 @@ class EditHandler(object):
heading = ""
help_text = ""
def object_classnames(self):
def classes(self):
"""
Additional classnames to add to the <li class="object"> when rendering this
within an ObjectList
Additional CSS classnames to add to whatever kind of object this is at output.
Subclasses of EditHandler should override this, invoking super(B, self).classes() to
append more classes specific to the situation.
"""
return ""
def field_classnames(self):
"""
Additional classnames to add to the <li> when rendering this within a
<ul class="fields">
"""
return ""
classes = []
try:
classes.append(self.classname)
except AttributeError:
pass
return classes
def field_type(self):
"""
@ -261,12 +267,6 @@ class BaseCompositeEditHandler(EditHandler):
"""
_widget_overrides = None
def object_classnames(self):
try:
return "multi-field " + self.classname
except (AttributeError, TypeError):
return "multi-field"
@classmethod
def widget_overrides(cls):
if cls._widget_overrides is None:
@ -326,18 +326,33 @@ class BaseObjectList(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/object_list.html"
def ObjectList(children, heading=""):
def ObjectList(children, heading="", classname=""):
return type('_ObjectList', (BaseObjectList,), {
'children': children,
'heading': heading,
'classname': classname
})
class BaseFieldRowPanel(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/field_row_panel.html"
def FieldRowPanel(children, classname=""):
return type('_FieldRowPanel', (BaseFieldRowPanel,), {
'children': children,
'classname': classname,
})
class BaseMultiFieldPanel(BaseCompositeEditHandler):
template = "wagtailadmin/edit_handlers/multi_field_panel.html"
def classes(self):
classes = super(BaseMultiFieldPanel, self).classes()
classes.append("multi-field")
return classes
def MultiFieldPanel(children, heading="", classname=None):
def MultiFieldPanel(children, heading="", classname=""):
return type('_MultiFieldPanel', (BaseMultiFieldPanel,), {
'children': children,
'heading': heading,
@ -353,25 +368,23 @@ class BaseFieldPanel(EditHandler):
self.heading = self.bound_field.label
self.help_text = self.bound_field.help_text
def object_classnames(self):
try:
return "single-field " + self.classname
except (AttributeError, TypeError):
return "single-field"
def classes(self):
classes = super(BaseFieldPanel, self).classes();
if self.bound_field.field.required:
classes.append("required")
if self.bound_field.errors:
classes.append("error")
classes.append(self.field_type())
classes.append("single-field")
return classes
def field_type(self):
return camelcase_to_underscore(self.bound_field.field.__class__.__name__)
def field_classnames(self):
classname = self.field_type()
if self.bound_field.field.required:
classname += " required"
if self.bound_field.errors:
classname += " error"
return classname
object_template = "wagtailadmin/edit_handlers/field_panel_object.html"
object_template = "wagtailadmin/edit_handlers/single_field_panel.html"
def render_as_object(self):
return mark_safe(render_to_string(self.object_template, {
@ -401,7 +414,7 @@ class BaseFieldPanel(EditHandler):
return [self.field_name]
def FieldPanel(field_name, classname=None):
def FieldPanel(field_name, classname=""):
return type('_FieldPanel', (BaseFieldPanel,), {
'field_name': field_name,
'classname': classname,
@ -597,10 +610,23 @@ def InlinePanel(base_model, relation_name, panels=None, label='', help_text=''):
})
# This allows users to include the publishing panel in their own per-model override
# without having to write these fields out by hand, potentially losing 'classname'
# and therefore the associated styling of the publishing panel
def PublishingPanel():
return MultiFieldPanel([
FieldRowPanel([
FieldPanel('go_live_at'),
FieldPanel('expire_at'),
], classname="label-above"),
], ugettext_lazy('Scheduled publishing'), classname="publishing")
# Now that we've defined EditHandlers, we can set up wagtailcore.Page to have some.
Page.content_panels = [
FieldPanel('title', classname="full title"),
]
Page.promote_panels = [
MultiFieldPanel([
FieldPanel('slug'),
@ -609,3 +635,7 @@ Page.promote_panels = [
FieldPanel('search_description'),
], ugettext_lazy('Common page configuration')),
]
Page.settings_panels = [
PublishingPanel()
]

View file

@ -1,97 +0,0 @@
(function(jQuery) {
return jQuery.widget('IKS.halloToolbarFixed', {
toolbar: null,
options: {
parentElement: 'body',
editable: null,
toolbar: null,
affix: true,
affixTopOffset: 2
},
_create: function() {
var el, widthToAdd,
_this = this;
this.toolbar = this.options.toolbar;
this.toolbar.show();
jQuery(this.options.parentElement).append(this.toolbar);
this._bindEvents();
jQuery(window).resize(function(event) {
return _this.setPosition();
});
jQuery(window).scroll(function(event) {
return _this.setPosition();
});
if (this.options.parentElement === 'body') {
el = jQuery(this.element);
widthToAdd = parseFloat(el.css('padding-left'));
widthToAdd += parseFloat(el.css('padding-right'));
widthToAdd += parseFloat(el.css('border-left-width'));
widthToAdd += parseFloat(el.css('border-right-width'));
widthToAdd += (parseFloat(el.css('outline-width'))) * 2;
widthToAdd += (parseFloat(el.css('outline-offset'))) * 2;
return jQuery(this.toolbar).css("width", el.width() + widthToAdd);
}
},
_getPosition: function(event, selection) {
var offset, position, width;
if (!event) {
return;
}
width = parseFloat(this.element.css('outline-width'));
offset = width + parseFloat(this.element.css('outline-offset'));
return position = {
top: this.element.offset().top - this.toolbar.outerHeight() - offset,
left: this.element.offset().left - offset
};
},
_getCaretPosition: function(range) {
var newRange, position, tmpSpan;
tmpSpan = jQuery("<span/>");
newRange = rangy.createRange();
newRange.setStart(range.endContainer, range.endOffset);
newRange.insertNode(tmpSpan.get(0));
position = {
top: tmpSpan.offset().top,
left: tmpSpan.offset().left
};
tmpSpan.remove();
return position;
},
setPosition: function() {
var elementBottom, elementTop, height, offset, scrollTop, topOffset;
if (this.options.parentElement !== 'body') {
return;
}
this.toolbar.css('top', this.element.offset().top - this.toolbar.outerHeight());
if (this.options.affix) {
this.toolbar.removeClass('affixed');
scrollTop = jQuery(window).scrollTop();
offset = this.element.offset();
height = this.element.height();
topOffset = this.options.affixTopOffset;
elementTop = offset.top - (this.toolbar.height() + this.options.affixTopOffset);
elementBottom = (height - topOffset) + (offset.top - this.toolbar.height());
if (scrollTop > elementTop && scrollTop < elementBottom) {
this.toolbar.addClass('affixed');
this.toolbar.css('top', this.options.affixTopOffset);
}
} else {
}
return this.toolbar;
},
_updatePosition: function(position) {},
_bindEvents: function() {
var _this = this;
this.element.on('halloactivated', function(event, data) {
_this.setPosition();
return _this.toolbar.show();
});
return this.element.on('hallodeactivated', function(event, data) {
return _this.toolbar.hide();
});
}
});
})(jQuery);

View file

@ -1,11 +1,14 @@
"use strict";
var halloPlugins = {
'halloformat': {},
'halloheadings': {formatBlocks: ["p", "h2", "h3", "h4", "h5"]},
'hallolists': {},
'hallohr': {},
'halloreundo': {},
'hallowagtaillink': {},
'hallowagtaillink': {}
};
function registerHalloPlugin(name, opts) {
halloPlugins[name] = (opts || {});
}
@ -30,7 +33,7 @@ function makeRichTextEditable(id) {
richText.hallo({
toolbar: 'halloToolbarFixed',
toolbarcssClass: 'testy',
toolbarCssClass: (input.closest('.object').hasClass('full')) ? 'full' : '',
plugins: halloPlugins
}).bind('hallomodified', function(event, data) {
input.val(data.content);
@ -57,6 +60,7 @@ function initDateChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
timepicker: false,
scrollInput:false,
format: 'Y-m-d',
i18n: {
lang: window.dateTimePickerTranslations
@ -66,6 +70,7 @@ function initDateChooser(id) {
} else {
$('#' + id).datetimepicker({
timepicker: false,
scrollInput:false,
format: 'Y-m-d',
});
}
@ -75,6 +80,7 @@ function initTimeChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
datepicker: false,
scrollInput:false,
format: 'H:i',
i18n: {
lang: window.dateTimePickerTranslations
@ -93,6 +99,7 @@ function initDateTimeChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
format: 'Y-m-d H:i',
scrollInput:false,
i18n: {
lang: window.dateTimePickerTranslations
},
@ -197,7 +204,7 @@ function InlinePanel(opts) {
self.updateMoveButtonDisabledStates = function() {
if (opts.canOrder) {
forms = self.formsUl.children('li:visible');
var forms = self.formsUl.children('li:visible');
forms.each(function(i) {
$('ul.controls .inline-child-move-up', this).toggleClass('disabled', i === 0).toggleClass('enabled', i !== 0);
$('ul.controls .inline-child-move-down', this).toggleClass('disabled', i === forms.length - 1).toggleClass('enabled', i != forms.length - 1);

View file

@ -2829,6 +2829,7 @@
} else {
}
return this.toolbar.css('left', this.element.offset().left - 2);
},
_updatePosition: function(position) {},

View file

@ -246,7 +246,7 @@
.xdsoft_calendar td.xdsoft_default,
.xdsoft_calendar td.xdsoft_current,
.xdsoft_timepicker .xdsoft_time_box > div > div.xdsoft_current{
background: $color-orange;
background: $color-salmon;
color:#fff;
font-weight: 700;
}

View file

@ -1,3 +1,9 @@
/*
These are the generic stylings for forms of any type.
If you're styling something specific to the page editing interface,
it probably ought to go in layouts/page-editor.scss
*/
form {
ul, li{
list-style-type:none;
@ -6,9 +12,6 @@ form {
margin:0;
padding:0;
}
li{
@include row();
}
}
fieldset{
@ -315,6 +318,7 @@ button.icon{
overflow:hidden;
> li{
@include row();
position:relative;
background-color:white;
padding:1em 10em 1em 1.5em; /* 10em padding leaves room for controls */
@ -435,58 +439,112 @@ li.focused > .help{
.boolean_field .help, .radio .help{
opacity:1;
}
.iconfield {
position:relative;
/*
This is expected to go on the parent of the input/select/textarea
so in most cases .input
*/
.iconfield, /* generic */
.date_field,
.time_field,
.date_time_field,
.url_field{
.input{
position:relative;
&:before, &:after{
font-family:wagtail;
position:absolute;
top:0.5em;
line-height:100%;
font-size:2em;
color:$color-grey-3;
}
&:before{
left:0.3em;
}
&:after{
right:0.5em;
}
}
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;
/* smaller fields required slight repositioning of icons */
&.field-small{
.input{
&:before, &:after{
font-size:1.5em;
top:0.3em;
}
&: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;
}
}
.fields li{
.date_field,
.date_time_field{
.input:before{
@extend .icon-date:before;
}
}
.time_field{
.input:before{
@extend .icon-time:before;
}
}
.url_field{
.input:before{
@extend .icon-link:before;
}
}
/* This is specifically for model that are a generated set of checkboxes/radios */
.model_multiple_choice_field .input li,
.choice_field .input li{
label{
display:block;
width:auto;
float:none;
}
}
.fields > li,
.field-col{
@include clearfix();
padding-top:0.5em;
padding-bottom:1.2em;
}
.field-content .input li{
label{
width:auto;
float:none;
}
.field-row{
@include clearfix();
/* negative margin the bottom so it doesn't add too much space */
margin-bottom:-1.2em;
}
.input{
clear:both;
}
/* field sizing */
/* field sizing and alignment */
.field-small{
input, textarea, select, .richtext, .tagit{
@ -697,9 +755,14 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
.file_field &{
padding-top:0;
}
}
.boolean_field &{
padding-bottom:0;
.label-above{
.field > label{
display:block;
padding:0 0 0.8em 0;
float:none;
width:auto;
}
}
@ -758,8 +821,14 @@ input[type=submit], input[type=reset], input[type=button], .button, button{
@include row();
}
.field-col{
float:left;
padding-left:0 !important;
}
.field-content{
@include column(10);
padding-right:0;
}
padding-left:0;
}
}

View file

@ -97,13 +97,6 @@ header{
}
}
/* mozilla specific hack */
@-moz-document url-prefix() {
.iconfield.icon-spinner:after{
line-height:20px;
}
}
.page-explorer header{
margin-bottom:0;
padding-bottom:0em;

View file

@ -96,7 +96,6 @@
.icon-unlocked:before {
content: "p";
}
.icon-doc-full-inverse:before {
content: "r";
}
@ -210,9 +209,9 @@
}
.icon-spinner:after{
width:1em;
animation: spin 1s infinite;
-webkit-animation: spin 1s infinite;
-moz-animation: spin 1s infinite;
animation: spin 0.5s infinite linear;
-webkit-animation: spin 0.5s infinite linear;
-moz-animation: spin 0.5s infinite linear;
content:"1";
}
.icon-pick:before{
@ -236,6 +235,9 @@
.icon-date:before{
content:"7";
}
.icon-time:before{
content:"8";
}
.icon-success:before{
content:"9";
}
@ -248,6 +250,7 @@
.icon-form:before{
content:"$";
}
.icon.text-replace{
font-size:0em;
line-height:0;

View file

@ -1,32 +1,30 @@
.tab-nav{
@include clearfix();
@include row();
padding:0;
background:$color-grey-4;
li{
list-style-type:none;
width:48%;
width:33%;
float:left;
padding:0;
position:relative;
&:before,&:after{
display:none;
}
margin-right:1px;
}
a{
@include transition(border-color 0.2s ease);
background-color:lighten($color-teal-darker, 3%);
outline:none;
line-height:3em;
text-transform:uppercase;
font-weight:700;
font-size:1.2em;
text-decoration:none;
display:block;
padding:0 20px;
padding:0.7em;
color:white;
border-top:0.3em solid lighten($color-teal-darker, 3%);
border-bottom:1px solid transparent;
max-height:1.2em;
overflow:hidden;
&:hover{
color:white;
@ -45,7 +43,6 @@
min-width:0.9em;
color:white;
background:$color-red;
content:attr(data-count);
padding:0 0.3em;
line-height:1.4em;
@ -61,10 +58,21 @@
border-top:0.3em solid $color-grey-1;
}
li.settings a{
&:before{
font-family:wagtail;
vertical-align:middle;
text-transform:none;
content:"w";
margin-right:0.5em;
font-size:1.2em;
}
}
/* For cases where tab-nav should merge with header */
&.merged{
background-color:$color-header-bg;
margin-top:0;
background-color:$color-header-bg;
}
}
.tab-content{
@ -79,14 +87,27 @@
}
@media screen and (min-width: $breakpoint-mobile){
.tab-nav li{
width:auto;
padding:0;
margin-left:0.7em;
}
.tab-nav{
/* For cases where tab-nav should merge with header */
&.merged{
background-color:$color-header-bg;
}
.tab-nav a{
padding:0 50px;
li{
width:auto;
padding:0;
margin-left:0.7em;
}
a{
padding-left:$desktop-nice-padding - 10;
padding-right:$desktop-nice-padding - 10;
}
li.settings a{
padding-left:2em;
padding-right:2em;
}
}
.modal-content .tab-nav li{

View file

@ -1,6 +1,29 @@
{
"IcoMoonType": "selection",
"icons": [
{
"icon": {
"paths": [
"M632.913 707.493l-173.647-173.649v-232.782h105.469v189.094l142.759 142.757zM512 90.125c-232.995 0-421.875 188.88-421.875 421.875s188.88 421.875 421.875 421.875 421.875-188.88 421.875-421.875-188.88-421.875-421.875-421.875zM512 828.406c-174.747 0-316.406-141.659-316.406-316.406s141.659-316.406 316.406-316.406c174.747 0 316.406 141.659 316.406 316.406s-141.659 316.406-316.406 316.406z"
],
"tags": [
"clock",
"time",
"schedule"
],
"grid": 16
},
"properties": {
"id": 72,
"order": 9,
"prevSize": 32,
"code": 56,
"name": "clock",
"ligatures": ""
},
"setIdx": 0,
"iconIdx": 72
},
{
"icon": {
"paths": [
@ -20,7 +43,7 @@
"name": "lock39copy",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 0
},
{
@ -42,7 +65,7 @@
"name": "lock39-open",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 1
},
{
@ -63,7 +86,7 @@
"name": "form",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 2
},
{
@ -82,7 +105,7 @@
"name": "uni61",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 3
},
{
@ -101,7 +124,7 @@
"name": "uni62",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 4
},
{
@ -120,7 +143,7 @@
"name": "uni63",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 5
},
{
@ -139,7 +162,7 @@
"name": "uni64",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 6
},
{
@ -158,7 +181,7 @@
"name": "uni65",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 7
},
{
@ -177,7 +200,7 @@
"name": "uni66",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 8
},
{
@ -196,7 +219,7 @@
"name": "uni67",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 9
},
{
@ -215,7 +238,7 @@
"name": "uni69",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 10
},
{
@ -234,7 +257,7 @@
"name": "uni6A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 11
},
{
@ -253,7 +276,7 @@
"name": "uni6B",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 12
},
{
@ -272,7 +295,7 @@
"name": "uni6C",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 13
},
{
@ -291,7 +314,7 @@
"name": "uni6E",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 14
},
{
@ -310,7 +333,7 @@
"name": "uni68",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 15
},
{
@ -329,7 +352,7 @@
"name": "uni6F",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 16
},
{
@ -348,7 +371,7 @@
"name": "uni71",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 17
},
{
@ -367,7 +390,7 @@
"name": "uni72",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 18
},
{
@ -386,7 +409,7 @@
"name": "uni73",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 19
},
{
@ -405,7 +428,7 @@
"name": "uni74",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 20
},
{
@ -424,7 +447,7 @@
"name": "uni75",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 21
},
{
@ -443,7 +466,7 @@
"name": "uni76",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 22
},
{
@ -462,7 +485,7 @@
"name": "uni77",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 23
},
{
@ -481,7 +504,7 @@
"name": "uni78",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 24
},
{
@ -500,7 +523,7 @@
"name": "uni7A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 25
},
{
@ -519,7 +542,7 @@
"name": "uni41",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 26
},
{
@ -538,7 +561,7 @@
"name": "uni42",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 27
},
{
@ -557,7 +580,7 @@
"name": "uni44",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 28
},
{
@ -576,7 +599,7 @@
"name": "uni43",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 29
},
{
@ -595,7 +618,7 @@
"name": "uni45",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 30
},
{
@ -614,7 +637,7 @@
"name": "uni46",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 31
},
{
@ -633,7 +656,7 @@
"name": "uni47",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 32
},
{
@ -652,7 +675,7 @@
"name": "uni48",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 33
},
{
@ -671,7 +694,7 @@
"name": "uni49",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 34
},
{
@ -690,7 +713,7 @@
"name": "uni4A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 35
},
{
@ -709,7 +732,7 @@
"name": "uni4B",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 36
},
{
@ -728,7 +751,7 @@
"name": "uni4C",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 37
},
{
@ -747,7 +770,7 @@
"name": "uni4D",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 38
},
{
@ -766,7 +789,7 @@
"name": "uni4E",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 39
},
{
@ -785,7 +808,7 @@
"name": "uni4F",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 40
},
{
@ -804,7 +827,7 @@
"name": "uni50",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 41
},
{
@ -823,7 +846,7 @@
"name": "uni51",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 42
},
{
@ -842,7 +865,7 @@
"name": "uni79",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 43
},
{
@ -861,7 +884,7 @@
"name": "uni52",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 44
},
{
@ -880,7 +903,7 @@
"name": "uni54",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 45
},
{
@ -899,7 +922,7 @@
"name": "uni57",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 46
},
{
@ -918,7 +941,7 @@
"name": "uni58",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 47
},
{
@ -937,7 +960,7 @@
"name": "uni59",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 48
},
{
@ -956,7 +979,7 @@
"name": "uni5A",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 49
},
{
@ -975,7 +998,7 @@
"name": "uni56",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 50
},
{
@ -994,7 +1017,7 @@
"name": "uni31",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 51
},
{
@ -1013,7 +1036,7 @@
"name": "uni55",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 52
},
{
@ -1032,7 +1055,7 @@
"name": "uni33",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 53
},
{
@ -1051,7 +1074,7 @@
"name": "uni32",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 54
},
{
@ -1070,7 +1093,7 @@
"name": "uni35",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 55
},
{
@ -1089,7 +1112,7 @@
"name": "uni36",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 56
},
{
@ -1108,7 +1131,7 @@
"name": "uni30",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 57
},
{
@ -1127,7 +1150,7 @@
"name": "uni3F",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 58
},
{
@ -1146,7 +1169,7 @@
"name": "uni21",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 59
},
{
@ -1165,7 +1188,7 @@
"name": "uni39",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 60
},
{
@ -1184,7 +1207,7 @@
"name": "uni53",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 61
},
{
@ -1203,7 +1226,7 @@
"name": "uni34",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 62
},
{
@ -1222,7 +1245,7 @@
"name": "uni37",
"ligatures": ""
},
"setIdx": 0,
"setIdx": 1,
"iconIdx": 63
}
],
@ -1245,13 +1268,14 @@
"baseline": 6.25,
"whitespace": 50
},
"showMetadata": false,
"showMetrics": true,
"useClassSelector": false,
"classSelector": ".icon",
"embed": false
"resetPoint": 58880
},
"imagePref": {
"color": 0,
"height": 32,
"columns": 16,
"margin": 16
},
"imagePref": {},
"historySize": 100,
"showCodes": true,
"search": "",

View file

@ -17,6 +17,7 @@
<glyph unicode="&#x35;" d="M135 424h241v-23h-241zM405 247l-127-124v222h-45v-220l-125 122-33-32 181-181 181 181z" />
<glyph unicode="&#x36;" d="M136 424h241v-23h-241zM108 122l126 124v-222h45v220l126-122 32 32-181 181-181-181z" />
<glyph unicode="&#x37;" d="M387.836 13.063h-263.672c-43.671 0-79.102 35.431-79.102 79.101v263.672c0 34.607 22.248 63.446 52.734 74.158v-34.607c0-22.248 18.127-39.551 39.551-39.551s39.551 17.303 39.551 39.551v39.551h158.203v-39.551c0-22.248 18.127-39.551 39.551-39.551s39.551 17.303 39.551 39.551v34.607c30.487-10.712 52.735-39.551 52.735-74.158v-263.672c0-43.671-35.431-79.101-79.101-79.101zM414.203 303.101h-316.406v-210.938c0-14.832 11.535-26.367 26.367-26.367h263.672c14.832 0 26.367 11.536 26.367 26.367zM308.735 171.265h52.735v-52.735h-52.735zM308.735 250.367h52.735v-52.734h-52.735zM229.633 171.265h52.734v-52.735h-52.734zM229.633 250.367h52.734v-52.734h-52.734zM150.531 171.265h52.734v-52.735h-52.734zM150.531 250.367h52.734v-52.734h-52.734zM374.652 382.203c-7.416 0-13.183 5.768-13.183 13.184v39.551h26.367v-39.551c0-7.416-5.768-13.184-13.183-13.184zM137.347 382.203c-7.416 0-13.183 5.768-13.183 13.184v39.551h26.367v-39.551c0-7.416-5.768-13.184-13.184-13.184z" />
<glyph unicode="&#x38;" d="M316.457 126.253l-86.823 86.825v116.391h52.734v-94.547l71.38-71.379zM256 434.938c-116.498 0-210.938-94.44-210.938-210.938s94.44-210.938 210.938-210.938 210.938 94.44 210.938 210.938-94.44 210.938-210.938 210.938zM256 65.797c-87.374 0-158.203 70.829-158.203 158.203s70.829 158.203 158.203 158.203c87.374 0 158.203-70.829 158.203-158.203s-70.829-158.203-158.203-158.203z" />
<glyph unicode="&#x39;" d="M256 449c-123.926 0-225-101.074-225-225s101.074-225 225-225c123.926 0 225 101.074 225 225s-101.074 225-225 225zM220.844 120.289l-102.832 103.711 39.551 39.551 63.281-64.16 135.351 135.351 39.551-39.551z" />
<glyph unicode="&#x3f;" d="M253.188 445.25c60.938 0 112.5-20.625 156.563-62.813 43.125-42.188 65.625-93.75 67.5-154.688 0-60.938-20.625-113.438-63.75-156.563-42.188-44.063-93.75-66.563-154.688-68.438-60.938 0-113.438 20.625-156.563 63.75-44.063 42.188-66.563 93.75-67.5 154.688s19.688 113.438 62.813 156.563c43.125 44.063 94.688 66.563 155.625 67.5zM252.25 89.938c9.375 0 17.813 2.813 23.438 8.438 5.625 6.563 9.375 14.063 9.375 22.5 0 10.313-1.875 17.813-8.438 24.375-5.625 5.625-14.063 8.438-23.438 8.438 0 0-0.938 0-0.938 0-9.375 0-16.875-2.813-22.5-8.438-6.563-5.625-9.375-13.125-10.313-22.5 0-9.375 2.813-16.875 9.375-23.438 5.625-5.625 13.125-9.375 22.5-9.375 0 0 0.938 0 0.938 0zM331.938 247.438c8.438 10.313 12.188 22.5 12.188 37.5 0 24.375-8.438 43.125-25.313 55.313s-38.438 17.813-64.688 17.813c-20.625 0-37.5-3.75-49.688-12.188-22.5-13.125-33.75-36.563-34.688-70.313 0 0 0-1.875 0-1.875s52.5 0 52.5 0c0 0 0 1.875 0 1.875 0 8.438 2.813 16.875 7.5 26.25 5.625 7.5 14.063 11.25 26.25 11.25 13.125 0 21.563-2.813 25.313-9.375 4.688-6.563 7.5-13.125 7.5-21.563 0-5.625-2.813-12.188-7.5-18.75-2.813-3.75-6.563-7.5-10.313-9.375 0 0-2.813-1.875-2.813-1.875-1.875-1.875-3.75-3.75-7.5-5.625-2.813-1.875-6.563-4.688-9.375-7.5-3.75-1.875-6.563-4.688-10.313-7.5s-6.563-5.625-8.438-8.438c-3.75-6.563-6.563-18.75-8.438-37.5 0 0 0-3.75 0-3.75s52.5 0 52.5 0c0 0 0 1.875 0 1.875 0 3.75 0 8.438 1.875 13.125 1.875 6.563 5.625 12.188 13.125 17.813 0 0 13.125 8.438 13.125 8.438 15 11.25 23.438 18.75 27.188 24.375z" />
<glyph unicode="&#x41;" d="M232 109l176 175c3 4 5 8 5 13s-2 9-5 13l-29 29c-4 4-8 6-13 6s-10-2-13-6l-134-133-60 60c-3 4-8 5-13 5s-9-1-13-5l-29-29c-3-4-5-8-5-13s2-9 5-13l103-102c3-4 7-6 12-6s10 2 13 6zM475 361v-274c0-23-8-42-24-58s-35-24-58-24h-274c-23 0-42 8-58 24s-24 35-24 58v274c0 23 8 42 24 58s35 24 58 24h274c23 0 42-8 58-24s24-35 24-58z" />

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -44,6 +44,7 @@ form{
}
}
label{
width:auto;
color:white;
}
input[type=submit]{
@ -83,24 +84,9 @@ form{
.field{
padding:0;
}
.iconfield:before{
.iconfield .input:before{
display:none;
}
.full label{
@include border-radius(2px);
text-transform:uppercase;
padding:2px 5px;
position:absolute;
top:0;
left:0;
margin-top:-1px;
font-size:0.7em;
z-index:1;
margin:0.2em 0;
line-height:1.5em;
font-weight:normal;
}
}
/* Special full-width, one-off fields i.e a single text or textarea input */
.full input{
@ -173,7 +159,7 @@ form{
margin:0px (-$desktop-nice-padding);
.iconfield{
&:before{
.input:before{
display:inline-block;
position: absolute;
color:$color-grey-4;

View file

@ -25,9 +25,6 @@
}
}
.objects{
background:url("#{$static-root}bg-dark-diag.svg");
}
.object{
position:relative;
overflow:hidden;
@ -80,9 +77,9 @@
> h2, &.single-field label{
-webkit-font-smoothing: auto;
background:$color-grey-3;
background:$color-salmon-light;
text-transform:uppercase;
padding:0.9em 0 0.9em 4em;
padding:0.9em 0 0.9em 4.1em;
font-size:0.95em;
margin:0 0 0.2em 0;
line-height:1.5em;
@ -92,10 +89,10 @@
left:0;
right:0;
z-index:1;
text-shadow:1px 1px 1px rgba(255,255,255,0.5);
@include box-shadow(0 0 7px 0 rgba(0,0,0,0.4));
overflow:hidden;
&:before{
text-shadow:none;
font-family:wagtail;
text-transform:none;
content:"q";
@ -108,10 +105,11 @@
line-height:1.8em;
left:0px;
width:1.7em;
opacity:0.15;
color:white;
padding:0;
margin:0;
cursor:pointer;
background-color:$color-salmon;
}
}
@ -186,6 +184,17 @@
}
}
/* special panel for the publishing fields, requires a bit more pizzazz */
&.publishing{
h2:before{
content:"7";
font-size:2.4em;
line-height:1.4em;
width:1.4em;
}
}
&.title input,
&.title textarea{
font-size:2em;
@ -235,7 +244,6 @@
top:0px;
left:0px;
width:3.3em;
background-color:$color-teal;
padding:0;
margin:0 0 0 -20px;
cursor:pointer;
@ -246,6 +254,7 @@
display:inline-block;
padding:0;
width:3.45em;
background-color:$color-salmon;
&:before{
position:relative;

View file

@ -68,6 +68,12 @@ section{
.color-grey-5{
background-color:$color-grey-5;
}
.color-salmon{
background-color:$color-salmon;
}
.color-salmon-light{
background-color:$color-salmon-light;
}
}

View file

@ -3,10 +3,13 @@
.hallotoolbar{
position:absolute;
left:$mobile-nice-padding;
z-index:5;
margin-top:4em;
margin-left:0em;
margin-left:1.2em;
}
/* full is added to hallotoolbar when invoked on a field set to the full layout style */
.hallotoolbar.full{
margin-left:$mobile-nice-padding;
}
.hallotoolbar.affixed{
position:fixed;
@ -150,7 +153,7 @@
}
@media screen and (min-width: $breakpoint-mobile){
.hallotoolbar{
left:$menu-width + $desktop-nice-padding;
.hallotoolbar.full{
margin-left:$desktop-nice-padding;
}
}

View file

@ -27,9 +27,11 @@ $breakpoint-desktop-larger: 100em; /* 1600px */
$color-teal: #43b1b0;
$color-teal-darker: darken($color-teal, 10%);
$color-teal-dark: #246060;
$color-red: #f7474e;
$color-red: #cd3238;
$color-orange:#e9b04d;
$color-green: #189370;
$color-salmon: #f37e77;
$color-salmon-light: #fcf2f2;
/* darker to lighter */
$color-grey-1: #333333;

View file

@ -1,4 +1,4 @@
{% extends "wagtailadmin/edit_handlers/field_panel_field.html" %}
{% extends "wagtailadmin/shared/field.html" %}
{% load i18n %}
{% comment %}
Either the chosen or unchosen div will be shown, depending on the presence

View file

@ -1,23 +1 @@
<div class="field">
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ input_classes }} ">
{% block form_field %}
{{ field }}
{% endblock %}
<span></span>
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>
{% include "wagtailadmin/shared/field.html" %}

View file

@ -0,0 +1,7 @@
<ul class="field-row {{ self.classes|join:" " }}">
{% for child in self.children %}
<li class="field-col {{ child.classes|join:" " }}">
{{ child.render_as_field }}
</li>
{% endfor %}
</ul>

View file

@ -2,9 +2,7 @@
<legend>{{ self.heading }}</legend>
<ul class="fields">
{% for child in self.children %}
<li {% if child.field_classnames %}class="{{ child.field_classnames }}"{% endif %}>
{{ child.render_as_field }}
</li>
<li class="{{ child.classes|join:" " }}">{{ child.render_as_field }}</li>
{% endfor %}
</ul>
</fieldset>

View file

@ -1,6 +1,6 @@
<ul class="objects">
{% for child in self.children %}
<li class="object {{ child.object_classnames }}">
<li class="object {{ child.classes|join:" " }}">
{% if child.heading %}
<h2>{{ child.heading }}</h2>
{% endif %}

View file

@ -1,6 +1,6 @@
<fieldset>
<legend>{{ self.heading }}</legend>
<ul class="fields">
<li class="{{ self.field_classnames }}">{{ field_content }}</li>
<li>{{ field_content }}</li>
</ul>
</fieldset>

View file

@ -1,12 +1,12 @@
<ul class="tab-nav merged">
{% for child in self.children %}
<li class="{% if forloop.first %}active{% endif %}"><a href="#{{ child.heading|slugify }}" class="{% if forloop.first %}active{% endif %}">{{ child.heading }}</a></li>
<li class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}"><a href="#{{ child.heading|slugify }}" class="{% if forloop.first %}active{% endif %}">{{ child.heading }}</a></li>
{% endfor %}
</ul>
<div class="tab-content">
{% for child in self.children %}
<section id="{{ child.heading|slugify }}" class="{% if forloop.first %}active{% endif %}">
<section id="{{ child.heading|slugify }}" class="{{ child.classes|join:" " }} {% if forloop.first %}active{% endif %}">
{{ child.render_as_object }}
</section>
{% endfor %}

View file

@ -28,17 +28,17 @@
<ul class="fields">
<li class="full">
<div class="field">
<div class="field iconfield">
{{ form.username.label_tag }}
<div class="input iconfield icon-user">
<div class="input icon-user">
{{ form.username }}
</div>
</div>
</li>
<li class="full">
<div class="field">
<div class="field iconfield">
{{ form.password.label_tag }}
<div class="input iconfield icon-password">
<div class="input icon-password">
{{ form.password }}
</div>
</div>

View file

@ -16,11 +16,9 @@
<script src="{{ STATIC_URL }}wagtailadmin/js/vendor/tag-it.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/expanding_formset.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/modal-workflow.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-wagtail-toolbar.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/hallo-plugins/hallo-hr.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/page-editor.js"></script>
<script src="{{ STATIC_URL }}wagtailadmin/js/page-chooser.js"></script>
<script src="{{ STATIC_URL }}admin/js/urlify.js"></script>

View file

@ -0,0 +1,25 @@
{% load wagtailadmin_tags %}
<div class="field {{ field.field_classnames }} {{ field|fieldtype }} {{ field_classes }}">
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ field.input_classnames }} {{ input_classes }} ">
{% block form_field %}
{{ field }}
{% endblock %}
{# This span only used on rare occasions by certain types of input #}
<span></span>
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error|escape }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>

View file

@ -1,25 +1,4 @@
{% load wagtailadmin_tags %}
<li class="{% if field.field.required %}required{% endif %} {{ field.css_classes }} {{ field|fieldtype }} {{ li_classes }} {% if field.errors %}error{% endif %}">
<div class="field">
{{ field.label_tag }}
<div class="field-content">
<div class="input {{ input_classes }} ">
{% block form_field %}
{{ field }}
{% endblock %}
<span></span>
</div>
{% if field.help_text %}
<p class="help">{{ field.help_text }}</p>
{% endif %}
{% if field.errors %}
<p class="error-message">
{% for error in field.errors %}
<span>{{ error|escape }}</span>
{% endfor %}
</p>
{% endif %}
</div>
</div>
<li class="{% if field.field.required %}required{% endif %} {{ wrapper_classes }} {{ li_classes }} {% if field.errors %}error{% endif %}">
{% include "wagtailadmin/shared/field.html" %}
</li>

View file

@ -8,7 +8,7 @@
<form class="col search-form" action="{% url search_url %}" method="get">
<ul class="fields">
{% for field in search_form %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field input_classes="field-small iconfield icon-search" %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field field_classes="field-small iconfield" input_classes="icon-search" %}
{% endfor %}
<li class="submit visuallyhidden"><input type="submit" value="Search" class="button" /></li>
</ul>

View file

@ -1,8 +1,11 @@
from datetime import timedelta
from django.test import TestCase
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User, Permission
from django.core import mail
from django.core.paginator import Paginator
from django.utils import timezone
from wagtail.tests.models import SimplePage, EventPage, StandardIndex, StandardChild, BusinessIndex, BusinessChild, BusinessSubIndex
from wagtail.tests.utils import unittest, WagtailTestUtils
@ -170,6 +173,61 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertIsInstance(page, SimplePage)
self.assertFalse(page.live)
def test_create_simplepage_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Find the page and check the scheduled times
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEquals(page.go_live_at.date(), go_live_at.date())
self.assertEquals(page.expire_at.date(), expire_at.date())
self.assertEquals(page.expired, False)
self.assertTrue(page.status_string, "draft")
# No revisions with approved_go_live_at
self.assertFalse(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists())
def test_create_simplepage_scheduled_go_live_before_expiry(self):
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(timezone.now() + timedelta(days=2)).split('.')[0],
'expire_at': str(timezone.now() + timedelta(days=1)).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
self.assertEqual(response.status_code, 200)
# Check that a form error was raised
self.assertFormError(response, 'form', 'go_live_at', "Go live date/time must be before expiry date/time")
self.assertFormError(response, 'form', 'expire_at', "Go live date/time must be before expiry date/time")
def test_create_simplepage_scheduled_expire_in_the_past(self):
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
self.assertEqual(response.status_code, 200)
# Check that a form error was raised
self.assertFormError(response, 'form', 'expire_at', "Expiry date/time must be in the future")
def test_create_simplepage_post_publish(self):
# Connect a mock signal handler to page_published signal
signal_fired = [False]
@ -197,10 +255,40 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertIsInstance(page, SimplePage)
self.assertTrue(page.live)
<<<<<<< HEAD
# Check that the page_published signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], page)
self.assertEqual(signal_page[0], signal_page[0].specific)
=======
def test_create_simplepage_post_publish_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "New page!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_create', args=('tests', 'simplepage', self.root_page.id)), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
# Find the page and check it
page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific
self.assertEquals(page.go_live_at.date(), go_live_at.date())
self.assertEquals(page.expire_at.date(), expire_at.date())
self.assertEquals(page.expired, False)
# A revision with approved_go_live_at should exist now
self.assertTrue(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists())
# But Page won't be live
self.assertFalse(page.live)
self.assertTrue(page.status_string, "scheduled")
>>>>>>> master
def test_create_simplepage_post_submit(self):
# Create a moderator user for testing email
@ -341,6 +429,63 @@ class TestPageEdit(TestCase, WagtailTestUtils):
child_page_new = SimplePage.objects.get(id=self.child_page.id)
self.assertTrue(child_page_new.has_unpublished_changes)
def test_edit_post_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
child_page_new = SimplePage.objects.get(id=self.child_page.id)
# The page will still be live
self.assertTrue(child_page_new.live)
# A revision with approved_go_live_at should not exist
self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
# But a revision with go_live_at and expire_at in their content json *should* exist
self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(go_live_at.date())).exists())
self.assertTrue(PageRevision.objects.filter(page=child_page_new, content_json__contains=str(expire_at.date())).exists())
def test_edit_scheduled_go_live_before_expiry(self):
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'go_live_at': str(timezone.now() + timedelta(days=2)).split('.')[0],
'expire_at': str(timezone.now() + timedelta(days=1)).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
self.assertEqual(response.status_code, 200)
# Check that a form error was raised
self.assertFormError(response, 'form', 'go_live_at', "Go live date/time must be before expiry date/time")
self.assertFormError(response, 'form', 'expire_at', "Go live date/time must be before expiry date/time")
def test_edit_scheduled_expire_in_the_past(self):
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'expire_at': str(timezone.now() + timedelta(days=-1)).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
self.assertEqual(response.status_code, 200)
# Check that a form error was raised
self.assertFormError(response, 'form', 'expire_at', "Expiry date/time must be in the future")
def test_page_edit_post_publish(self):
# Connect a mock signal handler to page_published signal
signal_fired = [False]
@ -374,6 +519,77 @@ class TestPageEdit(TestCase, WagtailTestUtils):
# The page shouldn't have "has_unpublished_changes" flag set
self.assertFalse(child_page_new.has_unpublished_changes)
def test_edit_post_publish_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
child_page_new = SimplePage.objects.get(id=self.child_page.id)
# The page should not be live anymore
self.assertFalse(child_page_new.live)
# Instead a revision with approved_go_live_at should now exist
self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
def test_edit_post_publish_now_an_already_scheduled(self):
# First let's publish a page with a go_live_at in the future
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': str(go_live_at).split('.')[0],
'expire_at': str(expire_at).split('.')[0],
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
child_page_new = SimplePage.objects.get(id=self.child_page.id)
# The page should not be live anymore
self.assertFalse(child_page_new.live)
# Instead a revision with approved_go_live_at should now exist
self.assertTrue(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
# Now, let's edit it and publish it right now
go_live_at = timezone.now()
post_data = {
'title': "I've been edited!",
'content': "Some content",
'slug': 'hello-world',
'action-publish': "Publish",
'go_live_at': "",
}
response = self.client.post(reverse('wagtailadmin_pages_edit', args=(self.child_page.id, )), post_data)
# Should be redirected to explorer page
self.assertEqual(response.status_code, 302)
child_page_new = SimplePage.objects.get(id=self.child_page.id)
# The page should be live now
self.assertTrue(child_page_new.live)
# And a revision with approved_go_live_at should not exist
self.assertFalse(PageRevision.objects.filter(page=child_page_new).exclude(approved_go_live_at__isnull=True).exists())
def test_page_edit_post_submit(self):
# Create a moderator user for testing email
moderator = User.objects.create_superuser('moderator', 'moderator@email.com', 'password')

View file

@ -5,6 +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 import timezone
from django.utils.translation import ugettext as _
from django.views.decorators.vary import vary_on_headers
@ -142,21 +143,63 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
return slug
form.fields['slug'].clean = clean_slug
# Stick another validator into the form to check that the scheduled publishing settings are set correctly
def clean():
cleaned_data = form_class.clean(form)
# Go live must be before expire
go_live_at = cleaned_data.get('go_live_at')
expire_at = cleaned_data.get('expire_at')
if go_live_at and expire_at:
if go_live_at > expire_at:
msg = _('Go live date/time must be before expiry date/time')
form._errors['go_live_at'] = form.error_class([msg])
form._errors['expire_at'] = form.error_class([msg])
del cleaned_data['go_live_at']
del cleaned_data['expire_at']
# Expire must be in the future
expire_at = cleaned_data.get('expire_at')
if expire_at and expire_at < timezone.now():
form._errors['expire_at'] = form.error_class([_('Expiry date/time must be in the future')])
del cleaned_data['expire_at']
return cleaned_data
form.clean = clean
if form.is_valid():
page = form.save(commit=False) # don't save yet, as we need treebeard to assign tree params
is_publishing = bool(request.POST.get('action-publish')) and parent_page_perms.can_publish_subpage()
is_submitting = bool(request.POST.get('action-submit'))
go_live_at = form.cleaned_data.get('go_live_at')
future_go_live = go_live_at and go_live_at > timezone.now()
approved_go_live_at = None
if is_publishing:
page.live = True
page.has_unpublished_changes = False
page.expired = False
if future_go_live:
page.live = False
# Set approved_go_live_at only if is publishing
# and the future_go_live is actually in future
approved_go_live_at = go_live_at
else:
page.live = True
else:
page.live = False
page.has_unpublished_changes = True
parent_page.add_child(instance=page) # assign tree parameters - will cause page to be saved
page.save_revision(user=request.user, submitted_for_moderation=is_submitting)
# Pass approved_go_live_at to save_revision
page.save_revision(
user=request.user,
submitted_for_moderation=is_submitting,
approved_go_live_at=approved_go_live_at
)
if is_publishing:
page_published.send(sender=page_class, instance=page)
@ -174,7 +217,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
return redirect('wagtailadmin_explore', page.get_parent().id)
else:
messages.error(request, _("The page could not be created due to errors."))
messages.error(request, _("The page could not be created due to validation errors"))
edit_handler = edit_handler_class(instance=page, form=form)
else:
signals.init_new_page.send(sender=create, page=page, parent=parent_page)
@ -217,15 +260,54 @@ def edit(request, page_id):
return slug
form.fields['slug'].clean = clean_slug
# Stick another validator into the form to check that the scheduled publishing settings are set correctly
def clean():
cleaned_data = form_class.clean(form)
# Go live must be before expire
go_live_at = cleaned_data.get('go_live_at')
expire_at = cleaned_data.get('expire_at')
if go_live_at and expire_at:
if go_live_at > expire_at:
msg = _('Go live date/time must be before expiry date/time')
form._errors['go_live_at'] = form.error_class([msg])
form._errors['expire_at'] = form.error_class([msg])
del cleaned_data['go_live_at']
del cleaned_data['expire_at']
# Expire must be in the future
expire_at = cleaned_data.get('expire_at')
if expire_at and expire_at < timezone.now():
form._errors['expire_at'] = form.error_class([_('Expiry date/time must be in the future')])
del cleaned_data['expire_at']
return cleaned_data
form.clean = clean
if form.is_valid():
is_publishing = bool(request.POST.get('action-publish')) and page_perms.can_publish()
is_submitting = bool(request.POST.get('action-submit'))
go_live_at = form.cleaned_data.get('go_live_at')
future_go_live = go_live_at and go_live_at > timezone.now()
approved_go_live_at = None
if is_publishing:
page.live = True
page.has_unpublished_changes = False
page.expired = False
if future_go_live:
page.live = False
# Set approved_go_live_at only if publishing
approved_go_live_at = go_live_at
else:
page.live = True
form.save()
page.revisions.update(submitted_for_moderation=False)
# Clear approved_go_live_at for older revisions
page.revisions.update(
submitted_for_moderation=False,
approved_go_live_at=None,
)
else:
# not publishing the page
if page.live:
@ -237,7 +319,11 @@ def edit(request, page_id):
page.has_unpublished_changes = True
form.save()
page.save_revision(user=request.user, submitted_for_moderation=is_submitting)
page.save_revision(
user=request.user,
submitted_for_moderation=is_submitting,
approved_go_live_at=approved_go_live_at
)
if is_publishing:
page_published.send(sender=page.__class__, instance=page)
@ -256,6 +342,7 @@ def edit(request, page_id):
return redirect('wagtailadmin_explore', page.get_parent().id)
else:
messages.error(request, _("The page could not be saved due to validation errors"))
edit_handler = edit_handler_class(instance=page, form=form)
errors_debug = (
repr(edit_handler.form.errors)
@ -447,6 +534,8 @@ def unpublish(request, page_id):
parent_id = page.get_parent().id
page.live = False
page.save()
# Since page is unpublished clear the approved_go_live_at of all revisions
page.revisions.update(approved_go_live_at=None)
messages.success(request, _("Page '{0}' unpublished.").format(page.title))
return redirect('wagtailadmin_explore', parent_id)
@ -549,7 +638,8 @@ def get_page_edit_handler(page_class):
if page_class not in PAGE_EDIT_HANDLERS:
PAGE_EDIT_HANDLERS[page_class] = TabbedInterface([
ObjectList(page_class.content_panels, heading='Content'),
ObjectList(page_class.promote_panels, heading='Promote')
ObjectList(page_class.promote_panels, heading='Promote'),
ObjectList(page_class.settings_panels, heading='Settings', classname="settings")
])
return PAGE_EDIT_HANDLERS[page_class]

View file

@ -0,0 +1,109 @@
import datetime
import json
from optparse import make_option
from django.core.management.base import BaseCommand
from django.utils import dateparse, timezone
from wagtail.wagtailcore.models import Page, PageRevision
def revision_date_expired(r):
expiry_str = json.loads(r.content_json).get('expire_at')
if not expiry_str:
return False
expire_at = dateparse.parse_datetime(expiry_str)
if expire_at < timezone.now():
return True
else:
return False
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option(
'--dryrun',
action='store_true',
dest='dryrun',
default=False,
help='Dry run -- don\'t change anything.'),
)
def handle(self, *args, **options):
dryrun = False
if options['dryrun']:
print "Will do a dry run."
dryrun = True
# 1. get all expired pages with live = True
expired_pages = Page.objects.filter(
live=True,
expire_at__lt=timezone.now()
)
if dryrun:
if expired_pages:
print "Expired pages to be deactivated:"
print "Expiry datetime\t\tSlug\t\tName"
print "---------------\t\t----\t\t----"
for ep in expired_pages:
print "{0}\t{1}\t{2}".format(
ep.expire_at.strftime("%Y-%m-%d %H:%M"),
ep.slug,
ep.title
)
else:
print "No expired pages to be deactivated found."
else:
expired_pages.update(expired=True, live=False)
# 2. get all page revisions for moderation that have been expired
expired_revs = [
r for r in PageRevision.objects.filter(
submitted_for_moderation=True
) if revision_date_expired(r)
]
if dryrun:
print "---------------------------------"
if expired_revs:
print "Expired revisions to be dropped from moderation queue:"
print "Expiry datetime\t\tSlug\t\tName"
print "---------------\t\t----\t\t----"
for er in expired_revs:
rev_data = json.loads(er.content_json)
print "{0}\t{1}\t{2}".format(
dateparse.parse_datetime(
rev_data.get('expire_at')
).strftime("%Y-%m-%d %H:%M"),
rev_data.get('slug'),
rev_data.get('title')
)
else:
print "No expired revision to be dropped from moderation."
else:
for er in expired_revs:
er.submitted_for_moderation = False
er.save()
# 3. get all revisions that need to be published
revs_for_publishing = PageRevision.objects.filter(
approved_go_live_at__lt=timezone.now()
)
if dryrun:
print "---------------------------------"
if revs_for_publishing:
print "Revisions to be published:"
print "Go live datetime\t\tSlug\t\tName"
print "---------------\t\t\t----\t\t----"
for rp in revs_for_publishing:
rev_data = json.loads(rp.content_json)
print "{0}\t\t{1}\t{2}".format(
rp.approved_go_live_at.strftime("%Y-%m-%d %H:%M"),
rev_data.get('slug'),
rev_data.get('title')
)
else:
print "No pages to go live."
else:
for rp in revs_for_publishing:
# just run publish for the revision -- since the approved go
# live datetime is before now it will make the page live
rp.publish()

View file

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Removing unique constraint on 'Site', fields ['hostname']
db.delete_unique(u'wagtailcore_site', ['hostname'])
# Adding unique constraint on 'Site', fields ['hostname', 'port']
db.create_unique(u'wagtailcore_site', ['hostname', 'port'])
def backwards(self, orm):
# Removing unique constraint on 'Site', fields ['hostname', 'port']
db.delete_unique(u'wagtailcore_site', ['hostname', 'port'])
# Adding unique constraint on 'Site', fields ['hostname']
db.create_unique(u'wagtailcore_site', ['hostname'])
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'wagtailcore.grouppagepermission': {
'Meta': {'object_name': 'GroupPagePermission'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': u"orm['wagtailcore.Page']"}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
u'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['auth.User']"}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'wagtailcore.pagerevision': {
'Meta': {'object_name': 'PageRevision'},
'content_json': ('django.db.models.fields.TextField', [], {}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': u"orm['wagtailcore.Page']"}),
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'wagtailcore.site': {
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"})
}
}
complete_apps = ['wagtailcore']

View file

@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'PageRevision.approved_go_live_at'
db.add_column(u'wagtailcore_pagerevision', 'approved_go_live_at',
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
keep_default=False)
# Adding field 'Page.go_live_at'
db.add_column(u'wagtailcore_page', 'go_live_at',
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
keep_default=False)
# Adding field 'Page.expire_at'
db.add_column(u'wagtailcore_page', 'expire_at',
self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True),
keep_default=False)
# Adding field 'Page.expired'
db.add_column(u'wagtailcore_page', 'expired',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting field 'PageRevision.approved_go_live_at'
db.delete_column(u'wagtailcore_pagerevision', 'approved_go_live_at')
# Deleting field 'Page.go_live_at'
db.delete_column(u'wagtailcore_page', 'go_live_at')
# Deleting field 'Page.expire_at'
db.delete_column(u'wagtailcore_page', 'expire_at')
# Deleting field 'Page.expired'
db.delete_column(u'wagtailcore_page', 'expired')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'wagtailcore.grouppagepermission': {
'Meta': {'object_name': 'GroupPagePermission'},
'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'page_permissions'", 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_permissions'", 'to': u"orm['wagtailcore.Page']"}),
'permission_type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
},
u'wagtailcore.page': {
'Meta': {'object_name': 'Page'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'pages'", 'to': u"orm['contenttypes.ContentType']"}),
'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
'expire_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'expired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'has_unpublished_changes': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'live': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'owned_pages'", 'null': 'True', 'to': u"orm['auth.User']"}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
'search_description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'seo_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'show_in_menus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'})
},
u'wagtailcore.pagerevision': {
'Meta': {'object_name': 'PageRevision'},
'approved_go_live_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'content_json': ('django.db.models.fields.TextField', [], {}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'revisions'", 'to': u"orm['wagtailcore.Page']"}),
'submitted_for_moderation': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
},
u'wagtailcore.site': {
'Meta': {'unique_together': "(('hostname', 'port'),)", 'object_name': 'Site'},
'hostname': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_default_site': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '80'}),
'root_page': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sites_rooted_here'", 'to': u"orm['wagtailcore.Page']"})
}
}
complete_apps = ['wagtailcore']

View file

@ -14,7 +14,10 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Group
from django.conf import settings
from django.template.response import TemplateResponse
from django.utils import timezone
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from django.utils.functional import cached_property
from treebeard.mp_tree import MP_Node
@ -26,29 +29,47 @@ from wagtail.wagtailsearch import Indexed, get_search_backend
class SiteManager(models.Manager):
def get_by_natural_key(self, hostname):
return self.get(hostname=hostname)
def get_by_natural_key(self, hostname, port):
return self.get(hostname=hostname, port=port)
class Site(models.Model):
hostname = models.CharField(max_length=255, unique=True, db_index=True)
hostname = models.CharField(max_length=255, db_index=True)
port = models.IntegerField(default=80, help_text=_("Set this to something other than 80 if you need a specific port number to appear in URLs (e.g. development on port 8000). Does not affect request handling (so port forwarding still works)."))
root_page = models.ForeignKey('Page', related_name='sites_rooted_here')
is_default_site = models.BooleanField(default=False, help_text=_("If true, this site will handle requests for all other hostnames that do not have a site entry of their own"))
class Meta:
unique_together = ('hostname', 'port')
def natural_key(self):
return (self.hostname,)
return (self.hostname, self.port)
def __unicode__(self):
return self.hostname + ("" if self.port == 80 else (":%d" % self.port)) + (" [default]" if self.is_default_site else "")
@staticmethod
def find_for_request(request):
"""Find the site object responsible for responding to this HTTP request object"""
"""
Find the site object responsible for responding to this HTTP
request object. Try:
- unique hostname first
- then hostname and port
- if there is no matching hostname at all, or no matching
hostname:port combination, fall back to the unique default site,
or raise an exception
NB this means that high-numbered ports on an extant hostname may
still be routed to a different hostname which is set as the default
"""
try:
hostname = request.META['HTTP_HOST'].split(':')[0]
# find a Site matching this specific hostname
return Site.objects.get(hostname=hostname)
hostname = request.META['HTTP_HOST'].split(':')[0] # KeyError here goes to the final except clause
try:
# find a Site matching this specific hostname
return Site.objects.get(hostname=hostname) # Site.DoesNotExist here goes to the final except clause
except Site.MultipleObjectsReturned:
# as there were more than one, try matching by port too
port = request.META['SERVER_PORT'] # KeyError here goes to the final except clause
return Site.objects.get(hostname=hostname, port=int(port)) # Site.DoesNotExist here goes to the final except clause
except (Site.DoesNotExist, KeyError):
# If no matching site exists, or request does not specify an HTTP_HOST (which
# will often be the case for the Django test client), look for a catch-all Site.
@ -64,6 +85,24 @@ class Site(models.Model):
else:
return 'http://%s:%d' % (self.hostname, self.port)
def clean_fields(self, exclude=None):
super(Site, self).clean_fields(exclude)
# Only one site can have the is_default_site flag set
try:
default = Site.objects.get(is_default_site=True)
except Site.DoesNotExist:
pass
except Site.MultipleObjectsReturned:
raise
else:
if self.is_default_site and self.pk != default.pk:
raise ValidationError(
{'is_default_site': [
_("%(hostname)s is already configured as the default site. You must unset that before you can save this site as default.")
% { 'hostname': default.hostname }
]}
)
# clear the wagtail_site_root_paths cache whenever Site records are updated
def save(self, *args, **kwargs):
result = super(Site, self).save(*args, **kwargs)
@ -233,6 +272,10 @@ class Page(MP_Node, ClusterableModel, Indexed):
show_in_menus = models.BooleanField(default=False, help_text=_("Whether a link to this page will appear in automatically generated menus"))
search_description = models.TextField(blank=True)
go_live_at = models.DateTimeField(verbose_name=_("Go live date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
expired = models.BooleanField(default=False, editable=False)
indexed_fields = {
'title': {
'type': 'string',
@ -327,7 +370,7 @@ class Page(MP_Node, ClusterableModel, Indexed):
SET url_path = %s || substring(url_path from %s)
WHERE path LIKE %s AND id <> %s
"""
cursor.execute(update_statement,
cursor.execute(update_statement,
[new_url_path, len(old_url_path) + 1, self.path + '%', self.id])
@cached_property
@ -373,8 +416,13 @@ class Page(MP_Node, ClusterableModel, Indexed):
else:
raise Http404
def save_revision(self, user=None, submitted_for_moderation=False):
return self.revisions.create(content_json=self.to_json(), user=user, submitted_for_moderation=submitted_for_moderation)
def save_revision(self, user=None, submitted_for_moderation=False, approved_go_live_at=None):
return self.revisions.create(
content_json=self.to_json(),
user=user,
submitted_for_moderation=submitted_for_moderation,
approved_go_live_at=approved_go_live_at,
)
def get_latest_revision(self):
return self.revisions.order_by('-created_at').first()
@ -401,8 +449,8 @@ class Page(MP_Node, ClusterableModel, Indexed):
def serve(self, request, *args, **kwargs):
return TemplateResponse(
request,
self.get_template(request, *args, **kwargs),
request,
self.get_template(request, *args, **kwargs),
self.get_context(request, *args, **kwargs)
)
@ -533,13 +581,22 @@ class Page(MP_Node, ClusterableModel, Indexed):
@property
def status_string(self):
if not self.live:
return "draft"
if self.expired:
return "expired"
elif self.approved_schedule:
return "scheduled"
else:
return "draft"
else:
if self.has_unpublished_changes:
return "live + draft"
else:
return "live"
@property
def approved_schedule(self):
return self.revisions.exclude(approved_go_live_at__isnull=True).exists()
def has_unpublished_subtree(self):
"""
An awkwardly-defined flag used in determining whether unprivileged editors have
@ -733,6 +790,7 @@ class PageRevision(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True)
content_json = models.TextField()
approved_go_live_at = models.DateTimeField(null=True, blank=True)
objects = models.Manager()
submitted_revisions = SubmittedRevisionsManager()
@ -766,7 +824,19 @@ class PageRevision(models.Model):
def publish(self):
page = self.as_page_object()
page.live = True
if page.go_live_at and page.go_live_at > timezone.now():
# if we have a go_live in the future don't make the page live
page.live = False
# Instead set the approved_go_live_at of this revision
self.approved_go_live_at = page.go_live_at
self.save()
# And clear the the approved_go_live_at of any other revisions
page.revisions.exclude(id=self.id).update(approved_go_live_at=None)
else:
page.live = True
# If page goes live clear the approved_go_live_at of all revisions
page.revisions.update(approved_go_live_at=None)
page.expired = False # When a page is published it can't be expired
page.save()
self.submitted_for_moderation = False
page.revisions.update(submitted_for_moderation=False)
@ -774,6 +844,7 @@ class PageRevision(models.Model):
def __unicode__(self):
return '"' + unicode(self.page) + '" at ' + unicode(self.created_at)
PAGE_PERMISSION_TYPE_CHOICES = [
('add', 'Add'),
('edit', 'Edit'),

View file

@ -1,11 +1,13 @@
from StringIO import StringIO
from datetime import timedelta
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 django.utils import timezone
from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.wagtailcore.models import Page, PageRevision, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
@ -87,3 +89,107 @@ class TestReplaceTextCommand(TestCase):
# Check that its now about easter
self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter")
class TestPublishScheduledPagesCommand(TestCase):
def setUp(self):
# Find root page
self.root_page = Page.objects.get(id=2)
def test_go_live_page_will_be_published(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=False,
go_live_at=timezone.now() - timedelta(days=1),
)
self.root_page.add_child(instance=page)
page.save_revision(approved_go_live_at=timezone.now() - timedelta(days=1))
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
def test_future_go_live_page_will_not_be_published(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=False,
go_live_at=timezone.now() + timedelta(days=1),
)
self.root_page.add_child(instance=page)
page.save_revision(approved_go_live_at=timezone.now() - timedelta(days=1))
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())
def test_expired_page_will_be_unpublished(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=True,
expire_at=timezone.now() - timedelta(days=1),
)
self.root_page.add_child(instance=page)
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(p.expired)
def test_future_expired_page_will_not_be_unpublished(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=True,
expire_at=timezone.now() + timedelta(days=1),
)
self.root_page.add_child(instance=page)
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
self.assertFalse(p.expired)
def test_expired_pages_are_dropped_from_mod_queue(self):
page = SimplePage(
title="Hello world!",
slug="hello-world",
live=False,
expire_at=timezone.now() - timedelta(days=1),
)
self.root_page.add_child(instance=page)
page.save_revision(submitted_for_moderation=True)
p = Page.objects.get(slug='hello-world')
self.assertFalse(p.live)
self.assertTrue(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists())
management.call_command('publish_scheduled_pages')
p = Page.objects.get(slug='hello-world')
self.assertFalse(PageRevision.objects.filter(page=p, submitted_for_moderation=True).exists())

View file

@ -9,30 +9,96 @@ from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
from wagtail.tests.models import EventPage, EventIndex, SimplePage
class TestRouting(TestCase):
class TestSiteRouting(TestCase):
fixtures = ['test.json']
def test_find_site_for_request(self):
default_site = Site.objects.get(is_default_site=True)
def setUp(self):
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)
about_page = Page.objects.get(url_path='/home/about-us/')
self.events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
self.alternate_port_events_site = Site.objects.create(hostname='events.example.com', root_page=events_page, port='8765')
self.about_site = Site.objects.create(hostname='about.example.com', root_page=about_page)
self.unrecognised_port = '8000'
self.unrecognised_hostname = 'unknown.site.com'
def test_no_host_header_routes_to_default_site(self):
# 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)
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_valid_headers_route_to_specific_site(self):
# 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)
request.META['HTTP_HOST'] = self.events_site.hostname
request.META['SERVER_PORT'] = self.events_site.port
self.assertEqual(Site.find_for_request(request), self.events_site)
def test_ports_in_request_headers_are_respected(self):
# ports in the Host: header should be respected
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.alternate_port_events_site.hostname
request.META['SERVER_PORT'] = self.alternate_port_events_site.port
self.assertEqual(Site.find_for_request(request), self.alternate_port_events_site)
def test_unrecognised_host_header_routes_to_default_site(self):
# 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)
request.META['HTTP_HOST'] = self.unrecognised_hostname
request.META['SERVER_PORT'] = '80'
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_unrecognised_port_and_default_host_routes_to_default_site(self):
# requests to the default host on an unrecognised port should be directed to the default site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.default_site.hostname
request.META['SERVER_PORT'] = self.unrecognised_port
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_unrecognised_port_and_unrecognised_host_routes_to_default_site(self):
# requests with an unrecognised Host: header _and_ an unrecognised port
# hould be directed to the default site
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.unrecognised_hostname
request.META['SERVER_PORT'] = self.unrecognised_port
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_unrecognised_port_on_known_hostname_routes_there_if_no_ambiguity(self):
# requests on an unrecognised port should be directed to the site with
# matching hostname if there is no ambiguity
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.about_site.hostname
request.META['SERVER_PORT'] = self.unrecognised_port
self.assertEqual(Site.find_for_request(request), self.about_site)
def test_unrecognised_port_on_known_hostname_routes_to_default_site_if_ambiguity(self):
# requests on an unrecognised port should be directed to the default
# site, even if their hostname (but not port) matches more than one
# other entry
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = self.events_site.hostname
request.META['SERVER_PORT'] = self.unrecognised_port
self.assertEqual(Site.find_for_request(request), self.default_site)
def test_port_in_http_host_header_is_ignored(self):
# port in the HTTP_HOST header is ignored
request = HttpRequest()
request.path = '/'
request.META['HTTP_HOST'] = "%s:%s" % (self.events_site.hostname, self.events_site.port)
request.META['SERVER_PORT'] = self.alternate_port_events_site.port
self.assertEqual(Site.find_for_request(request), self.alternate_port_events_site)
class TestRouting(TestCase):
fixtures = ['test.json']
def test_urls(self):
default_site = Site.objects.get(is_default_site=True)

View file

@ -39,7 +39,7 @@
<div class="col search-bar">
<ul class="fields row rowflush">
{% for field in select_date_form %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field input_classes="field-small iconfield icon-date" li_classes="col4" %}
{% include "wagtailadmin/shared/field_as_li.html" with field=field field_classes="field-small" li_classes="col4" %}
{% endfor %}
<li class="submit col2">
<button name="action" value="filter" class="button">{% trans 'Filter' %}</button>