From d04798daac08493d27331647b84ff4cbcd7a7f36 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 19 Jun 2015 10:51:55 +0100 Subject: [PATCH 01/92] Add file_size field to Image --- .../migrations/0007_image_file_size.py | 19 +++++++++++++++++++ wagtail/wagtailimages/models.py | 14 ++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 wagtail/wagtailimages/migrations/0007_image_file_size.py diff --git a/wagtail/wagtailimages/migrations/0007_image_file_size.py b/wagtail/wagtailimages/migrations/0007_image_file_size.py new file mode 100644 index 000000000..e21800e65 --- /dev/null +++ b/wagtail/wagtailimages/migrations/0007_image_file_size.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailimages', '0006_add_verbose_names'), + ] + + operations = [ + migrations.AddField( + model_name='image', + name='file_size', + field=models.PositiveIntegerField(editable=False, null=True), + ), + ] diff --git a/wagtail/wagtailimages/models.py b/wagtail/wagtailimages/models.py index 620de757d..749bc44ab 100644 --- a/wagtail/wagtailimages/models.py +++ b/wagtail/wagtailimages/models.py @@ -71,6 +71,20 @@ class AbstractImage(models.Model, TagSearchable): focal_point_width = models.PositiveIntegerField(null=True, blank=True) focal_point_height = models.PositiveIntegerField(null=True, blank=True) + file_size = models.PositiveIntegerField(null=True, editable=False) + + def get_file_size(self): + if self.file_size is None: + try: + self.file_size = self.file.size + except OSError: + # File doesn't exist + return + + self.save(update_fields=['file_size']) + + return self.file_size + def get_usage(self): return get_object_usage(self) From a997a130588cd0a2167b18656a17d86d6fd94534 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 19 Jun 2015 10:52:32 +0100 Subject: [PATCH 02/92] Migration for tests app --- .../migrations/0005_image_file_size.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 wagtail/tests/testapp/migrations/0005_image_file_size.py diff --git a/wagtail/tests/testapp/migrations/0005_image_file_size.py b/wagtail/tests/testapp/migrations/0005_image_file_size.py new file mode 100644 index 000000000..0e8c2a192 --- /dev/null +++ b/wagtail/tests/testapp/migrations/0005_image_file_size.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tests', '0004_streammodel_richtext'), + ] + + operations = [ + migrations.AddField( + model_name='customimagewithadminformfields', + name='file_size', + field=models.PositiveIntegerField(null=True, editable=False), + ), + migrations.AddField( + model_name='customimagewithoutadminformfields', + name='file_size', + field=models.PositiveIntegerField(null=True, editable=False), + ), + ] From 297240ce45a4d77a8fe09f882a0c89f837d6b3c5 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 19 Jun 2015 10:53:41 +0100 Subject: [PATCH 03/92] Use get_file_size() in image edit view --- wagtail/wagtailimages/views/images.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/wagtail/wagtailimages/views/images.py b/wagtail/wagtailimages/views/images.py index eaf00cc96..a7d2310dc 100644 --- a/wagtail/wagtailimages/views/images.py +++ b/wagtail/wagtailimages/views/images.py @@ -118,21 +118,11 @@ def edit(request, image_id): except NoReverseMatch: url_generator_enabled = False - # Get file size - try: - filesize = image.file.size - except OSError: - # File doesn't exist - filesize = None - messages.error(request, _("The source image file could not be found. Please change the source or delete the image.").format(image.title), buttons=[ - messages.button(reverse('wagtailimages_delete_image', args=(image.id,)), _('Delete')) - ]) - return render(request, "wagtailimages/images/edit.html", { 'image': image, 'form': form, 'url_generator_enabled': url_generator_enabled, - 'filesize': filesize, + 'filesize': image.get_file_size(), }) From 4c85c39a78be19a09c585ff30d0dd6a466f922df Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 19 Jun 2015 11:25:02 +0100 Subject: [PATCH 04/92] Give error if image file doesn't exist --- wagtail/wagtailimages/views/images.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/wagtail/wagtailimages/views/images.py b/wagtail/wagtailimages/views/images.py index a7d2310dc..81912a96b 100644 --- a/wagtail/wagtailimages/views/images.py +++ b/wagtail/wagtailimages/views/images.py @@ -1,3 +1,4 @@ +import os import json from django.shortcuts import render, redirect, get_object_or_404 @@ -118,6 +119,19 @@ def edit(request, image_id): except NoReverseMatch: url_generator_enabled = False + try: + local_path = image.file.path + except NotImplementedError: + # Image is hosted externally (eg, S3) + local_path = None + + if local_path: + # Give error if image file doesn't exist + if not os.path.isfile(local_path): + messages.error(request, _("The source image file could not be found. Please change the source or delete the image.").format(image.title), buttons=[ + messages.button(reverse('wagtailimages_delete_image', args=(image.id,)), _('Delete')) + ]) + return render(request, "wagtailimages/images/edit.html", { 'image': image, 'form': form, From 5018aa1994bf2be26596e46da4b70d1d835bba61 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 1 Jul 2015 16:14:41 +0100 Subject: [PATCH 05/92] Set file size on upload --- .../wagtailimages/tests/test_admin_views.py | 23 +++++++++++++++++++ wagtail/wagtailimages/views/images.py | 7 ++++++ wagtail/wagtailimages/views/multiple.py | 1 + 3 files changed, 31 insertions(+) diff --git a/wagtail/wagtailimages/tests/test_admin_views.py b/wagtail/wagtailimages/tests/test_admin_views.py index 8a4b954bb..cd2f9aae1 100644 --- a/wagtail/wagtailimages/tests/test_admin_views.py +++ b/wagtail/wagtailimages/tests/test_admin_views.py @@ -86,6 +86,9 @@ class TestImageAddView(TestCase, WagtailTestUtils): self.assertEqual(image.width, 640) self.assertEqual(image.height, 480) + # Test that the file_size field was set + self.assertTrue(image.file_size) + def test_add_no_file_selected(self): response = self.post({ 'title': "Test image", @@ -151,6 +154,25 @@ class TestImageEditView(TestCase, WagtailTestUtils): image = Image.objects.get(id=self.image.id) self.assertEqual(image.title, "Edited") + def test_edit_with_new_image_file(self): + file_content = get_test_image_file().file.getvalue() + + # Change the file size of the image + self.image.file_size = 100000 + self.image.save() + + response = self.post({ + 'title': "Edited", + 'file': SimpleUploadedFile('new.png', file_content), + }) + + # Should redirect back to index + self.assertRedirects(response, reverse('wagtailimages_index')) + + # Check that the image file size changed (assume it changed to the correct value) + image = Image.objects.get(id=self.image.id) + self.assertNotEqual(image.file_size, 100000) + def test_with_missing_image_file(self): self.image.file.delete(False) @@ -330,6 +352,7 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils): # Check image self.assertIn('image', response.context) self.assertEqual(response.context['image'].title, 'test.png') + self.assertTrue(response.context['image'].file_size) # Check form self.assertIn('form', response.context) diff --git a/wagtail/wagtailimages/views/images.py b/wagtail/wagtailimages/views/images.py index 81912a96b..5ae6dbb00 100644 --- a/wagtail/wagtailimages/views/images.py +++ b/wagtail/wagtailimages/views/images.py @@ -97,6 +97,10 @@ def edit(request, image_id): # which definitely isn't what we want... original_file.storage.delete(original_file.name) image.renditions.all().delete() + + # Set new image file size + image.file_size = image.file.size + form.save() # Reindex the image to make sure all tags are indexed @@ -238,6 +242,9 @@ def add(request): image = ImageModel(uploaded_by_user=request.user) form = ImageForm(request.POST, request.FILES, instance=image) if form.is_valid(): + # Set image file size + image.file_size = image.file.size + form.save() # Reindex the image to make sure all tags are indexed diff --git a/wagtail/wagtailimages/views/multiple.py b/wagtail/wagtailimages/views/multiple.py index 13142aa44..81540cec4 100644 --- a/wagtail/wagtailimages/views/multiple.py +++ b/wagtail/wagtailimages/views/multiple.py @@ -62,6 +62,7 @@ def add(request): # Save it image = form.save(commit=False) image.uploaded_by_user = request.user + image.file_size = image.file.size image.save() # Success! Send back an edit form for this image to the user From 9ad8f67d73336d6b5311437b3276673f5237ce50 Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 7 Jul 2015 16:48:42 +0100 Subject: [PATCH 06/92] fixes #696 and improves rarely-used .collapsible classname on MultiFieldPanel, which had been forgotten in previous styling --- .../static_src/wagtailadmin/js/page-editor.js | 2 +- .../scss/layouts/page-editor.scss | 31 +++++-------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/wagtail/wagtailadmin/static_src/wagtailadmin/js/page-editor.js b/wagtail/wagtailadmin/static_src/wagtailadmin/js/page-editor.js index 552260b0b..a001f88c8 100644 --- a/wagtail/wagtailadmin/static_src/wagtailadmin/js/page-editor.js +++ b/wagtail/wagtailadmin/static_src/wagtailadmin/js/page-editor.js @@ -335,7 +335,7 @@ function initCollapsibleBlocks() { $fieldset.hide(); } - $li.find('h2').click(function() { + $li.find('> h2').click(function() { if (!$li.hasClass('collapsed')) { $li.addClass('collapsed'); $fieldset.hide('slow'); diff --git a/wagtail/wagtailadmin/static_src/wagtailadmin/scss/layouts/page-editor.scss b/wagtail/wagtailadmin/static_src/wagtailadmin/scss/layouts/page-editor.scss index d5c508eba..4e602c0b0 100644 --- a/wagtail/wagtailadmin/static_src/wagtailadmin/scss/layouts/page-editor.scss +++ b/wagtail/wagtailadmin/static_src/wagtailadmin/scss/layouts/page-editor.scss @@ -293,33 +293,18 @@ min-height: 41px; h2{ - cursor: pointer; - } - h2:before{ - content: '6'; - text-shadow:none; - line-height: 40px; - padding-right: 1px; - opacity: 1; - color: #666; - background-color: transparent; - @include transition(background-color 0.2s ease, color 0.2s ease); - } - h2:hover:before{ + &:before, label:before{ + content: '6'; + cursor: pointer; + } } + &.collapsed{ - background: #fff; h2{ - @include box-shadow(none); - } - h2:before{ - content: '5'; - color: #fff; - background-color:$color-teal; - } - h2:hover:before{ - background-color:$color-teal-darker; + &:before, label:before{ + content: '5'; + } } } } From 6846a9e20d9d7a8651534664a38f3bdd125eef23 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 7 Jul 2015 17:08:36 +0100 Subject: [PATCH 07/92] Moved Elasticsearch index settings to module level --- .../wagtailsearch/backends/elasticsearch.py | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/wagtail/wagtailsearch/backends/elasticsearch.py b/wagtail/wagtailsearch/backends/elasticsearch.py index 340bed4af..92c76120f 100644 --- a/wagtail/wagtailsearch/backends/elasticsearch.py +++ b/wagtail/wagtailsearch/backends/elasticsearch.py @@ -11,6 +11,51 @@ from wagtail.wagtailsearch.backends.base import BaseSearch, BaseSearchQuery, Bas from wagtail.wagtailsearch.index import SearchField, FilterField, class_is_indexed +INDEX_SETTINGS = { + 'settings': { + 'analysis': { + 'analyzer': { + 'ngram_analyzer': { + 'type': 'custom', + 'tokenizer': 'lowercase', + 'filter': ['asciifolding', 'ngram'] + }, + 'edgengram_analyzer': { + 'type': 'custom', + 'tokenizer': 'lowercase', + 'filter': ['asciifolding', 'edgengram'] + } + }, + 'tokenizer': { + 'ngram_tokenizer': { + 'type': 'nGram', + 'min_gram': 3, + 'max_gram': 15, + }, + 'edgengram_tokenizer': { + 'type': 'edgeNGram', + 'min_gram': 2, + 'max_gram': 15, + 'side': 'front' + } + }, + 'filter': { + 'ngram': { + 'type': 'nGram', + 'min_gram': 3, + 'max_gram': 15 + }, + 'edgengram': { + 'type': 'edgeNGram', + 'min_gram': 1, + 'max_gram': 15 + } + } + } + } +} + + class ElasticSearchMapping(object): TYPE_MAP = { 'AutoField': 'integer', @@ -353,51 +398,6 @@ class ElasticSearch(BaseSearch): except NotFoundError: pass - # Settings - INDEX_SETTINGS = { - 'settings': { - 'analysis': { - 'analyzer': { - 'ngram_analyzer': { - 'type': 'custom', - 'tokenizer': 'lowercase', - 'filter': ['asciifolding', 'ngram'] - }, - 'edgengram_analyzer': { - 'type': 'custom', - 'tokenizer': 'lowercase', - 'filter': ['asciifolding', 'edgengram'] - } - }, - 'tokenizer': { - 'ngram_tokenizer': { - 'type': 'nGram', - 'min_gram': 3, - 'max_gram': 15, - }, - 'edgengram_tokenizer': { - 'type': 'edgeNGram', - 'min_gram': 2, - 'max_gram': 15, - 'side': 'front' - } - }, - 'filter': { - 'ngram': { - 'type': 'nGram', - 'min_gram': 3, - 'max_gram': 15 - }, - 'edgengram': { - 'type': 'edgeNGram', - 'min_gram': 1, - 'max_gram': 15 - } - } - } - } - } - # Create new index self.es.indices.create(self.es_index, INDEX_SETTINGS) From b17c494d75b6e5150cec90f767d12c0921903b26 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 7 Jul 2015 17:18:19 +0100 Subject: [PATCH 08/92] Added new index rebuilder API --- wagtail/wagtailsearch/backends/base.py | 3 ++ .../wagtailsearch/backends/elasticsearch.py | 53 +++++++++++++++++++ .../management/commands/update_index.py | 27 ++++++---- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/wagtail/wagtailsearch/backends/base.py b/wagtail/wagtailsearch/backends/base.py index 3a2d25dd5..0f3765406 100644 --- a/wagtail/wagtailsearch/backends/base.py +++ b/wagtail/wagtailsearch/backends/base.py @@ -176,6 +176,9 @@ class BaseSearch(object): def __init__(self, params): pass + def get_rebuilder(self): + return None + def reset_index(self): raise NotImplementedError diff --git a/wagtail/wagtailsearch/backends/elasticsearch.py b/wagtail/wagtailsearch/backends/elasticsearch.py index 92c76120f..e9215760a 100644 --- a/wagtail/wagtailsearch/backends/elasticsearch.py +++ b/wagtail/wagtailsearch/backends/elasticsearch.py @@ -352,6 +352,56 @@ class ElasticSearchResults(BaseSearchResults): return max(hit_count, 0) +class ElasticSearchIndexRebuilder(object): + def __init__(self, es, index_name): + self.es = es + self.index_name = index_name + + def start(self): + # Delete old index + try: + self.es.indices.delete(self.index_name) + except NotFoundError: + pass + + # Create new index + self.es.indices.create(self.index_name, INDEX_SETTINGS) + + def add_model(self, model): + # Get mapping + mapping = ElasticSearchMapping(model) + + # Put mapping + self.es.indices.put_mapping(index=self.index_name, doc_type=mapping.get_document_type(), body=mapping.get_mapping()) + + def add_items(self, model, obj_list): + if not class_is_indexed(model): + return + + # Get mapping + mapping = ElasticSearchMapping(model) + doc_type = mapping.get_document_type() + + # Create list of actions + actions = [] + for obj in obj_list: + # Create the action + action = { + '_index': self.index_name, + '_type': doc_type, + '_id': mapping.get_document_id(obj), + } + action.update(mapping.get_document(obj)) + actions.append(action) + + # Run the actions + bulk(self.es, actions) + + def finish(self): + # Refresh index + self.es.indices.refresh(self.index_name) + + class ElasticSearch(BaseSearch): def __init__(self, params): super(ElasticSearch, self).__init__(params) @@ -391,6 +441,9 @@ class ElasticSearch(BaseSearch): timeout=self.es_timeout, **params) + def get_rebuilder(self): + return ElasticSearchIndexRebuilder(self.es, self.es_index) + def reset_index(self): # Delete old index try: diff --git a/wagtail/wagtailsearch/management/commands/update_index.py b/wagtail/wagtailsearch/management/commands/update_index.py index c6c092906..fcc98dabe 100644 --- a/wagtail/wagtailsearch/management/commands/update_index.py +++ b/wagtail/wagtailsearch/management/commands/update_index.py @@ -22,9 +22,16 @@ class Command(BaseCommand): # Get backend backend = get_search_backend(backend_name) - # Reset the index - self.stdout.write(backend_name + ": Reseting index") - backend.reset_index() + # Get rebuilder + rebuilder = backend.get_rebuilder() + + if not rebuilder: + self.stdout.write(backend_name + ": Backend doesn't support rebuild. Skipping") + return + + # Start rebuild + self.stdout.write(backend_name + ": Starting rebuild") + rebuilder.start() for model, queryset in object_list: self.stdout.write(backend_name + ": Indexing model '%s.%s'" % ( @@ -32,15 +39,15 @@ class Command(BaseCommand): model.__name__, )) - # Add type - backend.add_type(model) + # Add model + rebuilder.add_model(model) - # Add objects - backend.add_bulk(model, queryset) + # Add items + rebuilder.add_items(model, queryset) - # Refresh index - self.stdout.write(backend_name + ": Refreshing index") - backend.refresh_index() + # Finish rebuild + self.stdout.write(backend_name + ": Finishing rebuild") + rebuilder.finish() option_list = BaseCommand.option_list + ( make_option('--backend', From 641d3145f63c3c6bb2cf25f470deffe37019ed42 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 7 Jul 2015 20:47:47 +0100 Subject: [PATCH 09/92] Added atomic index rebuilder --- .../wagtailsearch/backends/elasticsearch.py | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/wagtail/wagtailsearch/backends/elasticsearch.py b/wagtail/wagtailsearch/backends/elasticsearch.py index e9215760a..f967a0c0a 100644 --- a/wagtail/wagtailsearch/backends/elasticsearch.py +++ b/wagtail/wagtailsearch/backends/elasticsearch.py @@ -7,6 +7,8 @@ from six.moves.urllib.parse import urlparse from elasticsearch import Elasticsearch, NotFoundError from elasticsearch.helpers import bulk +from django.utils.crypto import get_random_string + from wagtail.wagtailsearch.backends.base import BaseSearch, BaseSearchQuery, BaseSearchResults from wagtail.wagtailsearch.index import SearchField, FilterField, class_is_indexed @@ -402,6 +404,47 @@ class ElasticSearchIndexRebuilder(object): self.es.indices.refresh(self.index_name) +class ElasticSearchAtomicIndexRebuilder(ElasticSearchIndexRebuilder): + def __init__(self, es, alias_name): + self.es = es + self.alias_name = alias_name + self.index_name = alias_name + '_' + get_random_string(7).lower() + + def start(self): + # Create new index + self.es.indices.create(self.index_name, INDEX_SETTINGS) + + # Make sure there isn't currently an index that clashes with alias_name + # This can happen when the atomic rebuilder is first enabled + if not self.es.indices.exists_alias(self.alias_name): + try: + self.es.indices.delete(self.alias_name) + + # An index was deleted so there couldn't have been an alias there. + # Create an alias now so the site search doesn't break + self.es.indices.put_alias(name=self.alias_name, index=self.index_name) + except NotFoundError: + pass + + def finish(self): + # Refresh index + self.es.indices.refresh(self.index_name) + + # Find index that alias currently points to + old_index = set(self.es.indices.get_alias(name=self.alias_name).keys()) - {self.index_name} + + # Update alias to point to new index + self.es.indices.put_alias(name=self.alias_name, index=self.index_name) + + # Delete old index + # es.indicies.get_alias can return multiple indicies. Delete them all + if old_index: + try: + self.es.indices.delete(','.join(old_index)) + except NotFoundError: + pass + + class ElasticSearch(BaseSearch): def __init__(self, params): super(ElasticSearch, self).__init__(params) @@ -411,6 +454,11 @@ class ElasticSearch(BaseSearch): self.es_index = params.pop('INDEX', 'wagtail') self.es_timeout = params.pop('TIMEOUT', 10) + if params.pop('ATOMIC_REBUILD', False): + self.rebuilder_class = ElasticSearchAtomicIndexRebuilder + else: + self.rebuilder_class = ElasticSearchIndexRebuilder + # If HOSTS is not set, convert URLS setting to HOSTS es_urls = params.pop('URLS', ['http://localhost:9200']) if self.es_hosts is None: @@ -442,7 +490,7 @@ class ElasticSearch(BaseSearch): **params) def get_rebuilder(self): - return ElasticSearchIndexRebuilder(self.es, self.es_index) + return self.rebuilder_class(self.es, self.es_index) def reset_index(self): # Delete old index From d7014419e208ca6d14324a77d795fc78a87109f5 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 8 Jul 2015 14:17:30 +0100 Subject: [PATCH 10/92] Use rebuilder to reset the index --- .../wagtailsearch/backends/elasticsearch.py | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/wagtail/wagtailsearch/backends/elasticsearch.py b/wagtail/wagtailsearch/backends/elasticsearch.py index f967a0c0a..134904d18 100644 --- a/wagtail/wagtailsearch/backends/elasticsearch.py +++ b/wagtail/wagtailsearch/backends/elasticsearch.py @@ -359,7 +359,7 @@ class ElasticSearchIndexRebuilder(object): self.es = es self.index_name = index_name - def start(self): + def reset_index(self): # Delete old index try: self.es.indices.delete(self.index_name) @@ -369,6 +369,10 @@ class ElasticSearchIndexRebuilder(object): # Create new index self.es.indices.create(self.index_name, INDEX_SETTINGS) + def start(self): + # Reset the index + self.reset_index() + def add_model(self, model): # Get mapping mapping = ElasticSearchMapping(model) @@ -410,6 +414,20 @@ class ElasticSearchAtomicIndexRebuilder(ElasticSearchIndexRebuilder): self.alias_name = alias_name self.index_name = alias_name + '_' + get_random_string(7).lower() + def reset_index(self): + # Delete old index using the alias + # This should delete both the alias and the index + try: + self.es.indices.delete(self.alias_name) + except NotFoundError: + pass + + # Create new index + self.es.indices.create(self.index_name, INDEX_SETTINGS) + + # Create a new alias + self.es.indices.put_alias(name=self.alias_name, index=self.index_name) + def start(self): # Create new index self.es.indices.create(self.index_name, INDEX_SETTINGS) @@ -493,14 +511,8 @@ class ElasticSearch(BaseSearch): return self.rebuilder_class(self.es, self.es_index) def reset_index(self): - # Delete old index - try: - self.es.indices.delete(self.es_index) - except NotFoundError: - pass - - # Create new index - self.es.indices.create(self.es_index, INDEX_SETTINGS) + # Use the rebuilder to reset the index + self.get_rebuilder().reset_index() def add_type(self, model): # Get mapping From f286fdf0dc986b926256bfc7a940f3cdaf08f1e4 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 8 Jul 2015 14:32:44 +0100 Subject: [PATCH 11/92] Tests for rebuilders --- .../tests/test_elasticsearch_backend.py | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/wagtail/wagtailsearch/tests/test_elasticsearch_backend.py b/wagtail/wagtailsearch/tests/test_elasticsearch_backend.py index b0d81f7c1..47cde1ccb 100644 --- a/wagtail/wagtailsearch/tests/test_elasticsearch_backend.py +++ b/wagtail/wagtailsearch/tests/test_elasticsearch_backend.py @@ -10,6 +10,8 @@ import mock from django.test import TestCase from django.db.models import Q +from wagtail.wagtailsearch.backends import get_search_backend + from wagtail.tests.search import models from .test_backends import BackendTests @@ -748,3 +750,152 @@ class TestBackendConfiguration(TestCase): self.assertEqual(backend.es_hosts[3]['port'], 443) self.assertEqual(backend.es_hosts[3]['use_ssl'], True) self.assertEqual(backend.es_hosts[3]['url_prefix'], '/hello') + + +class TestRebuilder(TestCase): + def assertDictEqual(self, a, b): + default = self.JSONSerializer().default + self.assertEqual(json.dumps(a, sort_keys=True, default=default), json.dumps(b, sort_keys=True, default=default)) + + def setUp(self): + # Import using a try-catch block to prevent crashes if the elasticsearch-py + # module is not installed + try: + from wagtail.wagtailsearch.backends.elasticsearch import ElasticSearch + from wagtail.wagtailsearch.backends.elasticsearch import ElasticSearchMapping + from elasticsearch import NotFoundError, JSONSerializer + except ImportError: + raise unittest.SkipTest("elasticsearch-py not installed") + + self.ElasticSearch = ElasticSearch + self.ElasticSearchMapping = ElasticSearchMapping + self.NotFoundError = NotFoundError + self.JSONSerializer = JSONSerializer + + self.backend = get_search_backend('elasticsearch') + self.es = self.backend.es + self.rebuilder = self.backend.get_rebuilder() + + self.backend.reset_index() + + def test_start_creates_index(self): + # First, make sure the index is deleted + try: + self.es.indices.delete(self.backend.es_index) + except self.NotFoundError: + pass + + self.assertFalse(self.es.indices.exists(self.backend.es_index)) + + # Run start + self.rebuilder.start() + + # Check the index exists + self.assertTrue(self.es.indices.exists(self.backend.es_index)) + + def test_start_deletes_existing_index(self): + # Put an alias into the index so we can check it was deleted + self.es.indices.put_alias(name='this_index_should_be_deleted', index=self.backend.es_index) + self.assertTrue(self.es.indices.exists_alias(name='this_index_should_be_deleted', index=self.backend.es_index)) + + # Run start + self.rebuilder.start() + + # The alias should be gone (proving the index was deleted and recreated) + self.assertFalse(self.es.indices.exists_alias(name='this_index_should_be_deleted', index=self.backend.es_index)) + + def test_add_model(self): + self.rebuilder.start() + + # Add model + self.rebuilder.add_model(models.SearchTest) + + # Check the mapping went into Elasticsearch correctly + mapping = self.ElasticSearchMapping(models.SearchTest) + response = self.es.indices.get_mapping(self.backend.es_index, mapping.get_document_type()) + + # Make some minor tweaks to the mapping so it matches what is in ES + # These are generally minor issues with the way Wagtail is generating the mapping that are being cleaned up by Elasticsearch + # TODO: Would be nice to fix these + expected_mapping = mapping.get_mapping() + expected_mapping['searchtests_searchtest']['properties']['pk']['store'] = True + expected_mapping['searchtests_searchtest']['properties']['live_filter'].pop('index') + expected_mapping['searchtests_searchtest']['properties']['live_filter'].pop('include_in_all') + expected_mapping['searchtests_searchtest']['properties']['published_date_filter']['format'] = 'dateOptionalTime' + expected_mapping['searchtests_searchtest']['properties']['published_date_filter'].pop('index') + + self.assertDictEqual(expected_mapping, response[self.backend.es_index]['mappings']) + + +class TestAtomicRebuilder(TestCase): + def setUp(self): + # Import using a try-catch block to prevent crashes if the elasticsearch-py + # module is not installed + try: + from wagtail.wagtailsearch.backends.elasticsearch import ElasticSearch + from wagtail.wagtailsearch.backends.elasticsearch import ElasticSearchAtomicIndexRebuilder + from elasticsearch import NotFoundError + except ImportError: + raise unittest.SkipTest("elasticsearch-py not installed") + + self.ElasticSearch = ElasticSearch + self.NotFoundError = NotFoundError + + self.backend = get_search_backend('elasticsearch') + self.backend.rebuilder_class = ElasticSearchAtomicIndexRebuilder + self.es = self.backend.es + self.rebuilder = self.backend.get_rebuilder() + + self.backend.reset_index() + + def test_start_creates_new_index(self): + # Rebuilder should make up a new index name that doesn't currently exist + self.assertFalse(self.es.indices.exists(self.rebuilder.index_name)) + + # Run start + self.rebuilder.start() + + # Check the index exists + self.assertTrue(self.es.indices.exists(self.rebuilder.index_name)) + + def test_start_doesnt_delete_current_index(self): + # Get current index name + current_index_name = list(self.es.indices.get_alias(name=self.rebuilder.alias_name).keys())[0] + + # Run start + self.rebuilder.start() + + # The index should still exist + self.assertTrue(self.es.indices.exists(current_index_name)) + + # And the alias should still point to it + self.assertTrue(self.es.indices.exists_alias(name=self.rebuilder.alias_name, index=current_index_name)) + + def test_finish_updates_alias(self): + # Run start + self.rebuilder.start() + + # Check that the alias doesn't point to new index + self.assertFalse(self.es.indices.exists_alias(name=self.rebuilder.alias_name, index=self.rebuilder.index_name)) + + # Run finish + self.rebuilder.finish() + + # Check that the alias now points to the new index + self.assertTrue(self.es.indices.exists_alias(name=self.rebuilder.alias_name, index=self.rebuilder.index_name)) + + def test_finish_deletes_old_index(self): + # Get current index name + current_index_name = list(self.es.indices.get_alias(name=self.rebuilder.alias_name).keys())[0] + + # Run start + self.rebuilder.start() + + # Index should still exist + self.assertTrue(self.es.indices.exists(current_index_name)) + + # Run finish + self.rebuilder.finish() + + # Index should be gone + self.assertFalse(self.es.indices.exists(current_index_name)) From a2f3c79b980f931eb18653ae48aa1aa9cce2778d Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Wed, 8 Jul 2015 15:59:14 +0100 Subject: [PATCH 12/92] Docs for atomic rebuild --- docs/topics/search/backends.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/topics/search/backends.rst b/docs/topics/search/backends.rst index 1e0a956ad..0214d2e21 100644 --- a/docs/topics/search/backends.rst +++ b/docs/topics/search/backends.rst @@ -41,6 +41,20 @@ The ``AUTO_UPDATE`` setting allows you to disable this on a per-index basis: If you have disabled auto update, you must run the :ref:`update_index` command on a regular basis to keep the index in sync with the database. +``ATOMIC_REBUILD`` +================== + +.. versionadded:: 1.1 + +By default (when using the Elasticsearch backend), when the ``update_index`` command is run, Wagtail deletes the index and rebuilds it from scratch. This causes the search engine to not return results until the rebuild is complete and is also risky as you can't rollback if an error occurs. + +Setting the ``ATOMIC_REBUILD`` setting to ``True`` makes Wagtail rebuild into a separate index while keep the old index active until the new one is fully built. When the rebuild is finished, the indexes are swapped atomically and the old index is deleted. + +.. warning:: Experimental feature + + This feature is currently experimental. Please use it with caution. + + ``BACKEND`` =========== From 5726d62aba4f5fd071c2bb8cafbf50e9eae5547a Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 8 May 2015 15:02:11 +0100 Subject: [PATCH 13/92] Put all wagtailimages urls into a namespace This allows the urls to be registered multiple times with different app labels See: https://docs.djangoproject.com/en/1.8/topics/http/urls/#reversing-namespaced-urls --- wagtail/wagtailimages/admin_urls.py | 30 ++++---- .../wagtailimages/chooser/chooser.html | 6 +- .../wagtailimages/chooser/results.html | 2 +- .../wagtailimages/chooser/select_format.html | 4 +- .../homepage/site_summary_images.html | 2 +- .../templates/wagtailimages/images/add.html | 2 +- .../wagtailimages/images/confirm_delete.html | 2 +- .../templates/wagtailimages/images/edit.html | 6 +- .../templates/wagtailimages/images/index.html | 4 +- .../wagtailimages/images/results.html | 6 +- .../wagtailimages/images/url_generator.html | 4 +- .../templates/wagtailimages/multiple/add.html | 6 +- .../wagtailimages/multiple/edit_form.html | 6 +- .../wagtailimages/widgets/image_chooser.html | 2 +- .../wagtailimages/tests/test_admin_views.py | 74 +++++++++---------- wagtail/wagtailimages/tests/test_models.py | 8 +- wagtail/wagtailimages/views/chooser.py | 4 +- wagtail/wagtailimages/views/images.py | 14 ++-- wagtail/wagtailimages/wagtail_hooks.py | 10 +-- 19 files changed, 96 insertions(+), 96 deletions(-) diff --git a/wagtail/wagtailimages/admin_urls.py b/wagtail/wagtailimages/admin_urls.py index a095f9431..de64fbdbf 100644 --- a/wagtail/wagtailimages/admin_urls.py +++ b/wagtail/wagtailimages/admin_urls.py @@ -4,21 +4,21 @@ from wagtail.wagtailimages.views import images, chooser, multiple urlpatterns = [ - url(r'^$', images.index, name='wagtailimages_index'), - url(r'^(\d+)/$', images.edit, name='wagtailimages_edit_image'), - url(r'^(\d+)/delete/$', images.delete, name='wagtailimages_delete_image'), - url(r'^(\d+)/generate_url/$', images.url_generator, name='wagtailimages_url_generator'), - url(r'^(\d+)/generate_url/(.*)/$', images.generate_url, name='wagtailimages_generate_url'), - url(r'^(\d+)/preview/(.*)/$', images.preview, name='wagtailimages_preview'), - url(r'^add/$', images.add, name='wagtailimages_add_image'), - url(r'^usage/(\d+)/$', images.usage, name='wagtailimages_image_usage'), + url(r'^$', images.index, name='index'), + url(r'^(\d+)/$', images.edit, name='edit_image'), + url(r'^(\d+)/delete/$', images.delete, name='delete_image'), + url(r'^(\d+)/generate_url/$', images.url_generator, name='url_generator'), + url(r'^(\d+)/generate_url/(.*)/$', images.generate_url, name='generate_url'), + url(r'^(\d+)/preview/(.*)/$', images.preview, name='preview'), + url(r'^add/$', images.add, name='add_image'), + url(r'^usage/(\d+)/$', images.usage, name='image_usage'), - url(r'^multiple/add/$', multiple.add, name='wagtailimages_add_multiple'), - url(r'^multiple/(\d+)/$', multiple.edit, name='wagtailimages_edit_multiple'), - url(r'^multiple/(\d+)/delete/$', multiple.delete, name='wagtailimages_delete_multiple'), + url(r'^multiple/add/$', multiple.add, name='add_multiple'), + url(r'^multiple/(\d+)/$', multiple.edit, name='edit_multiple'), + url(r'^multiple/(\d+)/delete/$', multiple.delete, name='delete_multiple'), - url(r'^chooser/$', chooser.chooser, name='wagtailimages_chooser'), - url(r'^chooser/(\d+)/$', chooser.image_chosen, name='wagtailimages_image_chosen'), - url(r'^chooser/upload/$', chooser.chooser_upload, name='wagtailimages_chooser_upload'), - url(r'^chooser/(\d+)/select_format/$', chooser.chooser_select_format, name='wagtailimages_chooser_select_format'), + url(r'^chooser/$', chooser.chooser, name='chooser'), + url(r'^chooser/(\d+)/$', chooser.image_chosen, name='image_chosen'), + url(r'^chooser/upload/$', chooser.chooser_upload, name='chooser_upload'), + url(r'^chooser/(\d+)/select_format/$', chooser.chooser_select_format, name='chooser_select_format'), ] diff --git a/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html b/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html index c77190f5d..e42ffe818 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html +++ b/wagtail/wagtailimages/templates/wagtailimages/chooser/chooser.html @@ -12,7 +12,7 @@
{% if uploadform %}
- + {% csrf_token %}
    {% for field in uploadform %} diff --git a/wagtail/wagtailimages/templates/wagtailimages/chooser/results.html b/wagtail/wagtailimages/templates/wagtailimages/chooser/results.html index 497211f26..ca81c94f2 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/chooser/results.html +++ b/wagtail/wagtailimages/templates/wagtailimages/chooser/results.html @@ -16,7 +16,7 @@
      {% for image in images %}
    • - +
      {% image image max-165x165 %}

      {{ image.title|ellipsistrim:60 }}

      diff --git a/wagtail/wagtailimages/templates/wagtailimages/chooser/select_format.html b/wagtail/wagtailimages/templates/wagtailimages/chooser/select_format.html index 6aed9d005..37e9bdd96 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/chooser/select_format.html +++ b/wagtail/wagtailimages/templates/wagtailimages/chooser/select_format.html @@ -8,7 +8,7 @@ {% image image max-800x600 %}
- + {% csrf_token %}
    {% for field in form %} @@ -18,4 +18,4 @@
- \ No newline at end of file + diff --git a/wagtail/wagtailimages/templates/wagtailimages/homepage/site_summary_images.html b/wagtail/wagtailimages/templates/wagtailimages/homepage/site_summary_images.html index 7414ede22..42d17020d 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/homepage/site_summary_images.html +++ b/wagtail/wagtailimages/templates/wagtailimages/homepage/site_summary_images.html @@ -1,7 +1,7 @@ {% load i18n wagtailadmin_tags %}
  • - + {% blocktrans count counter=total_images with total_images|intcomma as total %} {{ total }} Image {% plural %} diff --git a/wagtail/wagtailimages/templates/wagtailimages/images/add.html b/wagtail/wagtailimages/templates/wagtailimages/images/add.html index 81a6a2832..bdfea0f38 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/images/add.html +++ b/wagtail/wagtailimages/templates/wagtailimages/images/add.html @@ -16,7 +16,7 @@ {% include "wagtailadmin/shared/header.html" with title=add_str icon="image" %}
    -
    + {% csrf_token %}
      {% for field in form %} diff --git a/wagtail/wagtailimages/templates/wagtailimages/images/confirm_delete.html b/wagtail/wagtailimages/templates/wagtailimages/images/confirm_delete.html index ac9915dc8..8a553fd43 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/images/confirm_delete.html +++ b/wagtail/wagtailimages/templates/wagtailimages/images/confirm_delete.html @@ -14,7 +14,7 @@

    {% trans "Are you sure you want to delete this image?" %}

    - + {% csrf_token %} diff --git a/wagtail/wagtailimages/templates/wagtailimages/images/edit.html b/wagtail/wagtailimages/templates/wagtailimages/images/edit.html index 8762bb555..56b3f5c7b 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/images/edit.html +++ b/wagtail/wagtailimages/templates/wagtailimages/images/edit.html @@ -28,7 +28,7 @@
    {% if url_generator_enabled %} - {% trans "URL Generator" %} + {% trans "URL Generator" %}
    {% endif %} diff --git a/wagtail/wagtailimages/templates/wagtailimages/images/index.html b/wagtail/wagtailimages/templates/wagtailimages/images/index.html index 2a9d4bf8c..d3d2d10db 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/images/index.html +++ b/wagtail/wagtailimages/templates/wagtailimages/images/index.html @@ -7,7 +7,7 @@ {% block extra_js %} {% endcompress %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/wagtail/wagtailimages/templates/wagtailimages/multiple/add.html b/wagtail/wagtailimages/templates/wagtailimages/multiple/add.html index b9fb8e418..e52665d96 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/multiple/add.html +++ b/wagtail/wagtailimages/templates/wagtailimages/multiple/add.html @@ -18,10 +18,10 @@

    {% trans "Drag and drop images into this area to upload immediately." %}

    {{ help_text }} -

    +
    - +
    {% csrf_token %}
    @@ -72,7 +72,7 @@ {% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %} """, - urlresolvers.reverse('wagtailimages_chooser') + urlresolvers.reverse('wagtailimages:chooser') ) From b3ba30eb006b403754ae0154cd00e1c663a836ae Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 10 Jul 2015 11:47:50 +0100 Subject: [PATCH 14/92] Put all wagtaildocs URLs into a namespace --- wagtail/wagtaildocs/admin_urls.py | 16 ++--- wagtail/wagtaildocs/models.py | 2 +- .../wagtaildocs/chooser/chooser.html | 4 +- .../templates/wagtaildocs/documents/add.html | 2 +- .../wagtaildocs/documents/confirm_delete.html | 2 +- .../templates/wagtaildocs/documents/edit.html | 4 +- .../wagtaildocs/documents/index.html | 4 +- .../templates/wagtaildocs/documents/list.html | 8 +-- .../wagtaildocs/documents/results.html | 4 +- .../homepage/site_summary_documents.html | 2 +- .../wagtaildocs/widgets/document_chooser.html | 2 +- wagtail/wagtaildocs/tests.py | 66 +++++++++---------- wagtail/wagtaildocs/views/chooser.py | 2 +- wagtail/wagtaildocs/views/documents.py | 12 ++-- wagtail/wagtaildocs/wagtail_hooks.py | 6 +- 15 files changed, 68 insertions(+), 68 deletions(-) diff --git a/wagtail/wagtaildocs/admin_urls.py b/wagtail/wagtaildocs/admin_urls.py index c26826b8c..929a86861 100644 --- a/wagtail/wagtaildocs/admin_urls.py +++ b/wagtail/wagtaildocs/admin_urls.py @@ -3,13 +3,13 @@ from wagtail.wagtaildocs.views import documents, chooser urlpatterns = [ - url(r'^$', documents.index, name='wagtaildocs_index'), - url(r'^add/$', documents.add, name='wagtaildocs_add_document'), - url(r'^edit/(\d+)/$', documents.edit, name='wagtaildocs_edit_document'), - url(r'^delete/(\d+)/$', documents.delete, name='wagtaildocs_delete_document'), + url(r'^$', documents.index, name='index'), + url(r'^add/$', documents.add, name='add_document'), + url(r'^edit/(\d+)/$', documents.edit, name='edit_document'), + url(r'^delete/(\d+)/$', documents.delete, name='delete_document'), - url(r'^chooser/$', chooser.chooser, name='wagtaildocs_chooser'), - url(r'^chooser/(\d+)/$', chooser.document_chosen, name='wagtaildocs_document_chosen'), - url(r'^chooser/upload/$', chooser.chooser_upload, name='wagtaildocs_chooser_upload'), - url(r'^usage/(\d+)/$', documents.usage, name='wagtaildocs_document_usage'), + url(r'^chooser/$', chooser.chooser, name='chooser'), + url(r'^chooser/(\d+)/$', chooser.document_chosen, name='document_chosen'), + url(r'^chooser/upload/$', chooser.chooser_upload, name='chooser_upload'), + url(r'^usage/(\d+)/$', documents.usage, name='document_usage'), ] diff --git a/wagtail/wagtaildocs/models.py b/wagtail/wagtaildocs/models.py index 4d4b3939a..1bb98f180 100644 --- a/wagtail/wagtaildocs/models.py +++ b/wagtail/wagtaildocs/models.py @@ -55,7 +55,7 @@ class Document(models.Model, TagSearchable): @property def usage_url(self): - return reverse('wagtaildocs_document_usage', + return reverse('wagtaildocs:document_usage', args=(self.id,)) def is_editable_by_user(self, user): diff --git a/wagtail/wagtaildocs/templates/wagtaildocs/chooser/chooser.html b/wagtail/wagtaildocs/templates/wagtaildocs/chooser/chooser.html index a30addd4a..9359f1f9a 100644 --- a/wagtail/wagtaildocs/templates/wagtaildocs/chooser/chooser.html +++ b/wagtail/wagtaildocs/templates/wagtaildocs/chooser/chooser.html @@ -12,7 +12,7 @@
    {% if uploadform %}
    - + {% csrf_token %}
      {% for field in uploadform %} diff --git a/wagtail/wagtaildocs/templates/wagtaildocs/documents/add.html b/wagtail/wagtaildocs/templates/wagtaildocs/documents/add.html index f9a221bb3..9d8593753 100644 --- a/wagtail/wagtaildocs/templates/wagtaildocs/documents/add.html +++ b/wagtail/wagtaildocs/templates/wagtaildocs/documents/add.html @@ -16,7 +16,7 @@ {% include "wagtailadmin/shared/header.html" with title=add_str icon="doc-full-inverse" %}
      - + {% csrf_token %}
        {% for field in form %} diff --git a/wagtail/wagtaildocs/templates/wagtaildocs/documents/confirm_delete.html b/wagtail/wagtaildocs/templates/wagtaildocs/documents/confirm_delete.html index 4e4d2134e..17e297196 100644 --- a/wagtail/wagtaildocs/templates/wagtaildocs/documents/confirm_delete.html +++ b/wagtail/wagtaildocs/templates/wagtaildocs/documents/confirm_delete.html @@ -8,7 +8,7 @@

        {% trans "Are you sure you want to delete this document?" %}

        - + {% csrf_token %} diff --git a/wagtail/wagtaildocs/templates/wagtaildocs/documents/edit.html b/wagtail/wagtaildocs/templates/wagtaildocs/documents/edit.html index 124cf1993..51a99622c 100644 --- a/wagtail/wagtaildocs/templates/wagtaildocs/documents/edit.html +++ b/wagtail/wagtaildocs/templates/wagtaildocs/documents/edit.html @@ -18,7 +18,7 @@
        -
        + {% csrf_token %}
        diff --git a/wagtail/wagtaildocs/templates/wagtaildocs/documents/index.html b/wagtail/wagtaildocs/templates/wagtaildocs/documents/index.html index e634d4080..b913d72ab 100644 --- a/wagtail/wagtaildocs/templates/wagtaildocs/documents/index.html +++ b/wagtail/wagtaildocs/templates/wagtaildocs/documents/index.html @@ -5,7 +5,7 @@ {% block extra_js %} """, - urlresolvers.reverse('wagtaildocs_chooser') + urlresolvers.reverse('wagtaildocs:chooser') ) From 106f00cf743b98bcd63660671905d0fddd7496ee Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 10 Jul 2015 11:53:47 +0100 Subject: [PATCH 15/92] Put all wagtailsites URLs into a namespace --- .../wagtailsites/confirm_delete.html | 2 +- .../templates/wagtailsites/create.html | 2 +- .../templates/wagtailsites/edit.html | 4 +-- .../templates/wagtailsites/index.html | 8 +++--- wagtail/wagtailsites/tests.py | 26 +++++++++---------- wagtail/wagtailsites/urls.py | 8 +++--- wagtail/wagtailsites/views.py | 10 +++---- wagtail/wagtailsites/wagtail_hooks.py | 4 +-- 8 files changed, 32 insertions(+), 32 deletions(-) diff --git a/wagtail/wagtailsites/templates/wagtailsites/confirm_delete.html b/wagtail/wagtailsites/templates/wagtailsites/confirm_delete.html index e87ca3f97..9a2ef6d85 100644 --- a/wagtail/wagtailsites/templates/wagtailsites/confirm_delete.html +++ b/wagtail/wagtailsites/templates/wagtailsites/confirm_delete.html @@ -9,7 +9,7 @@

        {% trans "Are you sure you want to delete this site?" %}

        -
        + {% csrf_token %}
        diff --git a/wagtail/wagtailsites/templates/wagtailsites/create.html b/wagtail/wagtailsites/templates/wagtailsites/create.html index 8a94b372a..a9ac982c4 100644 --- a/wagtail/wagtailsites/templates/wagtailsites/create.html +++ b/wagtail/wagtailsites/templates/wagtailsites/create.html @@ -9,7 +9,7 @@ {% trans "Add site" as add_site_str %} {% include "wagtailadmin/shared/header.html" with title=add_site_str icon="site" %} -
        + {% csrf_token %}
          diff --git a/wagtail/wagtailsites/templates/wagtailsites/edit.html b/wagtail/wagtailsites/templates/wagtailsites/edit.html index c1b2c31e2..96e06e2b0 100644 --- a/wagtail/wagtailsites/templates/wagtailsites/edit.html +++ b/wagtail/wagtailsites/templates/wagtailsites/edit.html @@ -10,7 +10,7 @@ {% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=site.hostname icon="site" %}
          - + {% csrf_token %} diff --git a/wagtail/wagtailsites/templates/wagtailsites/index.html b/wagtail/wagtailsites/templates/wagtailsites/index.html index 891183428..585b6cb2d 100644 --- a/wagtail/wagtailsites/templates/wagtailsites/index.html +++ b/wagtail/wagtailsites/templates/wagtailsites/index.html @@ -5,7 +5,7 @@ {% trans "Sites" as sites_str %} {% if perms.site.add_site %} {% trans "Add a site" as add_a_site_str %} - {% include "wagtailadmin/shared/header.html" with title=sites_str add_link="wagtailsites_create" add_text=add_a_site_str icon="site" %} + {% include "wagtailadmin/shared/header.html" with title=sites_str add_link="wagtailsites:create" add_text=add_a_site_str icon="site" %} {% else %} {% include "wagtailadmin/shared/header.html" with title=sites_str icon="site" %} {% endif %} @@ -18,9 +18,9 @@ {% trans "Site" %} {% if ordering == "site" %} - + {% else %} - + {% endif %} {% trans "Port" %} @@ -33,7 +33,7 @@

          - {{ site.hostname }} + {{ site.hostname }}

          {{ site.port }} diff --git a/wagtail/wagtailsites/tests.py b/wagtail/wagtailsites/tests.py index 13bfcdf0d..aa9366b7e 100644 --- a/wagtail/wagtailsites/tests.py +++ b/wagtail/wagtailsites/tests.py @@ -13,7 +13,7 @@ class TestSiteIndexView(TestCase, WagtailTestUtils): self.home_page = Page.objects.get(id=2) def get(self, params={}): - return self.client.get(reverse('wagtailsites_index'), params) + return self.client.get(reverse('wagtailsites:index'), params) def test_simple(self): response = self.get() @@ -34,10 +34,10 @@ class TestSiteCreateView(TestCase, WagtailTestUtils): self.localhost = Site.objects.all()[0] def get(self, params={}): - return self.client.get(reverse('wagtailsites_create'), params) + return self.client.get(reverse('wagtailsites:create'), params) def post(self, post_data={}): - return self.client.post(reverse('wagtailsites_create'), post_data) + return self.client.post(reverse('wagtailsites:create'), post_data) def create_site(self, hostname='testsite', port=80, is_default_site=False, root_page=None): root_page = root_page or self.home_page @@ -67,7 +67,7 @@ class TestSiteCreateView(TestCase, WagtailTestUtils): }) # Should redirect back to index - self.assertRedirects(response, reverse('wagtailsites_index')) + self.assertRedirects(response, reverse('wagtailsites:index')) # Check that the site was created self.assertEqual(Site.objects.filter(hostname='testsite').count(), 1) @@ -96,7 +96,7 @@ class TestSiteCreateView(TestCase, WagtailTestUtils): }) # Should redirect back to index - self.assertRedirects(response, reverse('wagtailsites_index')) + self.assertRedirects(response, reverse('wagtailsites:index')) # Check that the site was created self.assertEqual(Site.objects.filter(hostname='localhost').count(), 2) @@ -126,7 +126,7 @@ class TestSiteEditView(TestCase, WagtailTestUtils): self.localhost = Site.objects.all()[0] def get(self, params={}, site_id=None): - return self.client.get(reverse('wagtailsites_edit', args=(site_id or self.localhost.id, )), params) + return self.client.get(reverse('wagtailsites:edit', args=(site_id or self.localhost.id, )), params) def post(self, post_data={}, site_id=None): site_id = site_id or self.localhost.id @@ -143,7 +143,7 @@ class TestSiteEditView(TestCase, WagtailTestUtils): post_data['is_default_site'] = 'on' elif site.is_default_site: post_data['is_default_site'] = 'on' - return self.client.post(reverse('wagtailsites_edit', args=(site_id,)), post_data) + return self.client.post(reverse('wagtailsites:edit', args=(site_id,)), post_data) def test_simple(self): response = self.get() @@ -160,7 +160,7 @@ class TestSiteEditView(TestCase, WagtailTestUtils): }) # Should redirect back to index - self.assertRedirects(response, reverse('wagtailsites_index')) + self.assertRedirects(response, reverse('wagtailsites:index')) # Check that the site was edited self.assertEqual(Site.objects.get(id=self.localhost.id).hostname, edited_hostname) @@ -182,7 +182,7 @@ class TestSiteEditView(TestCase, WagtailTestUtils): ) # Should redirect back to index - self.assertRedirects(response, reverse('wagtailsites_index')) + self.assertRedirects(response, reverse('wagtailsites:index')) # Check that the site is no longer default self.assertEqual(Site.objects.get(id=self.localhost.id).is_default_site, False) @@ -195,7 +195,7 @@ class TestSiteEditView(TestCase, WagtailTestUtils): ) # Should redirect back to index - self.assertRedirects(response, reverse('wagtailsites_index')) + self.assertRedirects(response, reverse('wagtailsites:index')) # Check that the second site is now set as default self.assertEqual(Site.objects.get(id=second_site.id).is_default_site, True) @@ -228,10 +228,10 @@ class TestSiteDeleteView(TestCase, WagtailTestUtils): self.localhost = Site.objects.all()[0] def get(self, params={}, site_id=None): - return self.client.get(reverse('wagtailsites_delete', args=(site_id or self.localhost.id, )), params) + return self.client.get(reverse('wagtailsites:delete', args=(site_id or self.localhost.id, )), params) def post(self, post_data={}, site_id=None): - return self.client.post(reverse('wagtailsites_delete', args=(site_id or self.localhost.id, )), post_data) + return self.client.post(reverse('wagtailsites:delete', args=(site_id or self.localhost.id, )), post_data) def test_simple(self): response = self.get() @@ -247,7 +247,7 @@ class TestSiteDeleteView(TestCase, WagtailTestUtils): }) # Should redirect back to index - self.assertRedirects(response, reverse('wagtailsites_index')) + self.assertRedirects(response, reverse('wagtailsites:index')) # Check that the site was edited with self.assertRaises(Site.DoesNotExist): diff --git a/wagtail/wagtailsites/urls.py b/wagtail/wagtailsites/urls.py index 96e74a0b2..bb993a0d7 100644 --- a/wagtail/wagtailsites/urls.py +++ b/wagtail/wagtailsites/urls.py @@ -3,9 +3,9 @@ from wagtail.wagtailsites import views urlpatterns = [ - url(r'^$', views.index, name='wagtailsites_index'), - url(r'^new/$', views.create, name='wagtailsites_create'), - url(r'^(\d+)/$', views.edit, name='wagtailsites_edit'), - url(r'^(\d+)/delete/$', views.delete, name='wagtailsites_delete'), + url(r'^$', views.index, name='index'), + url(r'^new/$', views.create, name='create'), + url(r'^(\d+)/$', views.edit, name='edit'), + url(r'^(\d+)/delete/$', views.delete, name='delete'), ] diff --git a/wagtail/wagtailsites/views.py b/wagtail/wagtailsites/views.py index d7c6b9071..8d98ad431 100644 --- a/wagtail/wagtailsites/views.py +++ b/wagtail/wagtailsites/views.py @@ -29,9 +29,9 @@ def create(request): if form.is_valid(): site = form.save() messages.success(request, _("Site '{0}' created.").format(site.hostname), buttons=[ - messages.button(reverse('wagtailsites_edit', args=(site.id,)), _('Edit')) + messages.button(reverse('wagtailsites:edit', args=(site.id,)), _('Edit')) ]) - return redirect('wagtailsites_index') + return redirect('wagtailsites:index') else: messages.error(request, _("The site could not be created due to errors.")) else: @@ -51,9 +51,9 @@ def edit(request, site_id): if form.is_valid(): site = form.save() messages.success(request, _("Site '{0}' updated.").format(site.hostname), buttons=[ - messages.button(reverse('wagtailsites_edit', args=(site.id,)), _('Edit')) + messages.button(reverse('wagtailsites:edit', args=(site.id,)), _('Edit')) ]) - return redirect('wagtailsites_index') + return redirect('wagtailsites:index') else: messages.error(request, _("The site could not be saved due to errors.")) else: @@ -72,7 +72,7 @@ def delete(request, site_id): if request.POST: site.delete() messages.success(request, _("Site '{0}' deleted.").format(site.hostname)) - return redirect('wagtailsites_index') + return redirect('wagtailsites:index') return render(request, "wagtailsites/confirm_delete.html", { 'site': site, diff --git a/wagtail/wagtailsites/wagtail_hooks.py b/wagtail/wagtailsites/wagtail_hooks.py index 0b238f7ff..410ba0c51 100644 --- a/wagtail/wagtailsites/wagtail_hooks.py +++ b/wagtail/wagtailsites/wagtail_hooks.py @@ -11,7 +11,7 @@ from wagtail.wagtailsites import urls @hooks.register('register_admin_urls') def register_admin_urls(): return [ - url(r'^sites/', include(urls)), + url(r'^sites/', include(urls, namespace='wagtailsites')), ] @@ -21,4 +21,4 @@ class SitesMenuItem(MenuItem): @hooks.register('register_settings_menu_item') def register_sites_menu_item(): - return SitesMenuItem(_('Sites'), urlresolvers.reverse('wagtailsites_index'), classnames='icon icon-site', order=602) + return SitesMenuItem(_('Sites'), urlresolvers.reverse('wagtailsites:index'), classnames='icon icon-site', order=602) From 392aaff70a4b6776575ccf27e6d871f364944930 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 10 Jul 2015 11:58:18 +0100 Subject: [PATCH 16/92] Put all wagtailusers_users URLs into a namespace --- .../templates/wagtailusers/users/create.html | 2 +- .../templates/wagtailusers/users/edit.html | 2 +- .../templates/wagtailusers/users/index.html | 4 ++-- .../templates/wagtailusers/users/list.html | 10 +++++----- .../templates/wagtailusers/users/results.html | 2 +- wagtail/wagtailusers/tests.py | 14 +++++++------- wagtail/wagtailusers/urls/users.py | 6 +++--- wagtail/wagtailusers/views/users.py | 8 ++++---- wagtail/wagtailusers/wagtail_hooks.py | 4 ++-- 9 files changed, 26 insertions(+), 26 deletions(-) diff --git a/wagtail/wagtailusers/templates/wagtailusers/users/create.html b/wagtail/wagtailusers/templates/wagtailusers/users/create.html index 93ed45c70..ce0bcfd5b 100644 --- a/wagtail/wagtailusers/templates/wagtailusers/users/create.html +++ b/wagtail/wagtailusers/templates/wagtailusers/users/create.html @@ -13,7 +13,7 @@
        • {% trans "Roles" %}
        - +
        {% csrf_token %}
        diff --git a/wagtail/wagtailusers/templates/wagtailusers/users/edit.html b/wagtail/wagtailusers/templates/wagtailusers/users/edit.html index 78f54c7b4..d315b8796 100644 --- a/wagtail/wagtailusers/templates/wagtailusers/users/edit.html +++ b/wagtail/wagtailusers/templates/wagtailusers/users/edit.html @@ -13,7 +13,7 @@
      • {% trans "Roles" %}
      - +
      {% csrf_token %} diff --git a/wagtail/wagtailusers/templates/wagtailusers/users/index.html b/wagtail/wagtailusers/templates/wagtailusers/users/index.html index 67589aaaf..ea37f86d1 100644 --- a/wagtail/wagtailusers/templates/wagtailusers/users/index.html +++ b/wagtail/wagtailusers/templates/wagtailusers/users/index.html @@ -6,7 +6,7 @@ {% block extra_js %} ') def test_editor_css_and_js_hooks_on_edit(self): - response = self.client.get(reverse('wagtailadmin_pages_edit', args=(self.homepage.id, ))) + response = self.client.get(reverse('wagtailadmin_pages:edit', args=(self.homepage.id, ))) self.assertEqual(response.status_code, 200) self.assertContains(response, '') self.assertContains(response, '') diff --git a/wagtail/wagtailadmin/urls.py b/wagtail/wagtailadmin/urls.py index b16d9db67..965256ccd 100644 --- a/wagtail/wagtailadmin/urls.py +++ b/wagtail/wagtailadmin/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.conf.urls import url, include from django.contrib.auth.decorators import permission_required from django.contrib.auth import views as django_auth_views from django.views.decorators.cache import cache_control @@ -16,42 +16,46 @@ urlpatterns = [ url(r'^explorer-nav/$', pages.explorer_nav, name='wagtailadmin_explorer_nav'), + # TODO: Move into wagtailadmin_pages namespace url(r'^pages/$', pages.index, name='wagtailadmin_explore_root'), url(r'^pages/(\d+)/$', pages.index, name='wagtailadmin_explore'), - url(r'^pages/new/(\w+)/(\w+)/(\d+)/$', pages.create, name='wagtailadmin_pages_create'), - url(r'^pages/new/(\w+)/(\w+)/(\d+)/preview/$', pages.preview_on_create, name='wagtailadmin_pages_preview_on_create'), - url(r'^pages/usage/(\w+)/(\w+)/$', pages.content_type_use, name='wagtailadmin_pages_type_use'), + url(r'^pages/', include([ + url(r'^new/(\w+)/(\w+)/(\d+)/$', pages.create, name='create'), + url(r'^new/(\w+)/(\w+)/(\d+)/preview/$', pages.preview_on_create, name='preview_on_create'), + url(r'^usage/(\w+)/(\w+)/$', pages.content_type_use, name='type_use'), - url(r'^pages/(\d+)/edit/$', pages.edit, name='wagtailadmin_pages_edit'), - url(r'^pages/(\d+)/edit/preview/$', pages.preview_on_edit, name='wagtailadmin_pages_preview_on_edit'), + url(r'^(\d+)/edit/$', pages.edit, name='edit'), + url(r'^(\d+)/edit/preview/$', pages.preview_on_edit, name='preview_on_edit'), - url(r'^pages/preview/$', pages.preview, name='wagtailadmin_pages_preview'), - url(r'^pages/preview_loading/$', pages.preview_loading, name='wagtailadmin_pages_preview_loading'), + url(r'^preview/$', pages.preview, name='preview'), + url(r'^preview_loading/$', pages.preview_loading, name='preview_loading'), - url(r'^pages/(\d+)/view_draft/$', pages.view_draft, name='wagtailadmin_pages_view_draft'), - url(r'^pages/(\d+)/add_subpage/$', pages.add_subpage, name='wagtailadmin_pages_add_subpage'), - url(r'^pages/(\d+)/delete/$', pages.delete, name='wagtailadmin_pages_delete'), - url(r'^pages/(\d+)/unpublish/$', pages.unpublish, name='wagtailadmin_pages_unpublish'), + url(r'^(\d+)/view_draft/$', pages.view_draft, name='view_draft'), + url(r'^(\d+)/add_subpage/$', pages.add_subpage, name='add_subpage'), + url(r'^(\d+)/delete/$', pages.delete, name='delete'), + url(r'^(\d+)/unpublish/$', pages.unpublish, name='unpublish'), - url(r'^pages/search/$', pages.search, name='wagtailadmin_pages_search'), + url(r'^search/$', pages.search, name='search'), - url(r'^pages/(\d+)/move/$', pages.move_choose_destination, name='wagtailadmin_pages_move'), - url(r'^pages/(\d+)/move/(\d+)/$', pages.move_choose_destination, name='wagtailadmin_pages_move_choose_destination'), - url(r'^pages/(\d+)/move/(\d+)/confirm/$', pages.move_confirm, name='wagtailadmin_pages_move_confirm'), - url(r'^pages/(\d+)/set_position/$', pages.set_page_position, name='wagtailadmin_pages_set_page_position'), + url(r'^(\d+)/move/$', pages.move_choose_destination, name='move'), + url(r'^(\d+)/move/(\d+)/$', pages.move_choose_destination, name='move_choose_destination'), + url(r'^(\d+)/move/(\d+)/confirm/$', pages.move_confirm, name='move_confirm'), + url(r'^(\d+)/set_position/$', pages.set_page_position, name='set_page_position'), - url(r'^pages/(\d+)/copy/$', pages.copy, name='wagtailadmin_pages_copy'), + url(r'^(\d+)/copy/$', pages.copy, name='copy'), - url(r'^pages/moderation/(\d+)/approve/$', pages.approve_moderation, name='wagtailadmin_pages_approve_moderation'), - url(r'^pages/moderation/(\d+)/reject/$', pages.reject_moderation, name='wagtailadmin_pages_reject_moderation'), - url(r'^pages/moderation/(\d+)/preview/$', pages.preview_for_moderation, name='wagtailadmin_pages_preview_for_moderation'), + url(r'^moderation/(\d+)/approve/$', pages.approve_moderation, name='approve_moderation'), + url(r'^moderation/(\d+)/reject/$', pages.reject_moderation, name='reject_moderation'), + url(r'^moderation/(\d+)/preview/$', pages.preview_for_moderation, name='preview_for_moderation'), - url(r'^pages/(\d+)/privacy/$', page_privacy.set_privacy, name='wagtailadmin_pages_set_privacy'), + url(r'^(\d+)/privacy/$', page_privacy.set_privacy, name='set_privacy'), - url(r'^pages/(\d+)/lock/$', pages.lock, name='wagtailadmin_pages_lock'), - url(r'^pages/(\d+)/unlock/$', pages.unlock, name='wagtailadmin_pages_unlock'), + url(r'^(\d+)/lock/$', pages.lock, name='lock'), + url(r'^(\d+)/unlock/$', pages.unlock, name='unlock'), + ], namespace='wagtailadmin_pages')), + # TODO: Move into wagtailadmin_pages namespace url(r'^choose-page/$', chooser.browse, name='wagtailadmin_choose_page'), url(r'^choose-page/(\d+)/$', chooser.browse, name='wagtailadmin_choose_page_child'), url(r'^choose-page/search/$', chooser.search, name='wagtailadmin_choose_page_search'), diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 04720618d..12edac7f9 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -79,7 +79,7 @@ def add_subpage(request, parent_page_id): # Only one page type is available - redirect straight to the create form rather than # making the user choose content_type = page_types[0] - return redirect('wagtailadmin_pages_create', content_type.app_label, content_type.model, parent_page.id) + return redirect('wagtailadmin_pages:create', content_type.app_label, content_type.model, parent_page.id) return render(request, 'wagtailadmin/pages/add_subpage.html', { 'parent_page': parent_page, @@ -176,12 +176,12 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ if is_publishing: messages.success(request, _("Page '{0}' created and published.").format(page.title), buttons=[ messages.button(page.url, _('View live')), - messages.button(reverse('wagtailadmin_pages_edit', args=(page.id,)), _('Edit')) + messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit')) ]) elif is_submitting: messages.success(request, _("Page '{0}' created and submitted for moderation.").format(page.title), buttons=[ - messages.button(reverse('wagtailadmin_pages_view_draft', args=(page.id,)), _('View draft')), - messages.button(reverse('wagtailadmin_pages_edit', args=(page.id,)), _('Edit')) + messages.button(reverse('wagtailadmin_pages:view_draft', args=(page.id,)), _('View draft')), + messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit')) ]) send_notification(page.get_latest_revision().id, 'submitted', request.user.id) else: @@ -197,7 +197,7 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_ return redirect('wagtailadmin_explore', page.get_parent().id) else: # Just saving - remain on edit page for further edits - return redirect('wagtailadmin_pages_edit', page.id) + return redirect('wagtailadmin_pages:edit', page.id) else: messages.error(request, _("The page could not be created due to validation errors")) edit_handler = edit_handler_class(instance=page, form=form) @@ -256,12 +256,12 @@ def edit(request, page_id): if is_publishing: messages.success(request, _("Page '{0}' published.").format(page.title), buttons=[ messages.button(page.url, _('View live')), - messages.button(reverse('wagtailadmin_pages_edit', args=(page_id,)), _('Edit')) + messages.button(reverse('wagtailadmin_pages:edit', args=(page_id,)), _('Edit')) ]) elif is_submitting: messages.success(request, _("Page '{0}' submitted for moderation.").format(page.title), buttons=[ - messages.button(reverse('wagtailadmin_pages_view_draft', args=(page_id,)), _('View draft')), - messages.button(reverse('wagtailadmin_pages_edit', args=(page_id,)), _('Edit')) + messages.button(reverse('wagtailadmin_pages:view_draft', args=(page_id,)), _('View draft')), + messages.button(reverse('wagtailadmin_pages:edit', args=(page_id,)), _('Edit')) ]) send_notification(page.get_latest_revision().id, 'submitted', request.user.id) else: @@ -277,7 +277,7 @@ def edit(request, page_id): return redirect('wagtailadmin_explore', page.get_parent().id) else: # Just saving - remain on edit page for further edits - return redirect('wagtailadmin_pages_edit', page.id) + return redirect('wagtailadmin_pages:edit', page.id) else: if page.locked: messages.error(request, _("The page could not be saved as it is locked")) @@ -494,7 +494,7 @@ def unpublish(request, page_id): page.unpublish() messages.success(request, _("Page '{0}' unpublished.").format(page.title), buttons=[ - messages.button(reverse('wagtailadmin_pages_edit', args=(page.id,)), _('Edit')) + messages.button(reverse('wagtailadmin_pages:edit', args=(page.id,)), _('Edit')) ]) return redirect('wagtailadmin_explore', page.get_parent().id) @@ -546,7 +546,7 @@ def move_confirm(request, page_to_move_id, destination_id): page_to_move.move(destination, pos='last-child') messages.success(request, _("Page '{0}' moved.").format(page_to_move.title), buttons=[ - messages.button(reverse('wagtailadmin_pages_edit', args=(page_to_move.id,)), _('Edit')) + messages.button(reverse('wagtailadmin_pages:edit', args=(page_to_move.id,)), _('Edit')) ]) return redirect('wagtailadmin_explore', destination.id) @@ -733,7 +733,7 @@ def approve_moderation(request, revision_id): revision.approve_moderation() messages.success(request, _("Page '{0}' published.").format(revision.page.title), buttons=[ messages.button(revision.page.url, _('View live')), - messages.button(reverse('wagtailadmin_pages_edit', args=(revision.page.id,)), _('Edit')) + messages.button(reverse('wagtailadmin_pages:edit', args=(revision.page.id,)), _('Edit')) ]) send_notification(revision.id, 'approved', request.user.id) @@ -752,7 +752,7 @@ def reject_moderation(request, revision_id): if request.method == 'POST': revision.reject_moderation() messages.success(request, _("Page '{0}' rejected for publication.").format(revision.page.title), buttons=[ - messages.button(reverse('wagtailadmin_pages_edit', args=(revision.page.id,)), _('Edit')) + messages.button(reverse('wagtailadmin_pages:edit', args=(revision.page.id,)), _('Edit')) ]) send_notification(revision.id, 'rejected', request.user.id) diff --git a/wagtail/wagtaildocs/templates/wagtaildocs/documents/usage.html b/wagtail/wagtaildocs/templates/wagtaildocs/documents/usage.html index 497512c0e..20a5502f5 100644 --- a/wagtail/wagtaildocs/templates/wagtaildocs/documents/usage.html +++ b/wagtail/wagtaildocs/templates/wagtaildocs/documents/usage.html @@ -23,7 +23,7 @@ {% for page in used_by %} -

      {{ page.title }}

      +

      {{ page.title }}

      {% if page.get_parent %} diff --git a/wagtail/wagtailforms/templates/wagtailforms/list_forms.html b/wagtail/wagtailforms/templates/wagtailforms/list_forms.html index d09ca7151..9ce794ee0 100644 --- a/wagtail/wagtailforms/templates/wagtailforms/list_forms.html +++ b/wagtail/wagtailforms/templates/wagtailforms/list_forms.html @@ -15,7 +15,7 @@

      {{ fp|capfirst }}

      - {{ fp.content_type.name |capfirst }} ({{ fp.content_type.app_label }}.{{ fp.content_type.model }}) + {{ fp.content_type.name |capfirst }} ({{ fp.content_type.app_label }}.{{ fp.content_type.model }}) {% endfor %} diff --git a/wagtail/wagtailimages/templates/wagtailimages/images/usage.html b/wagtail/wagtailimages/templates/wagtailimages/images/usage.html index e8d12f194..2fba022d2 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/images/usage.html +++ b/wagtail/wagtailimages/templates/wagtailimages/images/usage.html @@ -23,7 +23,7 @@ {% for page in used_by %} -

      {{ page.title }}

      +

      {{ page.title }}

      {% if page.get_parent %} diff --git a/wagtail/wagtailredirects/templates/wagtailredirects/list.html b/wagtail/wagtailredirects/templates/wagtailredirects/list.html index 121e248b5..c8f0afcf7 100644 --- a/wagtail/wagtailredirects/templates/wagtailredirects/list.html +++ b/wagtail/wagtailredirects/templates/wagtailredirects/list.html @@ -26,7 +26,7 @@ {% if redirect.redirect_page %} - {{ redirect.redirect_page.title }} + {{ redirect.redirect_page.title }} {% else %} {{ redirect.link }} {% endif %} diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/list.html b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/list.html index 07fbb6589..59953db6f 100644 --- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/list.html +++ b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/list.html @@ -18,7 +18,7 @@ {% for editors_pick in query.editors_picks.all %} - {{ editors_pick.page.title }}{% if not forloop.last %}, {% endif %} + {{ editors_pick.page.title }}{% if not forloop.last %}, {% endif %} {% empty %} {% trans "None" %} {% endfor %} diff --git a/wagtail/wagtailsnippets/templates/wagtailsnippets/snippets/usage.html b/wagtail/wagtailsnippets/templates/wagtailsnippets/snippets/usage.html index b63a388d9..2111f105b 100644 --- a/wagtail/wagtailsnippets/templates/wagtailsnippets/snippets/usage.html +++ b/wagtail/wagtailsnippets/templates/wagtailsnippets/snippets/usage.html @@ -23,7 +23,7 @@ {% for page in used_by %} -

      {{ page.title }}

      +

      {{ page.title }}

      {% if page.get_parent %} From 51d2cb1656b9ef45d344478ef504fbb485cfb7b4 Mon Sep 17 00:00:00 2001 From: Tim Heap Date: Thu, 16 Jul 2015 12:41:51 +1000 Subject: [PATCH 23/92] Fix rich text link modal window tabs Some or all of the 'Internal link | External link | Email link' tabs at the top of the modal disappeared when changing tabs. This was caused by the `allow_external_link`/`allow_email_link` context variables not consistently being set across all instances where the tabs were printed. A new `shared_context(request)` function has been added that generates these context variables, which can easily be used to add these variables to any context that needs them. --- wagtail/wagtailadmin/views/chooser.py | 57 ++++++++++++++++----------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/wagtail/wagtailadmin/views/chooser.py b/wagtail/wagtailadmin/views/chooser.py index cd7470ea3..80f4804bf 100644 --- a/wagtail/wagtailadmin/views/chooser.py +++ b/wagtail/wagtailadmin/views/chooser.py @@ -19,6 +19,16 @@ def get_querystring(request): }) +def shared_context(request, extra_context={}): + context = { + 'allow_external_link': request.GET.get('allow_external_link'), + 'allow_email_link': request.GET.get('allow_email_link'), + 'querystring': get_querystring(request), + } + context.update(extra_context) + return context + + def browse(request, parent_page_id=None): ITEMS_PER_PAGE = 25 @@ -79,17 +89,18 @@ def browse(request, parent_page_id=None): except EmptyPage: pages = paginator.page(paginator.num_pages) - return render_modal_workflow(request, 'wagtailadmin/chooser/browse.html', 'wagtailadmin/chooser/browse.js', { - 'allow_external_link': request.GET.get('allow_external_link'), - 'allow_email_link': request.GET.get('allow_email_link'), - 'querystring': get_querystring(request), - 'parent_page': parent_page, - 'pages': pages, - 'search_form': search_form, - 'page_type_string': page_type, - 'page_type_name': desired_class.get_verbose_name(), - 'page_types_restricted': (page_type != 'wagtailcore.page') - }) + return render_modal_workflow( + request, + 'wagtailadmin/chooser/browse.html', 'wagtailadmin/chooser/browse.js', + shared_context(request, { + 'parent_page': parent_page, + 'pages': pages, + 'search_form': search_form, + 'page_type_string': page_type, + 'page_type_name': desired_class.get_verbose_name(), + 'page_types_restricted': (page_type != 'wagtailcore.page') + }) + ) def search(request, parent_page_id=None): @@ -115,11 +126,13 @@ def search(request, parent_page_id=None): page.can_choose = True shown_pages.append(page) - return render(request, 'wagtailadmin/chooser/_search_results.html', { - 'querystring': get_querystring(request), - 'searchform': search_form, - 'pages': shown_pages, - }) + return render( + request, 'wagtailadmin/chooser/_search_results.html', + shared_context(request, { + 'searchform': search_form, + 'pages': shown_pages, + }) + ) def external_link(request): @@ -147,11 +160,9 @@ def external_link(request): return render_modal_workflow( request, 'wagtailadmin/chooser/external_link.html', 'wagtailadmin/chooser/external_link.js', - { - 'querystring': get_querystring(request), - 'allow_email_link': request.GET.get('allow_email_link'), + shared_context(request, { 'form': form, - } + }) ) @@ -180,9 +191,7 @@ def email_link(request): return render_modal_workflow( request, 'wagtailadmin/chooser/email_link.html', 'wagtailadmin/chooser/email_link.js', - { - 'querystring': get_querystring(request), - 'allow_external_link': request.GET.get('allow_external_link'), + shared_context(request, { 'form': form, - } + }) ) From ef9d5b852d49d0963c9eb9f3863ff066fe494092 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Jul 2015 11:46:46 +0100 Subject: [PATCH 24/92] Use ViewSet as base class for API endpoints. --- setup.py | 1 + wagtail/contrib/wagtailapi/endpoints.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 73849d488..bd8fb70fc 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ install_requires = [ "django-modelcluster>=0.6", "django-taggit>=0.13.0", "django-treebeard==3.0", + "djangorestframework==3.1.3", "Pillow>=2.6.1", "beautifulsoup4>=4.3.2", "html5lib==0.999", diff --git a/wagtail/contrib/wagtailapi/endpoints.py b/wagtail/contrib/wagtailapi/endpoints.py index 318c2c3c4..c50d7eed6 100644 --- a/wagtail/contrib/wagtailapi/endpoints.py +++ b/wagtail/contrib/wagtailapi/endpoints.py @@ -10,6 +10,7 @@ from django.utils.encoding import force_text from django.shortcuts import get_object_or_404 from django.conf.urls import url from django.conf import settings +from rest_framework.viewsets import ViewSet from wagtail.wagtailcore.models import Page from wagtail.wagtailimages.models import get_image_model @@ -94,7 +95,7 @@ def get_api_data(obj, fields): continue -class BaseAPIEndpoint(object): +class BaseAPIEndpoint(ViewSet): known_query_parameters = frozenset([ 'limit', 'offset', From b6a4318379ac90e5b62efa26361c9933b28cea8d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Jul 2015 13:40:19 +0100 Subject: [PATCH 25/92] Refactor to use Responses and Renderers. --- wagtail/contrib/wagtailapi/api.py | 86 ++----------------------- wagtail/contrib/wagtailapi/endpoints.py | 77 +++++++++++++--------- wagtail/contrib/wagtailapi/renderers.py | 49 ++++++++++++++ wagtail/contrib/wagtailapi/utils.py | 22 +++++++ 4 files changed, 122 insertions(+), 112 deletions(-) create mode 100644 wagtail/contrib/wagtailapi/renderers.py diff --git a/wagtail/contrib/wagtailapi/api.py b/wagtail/contrib/wagtailapi/api.py index 586e3d524..33877429f 100644 --- a/wagtail/contrib/wagtailapi/api.py +++ b/wagtail/contrib/wagtailapi/api.py @@ -1,95 +1,21 @@ -import json -from functools import wraps - from django.conf.urls import url, include -from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound, Http404 -from django.core.serializers.json import DjangoJSONEncoder -from django.core.urlresolvers import reverse -from taggit.managers import _TaggableManager -from taggit.models import Tag - -from wagtail.utils.urlpatterns import decorate_urlpatterns -from wagtail.wagtailcore.blocks import StreamValue - -from .endpoints import URLPath, ObjectDetailURL, PagesAPIEndpoint, ImagesAPIEndpoint, DocumentsAPIEndpoint -from .utils import BadRequestError, get_base_url - - -def get_full_url(request, path): - base_url = get_base_url(request) or '' - return base_url + path +from .endpoints import PagesAPIEndpoint, ImagesAPIEndpoint, DocumentsAPIEndpoint class API(object): def __init__(self, endpoints): self.endpoints = endpoints - def find_model_detail_view(self, model): - for endpoint_name, endpoint in self.endpoints.items(): - if endpoint.has_model(model): - return 'wagtailapi_v1:%s:detail' % endpoint_name - - def make_response(self, request, data, response_cls=HttpResponse): - api = self - - class WagtailAPIJSONEncoder(DjangoJSONEncoder): - def default(self, o): - if isinstance(o, _TaggableManager): - return list(o.all()) - elif isinstance(o, Tag): - return o.name - elif isinstance(o, URLPath): - return get_full_url(request, o.path) - elif isinstance(o, ObjectDetailURL): - view = api.find_model_detail_view(o.model) - - if view: - return get_full_url(request, reverse(view, args=(o.pk, ))) - else: - return None - elif isinstance(o, StreamValue): - return o.stream_block.get_prep_value(o) - else: - return super(WagtailAPIJSONEncoder, self).default(o) - - return response_cls( - json.dumps(data, indent=4, cls=WagtailAPIJSONEncoder), - content_type='application/json' - ) - - def api_view(self, view): - """ - This is a decorator that is applied to all API views. - - It is responsible for serialising the responses from the endpoints - and handling errors. - """ - @wraps(view) - def wrapper(request, *args, **kwargs): - # Catch exceptions and format them as JSON documents - try: - return self.make_response(request, view(request, *args, **kwargs)) - except Http404 as e: - return self.make_response(request, { - 'message': str(e) - }, response_cls=HttpResponseNotFound) - except BadRequestError as e: - return self.make_response(request, { - 'message': str(e) - }, response_cls=HttpResponseBadRequest) - - return wrapper - def get_urlpatterns(self): - return decorate_urlpatterns([ + return [ url(r'^%s/' % name, include(endpoint.get_urlpatterns(), namespace=name)) for name, endpoint in self.endpoints.items() - ], self.api_view) + ] v1 = API({ - 'pages': PagesAPIEndpoint(), - 'images': ImagesAPIEndpoint(), - 'documents': DocumentsAPIEndpoint(), + 'pages': PagesAPIEndpoint, + 'images': ImagesAPIEndpoint, + 'documents': DocumentsAPIEndpoint, }) diff --git a/wagtail/contrib/wagtailapi/endpoints.py b/wagtail/contrib/wagtailapi/endpoints.py index c50d7eed6..4d6e15b72 100644 --- a/wagtail/contrib/wagtailapi/endpoints.py +++ b/wagtail/contrib/wagtailapi/endpoints.py @@ -10,6 +10,10 @@ from django.utils.encoding import force_text from django.shortcuts import get_object_or_404 from django.conf.urls import url from django.conf import settings +from django.http import Http404 + +from rest_framework import status +from rest_framework.response import Response from rest_framework.viewsets import ViewSet from wagtail.wagtailcore.models import Page @@ -19,29 +23,8 @@ from wagtail.wagtailcore.utils import resolve_model_string from wagtail.wagtailsearch.backends import get_search_backend from wagtail.utils.compat import get_related_model -from .utils import BadRequestError - - -class URLPath(object): - """ - This class represents a URL path that should be converted to a full URL. - - It is used when the domain that should be used is not known at the time - the URL was generated. It will get resolved to a full URL during - serialisation in api.py. - - One example use case is the documents endpoint adding download URLs into - the JSON. The endpoint does not know the domain name to use at the time so - returns one of these instead. - """ - def __init__(self, path): - self.path = path - - -class ObjectDetailURL(object): - def __init__(self, model, pk): - self.model = model - self.pk = pk +from .renderers import WagtailJSONRenderer +from .utils import BadRequestError, URLPath, ObjectDetailURL def get_api_data(obj, fields): @@ -96,6 +79,8 @@ def get_api_data(obj, fields): class BaseAPIEndpoint(ViewSet): + renderer_classes = [WagtailJSONRenderer] + known_query_parameters = frozenset([ 'limit', 'offset', @@ -104,6 +89,15 @@ class BaseAPIEndpoint(ViewSet): 'search', ]) + def handle_exception(self, exc): + if isinstance(exc, Http404): + data = {'message': str(exc)} + return Response(data, status=status.HTTP_404_NOT_FOUND) + elif isinstance(exc, BadRequestError): + data = {'message': str(exc)} + return Response(data, status=status.HTTP_400_BAD_REQUEST) + return super(BaseAPIEndpoint, self).handle_exception(exc) + def listing_view(self, request): return NotImplemented @@ -300,15 +294,28 @@ class BaseAPIEndpoint(ViewSet): return queryset[start:stop] - def get_urlpatterns(self): + @classmethod + def get_urlpatterns(cls): """ This returns a list of URL patterns for the endpoint """ return [ - url(r'^$', self.listing_view, name='listing'), - url(r'^(\d+)/$', self.detail_view, name='detail'), + url(r'^$', cls.as_view({'get': 'listing_view'}), name='listing'), + url(r'^(\d+)/$', cls.as_view({'get': 'detail_view'}), name='detail'), ] + def find_model_detail_view(self, model): + # TODO: Needs refactoring. This is currently duplicated, and also + # does a bit of a dance around instantiating these classes. + endpoints = { + 'pages': PagesAPIEndpoint(), + 'images': ImagesAPIEndpoint(), + 'documents': DocumentsAPIEndpoint(), + } + for endpoint_name, endpoint in endpoints.items(): + if endpoint.has_model(model): + return 'wagtailapi_v1:%s:detail' % endpoint_name + def has_model(self, model): return False @@ -443,7 +450,7 @@ class PagesAPIEndpoint(BaseAPIEndpoint): else: fields = {'title'} - return OrderedDict([ + data = OrderedDict([ ('meta', OrderedDict([ ('total_count', total_count), ])), @@ -452,10 +459,12 @@ class PagesAPIEndpoint(BaseAPIEndpoint): for page in queryset ]), ]) + return Response(data) def detail_view(self, request, pk): page = get_object_or_404(self.get_queryset(request), pk=pk).specific - return self.serialize_object(request, page, all_fields=True, show_details=True) + data = self.serialize_object(request, page, all_fields=True, show_details=True) + return Response(data) def has_model(self, model): return issubclass(model, Page) @@ -497,7 +506,7 @@ class ImagesAPIEndpoint(BaseAPIEndpoint): else: fields = {'title'} - return OrderedDict([ + data = OrderedDict([ ('meta', OrderedDict([ ('total_count', total_count), ])), @@ -506,10 +515,12 @@ class ImagesAPIEndpoint(BaseAPIEndpoint): for image in queryset ]), ]) + return Response(data) def detail_view(self, request, pk): image = get_object_or_404(self.get_queryset(request), pk=pk) - return self.serialize_object(request, image, all_fields=True) + data = self.serialize_object(request, image, all_fields=True) + return Response(data) def has_model(self, model): return model == self.model @@ -555,7 +566,7 @@ class DocumentsAPIEndpoint(BaseAPIEndpoint): else: fields = {'title'} - return OrderedDict([ + data = OrderedDict([ ('meta', OrderedDict([ ('total_count', total_count), ])), @@ -564,10 +575,12 @@ class DocumentsAPIEndpoint(BaseAPIEndpoint): for document in queryset ]), ]) + return Response(data) def detail_view(self, request, pk): document = get_object_or_404(Document, pk=pk) - return self.serialize_object(request, document, all_fields=True, show_details=True) + data = self.serialize_object(request, document, all_fields=True, show_details=True) + return Response(data) def has_model(self, model): return model == Document diff --git a/wagtail/contrib/wagtailapi/renderers.py b/wagtail/contrib/wagtailapi/renderers.py new file mode 100644 index 000000000..c58ea32c6 --- /dev/null +++ b/wagtail/contrib/wagtailapi/renderers.py @@ -0,0 +1,49 @@ +import json + +from django.core.serializers.json import DjangoJSONEncoder +from django.core.urlresolvers import reverse + +from rest_framework import renderers + +from taggit.managers import _TaggableManager +from taggit.models import Tag + +from wagtail.wagtailcore.blocks import StreamValue + +from .utils import URLPath, ObjectDetailURL, get_base_url + + +def get_full_url(request, path): + base_url = get_base_url(request) or '' + return base_url + path + + +class WagtailJSONRenderer(renderers.BaseRenderer): + media_type = 'application/json' + charset = None + + def render(self, data, media_type=None, renderer_context=None): + endpoint = renderer_context['view'] + request = renderer_context['request'] + + class WagtailAPIJSONEncoder(DjangoJSONEncoder): + def default(self, o): + if isinstance(o, _TaggableManager): + return list(o.all()) + elif isinstance(o, Tag): + return o.name + elif isinstance(o, URLPath): + return get_full_url(request, o.path) + elif isinstance(o, ObjectDetailURL): + view = endpoint.find_model_detail_view(o.model) + + if view: + return get_full_url(request, reverse(view, args=(o.pk, ))) + else: + return None + elif isinstance(o, StreamValue): + return o.stream_block.get_prep_value(o) + else: + return super(WagtailAPIJSONEncoder, self).default(o) + + return json.dumps(data, indent=4, cls=WagtailAPIJSONEncoder) diff --git a/wagtail/contrib/wagtailapi/utils.py b/wagtail/contrib/wagtailapi/utils.py index 11af445d2..483e4f51a 100644 --- a/wagtail/contrib/wagtailapi/utils.py +++ b/wagtail/contrib/wagtailapi/utils.py @@ -14,3 +14,25 @@ def get_base_url(request=None): base_url_parsed = urlparse(base_url) return base_url_parsed.scheme + '://' + base_url_parsed.netloc + + +class URLPath(object): + """ + This class represents a URL path that should be converted to a full URL. + + It is used when the domain that should be used is not known at the time + the URL was generated. It will get resolved to a full URL during + serialisation in api.py. + + One example use case is the documents endpoint adding download URLs into + the JSON. The endpoint does not know the domain name to use at the time so + returns one of these instead. + """ + def __init__(self, path): + self.path = path + + +class ObjectDetailURL(object): + def __init__(self, model, pk): + self.model = model + self.pk = pk From aee387e2c23967a78d4e2da4cc7785e78b394759 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Jul 2015 13:58:03 +0100 Subject: [PATCH 26/92] Drop 'api' module. --- wagtail/contrib/wagtailapi/api.py | 21 --------------------- wagtail/contrib/wagtailapi/urls.py | 11 +++++++++-- 2 files changed, 9 insertions(+), 23 deletions(-) delete mode 100644 wagtail/contrib/wagtailapi/api.py diff --git a/wagtail/contrib/wagtailapi/api.py b/wagtail/contrib/wagtailapi/api.py deleted file mode 100644 index 33877429f..000000000 --- a/wagtail/contrib/wagtailapi/api.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.conf.urls import url, include - -from .endpoints import PagesAPIEndpoint, ImagesAPIEndpoint, DocumentsAPIEndpoint - - -class API(object): - def __init__(self, endpoints): - self.endpoints = endpoints - - def get_urlpatterns(self): - return [ - url(r'^%s/' % name, include(endpoint.get_urlpatterns(), namespace=name)) - for name, endpoint in self.endpoints.items() - ] - - -v1 = API({ - 'pages': PagesAPIEndpoint, - 'images': ImagesAPIEndpoint, - 'documents': DocumentsAPIEndpoint, -}) diff --git a/wagtail/contrib/wagtailapi/urls.py b/wagtail/contrib/wagtailapi/urls.py index 1aa914e36..492772f28 100644 --- a/wagtail/contrib/wagtailapi/urls.py +++ b/wagtail/contrib/wagtailapi/urls.py @@ -2,9 +2,16 @@ from __future__ import absolute_import from django.conf.urls import url, include -from . import api +from .endpoints import PagesAPIEndpoint, ImagesAPIEndpoint, DocumentsAPIEndpoint + + +v1 = [ + url(r'^pages/', include(PagesAPIEndpoint.get_urlpatterns(), namespace='pages')), + url(r'^images/', include(ImagesAPIEndpoint.get_urlpatterns(), namespace='images')), + url(r'^documents/', include(DocumentsAPIEndpoint.get_urlpatterns(), namespace='documents')) +] urlpatterns = [ - url(r'^v1/', include(api.v1.get_urlpatterns(), namespace='wagtailapi_v1')), + url(r'^v1/', include(v1, namespace='wagtailapi_v1')), ] From 91f0e10f464ce23cd61b7a47f886144fe4e1453a Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 20 Jul 2015 14:37:45 +0100 Subject: [PATCH 27/92] renumber testapp migration --- .../{0005_image_file_size.py => 0006_image_file_size.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename wagtail/tests/testapp/migrations/{0005_image_file_size.py => 0006_image_file_size.py} (92%) diff --git a/wagtail/tests/testapp/migrations/0005_image_file_size.py b/wagtail/tests/testapp/migrations/0006_image_file_size.py similarity index 92% rename from wagtail/tests/testapp/migrations/0005_image_file_size.py rename to wagtail/tests/testapp/migrations/0006_image_file_size.py index 0e8c2a192..a80fb5015 100644 --- a/wagtail/tests/testapp/migrations/0005_image_file_size.py +++ b/wagtail/tests/testapp/migrations/0006_image_file_size.py @@ -7,7 +7,7 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('tests', '0004_streammodel_richtext'), + ('tests', '0005_streampage'), ] operations = [ From 0921bdec6da519e51ad6975e60820d25d8ec7ac6 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 20 Jul 2015 14:49:55 +0100 Subject: [PATCH 28/92] Release note for #1426 --- CHANGELOG.txt | 1 + docs/releases/1.1.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 371a7c348..3539965c1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -11,6 +11,7 @@ Changelog * Search backends can now be specified by module (e.g. `wagtail.wagtailsearch.backends.elasticsearch`), rather than a specific class (`wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch`) * Added ``descendant_of`` filter to the API (Michael Fillier) * Added optional directory argument to "wagtail start" command (Mitchel Cabuloy) + * Image file size is now stored in the database, to avoid unnecessary filesystem lookups * Fix: Text areas in the non-default tab of the page editor now resize to the correct height diff --git a/docs/releases/1.1.rst b/docs/releases/1.1.rst index fc8fbd822..2e29fc3d6 100644 --- a/docs/releases/1.1.rst +++ b/docs/releases/1.1.rst @@ -24,6 +24,7 @@ Minor features * Search backends can now be specified by module (e.g. ``wagtail.wagtailsearch.backends.elasticsearch``), rather than a specific class (``wagtail.wagtailsearch.backends.elasticsearch.ElasticSearch``) * Added ``descendant_of`` filter to the API * Added optional directory argument to "wagtail start" command + * Image file size is now stored in the database, to avoid unnecessary filesystem lookups Bug fixes ~~~~~~~~~ From e1978f6606ab6ed57ad822996593d1bfa21c3dfe Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Jul 2015 16:17:40 +0100 Subject: [PATCH 29/92] Refactor filters --- wagtail/contrib/wagtailapi/endpoints.py | 177 +++--------------------- wagtail/contrib/wagtailapi/filters.py | 148 ++++++++++++++++++++ 2 files changed, 168 insertions(+), 157 deletions(-) create mode 100644 wagtail/contrib/wagtailapi/filters.py diff --git a/wagtail/contrib/wagtailapi/endpoints.py b/wagtail/contrib/wagtailapi/endpoints.py index 4d6e15b72..594465eaf 100644 --- a/wagtail/contrib/wagtailapi/endpoints.py +++ b/wagtail/contrib/wagtailapi/endpoints.py @@ -14,15 +14,18 @@ from django.http import Http404 from rest_framework import status from rest_framework.response import Response -from rest_framework.viewsets import ViewSet +from rest_framework.viewsets import GenericViewSet from wagtail.wagtailcore.models import Page from wagtail.wagtailimages.models import get_image_model from wagtail.wagtaildocs.models import Document from wagtail.wagtailcore.utils import resolve_model_string -from wagtail.wagtailsearch.backends import get_search_backend from wagtail.utils.compat import get_related_model +from .filters import ( + FieldsFilter, OrderingFilter, SearchFilter, + ChildOfFilter, DescendantOfFilter +) from .renderers import WagtailJSONRenderer from .utils import BadRequestError, URLPath, ObjectDetailURL @@ -78,8 +81,9 @@ def get_api_data(obj, fields): continue -class BaseAPIEndpoint(ViewSet): +class BaseAPIEndpoint(GenericViewSet): renderer_classes = [WagtailJSONRenderer] + filter_classes = [] known_query_parameters = frozenset([ 'limit', @@ -174,98 +178,6 @@ class BaseAPIEndpoint(ViewSet): if unknown_parameters: raise BadRequestError("query parameter is not an operation or a recognised field: %s" % ', '.join(sorted(unknown_parameters))) - def do_field_filtering(self, request, queryset): - """ - This performs field level filtering on the result set - Eg: ?title=James Joyce - """ - fields = set(self.get_api_fields(queryset.model)).union({'id'}) - - for field_name, value in request.GET.items(): - if field_name in fields: - field = getattr(queryset.model, field_name, None) - - if isinstance(field, _TaggableManager): - for tag in value.split(','): - queryset = queryset.filter(**{field_name + '__name': tag}) - - # Stick a message on the queryset to indicate that tag filtering has been performed - # This will let the do_search method know that it must raise an error as searching - # and tag filtering at the same time is not supported - queryset._filtered_by_tag = True - else: - queryset = queryset.filter(**{field_name: value}) - - return queryset - - def do_ordering(self, request, queryset): - """ - This applies ordering to the result set - Eg: ?order=title - - It also supports reverse ordering - Eg: ?order=-title - - And random ordering - Eg: ?order=random - """ - if 'order' in request.GET: - # Prevent ordering while searching - if 'search' in request.GET: - raise BadRequestError("ordering with a search query is not supported") - - order_by = request.GET['order'] - - # Random ordering - if order_by == 'random': - # Prevent ordering by random with offset - if 'offset' in request.GET: - raise BadRequestError("random ordering with offset is not supported") - - return queryset.order_by('?') - - # Check if reverse ordering is set - if order_by.startswith('-'): - reverse_order = True - order_by = order_by[1:] - else: - reverse_order = False - - # Add ordering - if order_by == 'id' or order_by in self.get_api_fields(queryset.model): - queryset = queryset.order_by(order_by) - else: - # Unknown field - raise BadRequestError("cannot order by '%s' (unknown field)" % order_by) - - # Reverse order - if reverse_order: - queryset = queryset.reverse() - - return queryset - - def do_search(self, request, queryset): - """ - This performs a full-text search on the result set - Eg: ?search=James Joyce - """ - search_enabled = getattr(settings, 'WAGTAILAPI_SEARCH_ENABLED', True) - - if 'search' in request.GET: - if not search_enabled: - raise BadRequestError("search is disabled") - - # Searching and filtering by tag at the same time is not supported - if getattr(queryset, '_filtered_by_tag', False): - raise BadRequestError("filtering by tag with a search query is not supported") - - search_query = request.GET['search'] - - sb = get_search_backend() - queryset = sb.search(search_query, queryset) - - return queryset - def do_pagination(self, request, queryset): """ This performs limit/offset based pagination on the result set @@ -326,6 +238,10 @@ class PagesAPIEndpoint(BaseAPIEndpoint): 'child_of', 'descendant_of', ]) + filter_backends = [ + FieldsFilter, ChildOfFilter, DescendantOfFilter, + OrderingFilter, SearchFilter + ] def get_queryset(self, request, model=Page): # Get live pages that are not in a private section @@ -385,42 +301,6 @@ class PagesAPIEndpoint(BaseAPIEndpoint): except LookupError: raise BadRequestError("type doesn't exist") - def do_child_of_filter(self, request, queryset): - if 'child_of' in request.GET: - try: - parent_page_id = int(request.GET['child_of']) - assert parent_page_id >= 0 - except (ValueError, AssertionError): - raise BadRequestError("child_of must be a positive integer") - - try: - parent_page = self.get_queryset(request).get(id=parent_page_id) - queryset = queryset.child_of(parent_page) - queryset._filtered_by_child_of = True - return queryset - except Page.DoesNotExist: - raise BadRequestError("parent page doesn't exist") - - return queryset - - def do_descendant_of_filter(self, request, queryset): - if 'descendant_of' in request.GET: - if getattr(queryset, '_filtered_by_child_of', False): - raise BadRequestError("filtering by descendant_of with child_of is not supported") - try: - ancestor_page_id = int(request.GET['descendant_of']) - assert ancestor_page_id >= 0 - except (ValueError, AssertionError): - raise BadRequestError("descendant_of must be a positive integer") - - try: - ancestor_page = self.get_queryset(request).get(id=ancestor_page_id) - return queryset.descendant_of(ancestor_page) - except Page.DoesNotExist: - raise BadRequestError("ancestor page doesn't exist") - - return queryset - def listing_view(self, request): # Get model and queryset model = self.get_model(request) @@ -429,16 +309,8 @@ class PagesAPIEndpoint(BaseAPIEndpoint): # Check query paramters self.check_query_parameters(request, queryset) - # Filtering - queryset = self.do_field_filtering(request, queryset) - queryset = self.do_child_of_filter(request, queryset) - queryset = self.do_descendant_of_filter(request, queryset) - - # Ordering - queryset = self.do_ordering(request, queryset) - - # Search - queryset = self.do_search(request, queryset) + # Filtering, Ancestor/Descendant, Ordering, Search. + queryset = self.filter_queryset(queryset) # Pagination total_count = queryset.count() @@ -472,6 +344,7 @@ class PagesAPIEndpoint(BaseAPIEndpoint): class ImagesAPIEndpoint(BaseAPIEndpoint): model = get_image_model() + filter_backends = [FieldsFilter, OrderingFilter, SearchFilter] def get_queryset(self, request): return self.model.objects.all().order_by('id') @@ -487,14 +360,8 @@ class ImagesAPIEndpoint(BaseAPIEndpoint): # Check query paramters self.check_query_parameters(request, queryset) - # Filtering - queryset = self.do_field_filtering(request, queryset) - - # Ordering - queryset = self.do_ordering(request, queryset) - - # Search - queryset = self.do_search(request, queryset) + # Filtering, Ordering, Search. + queryset = self.filter_queryset(queryset) # Pagination total_count = queryset.count() @@ -527,6 +394,8 @@ class ImagesAPIEndpoint(BaseAPIEndpoint): class DocumentsAPIEndpoint(BaseAPIEndpoint): + filter_backends = [FieldsFilter, OrderingFilter, SearchFilter] + def get_api_fields(self, model): api_fields = ['title', 'tags'] api_fields.extend(super(DocumentsAPIEndpoint, self).get_api_fields(model)) @@ -547,14 +416,8 @@ class DocumentsAPIEndpoint(BaseAPIEndpoint): # Check query paramters self.check_query_parameters(request, queryset) - # Filtering - queryset = self.do_field_filtering(request, queryset) - - # Ordering - queryset = self.do_ordering(request, queryset) - - # Search - queryset = self.do_search(request, queryset) + # Filtering, Ordering, Search. + queryset = self.filter_queryset(queryset) # Pagination total_count = queryset.count() diff --git a/wagtail/contrib/wagtailapi/filters.py b/wagtail/contrib/wagtailapi/filters.py new file mode 100644 index 000000000..633769864 --- /dev/null +++ b/wagtail/contrib/wagtailapi/filters.py @@ -0,0 +1,148 @@ +from django.conf import settings + +from rest_framework.filters import BaseFilterBackend + +from taggit.managers import _TaggableManager + +from wagtail.wagtailcore.models import Page +from wagtail.wagtailsearch.backends import get_search_backend + +from .utils import BadRequestError + + +class FieldsFilter(BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + """ + This performs field level filtering on the result set + Eg: ?title=James Joyce + """ + fields = set(view.get_api_fields(queryset.model)).union({'id'}) + + for field_name, value in request.GET.items(): + if field_name in fields: + field = getattr(queryset.model, field_name, None) + + if isinstance(field, _TaggableManager): + for tag in value.split(','): + queryset = queryset.filter(**{field_name + '__name': tag}) + + # Stick a message on the queryset to indicate that tag filtering has been performed + # This will let the do_search method know that it must raise an error as searching + # and tag filtering at the same time is not supported + queryset._filtered_by_tag = True + else: + queryset = queryset.filter(**{field_name: value}) + + return queryset + + +class OrderingFilter(BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + """ + This applies ordering to the result set + Eg: ?order=title + + It also supports reverse ordering + Eg: ?order=-title + + And random ordering + Eg: ?order=random + """ + if 'order' in request.GET: + # Prevent ordering while searching + if 'search' in request.GET: + raise BadRequestError("ordering with a search query is not supported") + + order_by = request.GET['order'] + + # Random ordering + if order_by == 'random': + # Prevent ordering by random with offset + if 'offset' in request.GET: + raise BadRequestError("random ordering with offset is not supported") + + return queryset.order_by('?') + + # Check if reverse ordering is set + if order_by.startswith('-'): + reverse_order = True + order_by = order_by[1:] + else: + reverse_order = False + + # Add ordering + if order_by == 'id' or order_by in view.get_api_fields(queryset.model): + queryset = queryset.order_by(order_by) + else: + # Unknown field + raise BadRequestError("cannot order by '%s' (unknown field)" % order_by) + + # Reverse order + if reverse_order: + queryset = queryset.reverse() + + return queryset + + +class SearchFilter(BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + """ + This performs a full-text search on the result set + Eg: ?search=James Joyce + """ + search_enabled = getattr(settings, 'WAGTAILAPI_SEARCH_ENABLED', True) + + if 'search' in request.GET: + if not search_enabled: + raise BadRequestError("search is disabled") + + # Searching and filtering by tag at the same time is not supported + if getattr(queryset, '_filtered_by_tag', False): + raise BadRequestError("filtering by tag with a search query is not supported") + + search_query = request.GET['search'] + + sb = get_search_backend() + queryset = sb.search(search_query, queryset) + + return queryset + + +class ChildOfFilter(BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + if 'child_of' in request.GET: + try: + parent_page_id = int(request.GET['child_of']) + assert parent_page_id >= 0 + except (ValueError, AssertionError): + raise BadRequestError("child_of must be a positive integer") + + try: + parent_page = view.get_queryset(request).get(id=parent_page_id) + queryset = queryset.child_of(parent_page) + queryset._filtered_by_child_of = True + return queryset + except Page.DoesNotExist: + raise BadRequestError("parent page doesn't exist") + + return queryset + + +class DescendantOfFilter(BaseFilterBackend): + def filter_queryset(self, request, queryset, view): + if 'descendant_of' in request.GET: + if getattr(queryset, '_filtered_by_child_of', False): + raise BadRequestError("filtering by descendant_of with child_of is not supported") + try: + ancestor_page_id = int(request.GET['descendant_of']) + assert ancestor_page_id >= 0 + except (ValueError, AssertionError): + raise BadRequestError("descendant_of must be a positive integer") + + try: + ancestor_page = view.get_queryset(request).get(id=ancestor_page_id) + return queryset.descendant_of(ancestor_page) + except Page.DoesNotExist: + raise BadRequestError("ancestor page doesn't exist") + + return queryset From 3122d19a660a80a315d4bf847070c9eb8a2f1309 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Jul 2015 16:36:47 +0100 Subject: [PATCH 30/92] Refactor get_api_fields --- wagtail/contrib/wagtailapi/endpoints.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/wagtail/contrib/wagtailapi/endpoints.py b/wagtail/contrib/wagtailapi/endpoints.py index 594465eaf..1117e3a25 100644 --- a/wagtail/contrib/wagtailapi/endpoints.py +++ b/wagtail/contrib/wagtailapi/endpoints.py @@ -92,6 +92,7 @@ class BaseAPIEndpoint(GenericViewSet): 'order', 'search', ]) + extra_api_fields = [] def handle_exception(self, exc): if isinstance(exc, Http404): @@ -113,7 +114,7 @@ class BaseAPIEndpoint(GenericViewSet): This returns a list of field names that are allowed to be used in the API (excluding the id field). """ - api_fields = [] + api_fields = self.extra_api_fields[:] if hasattr(model, 'api_fields'): api_fields.extend(model.api_fields) @@ -238,6 +239,7 @@ class PagesAPIEndpoint(BaseAPIEndpoint): 'child_of', 'descendant_of', ]) + extra_api_fields = ['title'] filter_backends = [ FieldsFilter, ChildOfFilter, DescendantOfFilter, OrderingFilter, SearchFilter @@ -252,11 +254,6 @@ class PagesAPIEndpoint(BaseAPIEndpoint): return queryset - def get_api_fields(self, model): - api_fields = ['title'] - api_fields.extend(super(PagesAPIEndpoint, self).get_api_fields(model)) - return api_fields - def serialize_object_metadata(self, request, page, show_details=False): data = super(PagesAPIEndpoint, self).serialize_object_metadata(request, page, show_details=show_details) @@ -345,15 +342,11 @@ class PagesAPIEndpoint(BaseAPIEndpoint): class ImagesAPIEndpoint(BaseAPIEndpoint): model = get_image_model() filter_backends = [FieldsFilter, OrderingFilter, SearchFilter] + extra_api_fields = ['title', 'tags', 'width', 'height'] def get_queryset(self, request): return self.model.objects.all().order_by('id') - def get_api_fields(self, model): - api_fields = ['title', 'tags', 'width', 'height'] - api_fields.extend(super(ImagesAPIEndpoint, self).get_api_fields(model)) - return api_fields - def listing_view(self, request): queryset = self.get_queryset(request) @@ -395,11 +388,7 @@ class ImagesAPIEndpoint(BaseAPIEndpoint): class DocumentsAPIEndpoint(BaseAPIEndpoint): filter_backends = [FieldsFilter, OrderingFilter, SearchFilter] - - def get_api_fields(self, model): - api_fields = ['title', 'tags'] - api_fields.extend(super(DocumentsAPIEndpoint, self).get_api_fields(model)) - return api_fields + extra_api_fields = ['title', 'tags'] def serialize_object_metadata(self, request, document, show_details=False): data = super(DocumentsAPIEndpoint, self).serialize_object_metadata(request, document, show_details=show_details) From 76de8eab349722d53edf7a1fd42c1bf7db3e795f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Jul 2015 17:03:07 +0100 Subject: [PATCH 31/92] Refactor pagination --- wagtail/contrib/wagtailapi/endpoints.py | 87 +++++++----------------- wagtail/contrib/wagtailapi/pagination.py | 45 ++++++++++++ 2 files changed, 68 insertions(+), 64 deletions(-) create mode 100644 wagtail/contrib/wagtailapi/pagination.py diff --git a/wagtail/contrib/wagtailapi/endpoints.py b/wagtail/contrib/wagtailapi/endpoints.py index 1117e3a25..105cf2c87 100644 --- a/wagtail/contrib/wagtailapi/endpoints.py +++ b/wagtail/contrib/wagtailapi/endpoints.py @@ -27,6 +27,7 @@ from .filters import ( ChildOfFilter, DescendantOfFilter ) from .renderers import WagtailJSONRenderer +from .pagination import WagtailPagination from .utils import BadRequestError, URLPath, ObjectDetailURL @@ -83,6 +84,7 @@ def get_api_data(obj, fields): class BaseAPIEndpoint(GenericViewSet): renderer_classes = [WagtailJSONRenderer] + pagination_class = WagtailPagination filter_classes = [] known_query_parameters = frozenset([ @@ -179,34 +181,6 @@ class BaseAPIEndpoint(GenericViewSet): if unknown_parameters: raise BadRequestError("query parameter is not an operation or a recognised field: %s" % ', '.join(sorted(unknown_parameters))) - def do_pagination(self, request, queryset): - """ - This performs limit/offset based pagination on the result set - Eg: ?limit=10&offset=20 -- Returns 10 items starting at item 20 - """ - limit_max = getattr(settings, 'WAGTAILAPI_LIMIT_MAX', 20) - - try: - offset = int(request.GET.get('offset', 0)) - assert offset >= 0 - except (ValueError, AssertionError): - raise BadRequestError("offset must be a positive integer") - - try: - limit = int(request.GET.get('limit', min(20, limit_max))) - - if limit > limit_max: - raise BadRequestError("limit cannot be higher than %d" % limit_max) - - assert limit >= 0 - except (ValueError, AssertionError): - raise BadRequestError("limit must be a positive integer") - - start = offset - stop = offset + limit - - return queryset[start:stop] - @classmethod def get_urlpatterns(cls): """ @@ -234,6 +208,7 @@ class BaseAPIEndpoint(GenericViewSet): class PagesAPIEndpoint(BaseAPIEndpoint): + name = 'pages' known_query_parameters = BaseAPIEndpoint.known_query_parameters.union([ 'type', 'child_of', @@ -310,8 +285,7 @@ class PagesAPIEndpoint(BaseAPIEndpoint): queryset = self.filter_queryset(queryset) # Pagination - total_count = queryset.count() - queryset = self.do_pagination(request, queryset) + queryset = self.paginate_queryset(queryset) # Get list of fields to show in results if 'fields' in request.GET: @@ -319,16 +293,11 @@ class PagesAPIEndpoint(BaseAPIEndpoint): else: fields = {'title'} - data = OrderedDict([ - ('meta', OrderedDict([ - ('total_count', total_count), - ])), - ('pages', [ - self.serialize_object(request, page, fields=fields) - for page in queryset - ]), - ]) - return Response(data) + data = [ + self.serialize_object(request, page, fields=fields) + for page in queryset + ] + return self.get_paginated_response(data) def detail_view(self, request, pk): page = get_object_or_404(self.get_queryset(request), pk=pk).specific @@ -340,6 +309,7 @@ class PagesAPIEndpoint(BaseAPIEndpoint): class ImagesAPIEndpoint(BaseAPIEndpoint): + name = 'images' model = get_image_model() filter_backends = [FieldsFilter, OrderingFilter, SearchFilter] extra_api_fields = ['title', 'tags', 'width', 'height'] @@ -357,8 +327,7 @@ class ImagesAPIEndpoint(BaseAPIEndpoint): queryset = self.filter_queryset(queryset) # Pagination - total_count = queryset.count() - queryset = self.do_pagination(request, queryset) + queryset = self.paginate_queryset(queryset) # Get list of fields to show in results if 'fields' in request.GET: @@ -366,16 +335,11 @@ class ImagesAPIEndpoint(BaseAPIEndpoint): else: fields = {'title'} - data = OrderedDict([ - ('meta', OrderedDict([ - ('total_count', total_count), - ])), - ('images', [ - self.serialize_object(request, image, fields=fields) - for image in queryset - ]), - ]) - return Response(data) + data = [ + self.serialize_object(request, image, fields=fields) + for image in queryset + ] + return self.get_paginated_response(data) def detail_view(self, request, pk): image = get_object_or_404(self.get_queryset(request), pk=pk) @@ -387,6 +351,7 @@ class ImagesAPIEndpoint(BaseAPIEndpoint): class DocumentsAPIEndpoint(BaseAPIEndpoint): + name = 'documents' filter_backends = [FieldsFilter, OrderingFilter, SearchFilter] extra_api_fields = ['title', 'tags'] @@ -409,8 +374,7 @@ class DocumentsAPIEndpoint(BaseAPIEndpoint): queryset = self.filter_queryset(queryset) # Pagination - total_count = queryset.count() - queryset = self.do_pagination(request, queryset) + queryset = self.paginate_queryset(queryset) # Get list of fields to show in results if 'fields' in request.GET: @@ -418,16 +382,11 @@ class DocumentsAPIEndpoint(BaseAPIEndpoint): else: fields = {'title'} - data = OrderedDict([ - ('meta', OrderedDict([ - ('total_count', total_count), - ])), - ('documents', [ - self.serialize_object(request, document, fields=fields) - for document in queryset - ]), - ]) - return Response(data) + data = [ + self.serialize_object(request, document, fields=fields) + for document in queryset + ] + return self.get_paginated_response(data) def detail_view(self, request, pk): document = get_object_or_404(Document, pk=pk) diff --git a/wagtail/contrib/wagtailapi/pagination.py b/wagtail/contrib/wagtailapi/pagination.py new file mode 100644 index 000000000..6cb470e06 --- /dev/null +++ b/wagtail/contrib/wagtailapi/pagination.py @@ -0,0 +1,45 @@ +from collections import OrderedDict + +from django.conf import settings + +from rest_framework.pagination import BasePagination +from rest_framework.response import Response + +from .utils import BadRequestError + + +class WagtailPagination(BasePagination): + def paginate_queryset(self, queryset, request, view=None): + limit_max = getattr(settings, 'WAGTAILAPI_LIMIT_MAX', 20) + + try: + offset = int(request.GET.get('offset', 0)) + assert offset >= 0 + except (ValueError, AssertionError): + raise BadRequestError("offset must be a positive integer") + + try: + limit = int(request.GET.get('limit', min(20, limit_max))) + + if limit > limit_max: + raise BadRequestError("limit cannot be higher than %d" % limit_max) + + assert limit >= 0 + except (ValueError, AssertionError): + raise BadRequestError("limit must be a positive integer") + + start = offset + stop = offset + limit + + self.view = view + self.total_count = queryset.count() + return queryset[start:stop] + + def get_paginated_response(self, data): + data = OrderedDict([ + ('meta', OrderedDict([ + ('total_count', self.total_count), + ])), + (self.view.name, data), + ]) + return Response(data) From 067247d2a40c5891bdd5c144b5878c5436884786 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 20 Jul 2015 17:30:35 +0100 Subject: [PATCH 32/92] Refactor get_fields --- wagtail/contrib/wagtailapi/endpoints.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/wagtail/contrib/wagtailapi/endpoints.py b/wagtail/contrib/wagtailapi/endpoints.py index 105cf2c87..1a3a7e34d 100644 --- a/wagtail/contrib/wagtailapi/endpoints.py +++ b/wagtail/contrib/wagtailapi/endpoints.py @@ -181,6 +181,15 @@ class BaseAPIEndpoint(GenericViewSet): if unknown_parameters: raise BadRequestError("query parameter is not an operation or a recognised field: %s" % ', '.join(sorted(unknown_parameters))) + def get_fields(self, request): + """ + Return the set of fields that should be returned in the output + representation for listing views. + """ + if 'fields' in request.GET: + return set(request.GET['fields'].split(',')) + return {'title'} + @classmethod def get_urlpatterns(cls): """ @@ -288,10 +297,7 @@ class PagesAPIEndpoint(BaseAPIEndpoint): queryset = self.paginate_queryset(queryset) # Get list of fields to show in results - if 'fields' in request.GET: - fields = set(request.GET['fields'].split(',')) - else: - fields = {'title'} + fields = self.get_fields(request) data = [ self.serialize_object(request, page, fields=fields) @@ -330,10 +336,7 @@ class ImagesAPIEndpoint(BaseAPIEndpoint): queryset = self.paginate_queryset(queryset) # Get list of fields to show in results - if 'fields' in request.GET: - fields = set(request.GET['fields'].split(',')) - else: - fields = {'title'} + fields = self.get_fields(request) data = [ self.serialize_object(request, image, fields=fields) @@ -377,10 +380,7 @@ class DocumentsAPIEndpoint(BaseAPIEndpoint): queryset = self.paginate_queryset(queryset) # Get list of fields to show in results - if 'fields' in request.GET: - fields = set(request.GET['fields'].split(',')) - else: - fields = {'title'} + fields = self.get_fields(request) data = [ self.serialize_object(request, document, fields=fields) From b93a7210fa98f1b6cf2808fe67bbeb0b080ede99 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 20 Jul 2015 20:06:55 +0100 Subject: [PATCH 33/92] update remaining URL in wagtailimages tests --- wagtail/wagtailimages/tests/test_admin_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/wagtailimages/tests/test_admin_views.py b/wagtail/wagtailimages/tests/test_admin_views.py index 94a7d3e49..b12b0d013 100644 --- a/wagtail/wagtailimages/tests/test_admin_views.py +++ b/wagtail/wagtailimages/tests/test_admin_views.py @@ -167,7 +167,7 @@ class TestImageEditView(TestCase, WagtailTestUtils): }) # Should redirect back to index - self.assertRedirects(response, reverse('wagtailimages_index')) + self.assertRedirects(response, reverse('wagtailimages:index')) # Check that the image file size changed (assume it changed to the correct value) image = Image.objects.get(id=self.image.id) From 1a7bde76d6f6f1956f6104a228e64451798e41db Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 20 Jul 2015 20:09:15 +0100 Subject: [PATCH 34/92] Release note for #1508 --- CHANGELOG.txt | 1 + docs/releases/1.1.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 3539965c1..78e2602c4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -12,6 +12,7 @@ Changelog * Added ``descendant_of`` filter to the API (Michael Fillier) * Added optional directory argument to "wagtail start" command (Mitchel Cabuloy) * Image file size is now stored in the database, to avoid unnecessary filesystem lookups + * Updated URLs within the admin backend to use namespaces * Fix: Text areas in the non-default tab of the page editor now resize to the correct height diff --git a/docs/releases/1.1.rst b/docs/releases/1.1.rst index 2e29fc3d6..d43005033 100644 --- a/docs/releases/1.1.rst +++ b/docs/releases/1.1.rst @@ -25,6 +25,7 @@ Minor features * Added ``descendant_of`` filter to the API * Added optional directory argument to "wagtail start" command * Image file size is now stored in the database, to avoid unnecessary filesystem lookups + * Updated URLs within the admin backend to use namespaces Bug fixes ~~~~~~~~~ From 5fbef63c225d73a7964eaf45ee39ef27a2f09a6b Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 20 Jul 2015 20:09:55 +0100 Subject: [PATCH 35/92] update Wagtail version in project template requirements --- wagtail/project_template/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wagtail/project_template/requirements.txt b/wagtail/project_template/requirements.txt index b05edf068..101b80350 100644 --- a/wagtail/project_template/requirements.txt +++ b/wagtail/project_template/requirements.txt @@ -1,2 +1,2 @@ Django>=1.8,<1.9 -wagtail==1.0rc1 +wagtail==1.0 From 2c6c4667be480a56eb0858a65ea82cfb8492ac50 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 10 Jul 2015 12:55:04 +0100 Subject: [PATCH 36/92] All add/create URLs are now called "add" --- .../pages/_preview_button_on_create.html | 2 +- .../wagtailadmin/pages/add_subpage.html | 2 +- .../templates/wagtailadmin/pages/create.html | 2 +- .../wagtailadmin/tests/test_pages_views.py | 48 +++++++++---------- wagtail/wagtailadmin/tests/tests.py | 2 +- wagtail/wagtailadmin/urls.py | 4 +- wagtail/wagtailadmin/views/pages.py | 2 +- wagtail/wagtaildocs/admin_urls.py | 2 +- .../templates/wagtaildocs/documents/add.html | 2 +- .../wagtaildocs/documents/index.html | 2 +- .../wagtaildocs/documents/results.html | 2 +- wagtail/wagtaildocs/tests.py | 6 +-- wagtail/wagtailimages/admin_urls.py | 2 +- .../templates/wagtailimages/images/add.html | 2 +- .../templates/wagtailimages/multiple/add.html | 2 +- .../wagtailimages/tests/test_admin_views.py | 4 +- wagtail/wagtailimages/tests/test_models.py | 2 +- .../templates/wagtailredirects/add.html | 2 +- .../templates/wagtailredirects/index.html | 2 +- .../templates/wagtailredirects/results.html | 2 +- wagtail/wagtailredirects/tests.py | 4 +- wagtail/wagtailredirects/urls.py | 2 +- .../templates/wagtailsites/create.html | 2 +- .../templates/wagtailsites/index.html | 2 +- wagtail/wagtailsites/tests.py | 4 +- wagtail/wagtailsites/urls.py | 4 +- .../wagtailsnippets/chooser/choose.html | 2 +- .../wagtailsnippets/snippets/create.html | 2 +- .../wagtailsnippets/snippets/type_index.html | 4 +- wagtail/wagtailsnippets/tests.py | 4 +- wagtail/wagtailsnippets/urls.py | 2 +- .../templates/wagtailusers/groups/create.html | 2 +- .../templates/wagtailusers/groups/index.html | 2 +- .../wagtailusers/groups/results.html | 2 +- .../templates/wagtailusers/users/create.html | 2 +- .../templates/wagtailusers/users/index.html | 2 +- wagtail/wagtailusers/tests.py | 8 ++-- wagtail/wagtailusers/urls/groups.py | 2 +- wagtail/wagtailusers/urls/users.py | 2 +- 39 files changed, 73 insertions(+), 75 deletions(-) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_preview_button_on_create.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_preview_button_on_create.html index ff007076e..466854401 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_preview_button_on_create.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_preview_button_on_create.html @@ -1,4 +1,4 @@ diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/add_subpage.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/add_subpage.html index 933b0df40..ce74bc65e 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/add_subpage.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/add_subpage.html @@ -18,7 +18,7 @@
    • diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/create.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/create.html index c6794f622..5191e8b48 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/create.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/create.html @@ -17,7 +17,7 @@
      - + {% csrf_token %} {{ edit_handler.render_form_content }} diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py index 49ecd6715..ec81e6460 100644 --- a/wagtail/wagtailadmin/tests/test_pages_views.py +++ b/wagtail/wagtailadmin/tests/test_pages_views.py @@ -157,7 +157,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): self.assertEqual(response.status_code, 404) def test_create_simplepage(self): - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id))) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Content') self.assertContains(response, 'Promote') @@ -166,7 +166,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): """ Test that the Promote tab is not rendered for page classes that define it as empty """ - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'standardindex', self.root_page.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'standardindex', self.root_page.id))) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Content') self.assertNotContains(response, 'Promote') @@ -175,7 +175,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): """ Test that custom edit handlers are rendered """ - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'standardchild', self.root_page.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'standardchild', self.root_page.id))) self.assertEqual(response.status_code, 200) self.assertContains(response, 'Content') self.assertContains(response, 'Promote') @@ -190,7 +190,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): self.user.save() # Get page - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id, ))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id, ))) # Check that the user recieved a 403 response self.assertEqual(response.status_code, 403) @@ -201,7 +201,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'content': "Some content", 'slug': 'hello-world', } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) # Find the page and check it page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific @@ -227,7 +227,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'go_live_at': submittable_timestamp(go_live_at), 'expire_at': submittable_timestamp(expire_at), } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) # Should be redirected to explorer page self.assertEqual(response.status_code, 302) @@ -250,7 +250,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'go_live_at': submittable_timestamp(timezone.now() + timedelta(days=2)), 'expire_at': submittable_timestamp(timezone.now() + timedelta(days=1)), } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) self.assertEqual(response.status_code, 200) @@ -265,7 +265,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'slug': 'hello-world', 'expire_at': submittable_timestamp(timezone.now() + timedelta(days=-1)), } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) self.assertEqual(response.status_code, 200) @@ -284,7 +284,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'slug': 'hello-world', 'action-publish': "Publish", } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) # Find the page and check it page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific @@ -319,7 +319,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'go_live_at': submittable_timestamp(go_live_at), 'expire_at': submittable_timestamp(expire_at), } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) # Should be redirected to explorer page self.assertEqual(response.status_code, 302) @@ -348,7 +348,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'slug': 'hello-world', 'action-submit': "Submit", } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) # Find the page and check it page = Page.objects.get(path__startswith=self.root_page.path, slug='hello-world').specific @@ -385,7 +385,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'slug': 'hello-world', 'action-publish': "Publish", } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) # Should not be redirected (as the save should fail) self.assertEqual(response.status_code, 200) @@ -394,11 +394,11 @@ class TestPageCreation(TestCase, WagtailTestUtils): self.assertFormError(response, 'form', 'slug', "This slug is already in use") def test_create_nonexistantparent(self): - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', 100000))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', 100000))) self.assertEqual(response.status_code, 404) def test_create_nonpagetype(self): - response = self.client.get(reverse('wagtailadmin_pages:create', args=('wagtailimages', 'image', self.root_page.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('wagtailimages', 'image', self.root_page.id))) self.assertEqual(response.status_code, 404) def test_preview_on_create(self): @@ -408,7 +408,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'slug': 'hello-world', 'action-submit': "Submit", } - response = self.client.post(reverse('wagtailadmin_pages:preview_on_create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:preview_on_add', args=('tests', 'simplepage', self.root_page.id)), post_data) # Check the response self.assertEqual(response.status_code, 200) @@ -428,7 +428,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'action-submit': "Submit", 'seo_title': '\t', } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) # Check that a form error was raised self.assertFormError(response, 'form', 'title', "Value cannot be entirely whitespace characters") @@ -444,7 +444,7 @@ class TestPageCreation(TestCase, WagtailTestUtils): 'hello-world-hello-world-hello-world-hello-world-hello-world-hello-world', 'action-submit': "Submit", } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.root_page.id)), post_data) # Check that a form error was raised self.assertEqual(response.status_code, 200) @@ -1759,25 +1759,25 @@ class TestSubpageBusinessRules(TestCase, WagtailTestUtils): def test_cannot_add_invalid_subpage_type(self): # cannot add StandardChild as a child of BusinessIndex, as StandardChild is not present in subpage_types - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'standardchild', self.business_index.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'standardchild', self.business_index.id))) self.assertEqual(response.status_code, 403) # likewise for BusinessChild which has an empty subpage_types list - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'standardchild', self.business_child.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'standardchild', self.business_child.id))) self.assertEqual(response.status_code, 403) # cannot add BusinessChild to StandardIndex, as BusinessChild restricts is parent page types - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'businesschild', self.standard_index.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'businesschild', self.standard_index.id))) self.assertEqual(response.status_code, 403) # but we can add a BusinessChild to BusinessIndex - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'businesschild', self.business_index.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'businesschild', self.business_index.id))) self.assertEqual(response.status_code, 200) def test_not_prompted_for_page_type_when_only_one_choice(self): response = self.client.get(reverse('wagtailadmin_pages:add_subpage', args=(self.business_subindex.id, ))) # BusinessChild is the only valid subpage type of BusinessSubIndex, so redirect straight there - self.assertRedirects(response, reverse('wagtailadmin_pages:create', args=('tests', 'businesschild', self.business_subindex.id))) + self.assertRedirects(response, reverse('wagtailadmin_pages:add', args=('tests', 'businesschild', self.business_subindex.id))) class TestNotificationPreferences(TestCase, WagtailTestUtils): @@ -2159,7 +2159,7 @@ class TestChildRelationsOnSuperclass(TestCase, WagtailTestUtils): self.login() def test_get_create_form(self): - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'standardindex', self.root_page.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'standardindex', self.root_page.id))) self.assertEqual(response.status_code, 200) # Response should include an advert_placements formset labelled Adverts self.assertContains(response, "Adverts") @@ -2176,7 +2176,7 @@ class TestChildRelationsOnSuperclass(TestCase, WagtailTestUtils): 'advert_placements-0-colour': 'yellow', 'advert_placements-0-id': '', } - response = self.client.post(reverse('wagtailadmin_pages:create', args=('tests', 'standardindex', self.root_page.id)), post_data) + response = self.client.post(reverse('wagtailadmin_pages:add', args=('tests', 'standardindex', self.root_page.id)), post_data) # Find the page and check it page = Page.objects.get(path__startswith=self.root_page.path, slug='new-index').specific diff --git a/wagtail/wagtailadmin/tests/tests.py b/wagtail/wagtailadmin/tests/tests.py index 6a075cdc4..ab3ae83a9 100644 --- a/wagtail/wagtailadmin/tests/tests.py +++ b/wagtail/wagtailadmin/tests/tests.py @@ -44,7 +44,7 @@ class TestEditorHooks(TestCase, WagtailTestUtils): self.login() def test_editor_css_and_js_hooks_on_add(self): - response = self.client.get(reverse('wagtailadmin_pages:create', args=('tests', 'simplepage', self.homepage.id))) + response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'simplepage', self.homepage.id))) self.assertEqual(response.status_code, 200) self.assertContains(response, '') self.assertContains(response, '') diff --git a/wagtail/wagtailadmin/urls.py b/wagtail/wagtailadmin/urls.py index 965256ccd..8618a94a3 100644 --- a/wagtail/wagtailadmin/urls.py +++ b/wagtail/wagtailadmin/urls.py @@ -21,8 +21,8 @@ urlpatterns = [ url(r'^pages/(\d+)/$', pages.index, name='wagtailadmin_explore'), url(r'^pages/', include([ - url(r'^new/(\w+)/(\w+)/(\d+)/$', pages.create, name='create'), - url(r'^new/(\w+)/(\w+)/(\d+)/preview/$', pages.preview_on_create, name='preview_on_create'), + url(r'^add/(\w+)/(\w+)/(\d+)/$', pages.create, name='add'), + url(r'^add/(\w+)/(\w+)/(\d+)/preview/$', pages.preview_on_create, name='preview_on_add'), url(r'^usage/(\w+)/(\w+)/$', pages.content_type_use, name='type_use'), url(r'^(\d+)/edit/$', pages.edit, name='edit'), diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py index 12edac7f9..f774b9e5e 100644 --- a/wagtail/wagtailadmin/views/pages.py +++ b/wagtail/wagtailadmin/views/pages.py @@ -79,7 +79,7 @@ def add_subpage(request, parent_page_id): # Only one page type is available - redirect straight to the create form rather than # making the user choose content_type = page_types[0] - return redirect('wagtailadmin_pages:create', content_type.app_label, content_type.model, parent_page.id) + return redirect('wagtailadmin_pages:add', content_type.app_label, content_type.model, parent_page.id) return render(request, 'wagtailadmin/pages/add_subpage.html', { 'parent_page': parent_page, diff --git a/wagtail/wagtaildocs/admin_urls.py b/wagtail/wagtaildocs/admin_urls.py index 929a86861..dd0acb7b4 100644 --- a/wagtail/wagtaildocs/admin_urls.py +++ b/wagtail/wagtaildocs/admin_urls.py @@ -4,7 +4,7 @@ from wagtail.wagtaildocs.views import documents, chooser urlpatterns = [ url(r'^$', documents.index, name='index'), - url(r'^add/$', documents.add, name='add_document'), + url(r'^add/$', documents.add, name='add'), url(r'^edit/(\d+)/$', documents.edit, name='edit_document'), url(r'^delete/(\d+)/$', documents.delete, name='delete_document'), diff --git a/wagtail/wagtaildocs/templates/wagtaildocs/documents/add.html b/wagtail/wagtaildocs/templates/wagtaildocs/documents/add.html index 9d8593753..1da34859c 100644 --- a/wagtail/wagtaildocs/templates/wagtaildocs/documents/add.html +++ b/wagtail/wagtaildocs/templates/wagtaildocs/documents/add.html @@ -16,7 +16,7 @@ {% include "wagtailadmin/shared/header.html" with title=add_str icon="doc-full-inverse" %}
      - + {% csrf_token %}
        {% for field in form %} diff --git a/wagtail/wagtaildocs/templates/wagtaildocs/documents/index.html b/wagtail/wagtaildocs/templates/wagtaildocs/documents/index.html index b913d72ab..8bacdf626 100644 --- a/wagtail/wagtaildocs/templates/wagtaildocs/documents/index.html +++ b/wagtail/wagtaildocs/templates/wagtaildocs/documents/index.html @@ -15,7 +15,7 @@ {% block content %} {% trans "Documents" as doc_str %} {% trans "Add a document" as add_doc_str %} - {% include "wagtailadmin/shared/header.html" with title=doc_str add_link="wagtaildocs:add_document" icon="doc-full-inverse" add_text=add_doc_str search_url="wagtaildocs:index" %} + {% include "wagtailadmin/shared/header.html" with title=doc_str add_link="wagtaildocs:add" icon="doc-full-inverse" add_text=add_doc_str search_url="wagtaildocs:index" %}
        diff --git a/wagtail/wagtaildocs/templates/wagtaildocs/documents/results.html b/wagtail/wagtaildocs/templates/wagtaildocs/documents/results.html index 965be5b17..6a258860d 100644 --- a/wagtail/wagtaildocs/templates/wagtaildocs/documents/results.html +++ b/wagtail/wagtaildocs/templates/wagtaildocs/documents/results.html @@ -17,7 +17,7 @@ {% if is_searching %}

        {% blocktrans %}Sorry, no documents match "{{ query_string }}"{% endblocktrans %}

        {% else %} - {% url 'wagtaildocs:add_document' as wagtaildocs_add_document_url %} + {% url 'wagtaildocs:add' as wagtaildocs_add_document_url %}

        {% blocktrans %}You haven't uploaded any documents. Why not upload one now?{% endblocktrans %}

        {% endif %} {% endif %} diff --git a/wagtail/wagtaildocs/tests.py b/wagtail/wagtaildocs/tests.py index 677b2abb1..f1edcd206 100644 --- a/wagtail/wagtaildocs/tests.py +++ b/wagtail/wagtaildocs/tests.py @@ -124,7 +124,7 @@ class TestDocumentAddView(TestCase, WagtailTestUtils): self.login() def test_simple(self): - response = self.client.get(reverse('wagtaildocs:add_document')) + response = self.client.get(reverse('wagtaildocs:add')) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'wagtaildocs/documents/add.html') @@ -138,7 +138,7 @@ class TestDocumentAddView(TestCase, WagtailTestUtils): 'title': "Test document", 'file': fake_file, } - response = self.client.post(reverse('wagtaildocs:add_document'), post_data) + response = self.client.post(reverse('wagtaildocs:add'), post_data) # User should be redirected back to the index self.assertRedirects(response, reverse('wagtaildocs:index')) @@ -470,7 +470,7 @@ class TestIssue613(TestCase, WagtailTestUtils): 'file': fake_file, } post_data.update(params) - response = self.client.post(reverse('wagtaildocs:add_document'), post_data) + response = self.client.post(reverse('wagtaildocs:add'), post_data) # User should be redirected back to the index self.assertRedirects(response, reverse('wagtaildocs:index')) diff --git a/wagtail/wagtailimages/admin_urls.py b/wagtail/wagtailimages/admin_urls.py index de64fbdbf..7e60ade54 100644 --- a/wagtail/wagtailimages/admin_urls.py +++ b/wagtail/wagtailimages/admin_urls.py @@ -10,7 +10,7 @@ urlpatterns = [ url(r'^(\d+)/generate_url/$', images.url_generator, name='url_generator'), url(r'^(\d+)/generate_url/(.*)/$', images.generate_url, name='generate_url'), url(r'^(\d+)/preview/(.*)/$', images.preview, name='preview'), - url(r'^add/$', images.add, name='add_image'), + url(r'^add/$', images.add, name='add'), url(r'^usage/(\d+)/$', images.usage, name='image_usage'), url(r'^multiple/add/$', multiple.add, name='add_multiple'), diff --git a/wagtail/wagtailimages/templates/wagtailimages/images/add.html b/wagtail/wagtailimages/templates/wagtailimages/images/add.html index bdfea0f38..d8c88d8a2 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/images/add.html +++ b/wagtail/wagtailimages/templates/wagtailimages/images/add.html @@ -16,7 +16,7 @@ {% include "wagtailadmin/shared/header.html" with title=add_str icon="image" %}
        - + {% csrf_token %}
          {% for field in form %} diff --git a/wagtail/wagtailimages/templates/wagtailimages/multiple/add.html b/wagtail/wagtailimages/templates/wagtailimages/multiple/add.html index e52665d96..9e6c3cec6 100644 --- a/wagtail/wagtailimages/templates/wagtailimages/multiple/add.html +++ b/wagtail/wagtailimages/templates/wagtailimages/multiple/add.html @@ -72,7 +72,7 @@ {% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %} -{% endblock %} diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/confirm_delete.html b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/confirm_delete.html deleted file mode 100644 index 62cff8594..000000000 --- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/confirm_delete.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "wagtailadmin/base.html" %} -{% load i18n %} -{% block titletag %}{% blocktrans with query=query.query_string %}Delete {{ query }}{% endblocktrans %}{% endblock %} -{% block content %} - {% trans "Delete" as delete_str %} - {% include "wagtailadmin/shared/header.html" with title=delete_str subtitle=query.query_string %} - -
          -

          {% trans "Are you sure you want to delete all promoted results for this search term?" %}

          - - {% csrf_token %} - - -
          -{% endblock %} diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/edit.html b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/edit.html deleted file mode 100644 index fe4b4593c..000000000 --- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/edit.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "wagtailadmin/base.html" %} -{% load i18n %} -{% block titletag %}{% blocktrans with query=query.query_string %}Editing {{ query }}{% endblocktrans %}{% endblock %} -{% block content %} - {% trans "Editing" as editing_str %} - {% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=query.query_string icon="pick" %} - -
          - {% csrf_token %} - -
            -
          • - {% include "wagtailsearch/queries/chooser_field.html" with field=query_form.query_string only %} -
          • -
          • - {% include "wagtailsearch/editorspicks/includes/editorspicks_formset.html" with formset=editors_pick_formset only %} -
          • -
          • - - {% trans "Delete" %} -
          • -
          -
          -{% endblock %} - -{% block extra_css %} - {% include "wagtailadmin/pages/_editor_css.html" %} -{% endblock %} -{% block extra_js %} - {% include "wagtailadmin/pages/_editor_js.html" %} - - -{% endblock %} \ No newline at end of file diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_form.html b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_form.html deleted file mode 100644 index 67f3d2103..000000000 --- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_form.html +++ /dev/null @@ -1,24 +0,0 @@ -{% load i18n %} - diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_formset.html b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_formset.html deleted file mode 100644 index 0d4dec9aa..000000000 --- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_formset.html +++ /dev/null @@ -1,17 +0,0 @@ -{% load i18n wagtailadmin_tags %} -{{ formset.management_form }} -
            - {% for form in formset.forms %} - {% include "wagtailsearch/editorspicks/includes/editorspicks_form.html" with form=form only %} - {% endfor %} -
          - - - -

          - {% trans "Add recommended page" %} -

          diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_formset.js b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_formset.js deleted file mode 100644 index 3bf447c1f..000000000 --- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_formset.js +++ /dev/null @@ -1,13 +0,0 @@ -$(function() { - var panel = InlinePanel({ - formsetPrefix: "id_{{ formset.prefix }}", - emptyChildFormPrefix: "{{ formset.empty_form.prefix }}", - canOrder: true - }); - - {% for form in formset.forms %} - panel.initChildControls('{{ formset.prefix }}-{{ forloop.counter0 }}'); - {% endfor %} - - panel.updateMoveButtonDisabledStates(); -}); diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/index.html b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/index.html deleted file mode 100644 index e8e72f238..000000000 --- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/index.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "wagtailadmin/base.html" %} -{% load i18n %} -{% block titletag %}{% trans "Search Terms" %}{% endblock %} -{% block bodyclass %}menu-editorspicks{% endblock %} - -{% block extra_js %} - -{% endblock %} - -{% block content %} - {% trans "Promoted search results" as sp_title_str %} - {% trans "Add new promoted result" as sp_text_str %} - {% include "wagtailadmin/shared/header.html" with title=sp_title_str add_link="wagtailsearch_editorspicks_add" icon="pick" add_text=sp_text_str search_url="wagtailsearch_editorspicks_index" %} - -
          -
          - {% include "wagtailsearch/editorspicks/results.html" %} -
          -
          -{% endblock %} \ No newline at end of file diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/list.html b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/list.html deleted file mode 100644 index 59953db6f..000000000 --- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/list.html +++ /dev/null @@ -1,30 +0,0 @@ -{% load i18n %} - - - - - - - - - - - - - {% for query in queries %} - - - - - - {% endfor %} - -
          {% trans "Search term(s)" %}{% trans "Promoted results" %}{% trans "Views (past week)" %}
          -

          {{ query.query_string }}

          -
          - {% for editors_pick in query.editors_picks.all %} - {{ editors_pick.page.title }}{% if not forloop.last %}, {% endif %} - {% empty %} - {% trans "None" %} - {% endfor %} - {{ query.hits }}
          \ No newline at end of file diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/results.html b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/results.html deleted file mode 100644 index 390b18907..000000000 --- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/results.html +++ /dev/null @@ -1,23 +0,0 @@ -{% load i18n %} -{% if queries %} - {% if is_searching %} -

          - {% blocktrans count counter=queries|length %} - There is one match - {% plural %} - There are {{ counter }} matches - {% endblocktrans %} -

          - {% endif %} - - {% include "wagtailsearch/editorspicks/list.html" %} - - {% include "wagtailadmin/shared/pagination_nav.html" with items=queries is_searching=is_searching linkurl="wagtailsearch_editorspicks_index" %} -{% else %} - {% if is_searching %} -

          {% blocktrans %}Sorry, no promoted results match "{{ query_string }}"{% endblocktrans %}

          - {% else %} - {% url 'wagtailsearch_editorspicks_add' as wagtailsearch_editorspicks_add_url %} -

          {% blocktrans %}No promoted results have been created. Why not add one?{% endblocktrans %}

          - {% endif %} -{% endif %} diff --git a/wagtail/wagtailsearch/tests/test_editorspicks.py b/wagtail/wagtailsearch/tests/test_editorspicks.py deleted file mode 100644 index 0888d115c..000000000 --- a/wagtail/wagtailsearch/tests/test_editorspicks.py +++ /dev/null @@ -1,311 +0,0 @@ -from django.test import TestCase -from django.core.urlresolvers import reverse - -from wagtail.tests.utils import WagtailTestUtils -from wagtail.wagtailsearch import models - - -class TestEditorsPicks(TestCase): - def test_editors_pick_create(self): - # Create an editors pick to the root page - models.EditorsPick.objects.create( - query=models.Query.get("root page"), - page_id=1, - sort_order=0, - description="First editors pick", - ) - - # Check - self.assertEqual(models.Query.get("root page").editors_picks.count(), 1) - self.assertEqual(models.Query.get("root page").editors_picks.first().page_id, 1) - - def test_editors_pick_ordering(self): - # Add 3 editors picks in a different order to their sort_order values - # They should be ordered by their sort order values and not their insertion order - models.EditorsPick.objects.create( - query=models.Query.get("root page"), - page_id=1, - sort_order=0, - description="First editors pick", - ) - models.EditorsPick.objects.create( - query=models.Query.get("root page"), - page_id=1, - sort_order=2, - description="Last editors pick", - ) - models.EditorsPick.objects.create( - query=models.Query.get("root page"), - page_id=1, - sort_order=1, - description="Middle editors pick", - ) - - # Check - self.assertEqual(models.Query.get("root page").editors_picks.count(), 3) - self.assertEqual(models.Query.get("root page").editors_picks.first().description, "First editors pick") - self.assertEqual(models.Query.get("root page").editors_picks.last().description, "Last editors pick") - - -class TestEditorsPicksIndexView(TestCase, WagtailTestUtils): - def setUp(self): - self.login() - - def test_simple(self): - response = self.client.get(reverse('wagtailsearch_editorspicks_index')) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/index.html') - - def test_search(self): - response = self.client.get(reverse('wagtailsearch_editorspicks_index'), {'q': "Hello"}) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['query_string'], "Hello") - - def make_editors_picks(self): - for i in range(50): - models.EditorsPick.objects.create( - query=models.Query.get("query " + str(i)), - page_id=1, - sort_order=0, - description="First editors pick", - ) - - def test_pagination(self): - self.make_editors_picks() - - response = self.client.get(reverse('wagtailsearch_editorspicks_index'), {'p': 2}) - - # Check response - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/index.html') - - # Check that we got the correct page - self.assertEqual(response.context['queries'].number, 2) - - def test_pagination_invalid(self): - self.make_editors_picks() - - response = self.client.get(reverse('wagtailsearch_editorspicks_index'), {'p': 'Hello World!'}) - - # Check response - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/index.html') - - # Check that we got page one - self.assertEqual(response.context['queries'].number, 1) - - def test_pagination_out_of_range(self): - self.make_editors_picks() - - response = self.client.get(reverse('wagtailsearch_editorspicks_index'), {'p': 99999}) - - # Check response - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/index.html') - - # Check that we got the last page - self.assertEqual(response.context['queries'].number, response.context['queries'].paginator.num_pages) - - -class TestEditorsPicksAddView(TestCase, WagtailTestUtils): - def setUp(self): - self.login() - - def test_simple(self): - response = self.client.get(reverse('wagtailsearch_editorspicks_add')) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/add.html') - - def test_post(self): - # Submit - post_data = { - 'query_string': "test", - 'editors_picks-TOTAL_FORMS': 1, - 'editors_picks-INITIAL_FORMS': 0, - 'editors_picks-MAX_NUM_FORMS': 1000, - 'editors_picks-0-DELETE': '', - 'editors_picks-0-ORDER': 0, - 'editors_picks-0-page': 1, - 'editors_picks-0-description': "Hello", - } - response = self.client.post(reverse('wagtailsearch_editorspicks_add'), post_data) - - # User should be redirected back to the index - self.assertRedirects(response, reverse('wagtailsearch_editorspicks_index')) - - # Check that the editors pick was created - self.assertTrue(models.Query.get('test').editors_picks.filter(page_id=1).exists()) - - def test_post_without_recommendations(self): - # Submit - post_data = { - 'query_string': "test", - 'editors_picks-TOTAL_FORMS': 0, - 'editors_picks-INITIAL_FORMS': 0, - 'editors_picks-MAX_NUM_FORMS': 1000, - } - response = self.client.post(reverse('wagtailsearch_editorspicks_add'), post_data) - - # User should be given an error - self.assertEqual(response.status_code, 200) - self.assertFormsetError(response, 'editors_pick_formset', None, None, "Please specify at least one recommendation for this search term.") - - -class TestEditorsPicksEditView(TestCase, WagtailTestUtils): - def setUp(self): - self.login() - - # Create an editors pick to edit - self.query = models.Query.get("Hello") - self.editors_pick = self.query.editors_picks.create(page_id=1, description="Root page") - self.editors_pick_2 = self.query.editors_picks.create(page_id=2, description="Homepage") - - def test_simple(self): - response = self.client.get(reverse('wagtailsearch_editorspicks_edit', args=(self.query.id, ))) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/edit.html') - - def test_post(self): - # Submit - post_data = { - 'query_string': "Hello", - 'editors_picks-TOTAL_FORMS': 2, - 'editors_picks-INITIAL_FORMS': 2, - 'editors_picks-MAX_NUM_FORMS': 1000, - 'editors_picks-0-id': self.editors_pick.id, - 'editors_picks-0-DELETE': '', - 'editors_picks-0-ORDER': 0, - 'editors_picks-0-page': 1, - 'editors_picks-0-description': "Description has changed", # Change - 'editors_picks-1-id': self.editors_pick_2.id, - 'editors_picks-1-DELETE': '', - 'editors_picks-1-ORDER': 1, - 'editors_picks-1-page': 2, - 'editors_picks-1-description': "Homepage", - } - response = self.client.post(reverse('wagtailsearch_editorspicks_edit', args=(self.query.id, )), post_data) - - # User should be redirected back to the index - self.assertRedirects(response, reverse('wagtailsearch_editorspicks_index')) - - # Check that the editors pick description was edited - self.assertEqual(models.EditorsPick.objects.get(id=self.editors_pick.id).description, "Description has changed") - - def test_post_reorder(self): - # Check order before reordering - self.assertEqual(models.Query.get("Hello").editors_picks.all()[0], self.editors_pick) - self.assertEqual(models.Query.get("Hello").editors_picks.all()[1], self.editors_pick_2) - - # Submit - post_data = { - 'query_string': "Hello", - 'editors_picks-TOTAL_FORMS': 2, - 'editors_picks-INITIAL_FORMS': 2, - 'editors_picks-MAX_NUM_FORMS': 1000, - 'editors_picks-0-id': self.editors_pick.id, - 'editors_picks-0-DELETE': '', - 'editors_picks-0-ORDER': 1, # Change - 'editors_picks-0-page': 1, - 'editors_picks-0-description': "Root page", - 'editors_picks-1-id': self.editors_pick_2.id, - 'editors_picks-1-DELETE': '', - 'editors_picks-1-ORDER': 0, # Change - 'editors_picks-1-page': 2, - 'editors_picks-1-description': "Homepage", - } - response = self.client.post(reverse('wagtailsearch_editorspicks_edit', args=(self.query.id, )), post_data) - - # User should be redirected back to the index - self.assertRedirects(response, reverse('wagtailsearch_editorspicks_index')) - - # Check that the ordering has been saved correctly - self.assertEqual(models.EditorsPick.objects.get(id=self.editors_pick.id).sort_order, 1) - self.assertEqual(models.EditorsPick.objects.get(id=self.editors_pick_2.id).sort_order, 0) - - # Check that the recommendations were reordered - self.assertEqual(models.Query.get("Hello").editors_picks.all()[0], self.editors_pick_2) - self.assertEqual(models.Query.get("Hello").editors_picks.all()[1], self.editors_pick) - - def test_post_delete_recommendation(self): - # Submit - post_data = { - 'query_string': "Hello", - 'editors_picks-TOTAL_FORMS': 2, - 'editors_picks-INITIAL_FORMS': 2, - 'editors_picks-MAX_NUM_FORMS': 1000, - 'editors_picks-0-id': self.editors_pick.id, - 'editors_picks-0-DELETE': '', - 'editors_picks-0-ORDER': 0, - 'editors_picks-0-page': 1, - 'editors_picks-0-description': "Root page", - 'editors_picks-1-id': self.editors_pick_2.id, - 'editors_picks-1-DELETE': 1, - 'editors_picks-1-ORDER': 1, - 'editors_picks-1-page': 2, - 'editors_picks-1-description': "Homepage", - } - response = self.client.post(reverse('wagtailsearch_editorspicks_edit', args=(self.query.id, )), post_data) - - # User should be redirected back to the index - self.assertRedirects(response, reverse('wagtailsearch_editorspicks_index')) - - # Check that the recommendation was deleted - self.assertFalse(models.EditorsPick.objects.filter(id=self.editors_pick_2.id).exists()) - - # The other recommendation should still exist - self.assertTrue(models.EditorsPick.objects.filter(id=self.editors_pick.id).exists()) - - def test_post_without_recommendations(self): - # Submit - post_data = { - 'query_string': "Hello", - 'editors_picks-TOTAL_FORMS': 2, - 'editors_picks-INITIAL_FORMS': 2, - 'editors_picks-MAX_NUM_FORMS': 1000, - 'editors_picks-0-id': self.editors_pick.id, - 'editors_picks-0-DELETE': 1, - 'editors_picks-0-ORDER': 0, - 'editors_picks-0-page': 1, - 'editors_picks-0-description': "Description has changed", # Change - 'editors_picks-1-id': self.editors_pick_2.id, - 'editors_picks-1-DELETE': 1, - 'editors_picks-1-ORDER': 1, - 'editors_picks-1-page': 2, - 'editors_picks-1-description': "Homepage", - } - response = self.client.post(reverse('wagtailsearch_editorspicks_edit', args=(self.query.id, )), post_data) - - # User should be given an error - self.assertEqual(response.status_code, 200) - self.assertFormsetError(response, 'editors_pick_formset', None, None, "Please specify at least one recommendation for this search term.") - - -class TestEditorsPicksDeleteView(TestCase, WagtailTestUtils): - def setUp(self): - self.login() - - # Create an editors pick to delete - self.query = models.Query.get("Hello") - self.editors_pick = self.query.editors_picks.create(page_id=1, description="Root page") - self.editors_pick_2 = self.query.editors_picks.create(page_id=2, description="Homepage") - - def test_simple(self): - response = self.client.get(reverse('wagtailsearch_editorspicks_delete', args=(self.query.id, ))) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearch/editorspicks/confirm_delete.html') - - def test_post(self): - # Submit - post_data = { - 'foo': 'bar', - } - response = self.client.post(reverse('wagtailsearch_editorspicks_delete', args=(self.query.id, )), post_data) - - # User should be redirected back to the index - self.assertRedirects(response, reverse('wagtailsearch_editorspicks_index')) - - # Check that both recommendations were deleted - self.assertFalse(models.EditorsPick.objects.filter(id=self.editors_pick_2.id).exists()) - - # The other recommendation should still exist - self.assertFalse(models.EditorsPick.objects.filter(id=self.editors_pick.id).exists()) diff --git a/wagtail/wagtailsearch/urls/admin.py b/wagtail/wagtailsearch/urls/admin.py index 62b9d72c6..4944d72be 100644 --- a/wagtail/wagtailsearch/urls/admin.py +++ b/wagtail/wagtailsearch/urls/admin.py @@ -1,13 +1,8 @@ from django.conf.urls import url -from wagtail.wagtailsearch.views import editorspicks, queries +from wagtail.wagtailsearch.views import queries urlpatterns = [ - url(r"^editorspicks/$", editorspicks.index, name="wagtailsearch_editorspicks_index"), - url(r"^editorspicks/add/$", editorspicks.add, name="wagtailsearch_editorspicks_add"), - url(r"^editorspicks/(\d+)/$", editorspicks.edit, name="wagtailsearch_editorspicks_edit"), - url(r"^editorspicks/(\d+)/delete/$", editorspicks.delete, name="wagtailsearch_editorspicks_delete"), - url(r"^queries/chooser/$", queries.chooser, name="wagtailsearch_queries_chooser"), url(r"^queries/chooser/results/$", queries.chooserresults, name="wagtailsearch_queries_chooserresults"), ] diff --git a/wagtail/wagtailsearch/views/editorspicks.py b/wagtail/wagtailsearch/views/editorspicks.py deleted file mode 100644 index aeab32e4e..000000000 --- a/wagtail/wagtailsearch/views/editorspicks.py +++ /dev/null @@ -1,147 +0,0 @@ -from django.shortcuts import render, redirect, get_object_or_404 - -from django.core.urlresolvers import reverse -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger -from django.utils.translation import ugettext as _ -from django.views.decorators.vary import vary_on_headers - -from wagtail.wagtailsearch import models, forms -from wagtail.wagtailadmin.forms import SearchForm -from wagtail.wagtailadmin import messages - - -@vary_on_headers('X-Requested-With') -def index(request): - is_searching = False - page = request.GET.get('p', 1) - query_string = request.GET.get('q', "") - - queries = models.Query.objects.filter(editors_picks__isnull=False).distinct() - - # Search - if query_string: - queries = queries.filter(query_string__icontains=query_string) - is_searching = True - - # Pagination - paginator = Paginator(queries, 20) - try: - queries = paginator.page(page) - except PageNotAnInteger: - queries = paginator.page(1) - except EmptyPage: - queries = paginator.page(paginator.num_pages) - - if request.is_ajax(): - return render(request, "wagtailsearch/editorspicks/results.html", { - 'is_searching': is_searching, - 'queries': queries, - 'query_string': query_string, - }) - else: - return render(request, 'wagtailsearch/editorspicks/index.html', { - 'is_searching': is_searching, - 'queries': queries, - 'query_string': query_string, - 'search_form': SearchForm(data=dict(q=query_string) if query_string else None, placeholder=_("Search editor's picks")), - }) - - -def save_editorspicks(query, new_query, editors_pick_formset): - # Save - if editors_pick_formset.is_valid(): - # Set sort_order - for i, form in enumerate(editors_pick_formset.ordered_forms): - form.instance.sort_order = i - - # Make sure the form is marked as changed so it gets saved with the new order - form.has_changed = lambda: True - - editors_pick_formset.save() - - # If query was changed, move all editors picks to the new query - if query != new_query: - editors_pick_formset.get_queryset().update(query=new_query) - - return True - else: - return False - - -def add(request): - if request.POST: - # Get query - query_form = forms.QueryForm(request.POST) - if query_form.is_valid(): - query = models.Query.get(query_form['query_string'].value()) - - # Save editors picks - editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query) - if save_editorspicks(query, query, editors_pick_formset): - messages.success(request, _("Editor's picks for '{0}' created.").format(query), buttons=[ - messages.button(reverse('wagtailsearch_editorspicks_edit', args=(query.id,)), _('Edit')) - ]) - return redirect('wagtailsearch_editorspicks_index') - else: - if len(editors_pick_formset.non_form_errors()): - messages.error(request, " ".join(error for error in editors_pick_formset.non_form_errors())) # formset level error (e.g. no forms submitted) - else: - messages.error(request, _("Recommendations have not been created due to errors")) # specific errors will be displayed within form fields - else: - editors_pick_formset = forms.EditorsPickFormSet() - else: - query_form = forms.QueryForm() - editors_pick_formset = forms.EditorsPickFormSet() - - return render(request, 'wagtailsearch/editorspicks/add.html', { - 'query_form': query_form, - 'editors_pick_formset': editors_pick_formset, - }) - - -def edit(request, query_id): - query = get_object_or_404(models.Query, id=query_id) - - if request.POST: - # Get query - query_form = forms.QueryForm(request.POST) - # and the recommendations - editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query) - - if query_form.is_valid(): - new_query = models.Query.get(query_form['query_string'].value()) - - # Save editors picks - if save_editorspicks(query, new_query, editors_pick_formset): - messages.success(request, _("Editor's picks for '{0}' updated.").format(new_query), buttons=[ - messages.button(reverse('wagtailsearch_editorspicks_edit', args=(query.id,)), _('Edit')) - ]) - return redirect('wagtailsearch_editorspicks_index') - else: - if len(editors_pick_formset.non_form_errors()): - messages.error(request, " ".join(error for error in editors_pick_formset.non_form_errors())) # formset level error (e.g. no forms submitted) - else: - messages.error(request, _("Recommendations have not been saved due to errors")) # specific errors will be displayed within form fields - - else: - query_form = forms.QueryForm(initial=dict(query_string=query.query_string)) - editors_pick_formset = forms.EditorsPickFormSet(instance=query) - - return render(request, 'wagtailsearch/editorspicks/edit.html', { - 'query_form': query_form, - 'editors_pick_formset': editors_pick_formset, - 'query': query, - }) - - -def delete(request, query_id): - query = get_object_or_404(models.Query, id=query_id) - - if request.POST: - query.editors_picks.all().delete() - messages.success(request, _("Editor's picks deleted.")) - return redirect('wagtailsearch_editorspicks_index') - - return render(request, 'wagtailsearch/editorspicks/confirm_delete.html', { - 'query': query, - }) diff --git a/wagtail/wagtailsearch/wagtail_hooks.py b/wagtail/wagtailsearch/wagtail_hooks.py index 1b23a8def..f5c1544cc 100644 --- a/wagtail/wagtailsearch/wagtail_hooks.py +++ b/wagtail/wagtailsearch/wagtail_hooks.py @@ -1,25 +1,11 @@ -from django.core import urlresolvers from django.conf.urls import include, url -from django.utils.translation import ugettext_lazy as _ from wagtail.wagtailcore import hooks from wagtail.wagtailsearch.urls import admin as admin_urls -from wagtail.wagtailadmin.menu import MenuItem - @hooks.register('register_admin_urls') def register_admin_urls(): return [ url(r'^search/', include(admin_urls)), ] - - -class EditorsPicksMenuItem(MenuItem): - def is_shown(self, request): - # TEMPORARY: Only show if the user is a superuser - return request.user.is_superuser - -@hooks.register('register_settings_menu_item') -def register_editors_picks_menu_item(): - return EditorsPicksMenuItem(_('Promoted search results'), urlresolvers.reverse('wagtailsearch_editorspicks_index'), classnames='icon icon-pick', order=900) From 0c7f00e07834ffce04c768a627e1e43dd257615a Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Fri, 24 Apr 2015 10:38:59 +0100 Subject: [PATCH 58/92] Added searchpicks admin views --- .../contrib/wagtailsearchpicks/admin_urls.py | 10 + wagtail/contrib/wagtailsearchpicks/forms.py | 57 ++++ .../templates/wagtailsearchpicks/add.html | 48 +++ .../wagtailsearchpicks/confirm_delete.html | 15 + .../templates/wagtailsearchpicks/edit.html | 40 +++ .../includes/searchpick_form.html | 24 ++ .../includes/searchpicks_formset.html | 17 + .../includes/searchpicks_formset.js | 13 + .../templates/wagtailsearchpicks/index.html | 26 ++ .../templates/wagtailsearchpicks/list.html | 30 ++ .../templates/wagtailsearchpicks/results.html | 23 ++ wagtail/contrib/wagtailsearchpicks/tests.py | 311 ++++++++++++++++++ wagtail/contrib/wagtailsearchpicks/views.py | 149 +++++++++ .../wagtailsearchpicks/wagtail_hooks.py | 26 ++ 14 files changed, 789 insertions(+) create mode 100644 wagtail/contrib/wagtailsearchpicks/admin_urls.py create mode 100644 wagtail/contrib/wagtailsearchpicks/forms.py create mode 100644 wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/add.html create mode 100644 wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/confirm_delete.html create mode 100644 wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/edit.html create mode 100644 wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpick_form.html create mode 100644 wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.html create mode 100644 wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.js create mode 100644 wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/index.html create mode 100644 wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/list.html create mode 100644 wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/results.html create mode 100644 wagtail/contrib/wagtailsearchpicks/tests.py create mode 100644 wagtail/contrib/wagtailsearchpicks/views.py create mode 100644 wagtail/contrib/wagtailsearchpicks/wagtail_hooks.py diff --git a/wagtail/contrib/wagtailsearchpicks/admin_urls.py b/wagtail/contrib/wagtailsearchpicks/admin_urls.py new file mode 100644 index 000000000..32cf91f25 --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/admin_urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from wagtail.contrib.wagtailsearchpicks import views + + +urlpatterns = [ + url(r'^$', views.index, name='index'), + url(r'^add/$', views.add, name='add'), + url(r'^(\d+)/$', views.edit, name='edit'), + url(r'^(\d+)/delete/$', views.delete, name='delete'), +] diff --git a/wagtail/contrib/wagtailsearchpicks/forms.py b/wagtail/contrib/wagtailsearchpicks/forms.py new file mode 100644 index 000000000..6016e33f6 --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/forms.py @@ -0,0 +1,57 @@ +from django import forms +from django.forms.models import inlineformset_factory +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailadmin.widgets import AdminPageChooser +from wagtail.wagtailsearch import models + + +class SearchPickForm(forms.ModelForm): + sort_order = forms.IntegerField(required=False) + + def __init__(self, *args, **kwargs): + super(SearchPickForm, self).__init__(*args, **kwargs) + self.fields['page'].widget = AdminPageChooser() + + class Meta: + model = models.EditorsPick + fields = ('query', 'page', 'description') + + widgets = { + 'description': forms.Textarea(attrs=dict(rows=3)), + } + + +SearchPicksFormSetBase = inlineformset_factory(models.Query, models.EditorsPick, form=SearchPickForm, can_order=True, can_delete=True, extra=0) + + +class SearchPicksFormSet(SearchPicksFormSetBase): + minimum_forms = 1 + minimum_forms_message = _("Please specify at least one recommendation for this search term.") + + def add_fields(self, form, *args, **kwargs): + super(SearchPicksFormSet, self).add_fields(form, *args, **kwargs) + + # Hide delete and order fields + form.fields['DELETE'].widget = forms.HiddenInput() + form.fields['ORDER'].widget = forms.HiddenInput() + + # Remove query field + del form.fields['query'] + + def clean(self): + # Search pick must have at least one recommended page to be valid + # Check there is at least one non-deleted form. + non_deleted_forms = self.total_form_count() + non_empty_forms = 0 + for i in range(0, self.total_form_count()): + form = self.forms[i] + if self.can_delete and self._should_delete_form(form): + non_deleted_forms -= 1 + if not (form.instance.id is None and not form.has_changed()): + non_empty_forms += 1 + if ( + non_deleted_forms < self.minimum_forms + or non_empty_forms < self.minimum_forms + ): + raise forms.ValidationError(self.minimum_forms_message) diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/add.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/add.html new file mode 100644 index 000000000..5915404bf --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/add.html @@ -0,0 +1,48 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% block titletag %}{% trans "Add search pick" %}{% endblock %} +{% block content %} + {% trans "Add search pick" as add_str %} + {% include "wagtailadmin/shared/header.html" with title=add_str icon="pick" %} + +
          +
          + {% blocktrans %} +

          Promoted search results are a means of recommending specific pages that might not organically come high up in search results. E.g recommending your primary donation page to a user searching with the less common term "giving".

          + {% endblocktrans %} + + {% blocktrans %} +

          The "Search term(s)/phrase" field below must contain the full and exact search for which you wish to provide recommended results, including any misspellings/user error. To help, you can choose from search terms that have been popular with users of your site.

          + {% endblocktrans %} +
          +
          + {% csrf_token %} + +
            +
          • + {% include "wagtailsearch/queries/chooser_field.html" with field=query_form.query_string only %} +
          • +
          • + {% include "wagtailsearchpicks/includes/searchpicks_formset.html" with formset=searchpicks_formset only %} +
          • +
          • +
          +
          +
          +{% endblock %} + +{% block extra_css %} + {% include "wagtailadmin/pages/_editor_css.html" %} +{% endblock %} +{% block extra_js %} + {% include "wagtailadmin/pages/_editor_js.html" %} + + +{% endblock %} diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/confirm_delete.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/confirm_delete.html new file mode 100644 index 000000000..eeadc17be --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/confirm_delete.html @@ -0,0 +1,15 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% block titletag %}{% blocktrans with query=query.query_string %}Delete {{ query }}{% endblocktrans %}{% endblock %} +{% block content %} + {% trans "Delete" as delete_str %} + {% include "wagtailadmin/shared/header.html" with title=delete_str subtitle=query.query_string %} + +
          +

          {% trans "Are you sure you want to delete all promoted results for this search term?" %}

          +
          + {% csrf_token %} + +
          +
          +{% endblock %} diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/edit.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/edit.html new file mode 100644 index 000000000..a64892dc1 --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/edit.html @@ -0,0 +1,40 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% block titletag %}{% blocktrans with query=query.query_string %}Editing {{ query }}{% endblocktrans %}{% endblock %} +{% block content %} + {% trans "Editing" as editing_str %} + {% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=query.query_string icon="pick" %} + +
          + {% csrf_token %} + +
            +
          • + {% include "wagtailsearch/queries/chooser_field.html" with field=query_form.query_string only %} +
          • +
          • + {% include "wagtailsearchpicks/includes/searchpicks_formset.html" with formset=searchpicks_formset only %} +
          • +
          • + + {% trans "Delete" %} +
          • +
          +
          +{% endblock %} + +{% block extra_css %} + {% include "wagtailadmin/pages/_editor_css.html" %} +{% endblock %} +{% block extra_js %} + {% include "wagtailadmin/pages/_editor_js.html" %} + + +{% endblock %} diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpick_form.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpick_form.html new file mode 100644 index 000000000..67f3d2103 --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpick_form.html @@ -0,0 +1,24 @@ +{% load i18n %} + diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.html new file mode 100644 index 000000000..3912cf4f5 --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.html @@ -0,0 +1,17 @@ +{% load i18n wagtailadmin_tags %} +{{ formset.management_form }} +
            + {% for form in formset.forms %} + {% include "wagtailsearchpicks/includes/searchpick_form.html" with form=form only %} + {% endfor %} +
          + + + +

          + {% trans "Add recommended page" %} +

          diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.js b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.js new file mode 100644 index 000000000..3bf447c1f --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.js @@ -0,0 +1,13 @@ +$(function() { + var panel = InlinePanel({ + formsetPrefix: "id_{{ formset.prefix }}", + emptyChildFormPrefix: "{{ formset.empty_form.prefix }}", + canOrder: true + }); + + {% for form in formset.forms %} + panel.initChildControls('{{ formset.prefix }}-{{ forloop.counter0 }}'); + {% endfor %} + + panel.updateMoveButtonDisabledStates(); +}); diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/index.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/index.html new file mode 100644 index 000000000..41bc97a62 --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/index.html @@ -0,0 +1,26 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% block titletag %}{% trans "Search Terms" %}{% endblock %} +{% block bodyclass %}menu-editorspicks{% endblock %} + +{% block extra_js %} + +{% endblock %} + +{% block content %} + {% trans "Promoted search results" as sp_title_str %} + {% trans "Add new promoted result" as sp_text_str %} + {% include "wagtailadmin/shared/header.html" with title=sp_title_str add_link="wagtailsearchpicks:add" icon="pick" add_text=sp_text_str search_url="wagtailsearchpicks:index" %} + +
          +
          + {% include "wagtailsearchpicks/results.html" %} +
          +
          +{% endblock %} diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/list.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/list.html new file mode 100644 index 000000000..69d33282a --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/list.html @@ -0,0 +1,30 @@ +{% load i18n %} + + + + + + + + + + + + + {% for query in queries %} + + + + + + {% endfor %} + +
          {% trans "Search term(s)" %}{% trans "Promoted results" %}{% trans "Views (past week)" %}
          +

          {{ query.query_string }}

          +
          + {% for searchpick in query.editors_picks.all %} + {{ searchpick.page.title }}{% if not forloop.last %}, {% endif %} + {% empty %} + {% trans "None" %} + {% endfor %} + {{ query.hits }}
          diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/results.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/results.html new file mode 100644 index 000000000..7bb0b342f --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/results.html @@ -0,0 +1,23 @@ +{% load i18n %} +{% if queries %} + {% if is_searching %} +

          + {% blocktrans count counter=queries|length %} + There is one match + {% plural %} + There are {{ counter }} matches + {% endblocktrans %} +

          + {% endif %} + + {% include "wagtailsearchpicks/list.html" %} + + {% include "wagtailadmin/shared/pagination_nav.html" with items=queries is_searching=is_searching linkurl="wagtailsearchpicks_index" %} +{% else %} + {% if is_searching %} +

          {% blocktrans %}Sorry, no promoted results match "{{ query_string }}"{% endblocktrans %}

          + {% else %} + {% url 'wagtailsearchpicks:add' as wagtailsearchpicks_add_url %} +

          {% blocktrans %}No promoted results have been created. Why not add one?{% endblocktrans %}

          + {% endif %} +{% endif %} diff --git a/wagtail/contrib/wagtailsearchpicks/tests.py b/wagtail/contrib/wagtailsearchpicks/tests.py new file mode 100644 index 000000000..53d1852ee --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/tests.py @@ -0,0 +1,311 @@ +from django.test import TestCase +from django.core.urlresolvers import reverse + +from wagtail.tests.utils import WagtailTestUtils +from wagtail.wagtailsearch import models + + +class TestSearchPicks(TestCase): + def test_search_pick_create(self): + # Create a search pick to the root page + models.EditorsPick.objects.create( + query=models.Query.get("root page"), + page_id=1, + sort_order=0, + description="First search pick", + ) + + # Check + self.assertEqual(models.Query.get("root page").editors_picks.count(), 1) + self.assertEqual(models.Query.get("root page").editors_picks.first().page_id, 1) + + def test_search_pick_ordering(self): + # Add 3 search picks in a different order to their sort_order values + # They should be ordered by their sort order values and not their insertion order + models.EditorsPick.objects.create( + query=models.Query.get("root page"), + page_id=1, + sort_order=0, + description="First search pick", + ) + models.EditorsPick.objects.create( + query=models.Query.get("root page"), + page_id=1, + sort_order=2, + description="Last search pick", + ) + models.EditorsPick.objects.create( + query=models.Query.get("root page"), + page_id=1, + sort_order=1, + description="Middle search pick", + ) + + # Check + self.assertEqual(models.Query.get("root page").editors_picks.count(), 3) + self.assertEqual(models.Query.get("root page").editors_picks.first().description, "First search pick") + self.assertEqual(models.Query.get("root page").editors_picks.last().description, "Last search pick") + + +class TestSearchPicksIndexView(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + + def test_simple(self): + response = self.client.get(reverse('wagtailsearchpicks:index')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsearchpicks/index.html') + + def test_search(self): + response = self.client.get(reverse('wagtailsearchpicks:index'), {'q': "Hello"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['query_string'], "Hello") + + def make_search_picks(self): + for i in range(50): + models.EditorsPick.objects.create( + query=models.Query.get("query " + str(i)), + page_id=1, + sort_order=0, + description="First search pick", + ) + + def test_pagination(self): + self.make_search_picks() + + response = self.client.get(reverse('wagtailsearchpicks:index'), {'p': 2}) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsearchpicks/index.html') + + # Check that we got the correct page + self.assertEqual(response.context['queries'].number, 2) + + def test_pagination_invalid(self): + self.make_search_picks() + + response = self.client.get(reverse('wagtailsearchpicks:index'), {'p': 'Hello World!'}) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsearchpicks/index.html') + + # Check that we got page one + self.assertEqual(response.context['queries'].number, 1) + + def test_pagination_out_of_range(self): + self.make_search_picks() + + response = self.client.get(reverse('wagtailsearchpicks:index'), {'p': 99999}) + + # Check response + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsearchpicks/index.html') + + # Check that we got the last page + self.assertEqual(response.context['queries'].number, response.context['queries'].paginator.num_pages) + + +class TestSearchPicksAddView(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + + def test_simple(self): + response = self.client.get(reverse('wagtailsearchpicks:add')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsearchpicks/add.html') + + def test_post(self): + # Submit + post_data = { + 'query_string': "test", + 'editors_picks-TOTAL_FORMS': 1, + 'editors_picks-INITIAL_FORMS': 0, + 'editors_picks-MAX_NUM_FORMS': 1000, + 'editors_picks-0-DELETE': '', + 'editors_picks-0-ORDER': 0, + 'editors_picks-0-page': 1, + 'editors_picks-0-description': "Hello", + } + response = self.client.post(reverse('wagtailsearchpicks:add'), post_data) + + # User should be redirected back to the index + self.assertRedirects(response, reverse('wagtailsearchpicks:index')) + + # Check that the search pick was created + self.assertTrue(models.Query.get('test').editors_picks.filter(page_id=1).exists()) + + def test_post_without_recommendations(self): + # Submit + post_data = { + 'query_string': "test", + 'editors_picks-TOTAL_FORMS': 0, + 'editors_picks-INITIAL_FORMS': 0, + 'editors_picks-MAX_NUM_FORMS': 1000, + } + response = self.client.post(reverse('wagtailsearchpicks:add'), post_data) + + # User should be given an error + self.assertEqual(response.status_code, 200) + self.assertFormsetError(response, 'searchpicks_formset', None, None, "Please specify at least one recommendation for this search term.") + + +class TestSearchPicksEditView(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + + # Create an search pick to edit + self.query = models.Query.get("Hello") + self.search_pick = self.query.editors_picks.create(page_id=1, description="Root page") + self.search_pick_2 = self.query.editors_picks.create(page_id=2, description="Homepage") + + def test_simple(self): + response = self.client.get(reverse('wagtailsearchpicks:edit', args=(self.query.id, ))) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsearchpicks/edit.html') + + def test_post(self): + # Submit + post_data = { + 'query_string': "Hello", + 'editors_picks-TOTAL_FORMS': 2, + 'editors_picks-INITIAL_FORMS': 2, + 'editors_picks-MAX_NUM_FORMS': 1000, + 'editors_picks-0-id': self.search_pick.id, + 'editors_picks-0-DELETE': '', + 'editors_picks-0-ORDER': 0, + 'editors_picks-0-page': 1, + 'editors_picks-0-description': "Description has changed", # Change + 'editors_picks-1-id': self.search_pick_2.id, + 'editors_picks-1-DELETE': '', + 'editors_picks-1-ORDER': 1, + 'editors_picks-1-page': 2, + 'editors_picks-1-description': "Homepage", + } + response = self.client.post(reverse('wagtailsearchpicks:edit', args=(self.query.id, )), post_data) + + # User should be redirected back to the index + self.assertRedirects(response, reverse('wagtailsearchpicks:index')) + + # Check that the search pick description was edited + self.assertEqual(models.EditorsPick.objects.get(id=self.search_pick.id).description, "Description has changed") + + def test_post_reorder(self): + # Check order before reordering + self.assertEqual(models.Query.get("Hello").editors_picks.all()[0], self.search_pick) + self.assertEqual(models.Query.get("Hello").editors_picks.all()[1], self.search_pick_2) + + # Submit + post_data = { + 'query_string': "Hello", + 'editors_picks-TOTAL_FORMS': 2, + 'editors_picks-INITIAL_FORMS': 2, + 'editors_picks-MAX_NUM_FORMS': 1000, + 'editors_picks-0-id': self.search_pick.id, + 'editors_picks-0-DELETE': '', + 'editors_picks-0-ORDER': 1, # Change + 'editors_picks-0-page': 1, + 'editors_picks-0-description': "Root page", + 'editors_picks-1-id': self.search_pick_2.id, + 'editors_picks-1-DELETE': '', + 'editors_picks-1-ORDER': 0, # Change + 'editors_picks-1-page': 2, + 'editors_picks-1-description': "Homepage", + } + response = self.client.post(reverse('wagtailsearchpicks:edit', args=(self.query.id, )), post_data) + + # User should be redirected back to the index + self.assertRedirects(response, reverse('wagtailsearchpicks:index')) + + # Check that the ordering has been saved correctly + self.assertEqual(models.EditorsPick.objects.get(id=self.search_pick.id).sort_order, 1) + self.assertEqual(models.EditorsPick.objects.get(id=self.search_pick_2.id).sort_order, 0) + + # Check that the recommendations were reordered + self.assertEqual(models.Query.get("Hello").editors_picks.all()[0], self.search_pick_2) + self.assertEqual(models.Query.get("Hello").editors_picks.all()[1], self.search_pick) + + def test_post_delete_recommendation(self): + # Submit + post_data = { + 'query_string': "Hello", + 'editors_picks-TOTAL_FORMS': 2, + 'editors_picks-INITIAL_FORMS': 2, + 'editors_picks-MAX_NUM_FORMS': 1000, + 'editors_picks-0-id': self.search_pick.id, + 'editors_picks-0-DELETE': '', + 'editors_picks-0-ORDER': 0, + 'editors_picks-0-page': 1, + 'editors_picks-0-description': "Root page", + 'editors_picks-1-id': self.search_pick_2.id, + 'editors_picks-1-DELETE': 1, + 'editors_picks-1-ORDER': 1, + 'editors_picks-1-page': 2, + 'editors_picks-1-description': "Homepage", + } + response = self.client.post(reverse('wagtailsearchpicks:edit', args=(self.query.id, )), post_data) + + # User should be redirected back to the index + self.assertRedirects(response, reverse('wagtailsearchpicks:index')) + + # Check that the recommendation was deleted + self.assertFalse(models.EditorsPick.objects.filter(id=self.search_pick_2.id).exists()) + + # The other recommendation should still exist + self.assertTrue(models.EditorsPick.objects.filter(id=self.search_pick.id).exists()) + + def test_post_without_recommendations(self): + # Submit + post_data = { + 'query_string': "Hello", + 'editors_picks-TOTAL_FORMS': 2, + 'editors_picks-INITIAL_FORMS': 2, + 'editors_picks-MAX_NUM_FORMS': 1000, + 'editors_picks-0-id': self.search_pick.id, + 'editors_picks-0-DELETE': 1, + 'editors_picks-0-ORDER': 0, + 'editors_picks-0-page': 1, + 'editors_picks-0-description': "Description has changed", # Change + 'editors_picks-1-id': self.search_pick_2.id, + 'editors_picks-1-DELETE': 1, + 'editors_picks-1-ORDER': 1, + 'editors_picks-1-page': 2, + 'editors_picks-1-description': "Homepage", + } + response = self.client.post(reverse('wagtailsearchpicks:edit', args=(self.query.id, )), post_data) + + # User should be given an error + self.assertEqual(response.status_code, 200) + self.assertFormsetError(response, 'searchpicks_formset', None, None, "Please specify at least one recommendation for this search term.") + + +class TestSearchPicksDeleteView(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + + # Create an search pick to delete + self.query = models.Query.get("Hello") + self.search_pick = self.query.editors_picks.create(page_id=1, description="Root page") + self.search_pick_2 = self.query.editors_picks.create(page_id=2, description="Homepage") + + def test_simple(self): + response = self.client.get(reverse('wagtailsearchpicks:delete', args=(self.query.id, ))) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'wagtailsearchpicks/confirm_delete.html') + + def test_post(self): + # Submit + post_data = { + 'foo': 'bar', + } + response = self.client.post(reverse('wagtailsearchpicks:delete', args=(self.query.id, )), post_data) + + # User should be redirected back to the index + self.assertRedirects(response, reverse('wagtailsearchpicks:index')) + + # Check that both recommendations were deleted + self.assertFalse(models.EditorsPick.objects.filter(id=self.search_pick_2.id).exists()) + + # The other recommendation should still exist + self.assertFalse(models.EditorsPick.objects.filter(id=self.search_pick.id).exists()) diff --git a/wagtail/contrib/wagtailsearchpicks/views.py b/wagtail/contrib/wagtailsearchpicks/views.py new file mode 100644 index 000000000..5ad523d1c --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/views.py @@ -0,0 +1,149 @@ +from django.shortcuts import render, redirect, get_object_or_404 + +from django.core.urlresolvers import reverse +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.utils.translation import ugettext as _ +from django.views.decorators.vary import vary_on_headers + +from wagtail.wagtailsearch import models, forms as search_forms +from wagtail.wagtailadmin.forms import SearchForm +from wagtail.wagtailadmin import messages + +from wagtail.contrib.wagtailsearchpicks import forms + + +@vary_on_headers('X-Requested-With') +def index(request): + is_searching = False + page = request.GET.get('p', 1) + query_string = request.GET.get('q', "") + + queries = models.Query.objects.filter(editors_picks__isnull=False).distinct() + + # Search + if query_string: + queries = queries.filter(query_string__icontains=query_string) + is_searching = True + + # Pagination + paginator = Paginator(queries, 20) + try: + queries = paginator.page(page) + except PageNotAnInteger: + queries = paginator.page(1) + except EmptyPage: + queries = paginator.page(paginator.num_pages) + + if request.is_ajax(): + return render(request, "wagtailsearchpicks/results.html", { + 'is_searching': is_searching, + 'queries': queries, + 'query_string': query_string, + }) + else: + return render(request, 'wagtailsearchpicks/index.html', { + 'is_searching': is_searching, + 'queries': queries, + 'query_string': query_string, + 'search_form': SearchForm(data=dict(q=query_string) if query_string else None, placeholder=_("Search editor's picks")), + }) + + +def save_searchpicks(query, new_query, searchpicks_formset): + # Save + if searchpicks_formset.is_valid(): + # Set sort_order + for i, form in enumerate(searchpicks_formset.ordered_forms): + form.instance.sort_order = i + + # Make sure the form is marked as changed so it gets saved with the new order + form.has_changed = lambda: True + + searchpicks_formset.save() + + # If query was changed, move all search picks to the new query + if query != new_query: + searchpicks_formset.get_queryset().update(query=new_query) + + return True + else: + return False + + +def add(request): + if request.POST: + # Get query + query_form = search_forms.QueryForm(request.POST) + if query_form.is_valid(): + query = models.Query.get(query_form['query_string'].value()) + + # Save search picks + searchpicks_formset = forms.SearchPicksFormSet(request.POST, instance=query) + if save_searchpicks(query, query, searchpicks_formset): + messages.success(request, _("Editor's picks for '{0}' created.").format(query), buttons=[ + messages.button(reverse('wagtailsearchpicks:edit', args=(query.id,)), _('Edit')) + ]) + return redirect('wagtailsearchpicks:index') + else: + if len(searchpicks_formset.non_form_errors()): + messages.error(request, " ".join(error for error in searchpicks_formset.non_form_errors())) # formset level error (e.g. no forms submitted) + else: + messages.error(request, _("Recommendations have not been created due to errors")) # specific errors will be displayed within form fields + else: + searchpicks_formset = forms.SearchPicksFormSet() + else: + query_form = search_forms.QueryForm() + searchpicks_formset = forms.SearchPicksFormSet() + + return render(request, 'wagtailsearchpicks/add.html', { + 'query_form': query_form, + 'searchpicks_formset': searchpicks_formset, + }) + + +def edit(request, query_id): + query = get_object_or_404(models.Query, id=query_id) + + if request.POST: + # Get query + query_form = search_forms.QueryForm(request.POST) + # and the recommendations + searchpicks_formset = forms.SearchPicksFormSet(request.POST, instance=query) + + if query_form.is_valid(): + new_query = models.Query.get(query_form['query_string'].value()) + + # Save search picks + if save_searchpicks(query, new_query, searchpicks_formset): + messages.success(request, _("Editor's picks for '{0}' updated.").format(new_query), buttons=[ + messages.button(reverse('wagtailsearchpicks:edit', args=(query.id,)), _('Edit')) + ]) + return redirect('wagtailsearchpicks:index') + else: + if len(searchpicks_formset.non_form_errors()): + messages.error(request, " ".join(error for error in searchpicks_formset.non_form_errors())) # formset level error (e.g. no forms submitted) + else: + messages.error(request, _("Recommendations have not been saved due to errors")) # specific errors will be displayed within form fields + + else: + query_form = search_forms.QueryForm(initial=dict(query_string=query.query_string)) + searchpicks_formset = forms.SearchPicksFormSet(instance=query) + + return render(request, 'wagtailsearchpicks/edit.html', { + 'query_form': query_form, + 'searchpicks_formset': searchpicks_formset, + 'query': query, + }) + + +def delete(request, query_id): + query = get_object_or_404(models.Query, id=query_id) + + if request.POST: + query.editors_picks.all().delete() + messages.success(request, _("Editor's picks deleted.")) + return redirect('wagtailsearchpicks:index') + + return render(request, 'wagtailsearchpicks/confirm_delete.html', { + 'query': query, + }) diff --git a/wagtail/contrib/wagtailsearchpicks/wagtail_hooks.py b/wagtail/contrib/wagtailsearchpicks/wagtail_hooks.py new file mode 100644 index 000000000..361ed1105 --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/wagtail_hooks.py @@ -0,0 +1,26 @@ +from django.core import urlresolvers +from django.conf.urls import include, url +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailcore import hooks +from wagtail.contrib.wagtailsearchpicks import admin_urls + +from wagtail.wagtailadmin.menu import MenuItem + + +@hooks.register('register_admin_urls') +def register_admin_urls(): + return [ + url(r'^searchpicks/', include(admin_urls, namespace='wagtailsearchpicks')), + ] + + +class SearchPicksMenuItem(MenuItem): + def is_shown(self, request): + # TEMPORARY: Only show if the user is a superuser + return request.user.is_superuser + + +@hooks.register('register_settings_menu_item') +def register_search_picks_menu_item(): + return SearchPicksMenuItem(_('Promoted search results'), urlresolvers.reverse('wagtailsearchpicks:index'), classnames='icon icon-pick', order=900) From 049efc8281e45926d9f2ff80583668fe0770c8b0 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 6 Jul 2015 10:00:14 +0100 Subject: [PATCH 59/92] Moved editors pick model to searchpicks --- wagtail/contrib/wagtailsearchpicks/forms.py | 7 ++- .../migrations/0001_initial.py | 43 +++++++++++++ .../wagtailsearchpicks/migrations/__init__.py | 0 wagtail/contrib/wagtailsearchpicks/models.py | 18 ++++++ wagtail/contrib/wagtailsearchpicks/tests.py | 61 ++++++++++--------- wagtail/contrib/wagtailsearchpicks/views.py | 13 ++-- .../migrations/0003_remove_editors_pick.py | 30 +++++++++ wagtail/wagtailsearch/models.py | 14 ----- 8 files changed, 133 insertions(+), 53 deletions(-) create mode 100644 wagtail/contrib/wagtailsearchpicks/migrations/0001_initial.py create mode 100644 wagtail/contrib/wagtailsearchpicks/migrations/__init__.py create mode 100644 wagtail/wagtailsearch/migrations/0003_remove_editors_pick.py diff --git a/wagtail/contrib/wagtailsearchpicks/forms.py b/wagtail/contrib/wagtailsearchpicks/forms.py index 6016e33f6..2cf49063a 100644 --- a/wagtail/contrib/wagtailsearchpicks/forms.py +++ b/wagtail/contrib/wagtailsearchpicks/forms.py @@ -3,7 +3,8 @@ from django.forms.models import inlineformset_factory from django.utils.translation import ugettext_lazy as _ from wagtail.wagtailadmin.widgets import AdminPageChooser -from wagtail.wagtailsearch import models +from wagtail.wagtailsearch.models import Query +from wagtail.contrib.wagtailsearchpicks.models import SearchPick class SearchPickForm(forms.ModelForm): @@ -14,7 +15,7 @@ class SearchPickForm(forms.ModelForm): self.fields['page'].widget = AdminPageChooser() class Meta: - model = models.EditorsPick + model = SearchPick fields = ('query', 'page', 'description') widgets = { @@ -22,7 +23,7 @@ class SearchPickForm(forms.ModelForm): } -SearchPicksFormSetBase = inlineformset_factory(models.Query, models.EditorsPick, form=SearchPickForm, can_order=True, can_delete=True, extra=0) +SearchPicksFormSetBase = inlineformset_factory(Query, SearchPick, form=SearchPickForm, can_order=True, can_delete=True, extra=0) class SearchPicksFormSet(SearchPicksFormSetBase): diff --git a/wagtail/contrib/wagtailsearchpicks/migrations/0001_initial.py b/wagtail/contrib/wagtailsearchpicks/migrations/0001_initial.py new file mode 100644 index 000000000..e21d47383 --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0015_add_more_verbose_names'), + ('wagtailsearch', '0003_remove_editors_pick'), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.CreateModel( + name='EditorsPick', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('sort_order', models.IntegerField(editable=False, null=True, blank=True)), + ('description', models.TextField(verbose_name='Description', blank=True)), + ('page', models.ForeignKey(verbose_name='Page', to='wagtailcore.Page')), + ('query', models.ForeignKey(to='wagtailsearch.Query', related_name='editors_picks')), + ], + options={ + 'db_table': 'wagtailsearch_editorspick', + 'verbose_name': "Editor's Pick", + 'ordering': ('sort_order',), + }, + ), + ], + database_operations=[] + ), + migrations.AlterModelTable( + name='editorspick', + table=None, + ), + migrations.RenameModel( + old_name='EditorsPick', + new_name='SearchPick' + ) + ] diff --git a/wagtail/contrib/wagtailsearchpicks/migrations/__init__.py b/wagtail/contrib/wagtailsearchpicks/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/contrib/wagtailsearchpicks/models.py b/wagtail/contrib/wagtailsearchpicks/models.py index e69de29bb..a53ec3fa6 100644 --- a/wagtail/contrib/wagtailsearchpicks/models.py +++ b/wagtail/contrib/wagtailsearchpicks/models.py @@ -0,0 +1,18 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from wagtail.wagtailsearch.models import Query + + +class SearchPick(models.Model): + query = models.ForeignKey(Query, db_index=True, related_name='editors_picks') + page = models.ForeignKey('wagtailcore.Page', verbose_name=_('Page')) + sort_order = models.IntegerField(null=True, blank=True, editable=False) + description = models.TextField(verbose_name=_('Description'), blank=True) + + def __repr__(self): + return 'SearchPick(query="' + self.query.query_string + '", page="' + self.page.title + '")' + + class Meta: + ordering = ('sort_order', ) + verbose_name = _("Editor's Pick") diff --git a/wagtail/contrib/wagtailsearchpicks/tests.py b/wagtail/contrib/wagtailsearchpicks/tests.py index 53d1852ee..573f15215 100644 --- a/wagtail/contrib/wagtailsearchpicks/tests.py +++ b/wagtail/contrib/wagtailsearchpicks/tests.py @@ -2,49 +2,50 @@ from django.test import TestCase from django.core.urlresolvers import reverse from wagtail.tests.utils import WagtailTestUtils -from wagtail.wagtailsearch import models +from wagtail.wagtailsearch.models import Query +from wagtail.contrib.wagtailsearchpicks.models import SearchPick class TestSearchPicks(TestCase): def test_search_pick_create(self): # Create a search pick to the root page - models.EditorsPick.objects.create( - query=models.Query.get("root page"), + SearchPick.objects.create( + query=Query.get("root page"), page_id=1, sort_order=0, description="First search pick", ) # Check - self.assertEqual(models.Query.get("root page").editors_picks.count(), 1) - self.assertEqual(models.Query.get("root page").editors_picks.first().page_id, 1) + self.assertEqual(Query.get("root page").editors_picks.count(), 1) + self.assertEqual(Query.get("root page").editors_picks.first().page_id, 1) def test_search_pick_ordering(self): # Add 3 search picks in a different order to their sort_order values # They should be ordered by their sort order values and not their insertion order - models.EditorsPick.objects.create( - query=models.Query.get("root page"), + SearchPick.objects.create( + query=Query.get("root page"), page_id=1, sort_order=0, description="First search pick", ) - models.EditorsPick.objects.create( - query=models.Query.get("root page"), + SearchPick.objects.create( + query=Query.get("root page"), page_id=1, sort_order=2, description="Last search pick", ) - models.EditorsPick.objects.create( - query=models.Query.get("root page"), + SearchPick.objects.create( + query=Query.get("root page"), page_id=1, sort_order=1, description="Middle search pick", ) # Check - self.assertEqual(models.Query.get("root page").editors_picks.count(), 3) - self.assertEqual(models.Query.get("root page").editors_picks.first().description, "First search pick") - self.assertEqual(models.Query.get("root page").editors_picks.last().description, "Last search pick") + self.assertEqual(Query.get("root page").editors_picks.count(), 3) + self.assertEqual(Query.get("root page").editors_picks.first().description, "First search pick") + self.assertEqual(Query.get("root page").editors_picks.last().description, "Last search pick") class TestSearchPicksIndexView(TestCase, WagtailTestUtils): @@ -63,8 +64,8 @@ class TestSearchPicksIndexView(TestCase, WagtailTestUtils): def make_search_picks(self): for i in range(50): - models.EditorsPick.objects.create( - query=models.Query.get("query " + str(i)), + SearchPick.objects.create( + query=Query.get("query " + str(i)), page_id=1, sort_order=0, description="First search pick", @@ -134,7 +135,7 @@ class TestSearchPicksAddView(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailsearchpicks:index')) # Check that the search pick was created - self.assertTrue(models.Query.get('test').editors_picks.filter(page_id=1).exists()) + self.assertTrue(Query.get('test').editors_picks.filter(page_id=1).exists()) def test_post_without_recommendations(self): # Submit @@ -156,7 +157,7 @@ class TestSearchPicksEditView(TestCase, WagtailTestUtils): self.login() # Create an search pick to edit - self.query = models.Query.get("Hello") + self.query = Query.get("Hello") self.search_pick = self.query.editors_picks.create(page_id=1, description="Root page") self.search_pick_2 = self.query.editors_picks.create(page_id=2, description="Homepage") @@ -189,12 +190,12 @@ class TestSearchPicksEditView(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailsearchpicks:index')) # Check that the search pick description was edited - self.assertEqual(models.EditorsPick.objects.get(id=self.search_pick.id).description, "Description has changed") + self.assertEqual(SearchPick.objects.get(id=self.search_pick.id).description, "Description has changed") def test_post_reorder(self): # Check order before reordering - self.assertEqual(models.Query.get("Hello").editors_picks.all()[0], self.search_pick) - self.assertEqual(models.Query.get("Hello").editors_picks.all()[1], self.search_pick_2) + self.assertEqual(Query.get("Hello").editors_picks.all()[0], self.search_pick) + self.assertEqual(Query.get("Hello").editors_picks.all()[1], self.search_pick_2) # Submit post_data = { @@ -219,12 +220,12 @@ class TestSearchPicksEditView(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailsearchpicks:index')) # Check that the ordering has been saved correctly - self.assertEqual(models.EditorsPick.objects.get(id=self.search_pick.id).sort_order, 1) - self.assertEqual(models.EditorsPick.objects.get(id=self.search_pick_2.id).sort_order, 0) + self.assertEqual(SearchPick.objects.get(id=self.search_pick.id).sort_order, 1) + self.assertEqual(SearchPick.objects.get(id=self.search_pick_2.id).sort_order, 0) # Check that the recommendations were reordered - self.assertEqual(models.Query.get("Hello").editors_picks.all()[0], self.search_pick_2) - self.assertEqual(models.Query.get("Hello").editors_picks.all()[1], self.search_pick) + self.assertEqual(Query.get("Hello").editors_picks.all()[0], self.search_pick_2) + self.assertEqual(Query.get("Hello").editors_picks.all()[1], self.search_pick) def test_post_delete_recommendation(self): # Submit @@ -250,10 +251,10 @@ class TestSearchPicksEditView(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailsearchpicks:index')) # Check that the recommendation was deleted - self.assertFalse(models.EditorsPick.objects.filter(id=self.search_pick_2.id).exists()) + self.assertFalse(SearchPick.objects.filter(id=self.search_pick_2.id).exists()) # The other recommendation should still exist - self.assertTrue(models.EditorsPick.objects.filter(id=self.search_pick.id).exists()) + self.assertTrue(SearchPick.objects.filter(id=self.search_pick.id).exists()) def test_post_without_recommendations(self): # Submit @@ -285,7 +286,7 @@ class TestSearchPicksDeleteView(TestCase, WagtailTestUtils): self.login() # Create an search pick to delete - self.query = models.Query.get("Hello") + self.query = Query.get("Hello") self.search_pick = self.query.editors_picks.create(page_id=1, description="Root page") self.search_pick_2 = self.query.editors_picks.create(page_id=2, description="Homepage") @@ -305,7 +306,7 @@ class TestSearchPicksDeleteView(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailsearchpicks:index')) # Check that both recommendations were deleted - self.assertFalse(models.EditorsPick.objects.filter(id=self.search_pick_2.id).exists()) + self.assertFalse(SearchPick.objects.filter(id=self.search_pick_2.id).exists()) # The other recommendation should still exist - self.assertFalse(models.EditorsPick.objects.filter(id=self.search_pick.id).exists()) + self.assertFalse(SearchPick.objects.filter(id=self.search_pick.id).exists()) diff --git a/wagtail/contrib/wagtailsearchpicks/views.py b/wagtail/contrib/wagtailsearchpicks/views.py index 5ad523d1c..950395576 100644 --- a/wagtail/contrib/wagtailsearchpicks/views.py +++ b/wagtail/contrib/wagtailsearchpicks/views.py @@ -5,7 +5,8 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.utils.translation import ugettext as _ from django.views.decorators.vary import vary_on_headers -from wagtail.wagtailsearch import models, forms as search_forms +from wagtail.wagtailsearch import forms as search_forms +from wagtail.wagtailsearch.models import Query from wagtail.wagtailadmin.forms import SearchForm from wagtail.wagtailadmin import messages @@ -18,7 +19,7 @@ def index(request): page = request.GET.get('p', 1) query_string = request.GET.get('q', "") - queries = models.Query.objects.filter(editors_picks__isnull=False).distinct() + queries = Query.objects.filter(editors_picks__isnull=False).distinct() # Search if query_string: @@ -75,7 +76,7 @@ def add(request): # Get query query_form = search_forms.QueryForm(request.POST) if query_form.is_valid(): - query = models.Query.get(query_form['query_string'].value()) + query = Query.get(query_form['query_string'].value()) # Save search picks searchpicks_formset = forms.SearchPicksFormSet(request.POST, instance=query) @@ -102,7 +103,7 @@ def add(request): def edit(request, query_id): - query = get_object_or_404(models.Query, id=query_id) + query = get_object_or_404(Query, id=query_id) if request.POST: # Get query @@ -111,7 +112,7 @@ def edit(request, query_id): searchpicks_formset = forms.SearchPicksFormSet(request.POST, instance=query) if query_form.is_valid(): - new_query = models.Query.get(query_form['query_string'].value()) + new_query = Query.get(query_form['query_string'].value()) # Save search picks if save_searchpicks(query, new_query, searchpicks_formset): @@ -137,7 +138,7 @@ def edit(request, query_id): def delete(request, query_id): - query = get_object_or_404(models.Query, id=query_id) + query = get_object_or_404(Query, id=query_id) if request.POST: query.editors_picks.all().delete() diff --git a/wagtail/wagtailsearch/migrations/0003_remove_editors_pick.py b/wagtail/wagtailsearch/migrations/0003_remove_editors_pick.py new file mode 100644 index 000000000..95011dd82 --- /dev/null +++ b/wagtail/wagtailsearch/migrations/0003_remove_editors_pick.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailsearch', '0002_add_verbose_names'), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + migrations.RemoveField( + model_name='editorspick', + name='page', + ), + migrations.RemoveField( + model_name='editorspick', + name='query', + ), + migrations.DeleteModel( + name='EditorsPick', + ), + ], + database_operations=[], + ) + ] diff --git a/wagtail/wagtailsearch/models.py b/wagtail/wagtailsearch/models.py index 8c87cf5e5..bab3d216c 100644 --- a/wagtail/wagtailsearch/models.py +++ b/wagtail/wagtailsearch/models.py @@ -71,17 +71,3 @@ class QueryDailyHits(models.Model): ('query', 'date'), ) verbose_name = _('Query Daily Hits') - - -class EditorsPick(models.Model): - query = models.ForeignKey(Query, db_index=True, related_name='editors_picks') - page = models.ForeignKey('wagtailcore.Page', verbose_name=_('Page')) - sort_order = models.IntegerField(null=True, blank=True, editable=False) - description = models.TextField(verbose_name=_('Description'), blank=True) - - def __repr__(self): - return 'EditorsPick(query="' + self.query.query_string + '", page="' + self.page.title + '")' - - class Meta: - ordering = ('sort_order', ) - verbose_name = _("Editor's Pick") From dfebe2087d279f00841d04161f9019caa9bb64aa Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 6 Jul 2015 10:36:51 +0100 Subject: [PATCH 60/92] Added {% get_search_picks %} template tag --- .../templatetags/__init__.py | 0 .../templatetags/wagtailsearchpicks_tags.py | 11 +++++++++ wagtail/contrib/wagtailsearchpicks/tests.py | 24 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 wagtail/contrib/wagtailsearchpicks/templatetags/__init__.py create mode 100644 wagtail/contrib/wagtailsearchpicks/templatetags/wagtailsearchpicks_tags.py diff --git a/wagtail/contrib/wagtailsearchpicks/templatetags/__init__.py b/wagtail/contrib/wagtailsearchpicks/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/contrib/wagtailsearchpicks/templatetags/wagtailsearchpicks_tags.py b/wagtail/contrib/wagtailsearchpicks/templatetags/wagtailsearchpicks_tags.py new file mode 100644 index 000000000..5cee68f90 --- /dev/null +++ b/wagtail/contrib/wagtailsearchpicks/templatetags/wagtailsearchpicks_tags.py @@ -0,0 +1,11 @@ +from django import template + +from wagtail.wagtailsearch.models import Query + + +register = template.Library() + + +@register.assignment_tag() +def get_search_picks(search_query): + return Query.get(search_query).editors_picks.all() diff --git a/wagtail/contrib/wagtailsearchpicks/tests.py b/wagtail/contrib/wagtailsearchpicks/tests.py index 573f15215..8c78cc3d2 100644 --- a/wagtail/contrib/wagtailsearchpicks/tests.py +++ b/wagtail/contrib/wagtailsearchpicks/tests.py @@ -4,6 +4,7 @@ from django.core.urlresolvers import reverse from wagtail.tests.utils import WagtailTestUtils from wagtail.wagtailsearch.models import Query from wagtail.contrib.wagtailsearchpicks.models import SearchPick +from wagtail.contrib.wagtailsearchpicks.templatetags.wagtailsearchpicks_tags import get_search_picks class TestSearchPicks(TestCase): @@ -48,6 +49,29 @@ class TestSearchPicks(TestCase): self.assertEqual(Query.get("root page").editors_picks.last().description, "Last search pick") +class TestGetSearchPicksTemplateTag(TestCase): + def test_get_search_picks_template_tag(self): + # Create a search pick to the root page + pick = SearchPick.objects.create( + query=Query.get("root page"), + page_id=1, + sort_order=0, + description="First search pick", + ) + + # Create another search pick against a different query + SearchPick.objects.create( + query=Query.get("root page again"), + page_id=1, + sort_order=0, + description="Second search pick", + ) + + # Check + search_picks = list(get_search_picks("root page")) + self.assertEqual(search_picks, [pick]) + + class TestSearchPicksIndexView(TestCase, WagtailTestUtils): def setUp(self): self.login() From 8fc1277482416b1add46952f284e01baf4152be4 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 6 Jul 2015 10:42:07 +0100 Subject: [PATCH 61/92] Exclude searchpicks_formset.js from jscs --- .jscsrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jscsrc b/.jscsrc index 877f8f1d4..9dba4abee 100644 --- a/.jscsrc +++ b/.jscsrc @@ -12,7 +12,7 @@ "**/*.min.js", "**/vendor/**/*.js", "./wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/inline_panel.js", - "./wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/includes/editorspicks_formset.js", + "./wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.js", "./wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.js", "./wagtail/wagtailsnippets/templates/wagtailsnippets/chooser/chosen.js", "./wagtail/wagtailimages/templates/wagtailimages/chooser/image_chosen.js", From 669c7ac34c80f4a833fd0bd1eae321f44ad6ec31 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Mon, 6 Jul 2015 11:35:38 +0100 Subject: [PATCH 62/92] Docs for searchpicks module --- docs/reference/contrib/index.rst | 7 +++ docs/reference/contrib/searchpicks.rst | 63 ++++++++++++++++++++++++++ docs/topics/search/searching.rst | 24 ++-------- 3 files changed, 74 insertions(+), 20 deletions(-) create mode 100644 docs/reference/contrib/searchpicks.rst diff --git a/docs/reference/contrib/index.rst b/docs/reference/contrib/index.rst index cfd4f46f3..f0a66755c 100644 --- a/docs/reference/contrib/index.rst +++ b/docs/reference/contrib/index.rst @@ -13,6 +13,7 @@ Wagtail ships with a variety of extra optional modules. frontendcache routablepage api/index + searchpicks :doc:`forms` @@ -49,3 +50,9 @@ Provides a way of embedding Django URLconfs into pages. ---------------- A module for adding a read only, JSON based web API to your Wagtail site + + +:doc:`searchpicks` +------------------ + +A module for managing "Promoted Search Results" diff --git a/docs/reference/contrib/searchpicks.rst b/docs/reference/contrib/searchpicks.rst new file mode 100644 index 000000000..76a3d4d42 --- /dev/null +++ b/docs/reference/contrib/searchpicks.rst @@ -0,0 +1,63 @@ +.. _editors-picks: + +======================= +Promoted search results +======================= + +.. module:: wagtail.contrib.wagtailsearchpicks + +.. versionchanged:: 1.1 + + Before Wagtail 1.1, promoted search results were implemented in the :mod:`wagtail.wagtailsearch` core module and called "editors picks". + +The ``searchpicks`` module provides the models and user interface for managing "Promoted search results" and displaying them in a search results page. + +"Promoted search results" allow editors to explicitly link relevant content to search terms, so results pages can contain curated content in addition to results from the search engine. + + +Installation +============ + +The ``searchpicks`` module is not enabled by default. To install it, add ``wagtail.contrib.wagtailsearchpicks`` to ``INSTALLED_APPS`` in your project's Django settings file. + + +.. code-block:: python + + INSTALLED_APPS = [ + ... + + 'wagtail.contrib.wagtailsearchpicks', + ] + +This app contains migrations so make sure you run the ``migrate`` django-admin command after installing. + + +Usage +===== + +Once installed, a new menu item called "Promoted search results" should appear in the "Settings" menu. This is where you can assign pages to popular search terms. + + +Displaying on a search results page +----------------------------------- + +To retrieve a list of promoted search results for a particular search query, you can use the ``{% get_search_picks %}`` template tag from the ``wagtailsearchpicks_tags`` templatetag library: + +.. code-block:: HTML+Django + + {% load wagtailcore_tags wagtailsearchpicks_tags %} + + ... + + {% get_search_picks search_query as search_picks %} + + diff --git a/docs/topics/search/searching.rst b/docs/topics/search/searching.rst index 421d57301..012201f9b 100644 --- a/docs/topics/search/searching.rst +++ b/docs/topics/search/searching.rst @@ -100,28 +100,12 @@ And here's a template to go with it: {% endblock %} -.. _editors-picks: +Promoted search results +======================= +"Promoted search results" allow editors to explicitly link relevant content to search terms, so results pages can contain curated content in addition to results from the search engine. -Editor's picks -============== - -Editor's picks are a way of explicitly linking relevant content to search terms, so results pages can contain curated content in addition to results from the search algorithm. - -You can get a list of editors picks for a particular query using the ``Query`` class: - -.. code-block:: python - - editors_picks = Query.get(search_query).editors_picks.all() - - -Each editors pick contains the following fields: - - ``page`` - The page object associated with the pick. Use ``{% pageurl editors_pick.page %}`` to generate a URL or provide other properties of the page object. - - ``description`` - The description entered when choosing the pick, perhaps explaining why the page is relevant to the search terms. +This functionality is provided by the :mod:`~wagtail.contrib.wagtailsearchpicks` contrib module. Searching Images, Documents and custom models From aea1ec742ae5d20ff0f668b9e29f24daa3ccbf36 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 7 Jul 2015 16:01:19 +0100 Subject: [PATCH 63/92] SearchPick => SearchPromotion --- wagtail/contrib/wagtailsearchpicks/forms.py | 14 +++---- .../migrations/0001_initial.py | 8 +++- wagtail/contrib/wagtailsearchpicks/models.py | 6 +-- wagtail/contrib/wagtailsearchpicks/tests.py | 42 +++++++++---------- wagtail/contrib/wagtailsearchpicks/views.py | 10 ++--- 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/wagtail/contrib/wagtailsearchpicks/forms.py b/wagtail/contrib/wagtailsearchpicks/forms.py index 2cf49063a..82de1bc14 100644 --- a/wagtail/contrib/wagtailsearchpicks/forms.py +++ b/wagtail/contrib/wagtailsearchpicks/forms.py @@ -4,18 +4,18 @@ from django.utils.translation import ugettext_lazy as _ from wagtail.wagtailadmin.widgets import AdminPageChooser from wagtail.wagtailsearch.models import Query -from wagtail.contrib.wagtailsearchpicks.models import SearchPick +from wagtail.contrib.wagtailsearchpicks.models import SearchPromotion -class SearchPickForm(forms.ModelForm): +class SearchPromotionForm(forms.ModelForm): sort_order = forms.IntegerField(required=False) def __init__(self, *args, **kwargs): - super(SearchPickForm, self).__init__(*args, **kwargs) + super(SearchPromotionForm, self).__init__(*args, **kwargs) self.fields['page'].widget = AdminPageChooser() class Meta: - model = SearchPick + model = SearchPromotion fields = ('query', 'page', 'description') widgets = { @@ -23,15 +23,15 @@ class SearchPickForm(forms.ModelForm): } -SearchPicksFormSetBase = inlineformset_factory(Query, SearchPick, form=SearchPickForm, can_order=True, can_delete=True, extra=0) +SearchPromotionsFormSetBase = inlineformset_factory(Query, SearchPromotion, form=SearchPromotionForm, can_order=True, can_delete=True, extra=0) -class SearchPicksFormSet(SearchPicksFormSetBase): +class SearchPromotionsFormSet(SearchPromotionsFormSetBase): minimum_forms = 1 minimum_forms_message = _("Please specify at least one recommendation for this search term.") def add_fields(self, form, *args, **kwargs): - super(SearchPicksFormSet, self).add_fields(form, *args, **kwargs) + super(SearchPromotionsFormSet, self).add_fields(form, *args, **kwargs) # Hide delete and order fields form.fields['DELETE'].widget = forms.HiddenInput() diff --git a/wagtail/contrib/wagtailsearchpicks/migrations/0001_initial.py b/wagtail/contrib/wagtailsearchpicks/migrations/0001_initial.py index e21d47383..62809da8d 100644 --- a/wagtail/contrib/wagtailsearchpicks/migrations/0001_initial.py +++ b/wagtail/contrib/wagtailsearchpicks/migrations/0001_initial.py @@ -38,6 +38,10 @@ class Migration(migrations.Migration): ), migrations.RenameModel( old_name='EditorsPick', - new_name='SearchPick' - ) + new_name='SearchPromotion' + ), + migrations.AlterModelOptions( + name='searchpromotion', + options={'ordering': ('sort_order',), 'verbose_name': 'Search promotion'}, + ), ] diff --git a/wagtail/contrib/wagtailsearchpicks/models.py b/wagtail/contrib/wagtailsearchpicks/models.py index a53ec3fa6..28f33eb01 100644 --- a/wagtail/contrib/wagtailsearchpicks/models.py +++ b/wagtail/contrib/wagtailsearchpicks/models.py @@ -4,15 +4,15 @@ from django.utils.translation import ugettext_lazy as _ from wagtail.wagtailsearch.models import Query -class SearchPick(models.Model): +class SearchPromotion(models.Model): query = models.ForeignKey(Query, db_index=True, related_name='editors_picks') page = models.ForeignKey('wagtailcore.Page', verbose_name=_('Page')) sort_order = models.IntegerField(null=True, blank=True, editable=False) description = models.TextField(verbose_name=_('Description'), blank=True) def __repr__(self): - return 'SearchPick(query="' + self.query.query_string + '", page="' + self.page.title + '")' + return 'SearchPromotion(query="' + self.query.query_string + '", page="' + self.page.title + '")' class Meta: ordering = ('sort_order', ) - verbose_name = _("Editor's Pick") + verbose_name = _("Search promotion") diff --git a/wagtail/contrib/wagtailsearchpicks/tests.py b/wagtail/contrib/wagtailsearchpicks/tests.py index 8c78cc3d2..b4bc0dce8 100644 --- a/wagtail/contrib/wagtailsearchpicks/tests.py +++ b/wagtail/contrib/wagtailsearchpicks/tests.py @@ -3,14 +3,14 @@ from django.core.urlresolvers import reverse from wagtail.tests.utils import WagtailTestUtils from wagtail.wagtailsearch.models import Query -from wagtail.contrib.wagtailsearchpicks.models import SearchPick +from wagtail.contrib.wagtailsearchpicks.models import SearchPromotion from wagtail.contrib.wagtailsearchpicks.templatetags.wagtailsearchpicks_tags import get_search_picks -class TestSearchPicks(TestCase): +class TestSearchPromotions(TestCase): def test_search_pick_create(self): # Create a search pick to the root page - SearchPick.objects.create( + SearchPromotion.objects.create( query=Query.get("root page"), page_id=1, sort_order=0, @@ -24,19 +24,19 @@ class TestSearchPicks(TestCase): def test_search_pick_ordering(self): # Add 3 search picks in a different order to their sort_order values # They should be ordered by their sort order values and not their insertion order - SearchPick.objects.create( + SearchPromotion.objects.create( query=Query.get("root page"), page_id=1, sort_order=0, description="First search pick", ) - SearchPick.objects.create( + SearchPromotion.objects.create( query=Query.get("root page"), page_id=1, sort_order=2, description="Last search pick", ) - SearchPick.objects.create( + SearchPromotion.objects.create( query=Query.get("root page"), page_id=1, sort_order=1, @@ -49,10 +49,10 @@ class TestSearchPicks(TestCase): self.assertEqual(Query.get("root page").editors_picks.last().description, "Last search pick") -class TestGetSearchPicksTemplateTag(TestCase): +class TestGetSearchPromotionsTemplateTag(TestCase): def test_get_search_picks_template_tag(self): # Create a search pick to the root page - pick = SearchPick.objects.create( + pick = SearchPromotion.objects.create( query=Query.get("root page"), page_id=1, sort_order=0, @@ -60,7 +60,7 @@ class TestGetSearchPicksTemplateTag(TestCase): ) # Create another search pick against a different query - SearchPick.objects.create( + SearchPromotion.objects.create( query=Query.get("root page again"), page_id=1, sort_order=0, @@ -72,7 +72,7 @@ class TestGetSearchPicksTemplateTag(TestCase): self.assertEqual(search_picks, [pick]) -class TestSearchPicksIndexView(TestCase, WagtailTestUtils): +class TestSearchPromotionsIndexView(TestCase, WagtailTestUtils): def setUp(self): self.login() @@ -88,7 +88,7 @@ class TestSearchPicksIndexView(TestCase, WagtailTestUtils): def make_search_picks(self): for i in range(50): - SearchPick.objects.create( + SearchPromotion.objects.create( query=Query.get("query " + str(i)), page_id=1, sort_order=0, @@ -132,7 +132,7 @@ class TestSearchPicksIndexView(TestCase, WagtailTestUtils): self.assertEqual(response.context['queries'].number, response.context['queries'].paginator.num_pages) -class TestSearchPicksAddView(TestCase, WagtailTestUtils): +class TestSearchPromotionsAddView(TestCase, WagtailTestUtils): def setUp(self): self.login() @@ -176,7 +176,7 @@ class TestSearchPicksAddView(TestCase, WagtailTestUtils): self.assertFormsetError(response, 'searchpicks_formset', None, None, "Please specify at least one recommendation for this search term.") -class TestSearchPicksEditView(TestCase, WagtailTestUtils): +class TestSearchPromotionsEditView(TestCase, WagtailTestUtils): def setUp(self): self.login() @@ -214,7 +214,7 @@ class TestSearchPicksEditView(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailsearchpicks:index')) # Check that the search pick description was edited - self.assertEqual(SearchPick.objects.get(id=self.search_pick.id).description, "Description has changed") + self.assertEqual(SearchPromotion.objects.get(id=self.search_pick.id).description, "Description has changed") def test_post_reorder(self): # Check order before reordering @@ -244,8 +244,8 @@ class TestSearchPicksEditView(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailsearchpicks:index')) # Check that the ordering has been saved correctly - self.assertEqual(SearchPick.objects.get(id=self.search_pick.id).sort_order, 1) - self.assertEqual(SearchPick.objects.get(id=self.search_pick_2.id).sort_order, 0) + self.assertEqual(SearchPromotion.objects.get(id=self.search_pick.id).sort_order, 1) + self.assertEqual(SearchPromotion.objects.get(id=self.search_pick_2.id).sort_order, 0) # Check that the recommendations were reordered self.assertEqual(Query.get("Hello").editors_picks.all()[0], self.search_pick_2) @@ -275,10 +275,10 @@ class TestSearchPicksEditView(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailsearchpicks:index')) # Check that the recommendation was deleted - self.assertFalse(SearchPick.objects.filter(id=self.search_pick_2.id).exists()) + self.assertFalse(SearchPromotion.objects.filter(id=self.search_pick_2.id).exists()) # The other recommendation should still exist - self.assertTrue(SearchPick.objects.filter(id=self.search_pick.id).exists()) + self.assertTrue(SearchPromotion.objects.filter(id=self.search_pick.id).exists()) def test_post_without_recommendations(self): # Submit @@ -305,7 +305,7 @@ class TestSearchPicksEditView(TestCase, WagtailTestUtils): self.assertFormsetError(response, 'searchpicks_formset', None, None, "Please specify at least one recommendation for this search term.") -class TestSearchPicksDeleteView(TestCase, WagtailTestUtils): +class TestSearchPromotionsDeleteView(TestCase, WagtailTestUtils): def setUp(self): self.login() @@ -330,7 +330,7 @@ class TestSearchPicksDeleteView(TestCase, WagtailTestUtils): self.assertRedirects(response, reverse('wagtailsearchpicks:index')) # Check that both recommendations were deleted - self.assertFalse(SearchPick.objects.filter(id=self.search_pick_2.id).exists()) + self.assertFalse(SearchPromotion.objects.filter(id=self.search_pick_2.id).exists()) # The other recommendation should still exist - self.assertFalse(SearchPick.objects.filter(id=self.search_pick.id).exists()) + self.assertFalse(SearchPromotion.objects.filter(id=self.search_pick.id).exists()) diff --git a/wagtail/contrib/wagtailsearchpicks/views.py b/wagtail/contrib/wagtailsearchpicks/views.py index 950395576..ea16a7300 100644 --- a/wagtail/contrib/wagtailsearchpicks/views.py +++ b/wagtail/contrib/wagtailsearchpicks/views.py @@ -79,7 +79,7 @@ def add(request): query = Query.get(query_form['query_string'].value()) # Save search picks - searchpicks_formset = forms.SearchPicksFormSet(request.POST, instance=query) + searchpicks_formset = forms.SearchPromotionsFormSet(request.POST, instance=query) if save_searchpicks(query, query, searchpicks_formset): messages.success(request, _("Editor's picks for '{0}' created.").format(query), buttons=[ messages.button(reverse('wagtailsearchpicks:edit', args=(query.id,)), _('Edit')) @@ -91,10 +91,10 @@ def add(request): else: messages.error(request, _("Recommendations have not been created due to errors")) # specific errors will be displayed within form fields else: - searchpicks_formset = forms.SearchPicksFormSet() + searchpicks_formset = forms.SearchPromotionsFormSet() else: query_form = search_forms.QueryForm() - searchpicks_formset = forms.SearchPicksFormSet() + searchpicks_formset = forms.SearchPromotionsFormSet() return render(request, 'wagtailsearchpicks/add.html', { 'query_form': query_form, @@ -109,7 +109,7 @@ def edit(request, query_id): # Get query query_form = search_forms.QueryForm(request.POST) # and the recommendations - searchpicks_formset = forms.SearchPicksFormSet(request.POST, instance=query) + searchpicks_formset = forms.SearchPromotionsFormSet(request.POST, instance=query) if query_form.is_valid(): new_query = Query.get(query_form['query_string'].value()) @@ -128,7 +128,7 @@ def edit(request, query_id): else: query_form = search_forms.QueryForm(initial=dict(query_string=query.query_string)) - searchpicks_formset = forms.SearchPicksFormSet(instance=query) + searchpicks_formset = forms.SearchPromotionsFormSet(instance=query) return render(request, 'wagtailsearchpicks/edit.html', { 'query_form': query_form, From 0710facc0379403f6d349f1f02940efd5d344792 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 7 Jul 2015 16:02:59 +0100 Subject: [PATCH 64/92] {% get_search_picks %} => {% get_search_promotions %} --- docs/reference/contrib/searchpicks.rst | 12 ++++++------ .../templatetags/wagtailsearchpicks_tags.py | 2 +- wagtail/contrib/wagtailsearchpicks/tests.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/reference/contrib/searchpicks.rst b/docs/reference/contrib/searchpicks.rst index 76a3d4d42..034137f88 100644 --- a/docs/reference/contrib/searchpicks.rst +++ b/docs/reference/contrib/searchpicks.rst @@ -41,7 +41,7 @@ Once installed, a new menu item called "Promoted search results" should appear i Displaying on a search results page ----------------------------------- -To retrieve a list of promoted search results for a particular search query, you can use the ``{% get_search_picks %}`` template tag from the ``wagtailsearchpicks_tags`` templatetag library: +To retrieve a list of promoted search results for a particular search query, you can use the ``{% get_search_promotions %}`` template tag from the ``wagtailsearchpicks_tags`` templatetag library: .. code-block:: HTML+Django @@ -49,14 +49,14 @@ To retrieve a list of promoted search results for a particular search query, you ... - {% get_search_picks search_query as search_picks %} + {% get_search_promotions search_query as search_promotions %}
            - {% for search_pick in search_picks %} + {% for search_promotion in search_promotions %}
          • - -

            {{ search_pick.page.title }}

            -

            {{ search_pick.description }}

            +
            +

            {{ search_promotion.page.title }}

            +

            {{ search_promotion.description }}

          • {% endfor %} diff --git a/wagtail/contrib/wagtailsearchpicks/templatetags/wagtailsearchpicks_tags.py b/wagtail/contrib/wagtailsearchpicks/templatetags/wagtailsearchpicks_tags.py index 5cee68f90..06a2d56f8 100644 --- a/wagtail/contrib/wagtailsearchpicks/templatetags/wagtailsearchpicks_tags.py +++ b/wagtail/contrib/wagtailsearchpicks/templatetags/wagtailsearchpicks_tags.py @@ -7,5 +7,5 @@ register = template.Library() @register.assignment_tag() -def get_search_picks(search_query): +def get_search_promotions(search_query): return Query.get(search_query).editors_picks.all() diff --git a/wagtail/contrib/wagtailsearchpicks/tests.py b/wagtail/contrib/wagtailsearchpicks/tests.py index b4bc0dce8..a6761b64c 100644 --- a/wagtail/contrib/wagtailsearchpicks/tests.py +++ b/wagtail/contrib/wagtailsearchpicks/tests.py @@ -4,7 +4,7 @@ from django.core.urlresolvers import reverse from wagtail.tests.utils import WagtailTestUtils from wagtail.wagtailsearch.models import Query from wagtail.contrib.wagtailsearchpicks.models import SearchPromotion -from wagtail.contrib.wagtailsearchpicks.templatetags.wagtailsearchpicks_tags import get_search_picks +from wagtail.contrib.wagtailsearchpicks.templatetags.wagtailsearchpicks_tags import get_search_promotions class TestSearchPromotions(TestCase): @@ -50,7 +50,7 @@ class TestSearchPromotions(TestCase): class TestGetSearchPromotionsTemplateTag(TestCase): - def test_get_search_picks_template_tag(self): + def test_get_search_promotions_template_tag(self): # Create a search pick to the root page pick = SearchPromotion.objects.create( query=Query.get("root page"), @@ -68,7 +68,7 @@ class TestGetSearchPromotionsTemplateTag(TestCase): ) # Check - search_picks = list(get_search_picks("root page")) + search_picks = list(get_search_promotions("root page")) self.assertEqual(search_picks, [pick]) From 2b765b58ceaae00623a2cf6f9d22c759d2375935 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 7 Jul 2015 16:05:46 +0100 Subject: [PATCH 65/92] Renamed searchpicks templates to searchpromotions --- .jscsrc | 2 +- .../add.html | 4 ++-- .../confirm_delete.html | 0 .../edit.html | 4 ++-- .../includes/searchpromotion_form.html} | 0 .../includes/searchpromotions_formset.html} | 4 ++-- .../includes/searchpromotions_formset.js} | 0 .../index.html | 2 +- .../list.html | 0 .../results.html | 2 +- wagtail/contrib/wagtailsearchpicks/tests.py | 14 +++++++------- wagtail/contrib/wagtailsearchpicks/views.py | 10 +++++----- 12 files changed, 21 insertions(+), 21 deletions(-) rename wagtail/contrib/wagtailsearchpicks/templates/{wagtailsearchpicks => wagtailsearchpromotions}/add.html (88%) rename wagtail/contrib/wagtailsearchpicks/templates/{wagtailsearchpicks => wagtailsearchpromotions}/confirm_delete.html (100%) rename wagtail/contrib/wagtailsearchpicks/templates/{wagtailsearchpicks => wagtailsearchpromotions}/edit.html (84%) rename wagtail/contrib/wagtailsearchpicks/templates/{wagtailsearchpicks/includes/searchpick_form.html => wagtailsearchpromotions/includes/searchpromotion_form.html} (100%) rename wagtail/contrib/wagtailsearchpicks/templates/{wagtailsearchpicks/includes/searchpicks_formset.html => wagtailsearchpromotions/includes/searchpromotions_formset.html} (69%) rename wagtail/contrib/wagtailsearchpicks/templates/{wagtailsearchpicks/includes/searchpicks_formset.js => wagtailsearchpromotions/includes/searchpromotions_formset.js} (100%) rename wagtail/contrib/wagtailsearchpicks/templates/{wagtailsearchpicks => wagtailsearchpromotions}/index.html (92%) rename wagtail/contrib/wagtailsearchpicks/templates/{wagtailsearchpicks => wagtailsearchpromotions}/list.html (100%) rename wagtail/contrib/wagtailsearchpicks/templates/{wagtailsearchpicks => wagtailsearchpromotions}/results.html (93%) diff --git a/.jscsrc b/.jscsrc index 9dba4abee..010eaa554 100644 --- a/.jscsrc +++ b/.jscsrc @@ -12,7 +12,7 @@ "**/*.min.js", "**/vendor/**/*.js", "./wagtail/wagtailadmin/templates/wagtailadmin/edit_handlers/inline_panel.js", - "./wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.js", + "./wagtail/contrib/wagtailsearchpromotions/templates/wagtailsearchpromotions/includes/searchpromotions_formset.js", "./wagtail/wagtailusers/templates/wagtailusers/groups/includes/page_permissions_formset.js", "./wagtail/wagtailsnippets/templates/wagtailsnippets/chooser/chosen.js", "./wagtail/wagtailimages/templates/wagtailimages/chooser/image_chosen.js", diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/add.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/add.html similarity index 88% rename from wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/add.html rename to wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/add.html index 5915404bf..a7701b8ea 100644 --- a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/add.html +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/add.html @@ -23,7 +23,7 @@ {% include "wagtailsearch/queries/chooser_field.html" with field=query_form.query_string only %}
          • - {% include "wagtailsearchpicks/includes/searchpicks_formset.html" with formset=searchpicks_formset only %} + {% include "wagtailsearchpromotions/includes/searchpromotions_formset.html" with formset=searchpicks_formset only %}
          @@ -38,7 +38,7 @@ {% include "wagtailadmin/pages/_editor_js.html" %} diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.js b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/includes/searchpromotions_formset.js similarity index 100% rename from wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/includes/searchpicks_formset.js rename to wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/includes/searchpromotions_formset.js diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/index.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/index.html similarity index 92% rename from wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/index.html rename to wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/index.html index 41bc97a62..99c2c0f26 100644 --- a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/index.html +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/index.html @@ -20,7 +20,7 @@
          - {% include "wagtailsearchpicks/results.html" %} + {% include "wagtailsearchpromotions/results.html" %}
          {% endblock %} diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/list.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/list.html similarity index 100% rename from wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/list.html rename to wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/list.html diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/results.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/results.html similarity index 93% rename from wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/results.html rename to wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/results.html index 7bb0b342f..f796831d9 100644 --- a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpicks/results.html +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/results.html @@ -10,7 +10,7 @@ {% endif %} - {% include "wagtailsearchpicks/list.html" %} + {% include "wagtailsearchpromotions/list.html" %} {% include "wagtailadmin/shared/pagination_nav.html" with items=queries is_searching=is_searching linkurl="wagtailsearchpicks_index" %} {% else %} diff --git a/wagtail/contrib/wagtailsearchpicks/tests.py b/wagtail/contrib/wagtailsearchpicks/tests.py index a6761b64c..ce5d418a1 100644 --- a/wagtail/contrib/wagtailsearchpicks/tests.py +++ b/wagtail/contrib/wagtailsearchpicks/tests.py @@ -79,7 +79,7 @@ class TestSearchPromotionsIndexView(TestCase, WagtailTestUtils): def test_simple(self): response = self.client.get(reverse('wagtailsearchpicks:index')) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearchpicks/index.html') + self.assertTemplateUsed(response, 'wagtailsearchpromotions/index.html') def test_search(self): response = self.client.get(reverse('wagtailsearchpicks:index'), {'q': "Hello"}) @@ -102,7 +102,7 @@ class TestSearchPromotionsIndexView(TestCase, WagtailTestUtils): # Check response self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearchpicks/index.html') + self.assertTemplateUsed(response, 'wagtailsearchpromotions/index.html') # Check that we got the correct page self.assertEqual(response.context['queries'].number, 2) @@ -114,7 +114,7 @@ class TestSearchPromotionsIndexView(TestCase, WagtailTestUtils): # Check response self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearchpicks/index.html') + self.assertTemplateUsed(response, 'wagtailsearchpromotions/index.html') # Check that we got page one self.assertEqual(response.context['queries'].number, 1) @@ -126,7 +126,7 @@ class TestSearchPromotionsIndexView(TestCase, WagtailTestUtils): # Check response self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearchpicks/index.html') + self.assertTemplateUsed(response, 'wagtailsearchpromotions/index.html') # Check that we got the last page self.assertEqual(response.context['queries'].number, response.context['queries'].paginator.num_pages) @@ -139,7 +139,7 @@ class TestSearchPromotionsAddView(TestCase, WagtailTestUtils): def test_simple(self): response = self.client.get(reverse('wagtailsearchpicks:add')) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearchpicks/add.html') + self.assertTemplateUsed(response, 'wagtailsearchpromotions/add.html') def test_post(self): # Submit @@ -188,7 +188,7 @@ class TestSearchPromotionsEditView(TestCase, WagtailTestUtils): def test_simple(self): response = self.client.get(reverse('wagtailsearchpicks:edit', args=(self.query.id, ))) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearchpicks/edit.html') + self.assertTemplateUsed(response, 'wagtailsearchpromotions/edit.html') def test_post(self): # Submit @@ -317,7 +317,7 @@ class TestSearchPromotionsDeleteView(TestCase, WagtailTestUtils): def test_simple(self): response = self.client.get(reverse('wagtailsearchpicks:delete', args=(self.query.id, ))) self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'wagtailsearchpicks/confirm_delete.html') + self.assertTemplateUsed(response, 'wagtailsearchpromotions/confirm_delete.html') def test_post(self): # Submit diff --git a/wagtail/contrib/wagtailsearchpicks/views.py b/wagtail/contrib/wagtailsearchpicks/views.py index ea16a7300..b50de35d7 100644 --- a/wagtail/contrib/wagtailsearchpicks/views.py +++ b/wagtail/contrib/wagtailsearchpicks/views.py @@ -36,13 +36,13 @@ def index(request): queries = paginator.page(paginator.num_pages) if request.is_ajax(): - return render(request, "wagtailsearchpicks/results.html", { + return render(request, "wagtailsearchpromotions/results.html", { 'is_searching': is_searching, 'queries': queries, 'query_string': query_string, }) else: - return render(request, 'wagtailsearchpicks/index.html', { + return render(request, 'wagtailsearchpromotions/index.html', { 'is_searching': is_searching, 'queries': queries, 'query_string': query_string, @@ -96,7 +96,7 @@ def add(request): query_form = search_forms.QueryForm() searchpicks_formset = forms.SearchPromotionsFormSet() - return render(request, 'wagtailsearchpicks/add.html', { + return render(request, 'wagtailsearchpromotions/add.html', { 'query_form': query_form, 'searchpicks_formset': searchpicks_formset, }) @@ -130,7 +130,7 @@ def edit(request, query_id): query_form = search_forms.QueryForm(initial=dict(query_string=query.query_string)) searchpicks_formset = forms.SearchPromotionsFormSet(instance=query) - return render(request, 'wagtailsearchpicks/edit.html', { + return render(request, 'wagtailsearchpromotions/edit.html', { 'query_form': query_form, 'searchpicks_formset': searchpicks_formset, 'query': query, @@ -145,6 +145,6 @@ def delete(request, query_id): messages.success(request, _("Editor's picks deleted.")) return redirect('wagtailsearchpicks:index') - return render(request, 'wagtailsearchpicks/confirm_delete.html', { + return render(request, 'wagtailsearchpromotions/confirm_delete.html', { 'query': query, }) From facf1fba76621f540ec48e2cdad448521c11a784 Mon Sep 17 00:00:00 2001 From: Karl Hobley Date: Tue, 7 Jul 2015 16:08:11 +0100 Subject: [PATCH 66/92] Renamed searchpicks url namespace to searchpromotions --- .../wagtailsearchpromotions/add.html | 2 +- .../confirm_delete.html | 2 +- .../wagtailsearchpromotions/edit.html | 4 +- .../wagtailsearchpromotions/index.html | 4 +- .../wagtailsearchpromotions/list.html | 2 +- .../wagtailsearchpromotions/results.html | 6 +-- wagtail/contrib/wagtailsearchpicks/tests.py | 40 +++++++++---------- wagtail/contrib/wagtailsearchpicks/views.py | 10 ++--- .../wagtailsearchpicks/wagtail_hooks.py | 4 +- 9 files changed, 37 insertions(+), 37 deletions(-) diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/add.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/add.html index a7701b8ea..d55f313fd 100644 --- a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/add.html +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/add.html @@ -15,7 +15,7 @@

          The "Search term(s)/phrase" field below must contain the full and exact search for which you wish to provide recommended results, including any misspellings/user error. To help, you can choose from search terms that have been popular with users of your site.

          {% endblocktrans %}
        -
        + {% csrf_token %}
          diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/confirm_delete.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/confirm_delete.html index eeadc17be..72dbe308a 100644 --- a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/confirm_delete.html +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/confirm_delete.html @@ -7,7 +7,7 @@

          {% trans "Are you sure you want to delete all promoted results for this search term?" %}

          - + {% csrf_token %} diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/edit.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/edit.html index 6068de93f..b58fd48b4 100644 --- a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/edit.html +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/edit.html @@ -5,7 +5,7 @@ {% trans "Editing" as editing_str %} {% include "wagtailadmin/shared/header.html" with title=editing_str subtitle=query.query_string icon="pick" %} -
          + {% csrf_token %}
          diff --git a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/index.html b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/index.html index 99c2c0f26..757a681f0 100644 --- a/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/index.html +++ b/wagtail/contrib/wagtailsearchpicks/templates/wagtailsearchpromotions/index.html @@ -6,7 +6,7 @@ {% block extra_js %}