diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/shared/main_nav.html b/wagtail/wagtailadmin/templates/wagtailadmin/shared/main_nav.html
index f7eb6528a..ce06d895c 100644
--- a/wagtail/wagtailadmin/templates/wagtailadmin/shared/main_nav.html
+++ b/wagtail/wagtailadmin/templates/wagtailadmin/shared/main_nav.html
@@ -10,19 +10,5 @@
{% trans "Log out" %}
- {% if request.user.is_superuser %} {# for now, 'More' links will be superuser-only #}
-
- {% trans 'More' %}
-
-
-
- {% get_wagtailadmin_tab_urls as wagtailadmin_tab_urls %}
- {% for name, title in wagtailadmin_tab_urls %}
-
- {% endfor %}
-
-
- {% endif %}
-
diff --git a/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py b/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py
index 311163ca3..2d85c355e 100644
--- a/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py
+++ b/wagtail/wagtailadmin/templatetags/wagtailadmin_tags.py
@@ -26,17 +26,6 @@ def explorer_subnav(nodes):
}
-@register.assignment_tag
-def get_wagtailadmin_tab_urls():
- resolver = urlresolvers.get_resolver(None)
- return [
- (key, value[2].get("title", key))
- for key, value
- in resolver.reverse_dict.items()
- if isinstance(key, basestring) and key.startswith('wagtailadmin_tab_')
- ]
-
-
@register.inclusion_tag('wagtailadmin/shared/main_nav.html', takes_context=True)
def main_nav(context):
menu_items = [
diff --git a/wagtail/wagtailadmin/tests/test_page_chooser.py b/wagtail/wagtailadmin/tests/test_page_chooser.py
new file mode 100644
index 000000000..475e4f7b1
--- /dev/null
+++ b/wagtail/wagtailadmin/tests/test_page_chooser.py
@@ -0,0 +1,130 @@
+from django.test import TestCase
+from django.core.urlresolvers import reverse
+
+from wagtail.wagtailcore.models import Page
+from wagtail.tests.models import SimplePage
+from wagtail.tests.utils import WagtailTestUtils
+
+
+class TestChooserBrowse(TestCase, WagtailTestUtils):
+ def setUp(self):
+ self.root_page = Page.objects.get(id=2)
+
+ # Add child page
+ self.child_page = SimplePage()
+ self.child_page.title = "foobarbaz"
+ self.child_page.slug = "foobarbaz"
+ self.root_page.add_child(instance=self.child_page)
+
+ self.login()
+
+ def get(self, params={}):
+ return self.client.get(reverse('wagtailadmin_choose_page'), params)
+
+ def test_simple(self):
+ response = self.get()
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html')
+
+ def test_search(self):
+ response = self.get({'q': "foobarbaz"})
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "There is one match")
+ self.assertContains(response, "foobarbaz")
+
+ def test_search_no_results(self):
+ response = self.get({'q': "quux"})
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "There are 0 matches")
+
+ def test_get_invalid(self):
+ response = self.get({'page_type': 'foo.bar'})
+ self.assertEqual(response.status_code, 404)
+
+
+class TestChooserBrowseChild(TestCase, WagtailTestUtils):
+ def setUp(self):
+ self.root_page = Page.objects.get(id=2)
+
+ # Add child page
+ self.child_page = SimplePage()
+ self.child_page.title = "foobarbaz"
+ self.child_page.slug = "foobarbaz"
+ self.root_page.add_child(instance=self.child_page)
+
+ self.login()
+
+ def get(self, params={}):
+ return self.client.get(reverse('wagtailadmin_choose_page_child',
+ args=(self.root_page.id,)), params)
+
+ def get_invalid(self, params={}):
+ return self.client.get(reverse('wagtailadmin_choose_page_child',
+ args=(9999999,)), params)
+
+ def test_simple(self):
+ response = self.get()
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html')
+
+ def test_search(self):
+ response = self.get({'q': "foobarbaz"})
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "There is one match")
+ self.assertContains(response, "foobarbaz")
+
+ def test_search_no_results(self):
+ response = self.get({'q': "quux"})
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "There are 0 matches")
+
+ def test_get_invalid(self):
+ self.assertEqual(self.get_invalid().status_code, 404)
+
+
+class TestChooserExternalLink(TestCase, WagtailTestUtils):
+ def setUp(self):
+ self.login()
+
+ def get(self, params={}):
+ return self.client.get(reverse('wagtailadmin_choose_page_external_link'), params)
+
+ def post(self, post_data={}):
+ return self.client.post(reverse('wagtailadmin_choose_page_external_link'), post_data)
+
+ def test_simple(self):
+ response = self.get()
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'wagtailadmin/chooser/external_link.html')
+
+ def test_get_with_param(self):
+ self.assertEqual(self.get({'prompt_for_link_text': 'foo'}).status_code, 200)
+
+ def test_create_link(self):
+ request = self.post({'url': 'http://www.example.com'})
+ self.assertContains(request, "'url': 'http://www.example.com/',")
+ self.assertContains(request, "'title': 'http://www.example.com/'")
+
+
+class TestChooserEmailLink(TestCase, WagtailTestUtils):
+ def setUp(self):
+ self.login()
+
+ def get(self, params={}):
+ return self.client.get(reverse('wagtailadmin_choose_page_email_link'), params)
+
+ def post(self, post_data={}):
+ return self.client.post(reverse('wagtailadmin_choose_page_email_link'), post_data)
+
+ def test_simple(self):
+ response = self.get()
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'wagtailadmin/chooser/email_link.html')
+
+ def test_get_with_param(self):
+ self.assertEqual(self.get({'prompt_for_link_text': 'foo'}).status_code, 200)
+
+ def test_create_link(self):
+ request = self.post({'email_address': 'example@example.com'})
+ self.assertContains(request, "'url': 'mailto:example@example.com',")
+ self.assertContains(request, "'title': 'example@example.com'")
diff --git a/wagtail/wagtailadmin/tests/test_pages_views.py b/wagtail/wagtailadmin/tests/test_pages_views.py
index a34b29fd8..12c42f3e4 100644
--- a/wagtail/wagtailadmin/tests/test_pages_views.py
+++ b/wagtail/wagtailadmin/tests/test_pages_views.py
@@ -25,7 +25,7 @@ class TestPageExplorer(TestCase, WagtailTestUtils):
response = self.client.get(reverse('wagtailadmin_explore', args=(self.root_page.id, )))
self.assertEqual(response.status_code, 200)
self.assertEqual(self.root_page, response.context['parent_page'])
- self.assertTrue(response.context['pages'].filter(id=self.child_page.id).exists())
+ self.assertTrue(response.context['pages'].paginator.object_list.filter(id=self.child_page.id).exists())
class TestPageCreation(TestCase, WagtailTestUtils):
diff --git a/wagtail/wagtailadmin/views/pages.py b/wagtail/wagtailadmin/views/pages.py
index 9bf6dbf44..69e6b7f4f 100644
--- a/wagtail/wagtailadmin/views/pages.py
+++ b/wagtail/wagtailadmin/views/pages.py
@@ -33,6 +33,17 @@ def index(request, parent_page_id=None):
else:
ordering = 'title'
+ # Pagination
+ if ordering != 'ord':
+ p = request.GET.get('p', 1)
+ paginator = Paginator(pages, 50)
+ try:
+ pages = paginator.page(p)
+ except PageNotAnInteger:
+ pages = paginator.page(1)
+ except EmptyPage:
+ pages = paginator.page(paginator.num_pages)
+
return render(request, 'wagtailadmin/pages/index.html', {
'parent_page': parent_page,
'ordering': ordering,
diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py
index 30ffc8a77..b7e3d349e 100644
--- a/wagtail/wagtailcore/models.py
+++ b/wagtail/wagtailcore/models.py
@@ -394,23 +394,23 @@ class Page(MP_Node, ClusterableModel, Indexed):
return revision.as_page_object()
- def get_context(self, request):
+ def get_context(self, request, *args, **kwargs):
return {
'self': self,
'request': request,
}
- def get_template(self, request):
+ def get_template(self, request, *args, **kwargs):
if request.is_ajax():
return self.ajax_template or self.template
else:
return self.template
- def serve(self, request):
+ def serve(self, request, *args, **kwargs):
return TemplateResponse(
request,
- self.get_template(request),
- self.get_context(request)
+ self.get_template(request, *args, **kwargs),
+ self.get_context(request, *args, **kwargs)
)
def is_navigable(self):
diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py
deleted file mode 100644
index df66c6b93..000000000
--- a/wagtail/wagtailcore/tests.py
+++ /dev/null
@@ -1,860 +0,0 @@
-from django.test import TestCase, Client
-from django.http import HttpRequest, Http404
-from django.core import management
-from StringIO import StringIO
-
-from django.contrib.auth.models import User
-
-from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
-from wagtail.tests.models import EventPage, EventIndex, SimplePage
-
-
-class TestRouting(TestCase):
- fixtures = ['test.json']
-
- def test_find_site_for_request(self):
- default_site = Site.objects.get(is_default_site=True)
- events_page = Page.objects.get(url_path='/home/events/')
- events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
-
- # requests without a Host: header should be directed to the default site
- request = HttpRequest()
- request.path = '/'
- self.assertEqual(Site.find_for_request(request), default_site)
-
- # requests with a known Host: header should be directed to the specific site
- request = HttpRequest()
- request.path = '/'
- request.META['HTTP_HOST'] = 'events.example.com'
- self.assertEqual(Site.find_for_request(request), events_site)
-
- # requests with an unrecognised Host: header should be directed to the default site
- request = HttpRequest()
- request.path = '/'
- request.META['HTTP_HOST'] = 'unknown.example.com'
- self.assertEqual(Site.find_for_request(request), default_site)
-
- def test_urls(self):
- default_site = Site.objects.get(is_default_site=True)
- homepage = Page.objects.get(url_path='/home/')
- christmas_page = Page.objects.get(url_path='/home/events/christmas/')
-
- # Basic installation only has one site configured, so page.url will return local URLs
- self.assertEqual(homepage.full_url, 'http://localhost/')
- self.assertEqual(homepage.url, '/')
- self.assertEqual(homepage.relative_url(default_site), '/')
-
- self.assertEqual(christmas_page.full_url, 'http://localhost/events/christmas/')
- self.assertEqual(christmas_page.url, '/events/christmas/')
- self.assertEqual(christmas_page.relative_url(default_site), '/events/christmas/')
-
- def test_urls_with_multiple_sites(self):
- events_page = Page.objects.get(url_path='/home/events/')
- events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
-
- default_site = Site.objects.get(is_default_site=True)
- homepage = Page.objects.get(url_path='/home/')
- christmas_page = Page.objects.get(url_path='/home/events/christmas/')
-
- # with multiple sites, page.url will return full URLs to ensure that
- # they work across sites
- self.assertEqual(homepage.full_url, 'http://localhost/')
- self.assertEqual(homepage.url, 'http://localhost/')
- self.assertEqual(homepage.relative_url(default_site), '/')
- self.assertEqual(homepage.relative_url(events_site), 'http://localhost/')
-
- self.assertEqual(christmas_page.full_url, 'http://events.example.com/christmas/')
- self.assertEqual(christmas_page.url, 'http://events.example.com/christmas/')
- self.assertEqual(christmas_page.relative_url(default_site), 'http://events.example.com/christmas/')
- self.assertEqual(christmas_page.relative_url(events_site), '/christmas/')
-
- def test_request_routing(self):
- homepage = Page.objects.get(url_path='/home/')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
-
- request = HttpRequest()
- request.path = '/events/christmas/'
- response = homepage.route(request, ['events', 'christmas'])
-
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.context_data['self'], christmas_page)
- used_template = response.resolve_template(response.template_name)
- self.assertEqual(used_template.name, 'tests/event_page.html')
-
- def test_route_to_unknown_page_returns_404(self):
- homepage = Page.objects.get(url_path='/home/')
-
- request = HttpRequest()
- request.path = '/events/quinquagesima/'
- with self.assertRaises(Http404):
- homepage.route(request, ['events', 'quinquagesima'])
-
- def test_route_to_unpublished_page_returns_404(self):
- homepage = Page.objects.get(url_path='/home/')
-
- request = HttpRequest()
- request.path = '/events/tentative-unpublished-event/'
- with self.assertRaises(Http404):
- homepage.route(request, ['events', 'tentative-unpublished-event'])
-
-
-class TestServeView(TestCase):
- fixtures = ['test.json']
-
- def setUp(self):
- # Explicitly clear the cache of site root paths. Normally this would be kept
- # in sync by the Site.save logic, but this is bypassed when the database is
- # rolled back between tests using transactions.
- from django.core.cache import cache
- cache.delete('wagtail_site_root_paths')
-
- def test_serve(self):
- response = self.client.get('/events/christmas/')
-
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.templates[0].name, 'tests/event_page.html')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
- self.assertEqual(response.context['self'], christmas_page)
-
- self.assertContains(response, '
Christmas
')
- self.assertContains(response, '
Event
')
-
- def test_serve_unknown_page_returns_404(self):
- response = self.client.get('/events/quinquagesima/')
- self.assertEqual(response.status_code, 404)
-
- def test_serve_unpublished_page_returns_404(self):
- response = self.client.get('/events/tentative-unpublished-event/')
- self.assertEqual(response.status_code, 404)
-
- def test_serve_with_multiple_sites(self):
- events_page = Page.objects.get(url_path='/home/events/')
- Site.objects.create(hostname='events.example.com', root_page=events_page)
-
- response = self.client.get('/christmas/', HTTP_HOST='events.example.com')
- self.assertEqual(response.status_code, 200)
- self.assertEqual(response.templates[0].name, 'tests/event_page.html')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
- self.assertEqual(response.context['self'], christmas_page)
-
- self.assertContains(response, '
Christmas
')
- self.assertContains(response, '
Event
')
-
- # same request to the default host should return a 404
- c = Client()
- response = c.get('/christmas/', HTTP_HOST='localhost')
- self.assertEqual(response.status_code, 404)
-
- def test_serve_with_custom_context(self):
- response = self.client.get('/events/')
- self.assertEqual(response.status_code, 200)
-
- # should render the whole page
- self.assertContains(response, '
Events
')
-
- # response should contain data from the custom 'events' context variable
- self.assertContains(response, '
Christmas')
-
- def test_ajax_response(self):
- response = self.client.get('/events/', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
- self.assertEqual(response.status_code, 200)
-
- # should only render the content of includes/event_listing.html, not the whole page
- self.assertNotContains(response, '
Events
')
- self.assertContains(response, '
Christmas')
-
-
-class TestStaticSitePaths(TestCase):
- def setUp(self):
- self.root_page = Page.objects.get(id=1)
-
- # For simple tests
- self.home_page = self.root_page.add_child(instance=SimplePage(title="Homepage", slug="home"))
- self.about_page = self.home_page.add_child(instance=SimplePage(title="About us", slug="about"))
- self.contact_page = self.home_page.add_child(instance=SimplePage(title="Contact", slug="contact"))
-
- # For custom tests
- self.event_index = self.root_page.add_child(instance=EventIndex(title="Events", slug="events"))
- for i in range(20):
- self.event_index.add_child(instance=EventPage(title="Event " + str(i), slug="event" + str(i)))
-
- def test_local_static_site_paths(self):
- paths = list(self.about_page.get_static_site_paths())
-
- self.assertEqual(paths, ['/'])
-
- def test_child_static_site_paths(self):
- paths = list(self.home_page.get_static_site_paths())
-
- self.assertEqual(paths, ['/', '/about/', '/contact/'])
-
- def test_custom_static_site_paths(self):
- paths = list(self.event_index.get_static_site_paths())
-
- # Event index path
- expected_paths = ['/']
-
- # One path for each page of results
- expected_paths.extend(['/' + str(i + 1) + '/' for i in range(5)])
-
- # One path for each event page
- expected_paths.extend(['/event' + str(i) + '/' for i in range(20)])
-
- paths.sort()
- expected_paths.sort()
- self.assertEqual(paths, expected_paths)
-
-
-class TestPageUrlTags(TestCase):
- fixtures = ['test.json']
-
- def test_pageurl_tag(self):
- response = self.client.get('/events/')
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, '
Christmas')
-
- def test_slugurl_tag(self):
- response = self.client.get('/events/christmas/')
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, '
Back to events index')
-
-
-class TestPagePermission(TestCase):
- fixtures = ['test.json']
-
- def test_nonpublisher_page_permissions(self):
- event_editor = User.objects.get(username='eventeditor')
- homepage = Page.objects.get(url_path='/home/')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
- unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
- someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
-
- homepage_perms = homepage.permissions_for_user(event_editor)
- christmas_page_perms = christmas_page.permissions_for_user(event_editor)
- unpub_perms = unpublished_event_page.permissions_for_user(event_editor)
- someone_elses_event_perms = someone_elses_event_page.permissions_for_user(event_editor)
-
- self.assertFalse(homepage_perms.can_add_subpage())
- self.assertTrue(christmas_page_perms.can_add_subpage())
- self.assertTrue(unpub_perms.can_add_subpage())
- self.assertTrue(someone_elses_event_perms.can_add_subpage())
-
- self.assertFalse(homepage_perms.can_edit())
- self.assertTrue(christmas_page_perms.can_edit())
- self.assertTrue(unpub_perms.can_edit())
- self.assertFalse(someone_elses_event_perms.can_edit()) # basic 'add' permission doesn't allow editing pages owned by someone else
-
- self.assertFalse(homepage_perms.can_delete())
- self.assertFalse(christmas_page_perms.can_delete()) # cannot delete because it is published
- self.assertTrue(unpub_perms.can_delete())
- self.assertFalse(someone_elses_event_perms.can_delete())
-
- self.assertFalse(homepage_perms.can_publish())
- self.assertFalse(christmas_page_perms.can_publish())
- self.assertFalse(unpub_perms.can_publish())
-
- self.assertFalse(homepage_perms.can_unpublish())
- self.assertFalse(christmas_page_perms.can_unpublish())
- self.assertFalse(unpub_perms.can_unpublish())
-
- self.assertFalse(homepage_perms.can_publish_subpage())
- self.assertFalse(christmas_page_perms.can_publish_subpage())
- self.assertFalse(unpub_perms.can_publish_subpage())
-
- self.assertFalse(homepage_perms.can_reorder_children())
- self.assertFalse(christmas_page_perms.can_reorder_children())
- self.assertFalse(unpub_perms.can_reorder_children())
-
- self.assertFalse(homepage_perms.can_move())
- self.assertFalse(christmas_page_perms.can_move()) # cannot move because this would involve unpublishing from its current location
- self.assertTrue(unpub_perms.can_move())
- self.assertFalse(someone_elses_event_perms.can_move())
-
- self.assertFalse(christmas_page_perms.can_move_to(unpublished_event_page)) # cannot move because this would involve unpublishing from its current location
- self.assertTrue(unpub_perms.can_move_to(christmas_page))
- self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination
- self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself
-
-
- def test_publisher_page_permissions(self):
- event_moderator = User.objects.get(username='eventmoderator')
- homepage = Page.objects.get(url_path='/home/')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
- unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
-
- homepage_perms = homepage.permissions_for_user(event_moderator)
- christmas_page_perms = christmas_page.permissions_for_user(event_moderator)
- unpub_perms = unpublished_event_page.permissions_for_user(event_moderator)
-
- self.assertFalse(homepage_perms.can_add_subpage())
- self.assertTrue(christmas_page_perms.can_add_subpage())
- self.assertTrue(unpub_perms.can_add_subpage())
-
- self.assertFalse(homepage_perms.can_edit())
- self.assertTrue(christmas_page_perms.can_edit())
- self.assertTrue(unpub_perms.can_edit())
-
- self.assertFalse(homepage_perms.can_delete())
- self.assertTrue(christmas_page_perms.can_delete()) # cannot delete because it is published
- self.assertTrue(unpub_perms.can_delete())
-
- self.assertFalse(homepage_perms.can_publish())
- self.assertTrue(christmas_page_perms.can_publish())
- self.assertTrue(unpub_perms.can_publish())
-
- self.assertFalse(homepage_perms.can_unpublish())
- self.assertTrue(christmas_page_perms.can_unpublish())
- self.assertFalse(unpub_perms.can_unpublish()) # cannot unpublish a page that isn't published
-
- self.assertFalse(homepage_perms.can_publish_subpage())
- self.assertTrue(christmas_page_perms.can_publish_subpage())
- self.assertTrue(unpub_perms.can_publish_subpage())
-
- self.assertFalse(homepage_perms.can_reorder_children())
- self.assertTrue(christmas_page_perms.can_reorder_children())
- self.assertTrue(unpub_perms.can_reorder_children())
-
- self.assertFalse(homepage_perms.can_move())
- self.assertTrue(christmas_page_perms.can_move())
- self.assertTrue(unpub_perms.can_move())
-
- self.assertTrue(christmas_page_perms.can_move_to(unpublished_event_page))
- self.assertTrue(unpub_perms.can_move_to(christmas_page))
- self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination
- self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself
-
- def test_inactive_user_has_no_permissions(self):
- user = User.objects.get(username='inactiveuser')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
- unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
-
- christmas_page_perms = christmas_page.permissions_for_user(user)
- unpub_perms = unpublished_event_page.permissions_for_user(user)
-
- self.assertFalse(unpub_perms.can_add_subpage())
- self.assertFalse(unpub_perms.can_edit())
- self.assertFalse(unpub_perms.can_delete())
- self.assertFalse(unpub_perms.can_publish())
- self.assertFalse(christmas_page_perms.can_unpublish())
- self.assertFalse(unpub_perms.can_publish_subpage())
- self.assertFalse(unpub_perms.can_reorder_children())
- self.assertFalse(unpub_perms.can_move())
- self.assertFalse(unpub_perms.can_move_to(christmas_page))
-
- def test_superuser_has_full_permissions(self):
- user = User.objects.get(username='superuser')
- homepage = Page.objects.get(url_path='/home/')
- root = Page.objects.get(url_path='/')
- unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
-
- homepage_perms = homepage.permissions_for_user(user)
- root_perms = root.permissions_for_user(user)
- unpub_perms = unpublished_event_page.permissions_for_user(user)
-
- self.assertTrue(homepage_perms.can_add_subpage())
- self.assertTrue(root_perms.can_add_subpage())
-
- self.assertTrue(homepage_perms.can_edit())
- self.assertFalse(root_perms.can_edit()) # root is not a real editable page, even to superusers
-
- self.assertTrue(homepage_perms.can_delete())
- self.assertFalse(root_perms.can_delete())
-
- self.assertTrue(homepage_perms.can_publish())
- self.assertFalse(root_perms.can_publish())
-
- self.assertTrue(homepage_perms.can_unpublish())
- self.assertFalse(root_perms.can_unpublish())
- self.assertFalse(unpub_perms.can_unpublish())
-
- self.assertTrue(homepage_perms.can_publish_subpage())
- self.assertTrue(root_perms.can_publish_subpage())
-
- self.assertTrue(homepage_perms.can_reorder_children())
- self.assertTrue(root_perms.can_reorder_children())
-
- self.assertTrue(homepage_perms.can_move())
- self.assertFalse(root_perms.can_move())
-
- self.assertTrue(homepage_perms.can_move_to(root))
- self.assertFalse(homepage_perms.can_move_to(unpublished_event_page))
-
- def test_editable_pages_for_user_with_add_permission(self):
- event_editor = User.objects.get(username='eventeditor')
- homepage = Page.objects.get(url_path='/home/')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
- unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
- someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
-
- editable_pages = UserPagePermissionsProxy(event_editor).editable_pages()
-
- self.assertFalse(editable_pages.filter(id=homepage.id).exists())
- self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
- self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
- self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
-
- def test_editable_pages_for_user_with_edit_permission(self):
- event_moderator = User.objects.get(username='eventmoderator')
- homepage = Page.objects.get(url_path='/home/')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
- unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
- someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
-
- editable_pages = UserPagePermissionsProxy(event_moderator).editable_pages()
-
- self.assertFalse(editable_pages.filter(id=homepage.id).exists())
- self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
- self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
- self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
-
- def test_editable_pages_for_inactive_user(self):
- user = User.objects.get(username='inactiveuser')
- homepage = Page.objects.get(url_path='/home/')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
- unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
- someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
-
- editable_pages = UserPagePermissionsProxy(user).editable_pages()
-
- self.assertFalse(editable_pages.filter(id=homepage.id).exists())
- self.assertFalse(editable_pages.filter(id=christmas_page.id).exists())
- self.assertFalse(editable_pages.filter(id=unpublished_event_page.id).exists())
- self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
-
- def test_editable_pages_for_superuser(self):
- user = User.objects.get(username='superuser')
- homepage = Page.objects.get(url_path='/home/')
- christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
- unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
- someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
-
- editable_pages = UserPagePermissionsProxy(user).editable_pages()
-
- self.assertTrue(editable_pages.filter(id=homepage.id).exists())
- self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
- self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
- self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
-
-
-class TestPageQuerySet(TestCase):
- fixtures = ['test.json']
-
- def test_live(self):
- pages = Page.objects.live()
-
- # All pages must be live
- for page in pages:
- self.assertTrue(page.live)
-
- # Check that the homepage is in the results
- homepage = Page.objects.get(url_path='/home/')
- self.assertTrue(pages.filter(id=homepage.id).exists())
-
- def test_not_live(self):
- pages = Page.objects.not_live()
-
- # All pages must not be live
- for page in pages:
- self.assertFalse(page.live)
-
- # Check that "someone elses event" is in the results
- event = Page.objects.get(url_path='/home/events/someone-elses-event/')
- self.assertTrue(pages.filter(id=event.id).exists())
-
- def test_page(self):
- homepage = Page.objects.get(url_path='/home/')
- pages = Page.objects.page(homepage)
-
- # Should only select the homepage
- self.assertEqual(pages.count(), 1)
- self.assertEqual(pages.first(), homepage)
-
- def test_not_page(self):
- homepage = Page.objects.get(url_path='/home/')
- pages = Page.objects.not_page(homepage)
-
- # Should select everything except for the homepage
- self.assertEqual(pages.count(), Page.objects.all().count() - 1)
- for page in pages:
- self.assertNotEqual(page, homepage)
-
- def test_descendant_of(self):
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.descendant_of(events_index)
-
- # Check that all pages descend from events index
- for page in pages:
- self.assertTrue(page.get_ancestors().filter(id=events_index.id).exists())
-
- def test_descendant_of_inclusive(self):
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.descendant_of(events_index, inclusive=True)
-
- # Check that all pages descend from events index, includes event index
- for page in pages:
- self.assertTrue(page == events_index or page.get_ancestors().filter(id=events_index.id).exists())
-
- # Check that event index was included
- self.assertTrue(pages.filter(id=events_index.id).exists())
-
- def test_not_descendant_of(self):
- homepage = Page.objects.get(url_path='/home/')
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.not_descendant_of(events_index)
-
- # Check that no pages descend from events_index
- for page in pages:
- self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists())
-
- # As this is not inclusive, events index should be in the results
- self.assertTrue(pages.filter(id=events_index.id).exists())
-
- def test_not_descendant_of_inclusive(self):
- homepage = Page.objects.get(url_path='/home/')
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.not_descendant_of(events_index, inclusive=True)
-
- # Check that all pages descend from homepage but not events index
- for page in pages:
- self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists())
-
- # As this is inclusive, events index should not be in the results
- self.assertFalse(pages.filter(id=events_index.id).exists())
-
- def test_child_of(self):
- homepage = Page.objects.get(url_path='/home/')
- pages = Page.objects.child_of(homepage)
-
- # Check that all pages are children of homepage
- for page in pages:
- self.assertEqual(page.get_parent(), homepage)
-
- def test_not_child_of(self):
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.not_child_of(events_index)
-
- # Check that all pages are not children of events_index
- for page in pages:
- self.assertNotEqual(page.get_parent(), events_index)
-
- def test_ancestor_of(self):
- root_page = Page.objects.get(id=1)
- homepage = Page.objects.get(url_path='/home/')
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.ancestor_of(events_index)
-
- self.assertEqual(pages.count(), 2)
- self.assertEqual(pages[0], root_page)
- self.assertEqual(pages[1], homepage)
-
- def test_ancestor_of_inclusive(self):
- root_page = Page.objects.get(id=1)
- homepage = Page.objects.get(url_path='/home/')
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.ancestor_of(events_index, inclusive=True)
-
- self.assertEqual(pages.count(), 3)
- self.assertEqual(pages[0], root_page)
- self.assertEqual(pages[1], homepage)
- self.assertEqual(pages[2], events_index)
-
- def test_not_ancestor_of(self):
- root_page = Page.objects.get(id=1)
- homepage = Page.objects.get(url_path='/home/')
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.not_ancestor_of(events_index)
-
- # Test that none of the ancestors are in pages
- for page in pages:
- self.assertNotEqual(page, root_page)
- self.assertNotEqual(page, homepage)
-
- # Test that events index is in pages
- self.assertTrue(pages.filter(id=events_index.id).exists())
-
- def test_not_ancestor_of_inclusive(self):
- root_page = Page.objects.get(id=1)
- homepage = Page.objects.get(url_path='/home/')
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.not_ancestor_of(events_index, inclusive=True)
-
- # Test that none of the ancestors or the events_index are in pages
- for page in pages:
- self.assertNotEqual(page, root_page)
- self.assertNotEqual(page, homepage)
- self.assertNotEqual(page, events_index)
-
- def test_parent_of(self):
- homepage = Page.objects.get(url_path='/home/')
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.parent_of(events_index)
-
- # Pages must only contain homepage
- self.assertEqual(pages.count(), 1)
- self.assertEqual(pages[0], homepage)
-
- def test_not_parent_of(self):
- homepage = Page.objects.get(url_path='/home/')
- events_index = Page.objects.get(url_path='/home/events/')
- pages = Page.objects.not_parent_of(events_index)
-
- # Pages must not contain homepage
- for page in pages:
- self.assertNotEqual(page, homepage)
-
- # Test that events index is in pages
- self.assertTrue(pages.filter(id=events_index.id).exists())
-
- def test_sibling_of(self):
- events_index = Page.objects.get(url_path='/home/events/')
- event = Page.objects.get(url_path='/home/events/christmas/')
- pages = Page.objects.sibling_of(event)
-
- # Check that all pages are children of events_index
- for page in pages:
- self.assertEqual(page.get_parent(), events_index)
-
- # Check that the event is not included
- self.assertFalse(pages.filter(id=event.id).exists())
-
- def test_sibling_of_inclusive(self):
- events_index = Page.objects.get(url_path='/home/events/')
- event = Page.objects.get(url_path='/home/events/christmas/')
- pages = Page.objects.sibling_of(event, inclusive=True)
-
- # Check that all pages are children of events_index
- for page in pages:
- self.assertEqual(page.get_parent(), events_index)
-
- # Check that the event is included
- self.assertTrue(pages.filter(id=event.id).exists())
-
- def test_not_sibling_of(self):
- events_index = Page.objects.get(url_path='/home/events/')
- event = Page.objects.get(url_path='/home/events/christmas/')
- pages = Page.objects.not_sibling_of(event)
-
- # Check that all pages are not children of events_index
- for page in pages:
- if page != event:
- self.assertNotEqual(page.get_parent(), events_index)
-
- # Check that the event is included
- self.assertTrue(pages.filter(id=event.id).exists())
-
- # Test that events index is in pages
- self.assertTrue(pages.filter(id=events_index.id).exists())
-
- def test_not_sibling_of_inclusive(self):
- events_index = Page.objects.get(url_path='/home/events/')
- event = Page.objects.get(url_path='/home/events/christmas/')
- pages = Page.objects.not_sibling_of(event, inclusive=True)
-
- # Check that all pages are not children of events_index
- for page in pages:
- self.assertNotEqual(page.get_parent(), events_index)
-
- # Check that the event is not included
- self.assertFalse(pages.filter(id=event.id).exists())
-
- # Test that events index is in pages
- self.assertTrue(pages.filter(id=events_index.id).exists())
-
- def test_type(self):
- pages = Page.objects.type(EventPage)
-
- # Check that all objects are EventPages
- for page in pages:
- self.assertIsInstance(page.specific, EventPage)
-
- # Check that "someone elses event" is in the results
- event = Page.objects.get(url_path='/home/events/someone-elses-event/')
- self.assertTrue(pages.filter(id=event.id).exists())
-
- def test_not_type(self):
- pages = Page.objects.not_type(EventPage)
-
- # Check that no objects are EventPages
- for page in pages:
- self.assertNotIsInstance(page.specific, EventPage)
-
- # Check that the homepage is in the results
- homepage = Page.objects.get(url_path='/home/')
- self.assertTrue(pages.filter(id=homepage.id).exists())
-
-
-class TestMovePage(TestCase):
- fixtures = ['test.json']
-
- def test_move_page(self):
- about_us_page = SimplePage.objects.get(url_path='/home/about-us/')
- events_index = EventIndex.objects.get(url_path='/home/events/')
-
- events_index.move(about_us_page, pos='last-child')
-
- # re-fetch events index to confirm that db fields have been updated
- events_index = EventIndex.objects.get(id=events_index.id)
- self.assertEqual(events_index.url_path, '/home/about-us/events/')
- self.assertEqual(events_index.depth, 4)
- self.assertEqual(events_index.get_parent().id, about_us_page.id)
-
- # children of events_index should also have been updated
- christmas = events_index.get_children().get(slug='christmas')
- self.assertEqual(christmas.depth, 5)
- self.assertEqual(christmas.url_path, '/home/about-us/events/christmas/')
-
-
-class TestIssue7(TestCase):
- """
- This tests for an issue where if a site root page was moved, all the page
- urls in that site would change to None.
-
- The issue was caused by the 'wagtail_site_root_paths' cache variable not being
- cleared when a site root page was moved. Which left all the child pages
- thinking that they are no longer in the site and return None as their url.
-
- Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
- Discussion: https://github.com/torchbox/wagtail/issues/7
- """
-
- fixtures = ['test.json']
-
- def test_issue7(self):
- # Get homepage, root page and site
- root_page = Page.objects.get(id=1)
- homepage = Page.objects.get(url_path='/home/')
- default_site = Site.objects.get(is_default_site=True)
-
- # Create a new homepage under current homepage
- new_homepage = SimplePage(title="New Homepage", slug="new-homepage")
- homepage.add_child(instance=new_homepage)
-
- # Set new homepage as the site root page
- default_site.root_page = new_homepage
- default_site.save()
-
- # Warm up the cache by getting the url
- _ = homepage.url
-
- # Move new homepage to root
- new_homepage.move(root_page, pos='last-child')
-
- # Get fresh instance of new_homepage
- new_homepage = Page.objects.get(id=new_homepage.id)
-
- # Check url
- self.assertEqual(new_homepage.url, '/')
-
-
-class TestIssue157(TestCase):
- """
- This tests for an issue where if a site root pages slug was changed, all the page
- urls in that site would change to None.
-
- The issue was caused by the 'wagtail_site_root_paths' cache variable not being
- cleared when a site root page was changed. Which left all the child pages
- thinking that they are no longer in the site and return None as their url.
-
- Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
- Discussion: https://github.com/torchbox/wagtail/issues/157
- """
-
- fixtures = ['test.json']
-
- def test_issue157(self):
- # Get homepage
- homepage = Page.objects.get(url_path='/home/')
-
- # Warm up the cache by getting the url
- _ = homepage.url
-
- # Change homepage title and slug
- homepage.title = "New home"
- homepage.slug = "new-home"
- homepage.save()
-
- # Get fresh instance of homepage
- homepage = Page.objects.get(id=homepage.id)
-
- # Check url
- self.assertEqual(homepage.url, '/')
-
-
-class TestFixTreeCommand(TestCase):
- fixtures = ['test.json']
-
- def run_command(self):
- management.call_command('fixtree', interactive=False, stdout=StringIO())
-
- def test_fixes_numchild(self):
- # Get homepage and save old value
- homepage = Page.objects.get(url_path='/home/')
- old_numchild = homepage.numchild
-
- # Break it
- homepage.numchild = 12345
- homepage.save()
-
- # Check that its broken
- self.assertEqual(Page.objects.get(url_path='/home/').numchild, 12345)
-
- # Call command
- self.run_command()
-
- # Check if its fixed
- self.assertEqual(Page.objects.get(url_path='/home/').numchild, old_numchild)
-
- def test_fixes_depth(self):
- # Get homepage and save old value
- homepage = Page.objects.get(url_path='/home/')
- old_depth = homepage.depth
-
- # Break it
- homepage.depth = 12345
- homepage.save()
-
- # Check that its broken
- self.assertEqual(Page.objects.get(url_path='/home/').depth, 12345)
-
- # Call command
- self.run_command()
-
- # Check if its fixed
- self.assertEqual(Page.objects.get(url_path='/home/').depth, old_depth)
-
-
-class TestMovePagesCommand(TestCase):
- fixtures = ['test.json']
-
- def run_command(self, from_, to):
- management.call_command('move_pages', str(from_), str(to), interactive=False, stdout=StringIO())
-
- def test_move_pages(self):
- # Get pages
- events_index = Page.objects.get(url_path='/home/events/')
- about_us = Page.objects.get(url_path='/home/about-us/')
- page_ids = events_index.get_children().values_list('id', flat=True)
-
- # Move all events into "about us"
- self.run_command(events_index.id, about_us.id)
-
- # Check that all pages moved
- for page_id in page_ids:
- self.assertEqual(Page.objects.get(id=page_id).get_parent(), about_us)
-
-
-class TestReplaceTextCommand(TestCase):
- fixtures = ['test.json']
-
- def run_command(self, from_text, to_text):
- management.call_command('replace_text', from_text, to_text, interactive=False, stdout=StringIO())
-
- def test_replace_text(self):
- # Check that the christmas page is definitely about christmas
- self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Christmas")
-
- # Make it about easter
- self.run_command("Christmas", "Easter")
-
- # Check that its now about easter
- self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter")
diff --git a/wagtail/wagtailcore/tests/__init__.py b/wagtail/wagtailcore/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wagtail/wagtailcore/tests/test_management_commands.py b/wagtail/wagtailcore/tests/test_management_commands.py
new file mode 100644
index 000000000..787b808b5
--- /dev/null
+++ b/wagtail/wagtailcore/tests/test_management_commands.py
@@ -0,0 +1,89 @@
+from StringIO import StringIO
+
+from django.test import TestCase, Client
+from django.http import HttpRequest, Http404
+from django.core import management
+from django.contrib.auth.models import User
+
+from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
+from wagtail.tests.models import EventPage, EventIndex, SimplePage
+
+
+class TestFixTreeCommand(TestCase):
+ fixtures = ['test.json']
+
+ def run_command(self):
+ management.call_command('fixtree', interactive=False, stdout=StringIO())
+
+ def test_fixes_numchild(self):
+ # Get homepage and save old value
+ homepage = Page.objects.get(url_path='/home/')
+ old_numchild = homepage.numchild
+
+ # Break it
+ homepage.numchild = 12345
+ homepage.save()
+
+ # Check that its broken
+ self.assertEqual(Page.objects.get(url_path='/home/').numchild, 12345)
+
+ # Call command
+ self.run_command()
+
+ # Check if its fixed
+ self.assertEqual(Page.objects.get(url_path='/home/').numchild, old_numchild)
+
+ def test_fixes_depth(self):
+ # Get homepage and save old value
+ homepage = Page.objects.get(url_path='/home/')
+ old_depth = homepage.depth
+
+ # Break it
+ homepage.depth = 12345
+ homepage.save()
+
+ # Check that its broken
+ self.assertEqual(Page.objects.get(url_path='/home/').depth, 12345)
+
+ # Call command
+ self.run_command()
+
+ # Check if its fixed
+ self.assertEqual(Page.objects.get(url_path='/home/').depth, old_depth)
+
+
+class TestMovePagesCommand(TestCase):
+ fixtures = ['test.json']
+
+ def run_command(self, from_, to):
+ management.call_command('move_pages', str(from_), str(to), interactive=False, stdout=StringIO())
+
+ def test_move_pages(self):
+ # Get pages
+ events_index = Page.objects.get(url_path='/home/events/')
+ about_us = Page.objects.get(url_path='/home/about-us/')
+ page_ids = events_index.get_children().values_list('id', flat=True)
+
+ # Move all events into "about us"
+ self.run_command(events_index.id, about_us.id)
+
+ # Check that all pages moved
+ for page_id in page_ids:
+ self.assertEqual(Page.objects.get(id=page_id).get_parent(), about_us)
+
+
+class TestReplaceTextCommand(TestCase):
+ fixtures = ['test.json']
+
+ def run_command(self, from_text, to_text):
+ management.call_command('replace_text', from_text, to_text, interactive=False, stdout=StringIO())
+
+ def test_replace_text(self):
+ # Check that the christmas page is definitely about christmas
+ self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Christmas")
+
+ # Make it about easter
+ self.run_command("Christmas", "Easter")
+
+ # Check that its now about easter
+ self.assertEqual(Page.objects.get(url_path='/home/events/christmas/').title, "Easter")
diff --git a/wagtail/wagtailcore/tests/test_page_model.py b/wagtail/wagtailcore/tests/test_page_model.py
new file mode 100644
index 000000000..1bae57952
--- /dev/null
+++ b/wagtail/wagtailcore/tests/test_page_model.py
@@ -0,0 +1,226 @@
+from StringIO import StringIO
+
+from django.test import TestCase, Client
+from django.http import HttpRequest, Http404
+from django.core import management
+from django.contrib.auth.models import User
+
+from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
+from wagtail.tests.models import EventPage, EventIndex, SimplePage
+
+
+class TestRouting(TestCase):
+ fixtures = ['test.json']
+
+ def test_find_site_for_request(self):
+ default_site = Site.objects.get(is_default_site=True)
+ events_page = Page.objects.get(url_path='/home/events/')
+ events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
+
+ # requests without a Host: header should be directed to the default site
+ request = HttpRequest()
+ request.path = '/'
+ self.assertEqual(Site.find_for_request(request), default_site)
+
+ # requests with a known Host: header should be directed to the specific site
+ request = HttpRequest()
+ request.path = '/'
+ request.META['HTTP_HOST'] = 'events.example.com'
+ self.assertEqual(Site.find_for_request(request), events_site)
+
+ # requests with an unrecognised Host: header should be directed to the default site
+ request = HttpRequest()
+ request.path = '/'
+ request.META['HTTP_HOST'] = 'unknown.example.com'
+ self.assertEqual(Site.find_for_request(request), default_site)
+
+ def test_urls(self):
+ default_site = Site.objects.get(is_default_site=True)
+ homepage = Page.objects.get(url_path='/home/')
+ christmas_page = Page.objects.get(url_path='/home/events/christmas/')
+
+ # Basic installation only has one site configured, so page.url will return local URLs
+ self.assertEqual(homepage.full_url, 'http://localhost/')
+ self.assertEqual(homepage.url, '/')
+ self.assertEqual(homepage.relative_url(default_site), '/')
+
+ self.assertEqual(christmas_page.full_url, 'http://localhost/events/christmas/')
+ self.assertEqual(christmas_page.url, '/events/christmas/')
+ self.assertEqual(christmas_page.relative_url(default_site), '/events/christmas/')
+
+ def test_urls_with_multiple_sites(self):
+ events_page = Page.objects.get(url_path='/home/events/')
+ events_site = Site.objects.create(hostname='events.example.com', root_page=events_page)
+
+ default_site = Site.objects.get(is_default_site=True)
+ homepage = Page.objects.get(url_path='/home/')
+ christmas_page = Page.objects.get(url_path='/home/events/christmas/')
+
+ # with multiple sites, page.url will return full URLs to ensure that
+ # they work across sites
+ self.assertEqual(homepage.full_url, 'http://localhost/')
+ self.assertEqual(homepage.url, 'http://localhost/')
+ self.assertEqual(homepage.relative_url(default_site), '/')
+ self.assertEqual(homepage.relative_url(events_site), 'http://localhost/')
+
+ self.assertEqual(christmas_page.full_url, 'http://events.example.com/christmas/')
+ self.assertEqual(christmas_page.url, 'http://events.example.com/christmas/')
+ self.assertEqual(christmas_page.relative_url(default_site), 'http://events.example.com/christmas/')
+ self.assertEqual(christmas_page.relative_url(events_site), '/christmas/')
+
+ def test_request_routing(self):
+ homepage = Page.objects.get(url_path='/home/')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+
+ request = HttpRequest()
+ request.path = '/events/christmas/'
+ response = homepage.route(request, ['events', 'christmas'])
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.context_data['self'], christmas_page)
+ used_template = response.resolve_template(response.template_name)
+ self.assertEqual(used_template.name, 'tests/event_page.html')
+
+ def test_route_to_unknown_page_returns_404(self):
+ homepage = Page.objects.get(url_path='/home/')
+
+ request = HttpRequest()
+ request.path = '/events/quinquagesima/'
+ with self.assertRaises(Http404):
+ homepage.route(request, ['events', 'quinquagesima'])
+
+ def test_route_to_unpublished_page_returns_404(self):
+ homepage = Page.objects.get(url_path='/home/')
+
+ request = HttpRequest()
+ request.path = '/events/tentative-unpublished-event/'
+ with self.assertRaises(Http404):
+ homepage.route(request, ['events', 'tentative-unpublished-event'])
+
+
+class TestServeView(TestCase):
+ fixtures = ['test.json']
+
+ def setUp(self):
+ # Explicitly clear the cache of site root paths. Normally this would be kept
+ # in sync by the Site.save logic, but this is bypassed when the database is
+ # rolled back between tests using transactions.
+ from django.core.cache import cache
+ cache.delete('wagtail_site_root_paths')
+
+ def test_serve(self):
+ response = self.client.get('/events/christmas/')
+
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.templates[0].name, 'tests/event_page.html')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+ self.assertEqual(response.context['self'], christmas_page)
+
+ self.assertContains(response, '
Christmas
')
+ self.assertContains(response, '
Event
')
+
+ def test_serve_unknown_page_returns_404(self):
+ response = self.client.get('/events/quinquagesima/')
+ self.assertEqual(response.status_code, 404)
+
+ def test_serve_unpublished_page_returns_404(self):
+ response = self.client.get('/events/tentative-unpublished-event/')
+ self.assertEqual(response.status_code, 404)
+
+ def test_serve_with_multiple_sites(self):
+ events_page = Page.objects.get(url_path='/home/events/')
+ Site.objects.create(hostname='events.example.com', root_page=events_page)
+
+ response = self.client.get('/christmas/', HTTP_HOST='events.example.com')
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response.templates[0].name, 'tests/event_page.html')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+ self.assertEqual(response.context['self'], christmas_page)
+
+ self.assertContains(response, '
Christmas
')
+ self.assertContains(response, '
Event
')
+
+ # same request to the default host should return a 404
+ c = Client()
+ response = c.get('/christmas/', HTTP_HOST='localhost')
+ self.assertEqual(response.status_code, 404)
+
+ def test_serve_with_custom_context(self):
+ response = self.client.get('/events/')
+ self.assertEqual(response.status_code, 200)
+
+ # should render the whole page
+ self.assertContains(response, '
Events
')
+
+ # response should contain data from the custom 'events' context variable
+ self.assertContains(response, '
Christmas')
+
+ def test_ajax_response(self):
+ response = self.client.get('/events/', HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+ self.assertEqual(response.status_code, 200)
+
+ # should only render the content of includes/event_listing.html, not the whole page
+ self.assertNotContains(response, '
Events
')
+ self.assertContains(response, '
Christmas')
+
+
+class TestStaticSitePaths(TestCase):
+ def setUp(self):
+ self.root_page = Page.objects.get(id=1)
+
+ # For simple tests
+ self.home_page = self.root_page.add_child(instance=SimplePage(title="Homepage", slug="home"))
+ self.about_page = self.home_page.add_child(instance=SimplePage(title="About us", slug="about"))
+ self.contact_page = self.home_page.add_child(instance=SimplePage(title="Contact", slug="contact"))
+
+ # For custom tests
+ self.event_index = self.root_page.add_child(instance=EventIndex(title="Events", slug="events"))
+ for i in range(20):
+ self.event_index.add_child(instance=EventPage(title="Event " + str(i), slug="event" + str(i)))
+
+ def test_local_static_site_paths(self):
+ paths = list(self.about_page.get_static_site_paths())
+
+ self.assertEqual(paths, ['/'])
+
+ def test_child_static_site_paths(self):
+ paths = list(self.home_page.get_static_site_paths())
+
+ self.assertEqual(paths, ['/', '/about/', '/contact/'])
+
+ def test_custom_static_site_paths(self):
+ paths = list(self.event_index.get_static_site_paths())
+
+ # Event index path
+ expected_paths = ['/']
+
+ # One path for each page of results
+ expected_paths.extend(['/' + str(i + 1) + '/' for i in range(5)])
+
+ # One path for each event page
+ expected_paths.extend(['/event' + str(i) + '/' for i in range(20)])
+
+ paths.sort()
+ expected_paths.sort()
+ self.assertEqual(paths, expected_paths)
+
+
+class TestMovePage(TestCase):
+ fixtures = ['test.json']
+
+ def test_move_page(self):
+ about_us_page = SimplePage.objects.get(url_path='/home/about-us/')
+ events_index = EventIndex.objects.get(url_path='/home/events/')
+
+ events_index.move(about_us_page, pos='last-child')
+
+ # re-fetch events index to confirm that db fields have been updated
+ events_index = EventIndex.objects.get(id=events_index.id)
+ self.assertEqual(events_index.url_path, '/home/about-us/events/')
+ self.assertEqual(events_index.depth, 4)
+ self.assertEqual(events_index.get_parent().id, about_us_page.id)
+
+ # children of events_index should also have been updated
+ christmas = events_index.get_children().get(slug='christmas')
+ self.assertEqual(christmas.depth, 5)
+ self.assertEqual(christmas.url_path, '/home/about-us/events/christmas/')
diff --git a/wagtail/wagtailcore/tests/test_page_permissions.py b/wagtail/wagtailcore/tests/test_page_permissions.py
new file mode 100644
index 000000000..dbe35e39b
--- /dev/null
+++ b/wagtail/wagtailcore/tests/test_page_permissions.py
@@ -0,0 +1,226 @@
+from StringIO import StringIO
+
+from django.test import TestCase, Client
+from django.http import HttpRequest, Http404
+from django.core import management
+from django.contrib.auth.models import User
+
+from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
+from wagtail.tests.models import EventPage, EventIndex, SimplePage
+
+
+class TestPagePermission(TestCase):
+ fixtures = ['test.json']
+
+ def test_nonpublisher_page_permissions(self):
+ event_editor = User.objects.get(username='eventeditor')
+ homepage = Page.objects.get(url_path='/home/')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+ unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
+ someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
+
+ homepage_perms = homepage.permissions_for_user(event_editor)
+ christmas_page_perms = christmas_page.permissions_for_user(event_editor)
+ unpub_perms = unpublished_event_page.permissions_for_user(event_editor)
+ someone_elses_event_perms = someone_elses_event_page.permissions_for_user(event_editor)
+
+ self.assertFalse(homepage_perms.can_add_subpage())
+ self.assertTrue(christmas_page_perms.can_add_subpage())
+ self.assertTrue(unpub_perms.can_add_subpage())
+ self.assertTrue(someone_elses_event_perms.can_add_subpage())
+
+ self.assertFalse(homepage_perms.can_edit())
+ self.assertTrue(christmas_page_perms.can_edit())
+ self.assertTrue(unpub_perms.can_edit())
+ self.assertFalse(someone_elses_event_perms.can_edit()) # basic 'add' permission doesn't allow editing pages owned by someone else
+
+ self.assertFalse(homepage_perms.can_delete())
+ self.assertFalse(christmas_page_perms.can_delete()) # cannot delete because it is published
+ self.assertTrue(unpub_perms.can_delete())
+ self.assertFalse(someone_elses_event_perms.can_delete())
+
+ self.assertFalse(homepage_perms.can_publish())
+ self.assertFalse(christmas_page_perms.can_publish())
+ self.assertFalse(unpub_perms.can_publish())
+
+ self.assertFalse(homepage_perms.can_unpublish())
+ self.assertFalse(christmas_page_perms.can_unpublish())
+ self.assertFalse(unpub_perms.can_unpublish())
+
+ self.assertFalse(homepage_perms.can_publish_subpage())
+ self.assertFalse(christmas_page_perms.can_publish_subpage())
+ self.assertFalse(unpub_perms.can_publish_subpage())
+
+ self.assertFalse(homepage_perms.can_reorder_children())
+ self.assertFalse(christmas_page_perms.can_reorder_children())
+ self.assertFalse(unpub_perms.can_reorder_children())
+
+ self.assertFalse(homepage_perms.can_move())
+ self.assertFalse(christmas_page_perms.can_move()) # cannot move because this would involve unpublishing from its current location
+ self.assertTrue(unpub_perms.can_move())
+ self.assertFalse(someone_elses_event_perms.can_move())
+
+ self.assertFalse(christmas_page_perms.can_move_to(unpublished_event_page)) # cannot move because this would involve unpublishing from its current location
+ self.assertTrue(unpub_perms.can_move_to(christmas_page))
+ self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination
+ self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself
+
+
+ def test_publisher_page_permissions(self):
+ event_moderator = User.objects.get(username='eventmoderator')
+ homepage = Page.objects.get(url_path='/home/')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+ unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
+
+ homepage_perms = homepage.permissions_for_user(event_moderator)
+ christmas_page_perms = christmas_page.permissions_for_user(event_moderator)
+ unpub_perms = unpublished_event_page.permissions_for_user(event_moderator)
+
+ self.assertFalse(homepage_perms.can_add_subpage())
+ self.assertTrue(christmas_page_perms.can_add_subpage())
+ self.assertTrue(unpub_perms.can_add_subpage())
+
+ self.assertFalse(homepage_perms.can_edit())
+ self.assertTrue(christmas_page_perms.can_edit())
+ self.assertTrue(unpub_perms.can_edit())
+
+ self.assertFalse(homepage_perms.can_delete())
+ self.assertTrue(christmas_page_perms.can_delete()) # cannot delete because it is published
+ self.assertTrue(unpub_perms.can_delete())
+
+ self.assertFalse(homepage_perms.can_publish())
+ self.assertTrue(christmas_page_perms.can_publish())
+ self.assertTrue(unpub_perms.can_publish())
+
+ self.assertFalse(homepage_perms.can_unpublish())
+ self.assertTrue(christmas_page_perms.can_unpublish())
+ self.assertFalse(unpub_perms.can_unpublish()) # cannot unpublish a page that isn't published
+
+ self.assertFalse(homepage_perms.can_publish_subpage())
+ self.assertTrue(christmas_page_perms.can_publish_subpage())
+ self.assertTrue(unpub_perms.can_publish_subpage())
+
+ self.assertFalse(homepage_perms.can_reorder_children())
+ self.assertTrue(christmas_page_perms.can_reorder_children())
+ self.assertTrue(unpub_perms.can_reorder_children())
+
+ self.assertFalse(homepage_perms.can_move())
+ self.assertTrue(christmas_page_perms.can_move())
+ self.assertTrue(unpub_perms.can_move())
+
+ self.assertTrue(christmas_page_perms.can_move_to(unpublished_event_page))
+ self.assertTrue(unpub_perms.can_move_to(christmas_page))
+ self.assertFalse(unpub_perms.can_move_to(homepage)) # no permission to create pages at destination
+ self.assertFalse(unpub_perms.can_move_to(unpublished_event_page)) # cannot make page a child of itself
+
+ def test_inactive_user_has_no_permissions(self):
+ user = User.objects.get(username='inactiveuser')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+ unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
+
+ christmas_page_perms = christmas_page.permissions_for_user(user)
+ unpub_perms = unpublished_event_page.permissions_for_user(user)
+
+ self.assertFalse(unpub_perms.can_add_subpage())
+ self.assertFalse(unpub_perms.can_edit())
+ self.assertFalse(unpub_perms.can_delete())
+ self.assertFalse(unpub_perms.can_publish())
+ self.assertFalse(christmas_page_perms.can_unpublish())
+ self.assertFalse(unpub_perms.can_publish_subpage())
+ self.assertFalse(unpub_perms.can_reorder_children())
+ self.assertFalse(unpub_perms.can_move())
+ self.assertFalse(unpub_perms.can_move_to(christmas_page))
+
+ def test_superuser_has_full_permissions(self):
+ user = User.objects.get(username='superuser')
+ homepage = Page.objects.get(url_path='/home/')
+ root = Page.objects.get(url_path='/')
+ unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
+
+ homepage_perms = homepage.permissions_for_user(user)
+ root_perms = root.permissions_for_user(user)
+ unpub_perms = unpublished_event_page.permissions_for_user(user)
+
+ self.assertTrue(homepage_perms.can_add_subpage())
+ self.assertTrue(root_perms.can_add_subpage())
+
+ self.assertTrue(homepage_perms.can_edit())
+ self.assertFalse(root_perms.can_edit()) # root is not a real editable page, even to superusers
+
+ self.assertTrue(homepage_perms.can_delete())
+ self.assertFalse(root_perms.can_delete())
+
+ self.assertTrue(homepage_perms.can_publish())
+ self.assertFalse(root_perms.can_publish())
+
+ self.assertTrue(homepage_perms.can_unpublish())
+ self.assertFalse(root_perms.can_unpublish())
+ self.assertFalse(unpub_perms.can_unpublish())
+
+ self.assertTrue(homepage_perms.can_publish_subpage())
+ self.assertTrue(root_perms.can_publish_subpage())
+
+ self.assertTrue(homepage_perms.can_reorder_children())
+ self.assertTrue(root_perms.can_reorder_children())
+
+ self.assertTrue(homepage_perms.can_move())
+ self.assertFalse(root_perms.can_move())
+
+ self.assertTrue(homepage_perms.can_move_to(root))
+ self.assertFalse(homepage_perms.can_move_to(unpublished_event_page))
+
+ def test_editable_pages_for_user_with_add_permission(self):
+ event_editor = User.objects.get(username='eventeditor')
+ homepage = Page.objects.get(url_path='/home/')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+ unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
+ someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
+
+ editable_pages = UserPagePermissionsProxy(event_editor).editable_pages()
+
+ self.assertFalse(editable_pages.filter(id=homepage.id).exists())
+ self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
+ self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
+ self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
+
+ def test_editable_pages_for_user_with_edit_permission(self):
+ event_moderator = User.objects.get(username='eventmoderator')
+ homepage = Page.objects.get(url_path='/home/')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+ unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
+ someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
+
+ editable_pages = UserPagePermissionsProxy(event_moderator).editable_pages()
+
+ self.assertFalse(editable_pages.filter(id=homepage.id).exists())
+ self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
+ self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
+ self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
+
+ def test_editable_pages_for_inactive_user(self):
+ user = User.objects.get(username='inactiveuser')
+ homepage = Page.objects.get(url_path='/home/')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+ unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
+ someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
+
+ editable_pages = UserPagePermissionsProxy(user).editable_pages()
+
+ self.assertFalse(editable_pages.filter(id=homepage.id).exists())
+ self.assertFalse(editable_pages.filter(id=christmas_page.id).exists())
+ self.assertFalse(editable_pages.filter(id=unpublished_event_page.id).exists())
+ self.assertFalse(editable_pages.filter(id=someone_elses_event_page.id).exists())
+
+ def test_editable_pages_for_superuser(self):
+ user = User.objects.get(username='superuser')
+ homepage = Page.objects.get(url_path='/home/')
+ christmas_page = EventPage.objects.get(url_path='/home/events/christmas/')
+ unpublished_event_page = EventPage.objects.get(url_path='/home/events/tentative-unpublished-event/')
+ someone_elses_event_page = EventPage.objects.get(url_path='/home/events/someone-elses-event/')
+
+ editable_pages = UserPagePermissionsProxy(user).editable_pages()
+
+ self.assertTrue(editable_pages.filter(id=homepage.id).exists())
+ self.assertTrue(editable_pages.filter(id=christmas_page.id).exists())
+ self.assertTrue(editable_pages.filter(id=unpublished_event_page.id).exists())
+ self.assertTrue(editable_pages.filter(id=someone_elses_event_page.id).exists())
diff --git a/wagtail/wagtailcore/tests/test_page_queryset.py b/wagtail/wagtailcore/tests/test_page_queryset.py
new file mode 100644
index 000000000..06f2c3e21
--- /dev/null
+++ b/wagtail/wagtailcore/tests/test_page_queryset.py
@@ -0,0 +1,256 @@
+from StringIO import StringIO
+
+from django.test import TestCase, Client
+from django.http import HttpRequest, Http404
+from django.core import management
+from django.contrib.auth.models import User
+
+from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
+from wagtail.tests.models import EventPage, EventIndex, SimplePage
+
+
+class TestPageQuerySet(TestCase):
+ fixtures = ['test.json']
+
+ def test_live(self):
+ pages = Page.objects.live()
+
+ # All pages must be live
+ for page in pages:
+ self.assertTrue(page.live)
+
+ # Check that the homepage is in the results
+ homepage = Page.objects.get(url_path='/home/')
+ self.assertTrue(pages.filter(id=homepage.id).exists())
+
+ def test_not_live(self):
+ pages = Page.objects.not_live()
+
+ # All pages must not be live
+ for page in pages:
+ self.assertFalse(page.live)
+
+ # Check that "someone elses event" is in the results
+ event = Page.objects.get(url_path='/home/events/someone-elses-event/')
+ self.assertTrue(pages.filter(id=event.id).exists())
+
+ def test_page(self):
+ homepage = Page.objects.get(url_path='/home/')
+ pages = Page.objects.page(homepage)
+
+ # Should only select the homepage
+ self.assertEqual(pages.count(), 1)
+ self.assertEqual(pages.first(), homepage)
+
+ def test_not_page(self):
+ homepage = Page.objects.get(url_path='/home/')
+ pages = Page.objects.not_page(homepage)
+
+ # Should select everything except for the homepage
+ self.assertEqual(pages.count(), Page.objects.all().count() - 1)
+ for page in pages:
+ self.assertNotEqual(page, homepage)
+
+ def test_descendant_of(self):
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.descendant_of(events_index)
+
+ # Check that all pages descend from events index
+ for page in pages:
+ self.assertTrue(page.get_ancestors().filter(id=events_index.id).exists())
+
+ def test_descendant_of_inclusive(self):
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.descendant_of(events_index, inclusive=True)
+
+ # Check that all pages descend from events index, includes event index
+ for page in pages:
+ self.assertTrue(page == events_index or page.get_ancestors().filter(id=events_index.id).exists())
+
+ # Check that event index was included
+ self.assertTrue(pages.filter(id=events_index.id).exists())
+
+ def test_not_descendant_of(self):
+ homepage = Page.objects.get(url_path='/home/')
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.not_descendant_of(events_index)
+
+ # Check that no pages descend from events_index
+ for page in pages:
+ self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists())
+
+ # As this is not inclusive, events index should be in the results
+ self.assertTrue(pages.filter(id=events_index.id).exists())
+
+ def test_not_descendant_of_inclusive(self):
+ homepage = Page.objects.get(url_path='/home/')
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.not_descendant_of(events_index, inclusive=True)
+
+ # Check that all pages descend from homepage but not events index
+ for page in pages:
+ self.assertFalse(page.get_ancestors().filter(id=events_index.id).exists())
+
+ # As this is inclusive, events index should not be in the results
+ self.assertFalse(pages.filter(id=events_index.id).exists())
+
+ def test_child_of(self):
+ homepage = Page.objects.get(url_path='/home/')
+ pages = Page.objects.child_of(homepage)
+
+ # Check that all pages are children of homepage
+ for page in pages:
+ self.assertEqual(page.get_parent(), homepage)
+
+ def test_not_child_of(self):
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.not_child_of(events_index)
+
+ # Check that all pages are not children of events_index
+ for page in pages:
+ self.assertNotEqual(page.get_parent(), events_index)
+
+ def test_ancestor_of(self):
+ root_page = Page.objects.get(id=1)
+ homepage = Page.objects.get(url_path='/home/')
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.ancestor_of(events_index)
+
+ self.assertEqual(pages.count(), 2)
+ self.assertEqual(pages[0], root_page)
+ self.assertEqual(pages[1], homepage)
+
+ def test_ancestor_of_inclusive(self):
+ root_page = Page.objects.get(id=1)
+ homepage = Page.objects.get(url_path='/home/')
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.ancestor_of(events_index, inclusive=True)
+
+ self.assertEqual(pages.count(), 3)
+ self.assertEqual(pages[0], root_page)
+ self.assertEqual(pages[1], homepage)
+ self.assertEqual(pages[2], events_index)
+
+ def test_not_ancestor_of(self):
+ root_page = Page.objects.get(id=1)
+ homepage = Page.objects.get(url_path='/home/')
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.not_ancestor_of(events_index)
+
+ # Test that none of the ancestors are in pages
+ for page in pages:
+ self.assertNotEqual(page, root_page)
+ self.assertNotEqual(page, homepage)
+
+ # Test that events index is in pages
+ self.assertTrue(pages.filter(id=events_index.id).exists())
+
+ def test_not_ancestor_of_inclusive(self):
+ root_page = Page.objects.get(id=1)
+ homepage = Page.objects.get(url_path='/home/')
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.not_ancestor_of(events_index, inclusive=True)
+
+ # Test that none of the ancestors or the events_index are in pages
+ for page in pages:
+ self.assertNotEqual(page, root_page)
+ self.assertNotEqual(page, homepage)
+ self.assertNotEqual(page, events_index)
+
+ def test_parent_of(self):
+ homepage = Page.objects.get(url_path='/home/')
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.parent_of(events_index)
+
+ # Pages must only contain homepage
+ self.assertEqual(pages.count(), 1)
+ self.assertEqual(pages[0], homepage)
+
+ def test_not_parent_of(self):
+ homepage = Page.objects.get(url_path='/home/')
+ events_index = Page.objects.get(url_path='/home/events/')
+ pages = Page.objects.not_parent_of(events_index)
+
+ # Pages must not contain homepage
+ for page in pages:
+ self.assertNotEqual(page, homepage)
+
+ # Test that events index is in pages
+ self.assertTrue(pages.filter(id=events_index.id).exists())
+
+ def test_sibling_of(self):
+ events_index = Page.objects.get(url_path='/home/events/')
+ event = Page.objects.get(url_path='/home/events/christmas/')
+ pages = Page.objects.sibling_of(event)
+
+ # Check that all pages are children of events_index
+ for page in pages:
+ self.assertEqual(page.get_parent(), events_index)
+
+ # Check that the event is not included
+ self.assertFalse(pages.filter(id=event.id).exists())
+
+ def test_sibling_of_inclusive(self):
+ events_index = Page.objects.get(url_path='/home/events/')
+ event = Page.objects.get(url_path='/home/events/christmas/')
+ pages = Page.objects.sibling_of(event, inclusive=True)
+
+ # Check that all pages are children of events_index
+ for page in pages:
+ self.assertEqual(page.get_parent(), events_index)
+
+ # Check that the event is included
+ self.assertTrue(pages.filter(id=event.id).exists())
+
+ def test_not_sibling_of(self):
+ events_index = Page.objects.get(url_path='/home/events/')
+ event = Page.objects.get(url_path='/home/events/christmas/')
+ pages = Page.objects.not_sibling_of(event)
+
+ # Check that all pages are not children of events_index
+ for page in pages:
+ if page != event:
+ self.assertNotEqual(page.get_parent(), events_index)
+
+ # Check that the event is included
+ self.assertTrue(pages.filter(id=event.id).exists())
+
+ # Test that events index is in pages
+ self.assertTrue(pages.filter(id=events_index.id).exists())
+
+ def test_not_sibling_of_inclusive(self):
+ events_index = Page.objects.get(url_path='/home/events/')
+ event = Page.objects.get(url_path='/home/events/christmas/')
+ pages = Page.objects.not_sibling_of(event, inclusive=True)
+
+ # Check that all pages are not children of events_index
+ for page in pages:
+ self.assertNotEqual(page.get_parent(), events_index)
+
+ # Check that the event is not included
+ self.assertFalse(pages.filter(id=event.id).exists())
+
+ # Test that events index is in pages
+ self.assertTrue(pages.filter(id=events_index.id).exists())
+
+ def test_type(self):
+ pages = Page.objects.type(EventPage)
+
+ # Check that all objects are EventPages
+ for page in pages:
+ self.assertIsInstance(page.specific, EventPage)
+
+ # Check that "someone elses event" is in the results
+ event = Page.objects.get(url_path='/home/events/someone-elses-event/')
+ self.assertTrue(pages.filter(id=event.id).exists())
+
+ def test_not_type(self):
+ pages = Page.objects.not_type(EventPage)
+
+ # Check that no objects are EventPages
+ for page in pages:
+ self.assertNotIsInstance(page.specific, EventPage)
+
+ # Check that the homepage is in the results
+ homepage = Page.objects.get(url_path='/home/')
+ self.assertTrue(pages.filter(id=homepage.id).exists())
diff --git a/wagtail/wagtailcore/tests/test_whitelist.py b/wagtail/wagtailcore/tests/test_whitelist.py
new file mode 100644
index 000000000..28b63d9fb
--- /dev/null
+++ b/wagtail/wagtailcore/tests/test_whitelist.py
@@ -0,0 +1,136 @@
+from bs4 import BeautifulSoup, NavigableString
+
+from django.test import TestCase
+from wagtail.wagtailcore.whitelist import (
+ check_url,
+ attribute_rule,
+ allow_without_attributes,
+ Whitelister
+)
+
+class TestCheckUrl(TestCase):
+ def test_allowed_url_schemes(self):
+ for url_scheme in ['', 'http', 'https', 'ftp', 'mailto', 'tel']:
+ url = url_scheme + "://www.example.com"
+ self.assertTrue(bool(check_url(url)))
+
+ def test_disallowed_url_scheme(self):
+ self.assertFalse(bool(check_url("invalid://url")))
+
+
+class TestAttributeRule(TestCase):
+ def setUp(self):
+ self.soup = BeautifulSoup('
baz')
+
+ def test_no_rule_for_attr(self):
+ """
+ Test that attribute_rule() drops attributes for
+ which no rule has been defined.
+ """
+ tag = self.soup.b
+ fn = attribute_rule({'snowman': 'barbecue'})
+ fn(tag)
+ self.assertEqual(str(tag), '
baz')
+
+ def test_rule_true_for_attr(self):
+ """
+ Test that attribute_rule() does not change atrributes
+ when the corresponding rule returns True
+ """
+ tag = self.soup.b
+ fn = attribute_rule({'foo': True})
+ fn(tag)
+ self.assertEqual(str(tag), '
baz')
+
+ def test_rule_false_for_attr(self):
+ """
+ Test that attribute_rule() drops atrributes
+ when the corresponding rule returns False
+ """
+ tag = self.soup.b
+ fn = attribute_rule({'foo': False})
+ fn(tag)
+ self.assertEqual(str(tag), '
baz')
+
+ def test_callable_called_on_attr(self):
+ """
+ Test that when the rule returns a callable,
+ attribute_rule() replaces the attribute with
+ the result of calling the callable on the attribute.
+ """
+ tag = self.soup.b
+ fn = attribute_rule({'foo': len})
+ fn(tag)
+ self.assertEqual(str(tag), '
baz')
+
+ def test_callable_returns_None(self):
+ """
+ Test that when the rule returns a callable,
+ attribute_rule() replaces the attribute with
+ the result of calling the callable on the attribute.
+ """
+ tag = self.soup.b
+ fn = attribute_rule({'foo': lambda x: None})
+ fn(tag)
+ self.assertEqual(str(tag), '
baz')
+
+ def test_allow_without_attributes(self):
+ """
+ Test that attribute_rule() with will drop all
+ attributes.
+ """
+ soup = BeautifulSoup('
')
+ tag = soup.b
+ allow_without_attributes(tag)
+ self.assertEqual(str(tag), '
')
+
+
+class TestWhitelister(TestCase):
+ def test_clean_unknown_node(self):
+ """
+ Unknown node should remove a node from the parent document
+ """
+ soup = BeautifulSoup('
bazquux')
+ tag = soup.foo
+ Whitelister.clean_unknown_node('', soup.bar)
+ self.assertEqual(str(tag), '
quux')
+
+ def test_clean_tag_node_cleans_nested_recognised_node(self):
+ """
+
tags are allowed without attributes. This remains true
+ when tags are nested.
+ """
+ soup = BeautifulSoup('foo')
+ tag = soup.b
+ Whitelister.clean_tag_node(tag, tag)
+ self.assertEqual(str(tag), 'foo')
+
+ def test_clean_tag_node_disallows_nested_unrecognised_node(self):
+ """
+ tags should be removed, even when nested.
+ """
+ soup = BeautifulSoup('bar')
+ tag = soup.b
+ Whitelister.clean_tag_node(tag, tag)
+ self.assertEqual(str(tag), 'bar')
+
+ def test_clean_string_node_does_nothing(self):
+ soup = BeautifulSoup('bar')
+ string = soup.b.string
+ Whitelister.clean_string_node(string, string)
+ self.assertEqual(str(string), 'bar')
+
+ def test_clean_node_does_not_change_navigable_strings(self):
+ soup = BeautifulSoup('bar')
+ string = soup.b.string
+ Whitelister.clean_node(string, string)
+ self.assertEqual(str(string), 'bar')
+
+ def test_clean(self):
+ """
+ Whitelister.clean should remove disallowed tags and attributes from
+ a string
+ """
+ string = 'snowman Yorkshire'
+ cleaned_string = Whitelister.clean(string)
+ self.assertEqual(cleaned_string, 'snowman Yorkshire')
diff --git a/wagtail/wagtailcore/tests/tests.py b/wagtail/wagtailcore/tests/tests.py
new file mode 100644
index 000000000..d10474bba
--- /dev/null
+++ b/wagtail/wagtailcore/tests/tests.py
@@ -0,0 +1,99 @@
+from StringIO import StringIO
+
+from django.test import TestCase, Client
+from django.http import HttpRequest, Http404
+from django.core import management
+from django.contrib.auth.models import User
+
+from wagtail.wagtailcore.models import Page, Site, UserPagePermissionsProxy
+from wagtail.tests.models import EventPage, EventIndex, SimplePage
+
+
+class TestPageUrlTags(TestCase):
+ fixtures = ['test.json']
+
+ def test_pageurl_tag(self):
+ response = self.client.get('/events/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Christmas')
+
+ def test_slugurl_tag(self):
+ response = self.client.get('/events/christmas/')
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, 'Back to events index')
+
+
+class TestIssue7(TestCase):
+ """
+ This tests for an issue where if a site root page was moved, all the page
+ urls in that site would change to None.
+
+ The issue was caused by the 'wagtail_site_root_paths' cache variable not being
+ cleared when a site root page was moved. Which left all the child pages
+ thinking that they are no longer in the site and return None as their url.
+
+ Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
+ Discussion: https://github.com/torchbox/wagtail/issues/7
+ """
+
+ fixtures = ['test.json']
+
+ def test_issue7(self):
+ # Get homepage, root page and site
+ root_page = Page.objects.get(id=1)
+ homepage = Page.objects.get(url_path='/home/')
+ default_site = Site.objects.get(is_default_site=True)
+
+ # Create a new homepage under current homepage
+ new_homepage = SimplePage(title="New Homepage", slug="new-homepage")
+ homepage.add_child(instance=new_homepage)
+
+ # Set new homepage as the site root page
+ default_site.root_page = new_homepage
+ default_site.save()
+
+ # Warm up the cache by getting the url
+ _ = homepage.url
+
+ # Move new homepage to root
+ new_homepage.move(root_page, pos='last-child')
+
+ # Get fresh instance of new_homepage
+ new_homepage = Page.objects.get(id=new_homepage.id)
+
+ # Check url
+ self.assertEqual(new_homepage.url, '/')
+
+
+class TestIssue157(TestCase):
+ """
+ This tests for an issue where if a site root pages slug was changed, all the page
+ urls in that site would change to None.
+
+ The issue was caused by the 'wagtail_site_root_paths' cache variable not being
+ cleared when a site root page was changed. Which left all the child pages
+ thinking that they are no longer in the site and return None as their url.
+
+ Fix: d6cce69a397d08d5ee81a8cbc1977ab2c9db2682
+ Discussion: https://github.com/torchbox/wagtail/issues/157
+ """
+
+ fixtures = ['test.json']
+
+ def test_issue157(self):
+ # Get homepage
+ homepage = Page.objects.get(url_path='/home/')
+
+ # Warm up the cache by getting the url
+ _ = homepage.url
+
+ # Change homepage title and slug
+ homepage.title = "New home"
+ homepage.slug = "new-home"
+ homepage.save()
+
+ # Get fresh instance of homepage
+ homepage = Page.objects.get(id=homepage.id)
+
+ # Check url
+ self.assertEqual(homepage.url, '/')
diff --git a/wagtail/wagtailcore/whitelist.py b/wagtail/wagtailcore/whitelist.py
index 508682cda..a3d377bd6 100644
--- a/wagtail/wagtailcore/whitelist.py
+++ b/wagtail/wagtailcore/whitelist.py
@@ -89,7 +89,10 @@ class Whitelister(object):
cls.clean_string_node(doc, node)
elif isinstance(node, Tag):
cls.clean_tag_node(doc, node)
- else:
+ # This branch is here in case node is a BeautifulSoup object that does
+ # not inherit from NavigableString or Tag. I can't find any examples
+ # of such a thing at the moment, so this branch is untested.
+ else: # pragma: no cover
cls.clean_unknown_node(doc, node)
@classmethod
diff --git a/wagtail/wagtailembeds/embeds.py b/wagtail/wagtailembeds/embeds.py
index f8ab35f16..03029a23b 100644
--- a/wagtail/wagtailembeds/embeds.py
+++ b/wagtail/wagtailembeds/embeds.py
@@ -16,7 +16,6 @@ import json
class EmbedNotFoundException(Exception): pass
-
class EmbedlyException(Exception): pass
class AccessDeniedEmbedlyException(EmbedlyException): pass
@@ -52,7 +51,7 @@ def embedly(url, max_width=None, key=None):
key = settings.EMBEDLY_KEY
# Get embedly client
- client = Embedly(key=settings.EMBEDLY_KEY)
+ client = Embedly(key=key)
# Call embedly
if max_width is not None:
diff --git a/wagtail/wagtailembeds/tests.py b/wagtail/wagtailembeds/tests.py
index b1268729e..e10c3f4c0 100644
--- a/wagtail/wagtailembeds/tests.py
+++ b/wagtail/wagtailembeds/tests.py
@@ -1,7 +1,27 @@
+from mock import patch
+import urllib2
+
+try:
+ import embedly
+ no_embedly = False
+except ImportError:
+ no_embedly = True
+
from django.test import TestCase
from django.test.client import Client
-from wagtail.wagtailembeds import get_embed
+
from wagtail.tests.utils import WagtailTestUtils
+from wagtail.tests.utils import unittest
+
+from wagtail.wagtailembeds import get_embed
+from wagtail.wagtailembeds.embeds import (
+ EmbedNotFoundException,
+ EmbedlyException,
+ AccessDeniedEmbedlyException,
+)
+from wagtail.wagtailembeds.embeds import embedly as wagtail_embedly
+from wagtail.wagtailembeds.embeds import oembed as wagtail_oembed
+
class TestEmbeds(TestCase):
@@ -73,3 +93,158 @@ class TestChooser(TestCase, WagtailTestUtils):
self.assertEqual(r.status_code, 200)
# TODO: Test submitting
+
+class TestEmbedly(TestCase):
+ @unittest.skipIf(no_embedly, "Embedly is not installed")
+ def test_embedly_oembed_called_with_correct_arguments(self):
+ with patch('embedly.Embedly.oembed') as oembed:
+ oembed.return_value = {'type': 'photo',
+ 'url': 'http://www.example.com'}
+
+ wagtail_embedly('http://www.example.com', key='foo')
+ oembed.assert_called_with('http://www.example.com', better=False)
+
+ wagtail_embedly('http://www.example.com', max_width=100, key='foo')
+ oembed.assert_called_with('http://www.example.com', maxwidth=100, better=False)
+
+ @unittest.skipIf(no_embedly, "Embedly is not installed")
+ def test_embedly_401(self):
+ with patch('embedly.Embedly.oembed') as oembed:
+ oembed.return_value = {'type': 'photo',
+ 'url': 'http://www.example.com',
+ 'error': True,
+ 'error_code': 401}
+ self.assertRaises(AccessDeniedEmbedlyException,
+ wagtail_embedly, 'http://www.example.com', key='foo')
+
+ @unittest.skipIf(no_embedly, "Embedly is not installed")
+ def test_embedly_403(self):
+ with patch('embedly.Embedly.oembed') as oembed:
+ oembed.return_value = {'type': 'photo',
+ 'url': 'http://www.example.com',
+ 'error': True,
+ 'error_code': 403}
+ self.assertRaises(AccessDeniedEmbedlyException,
+ wagtail_embedly, 'http://www.example.com', key='foo')
+
+ @unittest.skipIf(no_embedly, "Embedly is not installed")
+ def test_embedly_404(self):
+ with patch('embedly.Embedly.oembed') as oembed:
+ oembed.return_value = {'type': 'photo',
+ 'url': 'http://www.example.com',
+ 'error': True,
+ 'error_code': 404}
+ self.assertRaises(EmbedNotFoundException,
+ wagtail_embedly, 'http://www.example.com', key='foo')
+
+ @unittest.skipIf(no_embedly, "Embedly is not installed")
+ def test_embedly_other_error(self):
+ with patch('embedly.Embedly.oembed') as oembed:
+ oembed.return_value = {'type': 'photo',
+ 'url': 'http://www.example.com',
+ 'error': True,
+ 'error_code': 999}
+ self.assertRaises(EmbedlyException, wagtail_embedly,
+ 'http://www.example.com', key='foo')
+
+ @unittest.skipIf(no_embedly, "Embedly is not installed")
+ def test_embedly_html_conversion(self):
+ with patch('embedly.Embedly.oembed') as oembed:
+ oembed.return_value = {'type': 'photo',
+ 'url': 'http://www.example.com'}
+ result = wagtail_embedly('http://www.example.com', key='foo')
+ self.assertEqual(result['html'], '
')
+
+ oembed.return_value = {'type': 'something else',
+ 'html': 'bar'}
+ result = wagtail_embedly('http://www.example.com', key='foo')
+ self.assertEqual(result['html'], 'bar')
+
+ @unittest.skipIf(no_embedly, "Embedly is not installed")
+ def test_embedly_return_value(self):
+ with patch('embedly.Embedly.oembed') as oembed:
+ oembed.return_value = {'type': 'something else',
+ 'html': 'bar'}
+ result = wagtail_embedly('http://www.example.com', key='foo')
+ self.assertEqual(result, {
+ 'title': '',
+ 'author_name': '',
+ 'provider_name': '',
+ 'type': 'something else',
+ 'thumbnail_url': None,
+ 'width': None,
+ 'height': None,
+ 'html': 'bar'})
+
+ oembed.return_value = {'type': 'something else',
+ 'author_name': 'Alice',
+ 'provider_name': 'Bob',
+ 'title': 'foo',
+ 'thumbnail_url': 'http://www.example.com',
+ 'width': 100,
+ 'height': 100,
+ 'html': 'bar'}
+ result = wagtail_embedly('http://www.example.com', key='foo')
+ self.assertEqual(result, {'type': 'something else',
+ 'author_name': 'Alice',
+ 'provider_name': 'Bob',
+ 'title': 'foo',
+ 'thumbnail_url': 'http://www.example.com',
+ 'width': 100,
+ 'height': 100,
+ 'html': 'bar'})
+
+
+class TestOembed(TestCase):
+ def setUp(self):
+ class DummyResponse(object):
+ def read(self):
+ return "foo"
+ self.dummy_response = DummyResponse()
+
+ def test_oembed_invalid_provider(self):
+ self.assertRaises(EmbedNotFoundException, wagtail_oembed, "foo")
+
+ def test_oembed_invalid_request(self):
+ config = {'side_effect': urllib2.URLError('foo')}
+ with patch.object(urllib2, 'urlopen', **config) as urlopen:
+ self.assertRaises(EmbedNotFoundException, wagtail_oembed,
+ "http://www.youtube.com/watch/")
+
+ @patch('urllib2.urlopen')
+ @patch('json.loads')
+ def test_oembed_photo_request(self, loads, urlopen) :
+ urlopen.return_value = self.dummy_response
+ loads.return_value = {'type': 'photo',
+ 'url': 'http://www.example.com'}
+ result = wagtail_oembed("http://www.youtube.com/watch/")
+ self.assertEqual(result['type'], 'photo')
+ self.assertEqual(result['html'], '
')
+ loads.assert_called_with("foo")
+
+ @patch('urllib2.urlopen')
+ @patch('json.loads')
+ def test_oembed_return_values(self, loads, urlopen):
+ urlopen.return_value = self.dummy_response
+ loads.return_value = {
+ 'type': 'something',
+ 'url': 'http://www.example.com',
+ 'title': 'test_title',
+ 'author_name': 'test_author',
+ 'provider_name': 'test_provider_name',
+ 'thumbnail_url': 'test_thumbail_url',
+ 'width': 'test_width',
+ 'height': 'test_height',
+ 'html': 'test_html'
+ }
+ result = wagtail_oembed("http://www.youtube.com/watch/")
+ self.assertEqual(result, {
+ 'type': 'something',
+ 'title': 'test_title',
+ 'author_name': 'test_author',
+ 'provider_name': 'test_provider_name',
+ 'thumbnail_url': 'test_thumbail_url',
+ 'width': 'test_width',
+ 'height': 'test_height',
+ 'html': 'test_html'
+ })
diff --git a/wagtail/wagtailimages/backends/base.py b/wagtail/wagtailimages/backends/base.py
index 25b7e882d..ce07420e3 100644
--- a/wagtail/wagtailimages/backends/base.py
+++ b/wagtail/wagtailimages/backends/base.py
@@ -1,6 +1,7 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
+
class InvalidImageBackendError(ImproperlyConfigured):
pass
@@ -8,23 +9,22 @@ class InvalidImageBackendError(ImproperlyConfigured):
class BaseImageBackend(object):
def __init__(self, params):
self.quality = getattr(settings, 'IMAGE_COMPRESSION_QUALITY', 85)
-
+
def open_image(self, input_file):
"""
- Open an image and return the backend specific image object to pass
- to other methods. The object return has to have a size attribute
+ Open an image and return the backend specific image object to pass
+ to other methods. The object return has to have a size attribute
which is a tuple with the width and height of the image and a format
attribute with the format of the image.
"""
raise NotImplementedError('subclasses of BaseImageBackend must provide an open_image() method')
-
-
+
def save_image(self, image, output):
"""
Save the image to the output
"""
raise NotImplementedError('subclasses of BaseImageBackend must provide a save_image() method')
-
+
def resize(self, image, size):
"""
resize image to the requested size, using highest quality settings
@@ -32,11 +32,9 @@ class BaseImageBackend(object):
"""
raise NotImplementedError('subclasses of BaseImageBackend must provide an resize() method')
-
def crop_to_centre(self, image, size):
raise NotImplementedError('subclasses of BaseImageBackend must provide a crop_to_centre() method')
-
def resize_to_max(self, image, size):
"""
Resize image down to fit within the given dimensions, preserving aspect ratio.
@@ -58,10 +56,8 @@ class BaseImageBackend(object):
final_size = (target_width, int(original_height * horz_scale))
else:
final_size = (int(original_width * vert_scale), target_height)
-
- return self.resize(image, final_size)
-
+ return self.resize(image, final_size)
def resize_to_min(self, image, size):
"""
@@ -87,7 +83,6 @@ class BaseImageBackend(object):
return self.resize(image, final_size)
-
def resize_to_width(self, image, target_width):
"""
Resize image down to the given width, preserving aspect ratio.
@@ -104,7 +99,6 @@ class BaseImageBackend(object):
return self.resize(image, final_size)
-
def resize_to_height(self, image, target_height):
"""
Resize image down to the given height, preserving aspect ratio.
@@ -121,7 +115,6 @@ class BaseImageBackend(object):
return self.resize(image, final_size)
-
def resize_to_fill(self, image, size):
"""
Resize down and crop image to fill the given dimensions. Most suitable for thumbnails.
@@ -130,3 +123,8 @@ class BaseImageBackend(object):
"""
resized_image = self.resize_to_min(image, size)
return self.crop_to_centre(resized_image, size)
+
+
+ def no_operation(self, image, param):
+ """Return the image unchanged"""
+ return image
diff --git a/wagtail/wagtailimages/backends/pillow.py b/wagtail/wagtailimages/backends/pillow.py
index 05d12d291..96976c277 100644
--- a/wagtail/wagtailimages/backends/pillow.py
+++ b/wagtail/wagtailimages/backends/pillow.py
@@ -1,6 +1,9 @@
-from base import BaseImageBackend
+from __future__ import absolute_import
+
+from .base import BaseImageBackend
import PIL.Image
+
class PillowBackend(BaseImageBackend):
def __init__(self, params):
super(PillowBackend, self).__init__(params)
@@ -32,4 +35,4 @@ class PillowBackend(BaseImageBackend):
top = (original_height - final_height) / 2
return image.crop(
(left, top, left + final_width, top + final_height)
- )
\ No newline at end of file
+ )
diff --git a/wagtail/wagtailimages/backends/wand.py b/wagtail/wagtailimages/backends/wand.py
index 36e352715..91f2d255a 100644
--- a/wagtail/wagtailimages/backends/wand.py
+++ b/wagtail/wagtailimages/backends/wand.py
@@ -1,8 +1,9 @@
from __future__ import absolute_import
from .base import BaseImageBackend
-
from wand.image import Image
+from wand.api import library
+
class WandBackend(BaseImageBackend):
def __init__(self, params):
@@ -10,6 +11,7 @@ class WandBackend(BaseImageBackend):
def open_image(self, input_file):
image = Image(file=input_file)
+ image.wand = library.MagickCoalesceImages(image.wand)
return image
def save_image(self, image, output, format):
@@ -18,8 +20,9 @@ class WandBackend(BaseImageBackend):
image.save(file=output)
def resize(self, image, size):
- image.resize(size[0], size[1])
- return image
+ new_image = image.clone()
+ new_image.resize(size[0], size[1])
+ return new_image
def crop_to_centre(self, image, size):
(original_width, original_height) = image.size
@@ -34,7 +37,9 @@ class WandBackend(BaseImageBackend):
left = (original_width - final_width) / 2
top = (original_height - final_height) / 2
- image.crop(
+
+ new_image = image.clone()
+ new_image.crop(
left=left, top=top, right=left + final_width, bottom=top + final_height
)
- return image
+ return new_image
diff --git a/wagtail/wagtailimages/models.py b/wagtail/wagtailimages/models.py
index 24fd1f0bf..8137971a7 100644
--- a/wagtail/wagtailimages/models.py
+++ b/wagtail/wagtailimages/models.py
@@ -1,5 +1,6 @@
import StringIO
import os.path
+import re
from taggit.managers import TaggableManager
@@ -150,6 +151,7 @@ class Filter(models.Model):
'width': 'resize_to_width',
'height': 'resize_to_height',
'fill': 'resize_to_fill',
+ 'original': 'no_operation',
}
def __init__(self, *args, **kwargs):
@@ -157,22 +159,34 @@ class Filter(models.Model):
self.method = None # will be populated when needed, by parsing the spec string
def _parse_spec_string(self):
- # parse the spec string, which is formatted as (method)-(arg),
- # and save the results to self.method_name and self.method_arg
- try:
- (method_name_simple, method_arg_string) = self.spec.split('-')
- self.method_name = Filter.OPERATION_NAMES[method_name_simple]
+ # parse the spec string and save the results to
+ # self.method_name and self.method_arg. There are various possible
+ # formats to match against:
+ # 'original'
+ # 'width-200'
+ # 'max-320x200'
- if method_name_simple in ('max', 'min', 'fill'):
- # method_arg_string is in the form 640x480
- (width, height) = [int(i) for i in method_arg_string.split('x')]
- self.method_arg = (width, height)
- else:
- # method_arg_string is a single number
- self.method_arg = int(method_arg_string)
+ if self.spec == 'original':
+ self.method_name = Filter.OPERATION_NAMES['original']
+ self.method_arg = None
+ return
- except (ValueError, KeyError):
- raise ValueError("Invalid image filter spec: %r" % self.spec)
+ match = re.match(r'(width|height)-(\d+)$', self.spec)
+ if match:
+ self.method_name = Filter.OPERATION_NAMES[match.group(1)]
+ self.method_arg = int(match.group(2))
+ return
+
+ match = re.match(r'(max|min|fill)-(\d+)x(\d+)$', self.spec)
+ if match:
+ self.method_name = Filter.OPERATION_NAMES[match.group(1)]
+ width = int(match.group(2))
+ height = int(match.group(3))
+ self.method_arg = (width, height)
+ return
+
+ # Spec is not one of our recognised patterns
+ raise ValueError("Invalid image filter spec: %r" % self.spec)
def process_image(self, input_file, backend_name='default'):
"""
@@ -180,27 +194,25 @@ class Filter(models.Model):
generate an output image with this filter applied, returning it
as another django.core.files.File object
"""
-
backend = get_image_backend(backend_name)
-
+
if not self.method:
self._parse_spec_string()
-
+
# If file is closed, open it
input_file.open('rb')
image = backend.open_image(input_file)
file_format = image.format
-
+
method = getattr(backend, self.method_name)
image = method(image, self.method_arg)
output = StringIO.StringIO()
backend.save_image(image, output, file_format)
-
+
# and then close the input file
input_file.close()
-
# generate new filename derived from old one, inserting the filter spec string before the extension
input_filename_parts = os.path.basename(input_file.name).split('.')
@@ -210,7 +222,6 @@ class Filter(models.Model):
output_filename = '.'.join(output_filename_parts)
output_file = File(output, name=output_filename)
-
return output_file
diff --git a/wagtail/wagtailimages/tests.py b/wagtail/wagtailimages/tests.py
index d01535eda..b31e33745 100644
--- a/wagtail/wagtailimages/tests.py
+++ b/wagtail/wagtailimages/tests.py
@@ -84,10 +84,10 @@ class TestRenditions(TestCase):
# default backend should be pillow
backend = get_image_backend()
self.assertTrue(isinstance(backend, PillowBackend))
-
+
def test_minification(self):
rendition = self.image.get_rendition('width-400')
-
+
# Check size
self.assertEqual(rendition.width, 400)
self.assertEqual(rendition.height, 300)
@@ -107,6 +107,13 @@ class TestRenditions(TestCase):
self.assertEqual(rendition.width, 160)
self.assertEqual(rendition.height, 120)
+ def test_resize_to_original(self):
+ rendition = self.image.get_rendition('original')
+
+ # Check size
+ self.assertEqual(rendition.width, 640)
+ self.assertEqual(rendition.height, 480)
+
def test_cache(self):
# Get two renditions with the same filter
first_rendition = self.image.get_rendition('width-400')
@@ -114,7 +121,7 @@ class TestRenditions(TestCase):
# Check that they are the same object
self.assertEqual(first_rendition, second_rendition)
-
+
class TestRenditionsWand(TestCase):
def setUp(self):
@@ -134,18 +141,18 @@ class TestRenditionsWand(TestCase):
def test_minification(self):
rendition = self.image.get_rendition('width-400')
-
+
# Check size
self.assertEqual(rendition.width, 400)
self.assertEqual(rendition.height, 300)
-
+
def test_resize_to_max(self):
rendition = self.image.get_rendition('max-100x100')
-
+
# Check size
self.assertEqual(rendition.width, 100)
self.assertEqual(rendition.height, 75)
-
+
def test_resize_to_min(self):
rendition = self.image.get_rendition('min-120x120')
@@ -153,6 +160,13 @@ class TestRenditionsWand(TestCase):
self.assertEqual(rendition.width, 160)
self.assertEqual(rendition.height, 120)
+ def test_resize_to_original(self):
+ rendition = self.image.get_rendition('original')
+
+ # Check size
+ self.assertEqual(rendition.width, 640)
+ self.assertEqual(rendition.height, 480)
+
def test_cache(self):
# Get two renditions with the same filter
first_rendition = self.image.get_rendition('width-400')
@@ -160,7 +174,7 @@ class TestRenditionsWand(TestCase):
# Check that they are the same object
self.assertEqual(first_rendition, second_rendition)
-
+
class TestImageTag(TestCase):
def setUp(self):
diff --git a/wagtail/wagtailredirects/wagtail_hooks.py b/wagtail/wagtailredirects/wagtail_hooks.py
index 22ae97320..4dbe1a028 100644
--- a/wagtail/wagtailredirects/wagtail_hooks.py
+++ b/wagtail/wagtailredirects/wagtail_hooks.py
@@ -1,11 +1,24 @@
+from django.core import urlresolvers
from django.conf.urls import include, url
+from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailredirects import urls
+from wagtail.wagtailadmin.menu import MenuItem
+
def register_admin_urls():
return [
url(r'^redirects/', include(urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
+
+
+def construct_main_menu(request, menu_items):
+ # TEMPORARY: Only show if the user is a superuser
+ if request.user.is_superuser:
+ menu_items.append(
+ MenuItem(_('Redirects'), urlresolvers.reverse('wagtailredirects_index'), classnames='icon icon-redirect', order=800)
+ )
+hooks.register('construct_main_menu', construct_main_menu)
diff --git a/wagtail/wagtailsearch/forms.py b/wagtail/wagtailsearch/forms.py
index 03e4ecd45..9485e4619 100644
--- a/wagtail/wagtailsearch/forms.py
+++ b/wagtail/wagtailsearch/forms.py
@@ -31,6 +31,8 @@ EditorsPickFormSetBase = inlineformset_factory(models.Query, models.EditorsPick,
class EditorsPickFormSet(EditorsPickFormSetBase):
+ minimum_forms = 1
+ minimum_forms_message = _("Please specify at least one recommendation for this search term.")
def add_fields(self, form, *args, **kwargs):
super(EditorsPickFormSet, self).add_fields(form, *args, **kwargs)
@@ -40,3 +42,20 @@ class EditorsPickFormSet(EditorsPickFormSetBase):
# Remove query field
del form.fields['query']
+
+ def clean(self):
+ # Editors 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 xrange(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/wagtailsearch/models.py b/wagtail/wagtailsearch/models.py
index 809e139ed..6b07872f5 100644
--- a/wagtail/wagtailsearch/models.py
+++ b/wagtail/wagtailsearch/models.py
@@ -5,9 +5,10 @@ from indexed import Indexed
import datetime
import string
+MAX_QUERY_STRING_LENGTH = 255
class Query(models.Model):
- query_string = models.CharField(max_length=255, unique=True)
+ query_string = models.CharField(max_length=MAX_QUERY_STRING_LENGTH, unique=True)
def save(self, *args, **kwargs):
# Normalise query string
@@ -48,6 +49,9 @@ class Query(models.Model):
@staticmethod
def normalise_query_string(query_string):
+ # Truncate query string
+ if len(query_string) > MAX_QUERY_STRING_LENGTH:
+ query_string = query_string[:MAX_QUERY_STRING_LENGTH]
# Convert query_string to lowercase
query_string = query_string.lower()
diff --git a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/add.html b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/add.html
index 3c927d96c..f443d1a6a 100644
--- a/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/add.html
+++ b/wagtail/wagtailsearch/templates/wagtailsearch/editorspicks/add.html
@@ -6,7 +6,7 @@
{% include "wagtailadmin/shared/header.html" with title=add_str icon="pick" %}
- {% blocktrans %}s
+ {% blocktrans %}
Editors picks 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 a less common term like "giving".
{% endblocktrans %}
{% blocktrans %}
diff --git a/wagtail/wagtailsearch/tests/test_queries.py b/wagtail/wagtailsearch/tests/test_queries.py
index ee5382db4..5c341d7d6 100644
--- a/wagtail/wagtailsearch/tests/test_queries.py
+++ b/wagtail/wagtailsearch/tests/test_queries.py
@@ -53,6 +53,16 @@ class TestQueryStringNormalisation(TestCase):
for query in queries:
self.assertNotEqual(self.query, models.Query.get(query))
+ def test_truncation(self):
+ test_querystring = 'a' * 1000
+ result = models.Query.normalise_query_string(test_querystring)
+ self.assertEqual(len(result), 255)
+
+ def test_no_truncation(self):
+ test_querystring = 'a' * 10
+ result = models.Query.normalise_query_string(test_querystring)
+ self.assertEqual(len(result), 10)
+
class TestQueryPopularity(TestCase):
def test_query_popularity(self):
diff --git a/wagtail/wagtailsearch/views/editorspicks.py b/wagtail/wagtailsearch/views/editorspicks.py
index 97332818e..bb39c02ba 100644
--- a/wagtail/wagtailsearch/views/editorspicks.py
+++ b/wagtail/wagtailsearch/views/editorspicks.py
@@ -45,12 +45,12 @@ def index(request):
def save_editorspicks(query, new_query, editors_pick_formset):
- # Set sort_order
- for i, form in enumerate(editors_pick_formset.ordered_forms):
- form.instance.sort_order = i
-
# 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
+
editors_pick_formset.save()
# If query was changed, move all editors picks to the new query
@@ -72,10 +72,14 @@ def add(request):
# 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))
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:
@@ -95,15 +99,22 @@ def edit(request, 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
- editors_pick_formset = forms.EditorsPickFormSet(request.POST, instance=query)
-
if save_editorspicks(query, new_query, editors_pick_formset):
messages.success(request, _("Editor's picks for '{0}' updated.").format(new_query))
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)
diff --git a/wagtail/wagtailsearch/wagtail_hooks.py b/wagtail/wagtailsearch/wagtail_hooks.py
index b8bbd2c06..1a656c0ef 100644
--- a/wagtail/wagtailsearch/wagtail_hooks.py
+++ b/wagtail/wagtailsearch/wagtail_hooks.py
@@ -1,11 +1,24 @@
+from django.core import urlresolvers
from django.conf.urls import include, url
+from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin import hooks
from wagtail.wagtailsearch.urls import admin as admin_urls
+from wagtail.wagtailadmin.menu import MenuItem
+
def register_admin_urls():
return [
url(r'^search/', include(admin_urls)),
]
hooks.register('register_admin_urls', register_admin_urls)
+
+
+def construct_main_menu(request, menu_items):
+ # TEMPORARY: Only show if the user is a superuser
+ if request.user.is_superuser:
+ menu_items.append(
+ MenuItem(_('Editors picks'), urlresolvers.reverse('wagtailsearch_editorspicks_index'), classnames='icon icon-pick', order=900)
+ )
+hooks.register('construct_main_menu', construct_main_menu)
diff --git a/wagtail/wagtailsnippets/models.py b/wagtail/wagtailsnippets/models.py
index b929f06aa..8af626655 100644
--- a/wagtail/wagtailsnippets/models.py
+++ b/wagtail/wagtailsnippets/models.py
@@ -19,3 +19,4 @@ def get_snippet_content_types():
def register_snippet(model):
if model not in SNIPPET_MODELS:
SNIPPET_MODELS.append(model)
+ SNIPPET_MODELS.sort(key=lambda x: x._meta.verbose_name)
diff --git a/wagtail/wagtailsnippets/tests.py b/wagtail/wagtailsnippets/tests.py
index 38cc95b20..9e2587794 100644
--- a/wagtail/wagtailsnippets/tests.py
+++ b/wagtail/wagtailsnippets/tests.py
@@ -3,7 +3,8 @@ from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from wagtail.tests.utils import unittest, WagtailTestUtils
-from wagtail.tests.models import Advert
+from wagtail.tests.models import Advert, AlphaSnippet, ZuluSnippet
+from wagtail.wagtailsnippets.models import register_snippet, SNIPPET_MODELS
from wagtail.wagtailsnippets.views.snippets import get_content_type_from_url_params, get_snippet_edit_handler
from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel
@@ -179,3 +180,16 @@ class TestSnippetChooserPanel(TestCase):
def test_render_js(self):
self.assertTrue("createSnippetChooser(fixPrefix('id_text'), 'contenttypes/contenttype');"
in self.snippet_chooser_panel.render_js())
+
+
+class TestSnippetOrdering(TestCase):
+ def setUp(self):
+ register_snippet(ZuluSnippet)
+ register_snippet(AlphaSnippet)
+
+ def test_snippets_ordering(self):
+ # Ensure AlphaSnippet is before ZuluSnippet
+ # Cannot check first and last position as other snippets
+ # may get registered elsewhere during test
+ self.assertLess(SNIPPET_MODELS.index(AlphaSnippet),
+ SNIPPET_MODELS.index(ZuluSnippet))