From 69b30d85c8b0161327fa42ef3f5a36f22593d8f7 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 3 Mar 2014 17:01:26 +0000 Subject: [PATCH 01/13] tests for Site.find_for_request --- wagtail/wagtailcore/tests.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index ccd91d0092..3e7cdfcf16 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -1,10 +1,34 @@ from django.test import TestCase +from django.http import HttpRequest + from wagtail.wagtailcore.models import Page, Site 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/') From 988892076222ab9ab4465024b1f04232fff5447c Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 4 Mar 2014 11:06:22 +0000 Subject: [PATCH 02/13] add tests for page routing --- wagtail/tests/fixtures/test.json | 29 ++++++++++++++++++- wagtail/tests/templates/tests/event_page.html | 10 +++++++ wagtail/wagtailcore/tests.py | 29 ++++++++++++++++++- 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 wagtail/tests/templates/tests/event_page.html diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index 98db24215a..0a468c92d9 100644 --- a/wagtail/tests/fixtures/test.json +++ b/wagtail/tests/fixtures/test.json @@ -39,7 +39,7 @@ "model": "wagtailcore.page", "fields": { "title": "Events", - "numchild": 1, + "numchild": 2, "show_in_menus": true, "live": true, "depth": 3, @@ -84,6 +84,33 @@ } }, +{ + "pk": 5, + "model": "wagtailcore.page", + "fields": { + "title": "Tentative Unpublished Event", + "numchild": 1, + "show_in_menus": true, + "live": false, + "depth": 4, + "content_type": ["tests", "eventpage"], + "path": "0001000100010002", + "url_path": "/home/events/tentative-unpublished-event/", + "slug": "tentative-unpublished-event" + } +}, +{ + "pk": 5, + "model": "tests.eventpage", + "fields": { + "date_from": "2015-07-04", + "audience": "public", + "location": "The moon", + "body": "

I haven't worked out the details yet, but it's going to have cake and ponies

", + "cost": "Free" + } +}, + { "pk": 1, "model": "wagtailcore.site", diff --git a/wagtail/tests/templates/tests/event_page.html b/wagtail/tests/templates/tests/event_page.html new file mode 100644 index 0000000000..917c835010 --- /dev/null +++ b/wagtail/tests/templates/tests/event_page.html @@ -0,0 +1,10 @@ + + + + Event: {{ self.title }} + + +

{{ self.title }}

+

Event

+ + diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index 3e7cdfcf16..46639bd740 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -1,5 +1,5 @@ from django.test import TestCase -from django.http import HttpRequest +from django.http import HttpRequest, Http404 from wagtail.wagtailcore.models import Page, Site @@ -62,3 +62,30 @@ class TestRouting(TestCase): 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/') + + request = HttpRequest() + request.path = '/events/christmas/' + response = homepage.route(request, ['events', 'christmas']) + + self.assertEqual(response.status_code, 200) + self.assertContains(response, '

Christmas

') + self.assertContains(response, '

Event

') + + 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']) From ecad517c621c6ac5ebf318bc8d510c3a48a59f2b Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 4 Mar 2014 11:50:37 +0000 Subject: [PATCH 03/13] Change Page.serve to return a TemplateResponse so that we can perform more targetted tests on it --- wagtail/wagtailcore/models.py | 4 ++-- wagtail/wagtailcore/tests.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index 0927520375..df6caf888e 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -6,11 +6,11 @@ from modelcluster.models import ClusterableModel from django.db import models, connection, transaction from django.db.models import get_model, Q from django.http import Http404 -from django.shortcuts import render from django.core.cache import cache from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import Group from django.conf import settings +from django.template.response import TemplateResponse from django.utils.translation import ugettext_lazy as _ from wagtail.wagtailcore.util import camelcase_to_underscore @@ -326,7 +326,7 @@ class Page(MP_Node, ClusterableModel, Indexed): return revision.as_page_object() def serve(self, request): - return render(request, self.template, { + return TemplateResponse(request, self.template, { 'self': self }) diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index 46639bd740..ccf7fa1c34 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -3,6 +3,8 @@ from django.http import HttpRequest, Http404 from wagtail.wagtailcore.models import Page, Site +from wagtail.tests.models import EventPage + class TestRouting(TestCase): fixtures = ['test.json'] @@ -65,12 +67,17 @@ class TestRouting(TestCase): 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') + self.assertContains(response, '

Christmas

') self.assertContains(response, '

Event

') From 036837d50b5058d57d9bb74a58497f1a14b27525 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 4 Mar 2014 12:01:56 +0000 Subject: [PATCH 04/13] add end-to-end tests for wagtailcore's 'serve' view --- wagtail/wagtailcore/tests.py | 50 +++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index ccf7fa1c34..d08bf8ddd5 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -1,4 +1,4 @@ -from django.test import TestCase +from django.test import TestCase, Client from django.http import HttpRequest, Http404 from wagtail.wagtailcore.models import Page, Site @@ -78,9 +78,6 @@ class TestRouting(TestCase): used_template = response.resolve_template(response.template_name) self.assertEqual(used_template.name, 'tests/event_page.html') - self.assertContains(response, '

Christmas

') - self.assertContains(response, '

Event

') - def test_route_to_unknown_page_returns_404(self): homepage = Page.objects.get(url_path='/home/') @@ -96,3 +93,48 @@ class TestRouting(TestCase): request.path = '/events/tentative-unpublished-event/' with self.assertRaises(Http404): homepage.route(request, ['events', 'tentative-unpublished-event']) + + +class TestServeView(TestCase): + fixtures = ['test.json'] + + def test_serve(self): + c = Client() + response = c.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): + c = Client() + response = c.get('/events/quinquagesima/') + self.assertEqual(response.status_code, 404) + + def test_serve_unpublished_page_returns_404(self): + c = Client() + response = c.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/') + events_site = Site.objects.create(hostname='events.example.com', root_page=events_page) + + c = Client() + response = c.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) From bd71eec0d52f48c0cfac1fefe893282507af3887 Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 4 Mar 2014 14:43:49 +0000 Subject: [PATCH 05/13] use self.client instead of creating our own instance --- wagtail/wagtailcore/tests.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index d08bf8ddd5..eb1b15a3dc 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -99,8 +99,7 @@ class TestServeView(TestCase): fixtures = ['test.json'] def test_serve(self): - c = Client() - response = c.get('/events/christmas/') + response = self.client.get('/events/christmas/') self.assertEqual(response.status_code, 200) self.assertEqual(response.templates[0].name, 'tests/event_page.html') @@ -111,21 +110,18 @@ class TestServeView(TestCase): self.assertContains(response, '

Event

') def test_serve_unknown_page_returns_404(self): - c = Client() - response = c.get('/events/quinquagesima/') + response = self.client.get('/events/quinquagesima/') self.assertEqual(response.status_code, 404) def test_serve_unpublished_page_returns_404(self): - c = Client() - response = c.get('/events/tentative-unpublished-event/') + 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/') - events_site = Site.objects.create(hostname='events.example.com', root_page=events_page) + Site.objects.create(hostname='events.example.com', root_page=events_page) - c = Client() - response = c.get('/christmas/', HTTP_HOST='events.example.com') + 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/') From 4f807cb4e249aba65a5daa6ad8bc3309c986708a Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Tue, 4 Mar 2014 15:47:16 +0000 Subject: [PATCH 06/13] Add page permission tests (and fix the ever-so-subtle bug in preventing moves into a descendant page) --- runtests.py | 3 + wagtail/tests/fixtures/test.json | 169 ++++++++++++++++++++++++++++++- wagtail/wagtailcore/models.py | 2 +- wagtail/wagtailcore/tests.py | 164 +++++++++++++++++++++++++++++- 4 files changed, 334 insertions(+), 4 deletions(-) diff --git a/runtests.py b/runtests.py index b8f81f20d5..4dd0476706 100755 --- a/runtests.py +++ b/runtests.py @@ -82,6 +82,9 @@ if not settings.configured: 'wagtail.wagtailredirects', 'wagtail.tests', ], + PASSWORD_HASHERS=( + 'django.contrib.auth.hashers.MD5PasswordHasher', # don't use the intentionally slow default password hasher + ), WAGTAILSEARCH_BACKENDS=WAGTAILSEARCH_BACKENDS, WAGTAIL_SITE_NAME='Test Site' ) diff --git a/wagtail/tests/fixtures/test.json b/wagtail/tests/fixtures/test.json index 0a468c92d9..99296f7b27 100644 --- a/wagtail/tests/fixtures/test.json +++ b/wagtail/tests/fixtures/test.json @@ -69,7 +69,8 @@ "content_type": ["tests", "eventpage"], "path": "0001000100010001", "url_path": "/home/events/christmas/", - "slug": "christmas" + "slug": "christmas", + "owner": 2 } }, { @@ -96,7 +97,8 @@ "content_type": ["tests", "eventpage"], "path": "0001000100010002", "url_path": "/home/events/tentative-unpublished-event/", - "slug": "tentative-unpublished-event" + "slug": "tentative-unpublished-event", + "owner": 2 } }, { @@ -111,6 +113,34 @@ } }, +{ + "pk": 6, + "model": "wagtailcore.page", + "fields": { + "title": "Someone Else's Event", + "numchild": 1, + "show_in_menus": true, + "live": false, + "depth": 4, + "content_type": ["tests", "eventpage"], + "path": "0001000100010003", + "url_path": "/home/events/someone-elses-event/", + "slug": "someone-elses-event", + "owner": 3 + } +}, +{ + "pk": 6, + "model": "tests.eventpage", + "fields": { + "date_from": "2015-07-04", + "audience": "private", + "location": "The moon", + "body": "

your name's not down, you're not coming in

", + "cost": "Free (but not for you)" + } +}, + { "pk": 1, "model": "wagtailcore.site", @@ -120,5 +150,140 @@ "port": 80, "is_default_site": true } +}, + +{ + "pk": 3, + "model": "auth.group", + "fields": { + "name": "Event editors", + "permissions": [ + ["access_admin", "wagtailadmin", "admin"], + ["add_image", "wagtailimages", "image"], + ["change_image", "wagtailimages", "image"], + ["delete_image", "wagtailimages", "image"] + ] + } +}, +{ + "pk": 4, + "model": "auth.group", + "fields": { + "name": "Event moderators", + "permissions": [ + ["access_admin", "wagtailadmin", "admin"], + ["add_image", "wagtailimages", "image"], + ["change_image", "wagtailimages", "image"], + ["delete_image", "wagtailimages", "image"] + ] + } +}, +{ + "pk": 1, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Event editors"], + "page": 3, + "permission_type": "add" + } +}, +{ + "pk": 2, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Event moderators"], + "page": 3, + "permission_type": "add" + } +}, +{ + "pk": 3, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Event moderators"], + "page": 3, + "permission_type": "edit" + } +}, +{ + "pk": 4, + "model": "wagtailcore.grouppagepermission", + "fields": { + "group": ["Event moderators"], + "page": 3, + "permission_type": "publish" + } +}, + +{ + "pk": 1, + "model": "auth.user", + "fields": { + "username": "superuser", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": true, + "is_staff": true, + "groups": [ + ], + "user_permissions": [], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "superuser@example.com" + } +}, +{ + "pk": 2, + "model": "auth.user", + "fields": { + "username": "eventeditor", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "groups": [ + ["Event editors"] + ], + "user_permissions": [], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "eventeditor@example.com" + } +}, +{ + "pk": 3, + "model": "auth.user", + "fields": { + "username": "eventmoderator", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "groups": [ + ["Event moderators"] + ], + "user_permissions": [], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "eventmoderator@example.com" + } +}, +{ + "pk": 4, + "model": "auth.user", + "fields": { + "username": "inactiveuser", + "first_name": "", + "last_name": "", + "is_active": false, + "is_superuser": false, + "is_staff": false, + "groups": [ + ["Event moderators"] + ], + "user_permissions": [], + "password": "md5$seasalt$1e9bf2bf5606aa5c39852cc30f0f6f22", + "email": "inactiveuser@example.com" + } } ] diff --git a/wagtail/wagtailcore/models.py b/wagtail/wagtailcore/models.py index df6caf888e..90e6998080 100644 --- a/wagtail/wagtailcore/models.py +++ b/wagtail/wagtailcore/models.py @@ -742,7 +742,7 @@ class PagePermissionTester(object): def can_move_to(self, destination): # reject the logically impossible cases first - if self.page == destination or destination.is_child_of(self.page): + if self.page == destination or destination.is_descendant_of(self.page): return False # and shortcut the trivial 'everything' / 'nothing' permissions diff --git a/wagtail/wagtailcore/tests.py b/wagtail/wagtailcore/tests.py index eb1b15a3dc..58ff51ada0 100644 --- a/wagtail/wagtailcore/tests.py +++ b/wagtail/wagtailcore/tests.py @@ -1,8 +1,9 @@ from django.test import TestCase, Client from django.http import HttpRequest, Http404 -from wagtail.wagtailcore.models import Page, Site +from django.contrib.auth.models import User +from wagtail.wagtailcore.models import Page, Site from wagtail.tests.models import EventPage @@ -134,3 +135,164 @@ class TestServeView(TestCase): c = Client() response = c.get('/christmas/', HTTP_HOST='localhost') self.assertEqual(response.status_code, 404) + + +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)) From 9c14d41e9b16e179b80b0e5b7c028a9f89ef41ce Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 4 Mar 2014 16:45:13 +0000 Subject: [PATCH 07/13] replaced coffeescript js with compiled, vanilla js --- .../js/hallo-plugins/hallo-hr.coffee | 28 ------- .../wagtailadmin/js/hallo-plugins/hallo-hr.js | 31 ++++++++ .../js/hallo-plugins/hallo-wagtaillink.coffee | 68 ----------------- .../js/hallo-plugins/hallo-wagtaillink.js | 74 +++++++++++++++++++ .../wagtailadmin/pages/_editor_js.html | 12 +-- .../hallo-plugins/hallo-wagtaildoclink.coffee | 45 ----------- .../js/hallo-plugins/hallo-wagtaildoclink.js | 51 +++++++++++++ .../hallo-plugins/hallo-wagtailembeds.coffee | 36 --------- .../js/hallo-plugins/hallo-wagtailembeds.js | 47 ++++++++++++ .../hallo-plugins/hallo-wagtailimage.coffee | 39 ---------- .../js/hallo-plugins/hallo-wagtailimage.js | 47 ++++++++++++ 11 files changed, 256 insertions(+), 222 deletions(-) delete mode 100644 wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-hr.coffee create mode 100644 wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-hr.js delete mode 100644 wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.coffee create mode 100644 wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js delete mode 100644 wagtail/wagtaildocs/static/wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.coffee create mode 100644 wagtail/wagtaildocs/static/wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js delete mode 100644 wagtail/wagtailembeds/static/wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.coffee create mode 100644 wagtail/wagtailembeds/static/wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js delete mode 100644 wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-wagtailimage.coffee create mode 100644 wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-wagtailimage.js diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-hr.coffee b/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-hr.coffee deleted file mode 100644 index 76eb5a04cf..0000000000 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-hr.coffee +++ /dev/null @@ -1,28 +0,0 @@ -# Hallo - a rich text editing jQuery UI widget -# (c) 2011 Henri Bergius, IKS Consortium -# Hallo may be freely distributed under the MIT license -((jQuery) -> - jQuery.widget "IKS.hallohr", - options: - editable: null - toolbar: null - uuid: '' - buttonCssClass: null - - populateToolbar: (toolbar) -> - buttonset = jQuery "" - - buttonElement = jQuery '' - buttonElement.hallobutton - uuid: @options.uuid - editable: @options.editable - label: "Horizontal rule" - command: "insertHorizontalRule" - icon: "icon-horizontalrule" - cssClass: @options.buttonCssClass - buttonset.append buttonElement - - buttonset.hallobuttonset() - toolbar.append buttonset - -)(jQuery) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-hr.js b/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-hr.js new file mode 100644 index 0000000000..0989d1fc66 --- /dev/null +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-hr.js @@ -0,0 +1,31 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + (function(jQuery) { + return jQuery.widget("IKS.hallohr", { + options: { + editable: null, + toolbar: null, + uuid: '', + buttonCssClass: null + }, + populateToolbar: function(toolbar) { + var buttonElement, buttonset; + + buttonset = jQuery(""); + buttonElement = jQuery(''); + buttonElement.hallobutton({ + uuid: this.options.uuid, + editable: this.options.editable, + label: "Horizontal rule", + command: "insertHorizontalRule", + icon: "icon-horizontalrule", + cssClass: this.options.buttonCssClass + }); + buttonset.append(buttonElement); + buttonset.hallobuttonset(); + return toolbar.append(buttonset); + } + }); + })(jQuery); + +}).call(this); \ No newline at end of file diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.coffee b/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.coffee deleted file mode 100644 index fbc405f8a2..0000000000 --- a/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.coffee +++ /dev/null @@ -1,68 +0,0 @@ -# plugin for hallo.js to allow inserting links using Wagtail's page chooser - -(($) -> - $.widget "IKS.hallowagtaillink", - options: - uuid: '' - editable: null - - populateToolbar: (toolbar) -> - widget = this - - getEnclosingLink = () -> - # if cursor is currently within a link element, return it, otherwise return null - node = widget.options.editable.getSelection().commonAncestorContainer - return $(node).parents('a').get(0) - - # Create an element for holding the button - button = $('') - button.hallobutton - uuid: @options.uuid - editable: @options.editable - label: 'Links' - icon: 'icon-link' - command: null - queryState: (event) -> - button.hallobutton('checked', !!getEnclosingLink()) - - # Append the button to toolbar - toolbar.append button - - button.on "click", (event) -> - enclosingLink = getEnclosingLink() - if enclosingLink - # remove existing link - $(enclosingLink).replaceWith(enclosingLink.innerHTML) - button.hallobutton('checked', false) - widget.options.editable.element.trigger('change') - else - # commence workflow to add a link - lastSelection = widget.options.editable.getSelection() - - if lastSelection.collapsed - # TODO: don't hard-code this, as it may be changed in urls.py - url = window.chooserUrls.pageChooser + '?allow_external_link=true&allow_email_link=true&prompt_for_link_text=true' - else - url = window.chooserUrls.pageChooser + '?allow_external_link=true&allow_email_link=true' - - ModalWorkflow - url: url - responses: - pageChosen: (pageData) -> - a = document.createElement('a') - a.setAttribute('href', pageData.url) - if pageData.id - a.setAttribute('data-id', pageData.id) - a.setAttribute('data-linktype', 'page') - - if (not lastSelection.collapsed) and lastSelection.canSurroundContents() - # use the selected content as the link text - lastSelection.surroundContents(a) - else - # no text is selected, so use the page title as link text - a.appendChild(document.createTextNode pageData.title) - lastSelection.insertNode(a) - - widget.options.editable.element.trigger('change') - -)(jQuery) diff --git a/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js b/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js new file mode 100644 index 0000000000..03732110d1 --- /dev/null +++ b/wagtail/wagtailadmin/static/wagtailadmin/js/hallo-plugins/hallo-wagtaillink.js @@ -0,0 +1,74 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + (function($) { + return $.widget("IKS.hallowagtaillink", { + options: { + uuid: '', + editable: null + }, + populateToolbar: function(toolbar) { + var button, getEnclosingLink, widget; + + widget = this; + getEnclosingLink = function() { + var node; + + node = widget.options.editable.getSelection().commonAncestorContainer; + return $(node).parents('a').get(0); + }; + button = $(''); + button.hallobutton({ + uuid: this.options.uuid, + editable: this.options.editable, + label: 'Links', + icon: 'icon-link', + command: null, + queryState: function(event) { + return button.hallobutton('checked', !!getEnclosingLink()); + } + }); + toolbar.append(button); + return button.on("click", function(event) { + var enclosingLink, lastSelection, url; + + enclosingLink = getEnclosingLink(); + if (enclosingLink) { + $(enclosingLink).replaceWith(enclosingLink.innerHTML); + button.hallobutton('checked', false); + return widget.options.editable.element.trigger('change'); + } else { + lastSelection = widget.options.editable.getSelection(); + if (lastSelection.collapsed) { + url = window.chooserUrls.pageChooser + '?allow_external_link=true&allow_email_link=true&prompt_for_link_text=true'; + } else { + url = window.chooserUrls.pageChooser + '?allow_external_link=true&allow_email_link=true'; + } + return ModalWorkflow({ + url: url, + responses: { + pageChosen: function(pageData) { + var a; + + a = document.createElement('a'); + a.setAttribute('href', pageData.url); + if (pageData.id) { + a.setAttribute('data-id', pageData.id); + a.setAttribute('data-linktype', 'page'); + } + if ((!lastSelection.collapsed) && lastSelection.canSurroundContents()) { + lastSelection.surroundContents(a); + } else { + a.appendChild(document.createTextNode(pageData.title)); + lastSelection.insertNode(a); + } + return widget.options.editable.element.trigger('change'); + } + } + }); + } + }); + } + }); + })(jQuery); + +}).call(this); \ No newline at end of file diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html index 518a2c59b4..da26289664 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/_editor_js.html @@ -13,16 +13,16 @@ - - - - - + + + + + {% comment %} - TODO: have a mechanism to specify image-chooser.js (and hallo-wagtailimage.coffee) + TODO: have a mechanism to specify image-chooser.js (and hallo-wagtailimage.js) within the wagtailimages app - ideally wagtailadmin shouldn't have to know anything at all about wagtailimages TODO: a method of injecting these sorts of things on demand when the modal is spawned. diff --git a/wagtail/wagtaildocs/static/wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.coffee b/wagtail/wagtaildocs/static/wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.coffee deleted file mode 100644 index 7e961444d3..0000000000 --- a/wagtail/wagtaildocs/static/wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.coffee +++ /dev/null @@ -1,45 +0,0 @@ -# plugin for hallo.js to allow inserting links using Wagtail's page chooser - -(($) -> - $.widget "IKS.hallowagtaildoclink", - options: - uuid: '' - editable: null - - populateToolbar: (toolbar) -> - widget = this - - # Create an element for holding the button - button = $('') - button.hallobutton - uuid: @options.uuid - editable: @options.editable - label: 'Documents' - icon: 'icon-file-text-alt' - command: null - - # Append the button to toolbar - toolbar.append button - - button.on "click", (event) -> - lastSelection = widget.options.editable.getSelection() - ModalWorkflow - url: window.chooserUrls.documentChooser - responses: - documentChosen: (docData) -> - a = document.createElement('a') - a.setAttribute('href', docData.url) - a.setAttribute('data-id', docData.id) - a.setAttribute('data-linktype', 'document') - - if (not lastSelection.collapsed) and lastSelection.canSurroundContents() - # use the selected content as the link text - lastSelection.surroundContents(a) - else - # no text is selected, so use the doc title as link text - a.appendChild(document.createTextNode docData.title) - lastSelection.insertNode(a) - - widget.options.editable.element.trigger('change') - -)(jQuery) diff --git a/wagtail/wagtaildocs/static/wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js b/wagtail/wagtaildocs/static/wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js new file mode 100644 index 0000000000..8f713a711b --- /dev/null +++ b/wagtail/wagtaildocs/static/wagtaildocs/js/hallo-plugins/hallo-wagtaildoclink.js @@ -0,0 +1,51 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + (function($) { + return $.widget("IKS.hallowagtaildoclink", { + options: { + uuid: '', + editable: null + }, + populateToolbar: function(toolbar) { + var button, widget; + + widget = this; + button = $(''); + button.hallobutton({ + uuid: this.options.uuid, + editable: this.options.editable, + label: 'Documents', + icon: 'icon-file-text-alt', + command: null + }); + toolbar.append(button); + return button.on("click", function(event) { + var lastSelection; + + lastSelection = widget.options.editable.getSelection(); + return ModalWorkflow({ + url: window.chooserUrls.documentChooser, + responses: { + documentChosen: function(docData) { + var a; + + a = document.createElement('a'); + a.setAttribute('href', docData.url); + a.setAttribute('data-id', docData.id); + a.setAttribute('data-linktype', 'document'); + if ((!lastSelection.collapsed) && lastSelection.canSurroundContents()) { + lastSelection.surroundContents(a); + } else { + a.appendChild(document.createTextNode(docData.title)); + lastSelection.insertNode(a); + } + return widget.options.editable.element.trigger('change'); + } + } + }); + }); + } + }); + })(jQuery); + +}).call(this); \ No newline at end of file diff --git a/wagtail/wagtailembeds/static/wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.coffee b/wagtail/wagtailembeds/static/wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.coffee deleted file mode 100644 index 99e844f8d2..0000000000 --- a/wagtail/wagtailembeds/static/wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.coffee +++ /dev/null @@ -1,36 +0,0 @@ -# plugin for hallo.js to allow inserting embeds - -(($) -> - $.widget "IKS.hallowagtailembeds", - options: - uuid: '' - editable: null - - populateToolbar: (toolbar) -> - widget = this - - # Create an element for holding the button - button = $('') - button.hallobutton - uuid: @options.uuid - editable: @options.editable - label: 'Embed' - icon: 'icon-media' - command: null - - # Append the button to toolbar - toolbar.append button - - button.on "click", (event) -> - lastSelection = widget.options.editable.getSelection() - insertionPoint = $(lastSelection.endContainer).parentsUntil('.richtext').last() - ModalWorkflow - url: window.chooserUrls.embedsChooser - responses: - embedChosen: (embedData) -> - elem = $(embedData).get(0) - lastSelection.insertNode(elem) - if elem.getAttribute('contenteditable') == 'false' - insertRichTextDeleteControl(elem) - widget.options.editable.element.trigger('change') -)(jQuery) diff --git a/wagtail/wagtailembeds/static/wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js b/wagtail/wagtailembeds/static/wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js new file mode 100644 index 0000000000..eb9f1e05a1 --- /dev/null +++ b/wagtail/wagtailembeds/static/wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js @@ -0,0 +1,47 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + (function($) { + return $.widget("IKS.hallowagtailembeds", { + options: { + uuid: '', + editable: null + }, + populateToolbar: function(toolbar) { + var button, widget; + + widget = this; + button = $(''); + button.hallobutton({ + uuid: this.options.uuid, + editable: this.options.editable, + label: 'Embed', + icon: 'icon-media', + command: null + }); + toolbar.append(button); + return button.on("click", function(event) { + var insertionPoint, lastSelection; + + lastSelection = widget.options.editable.getSelection(); + insertionPoint = $(lastSelection.endContainer).parentsUntil('.richtext').last(); + return ModalWorkflow({ + url: window.chooserUrls.embedsChooser, + responses: { + embedChosen: function(embedData) { + var elem; + + elem = $(embedData).get(0); + lastSelection.insertNode(elem); + if (elem.getAttribute('contenteditable') === 'false') { + insertRichTextDeleteControl(elem); + } + return widget.options.editable.element.trigger('change'); + } + } + }); + }); + } + }); + })(jQuery); + +}).call(this); \ No newline at end of file diff --git a/wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-wagtailimage.coffee b/wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-wagtailimage.coffee deleted file mode 100644 index 4a4bcaf7ad..0000000000 --- a/wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-wagtailimage.coffee +++ /dev/null @@ -1,39 +0,0 @@ -# plugin for hallo.js to allow inserting images from the Wagtail image library - -(($) -> - $.widget "IKS.hallowagtailimage", - options: - uuid: '' - editable: null - - populateToolbar: (toolbar) -> - widget = this - - # Create an element for holding the button - button = $('') - button.hallobutton - uuid: @options.uuid - editable: @options.editable - label: 'Images' - icon: 'icon-picture' - command: null - - # Append the button to toolbar - toolbar.append button - - button.on "click", (event) -> - lastSelection = widget.options.editable.getSelection() - insertionPoint = $(lastSelection.endContainer).parentsUntil('.richtext').last() - ModalWorkflow - url: window.chooserUrls.imageChooser + '?select_format=true' - responses: - imageChosen: (imageData) -> - elem = $(imageData.html).get(0) - - lastSelection.insertNode(elem) - - if elem.getAttribute('contenteditable') == 'false' - insertRichTextDeleteControl(elem) - widget.options.editable.element.trigger('change') - -)(jQuery) diff --git a/wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-wagtailimage.js b/wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-wagtailimage.js new file mode 100644 index 0000000000..a0c9bb14b0 --- /dev/null +++ b/wagtail/wagtailimages/static/wagtailimages/js/hallo-plugins/hallo-wagtailimage.js @@ -0,0 +1,47 @@ +// Generated by CoffeeScript 1.6.2 +(function() { + (function($) { + return $.widget("IKS.hallowagtailimage", { + options: { + uuid: '', + editable: null + }, + populateToolbar: function(toolbar) { + var button, widget; + + widget = this; + button = $(''); + button.hallobutton({ + uuid: this.options.uuid, + editable: this.options.editable, + label: 'Images', + icon: 'icon-picture', + command: null + }); + toolbar.append(button); + return button.on("click", function(event) { + var insertionPoint, lastSelection; + + lastSelection = widget.options.editable.getSelection(); + insertionPoint = $(lastSelection.endContainer).parentsUntil('.richtext').last(); + return ModalWorkflow({ + url: window.chooserUrls.imageChooser + '?select_format=true', + responses: { + imageChosen: function(imageData) { + var elem; + + elem = $(imageData.html).get(0); + lastSelection.insertNode(elem); + if (elem.getAttribute('contenteditable') === 'false') { + insertRichTextDeleteControl(elem); + } + return widget.options.editable.element.trigger('change'); + } + } + }); + }); + } + }); + })(jQuery); + +}).call(this); \ No newline at end of file From 5c26ef2709d3576e87744916c688a4ceb0f23f7b Mon Sep 17 00:00:00 2001 From: Dave Cranwell Date: Tue, 4 Mar 2014 16:54:12 +0000 Subject: [PATCH 08/13] status tags shouldn't be linked when choosing OR moving --- wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html b/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html index fdbfb19ff6..a0faa0da18 100644 --- a/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html +++ b/wagtail/wagtailadmin/templates/wagtailadmin/pages/list.html @@ -75,7 +75,7 @@ {{ parent_page.content_type.model_class.get_verbose_name }} - {% if not choosing and parent_page.live and not parent_page.is_root and 'view_live' not in hide_actions|default:'' %} + {% if not choosing and not moving and parent_page.live and not parent_page.is_root and 'view_live' not in hide_actions|default:'' %} {{ parent_page.status_string|capfirst }} {% else %} {{ parent_page.status_string|capfirst }} @@ -208,7 +208,7 @@ {% endif %} {{ page.content_type.model_class.get_verbose_name }} - {% if not choosing and page.live and 'view_live' not in hide_actions|default:'' %} + {% if not choosing and not moving and page.live and 'view_live' not in hide_actions|default:'' %} {{ page.status_string }} {% else %} {{ page.status_string }} From 6b97e90eb0f646f989173dc983342876218453a6 Mon Sep 17 00:00:00 2001 From: Tom Dyson Date: Tue, 4 Mar 2014 17:10:04 +0000 Subject: [PATCH 09/13] Ubuntu installation script Production Wagtail one-liner for clean Ubuntu 13.04 boxes --- scripts/install/ubuntu.sh | 119 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 scripts/install/ubuntu.sh diff --git a/scripts/install/ubuntu.sh b/scripts/install/ubuntu.sh new file mode 100644 index 0000000000..a46e12ec3e --- /dev/null +++ b/scripts/install/ubuntu.sh @@ -0,0 +1,119 @@ +# Production-ready Wagtail installation, tested on Ubuntu 13.04 +# Tom Dyson and Neal Todd + +PROJECT=mywagtail +PROJECT_ROOT=/usr/local/django + +echo "This script overwrites key files, and should only be run on a new box." +read -p "Type 'yes' to confirm: " CONFIRM +[ “$CONFIRM” == “yes” ] || exit + +read -p "Enter a name for your project [$PROJECT]: " U_PROJECT +if [ ! -z "$U_PROJECT" ]; then + PROJECT=$U_PROJECT +fi + +read -p "Enter the root of your project, without trailing slash [$PROJECT_ROOT]: " U_PROJECT_ROOT +if [ ! -z "$U_PROJECT_ROOT" ]; then + PROJECT_ROOT=$U_PROJECT_ROOT +fi + +if [ ! -z "$PROJECT_ROOT" ]; then + mkdir -p $PROJECT_ROOT || exit +fi + +echo "Please come back in a few minutes, when we'll need you to create an admin account." +sleep 5 + +aptitude update +aptitude -y install git python-pip nginx postgresql redis-server +aptitude -y install postgresql-server-dev-all python-dev libxml2-dev libxslt-dev libjpeg62-dev + +aptitude -y install npm +ln -s /usr/bin/nodejs /usr/bin/node +npm install -g coffee-script less + +perl -pi -e "s/^(local\s+all\s+postgres\s+)peer$/\1trust/" /etc/postgresql/9.1/main/pg_hba.conf +service postgresql reload + +aptitude -y install openjdk-7-jre-headless +curl -o elasticsearch-1.0.0.deb https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb +dpkg -i elasticsearch-1.0.0.deb +rm elasticsearch-1.0.0.deb +update-rc.d elasticsearch defaults 95 10 +service elasticsearch start + +cd $PROJECT_ROOT +git clone https://github.com/torchbox/wagtaildemo.git $PROJECT +cd $PROJECT +mv wagtaildemo $PROJECT +perl -pi -e"s/wagtaildemo/$PROJECT/" manage.py $PROJECT/wsgi.py $PROJECT/settings/*.py +rm -r etc README.md Vagrantfile* .git .gitignore + +dd if=/dev/zero of=/tmpswap bs=1024 count=524288 +mkswap /tmpswap +swapon /tmpswap +pip install -r requirements/production.txt +swapoff -v /tmpswap +rm /tmpswap + +echo SECRET_KEY = \"`python -c 'import random; print "".join([random.SystemRandom().choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50)])'`\" > $PROJECT/settings.local.py +echo ALLOWED_HOSTS = [\'`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1`\',] >> $PROJECT/settings/local.py +createdb -Upostgres $PROJECT +./manage.py syncdb --settings=$PROJECT.settings.production +./manage.py migrate --settings=$PROJECT.settings.production +./manage.py update_index --settings=$PROJECT.settings.production +./manage.py collectstatic --settings=$PROJECT.settings.production --noinput + +pip install uwsgi +cp $PROJECT/wsgi.py $PROJECT/wsgi_production.py +perl -pi -e"s/($PROJECT.settings)/\1.production/" $PROJECT/wsgi_production.py + +curl -O https://raw2.github.com/nginx/nginx/master/conf/uwsgi_params +cat << EOF > /etc/nginx/sites-enabled/default +upstream django { + server unix://$PROJECT_ROOT/$PROJECT/uwsgi.sock; +} +server { + listen 80; + charset utf-8; + client_max_body_size 75M; # max upload size + location /media { + alias $PROJECT_ROOT/$PROJECT/media; } + location /static { + alias $PROJECT_ROOT/$PROJECT/static; + } + location / { + uwsgi_pass django; + include $PROJECT_ROOT/$PROJECT/uwsgi_params; } +} +EOF + +cat << EOF > $PROJECT_ROOT/$PROJECT/uwsgi_conf.ini +[uwsgi] +chdir = $PROJECT_ROOT/$PROJECT +module = $PROJECT.wsgi_production +master = true +processes = 10 +socket = $PROJECT_ROOT/$PROJECT/uwsgi.sock +chmod-socket = 666 +vacuum = true +EOF + +mkdir -p /etc/uwsgi/vassals/ +ln -s $PROJECT_ROOT/$PROJECT/uwsgi_conf.ini /etc/uwsgi/vassals/ + +cat << EOF > /etc/init/uwsgi.conf +description "uwsgi for wagtail" +start on runlevel [2345] +stop on runlevel [06] +exec uwsgi --emperor /etc/uwsgi/vassals +EOF + +service uwsgi start +service nginx restart + +URL="http://`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1`" +echo "Wagtail lives!" +echo "The public site is at $URL/" +echo "and the admin interface is at $URL/admin/" From 00837af131f215c0b79b941e7bae034141a6b32f Mon Sep 17 00:00:00 2001 From: Tom Dyson Date: Tue, 4 Mar 2014 17:23:42 +0000 Subject: [PATCH 10/13] Trim Ubuntu installation Remove aptitude update - we don't have to manage their server setup - and recently-redundant coffeescript. --- scripts/install/ubuntu.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/install/ubuntu.sh b/scripts/install/ubuntu.sh index a46e12ec3e..5c935fadc6 100644 --- a/scripts/install/ubuntu.sh +++ b/scripts/install/ubuntu.sh @@ -25,13 +25,12 @@ fi echo "Please come back in a few minutes, when we'll need you to create an admin account." sleep 5 -aptitude update aptitude -y install git python-pip nginx postgresql redis-server aptitude -y install postgresql-server-dev-all python-dev libxml2-dev libxslt-dev libjpeg62-dev aptitude -y install npm ln -s /usr/bin/nodejs /usr/bin/node -npm install -g coffee-script less +npm install -g less perl -pi -e "s/^(local\s+all\s+postgres\s+)peer$/\1trust/" /etc/postgresql/9.1/main/pg_hba.conf service postgresql reload From 5ef4f0b07af23949f6de904194229f871a7001d4 Mon Sep 17 00:00:00 2001 From: Tom Dyson Date: Tue, 4 Mar 2014 22:24:54 +0000 Subject: [PATCH 11/13] Updated Ubuntu version for installation script --- scripts/install/ubuntu.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install/ubuntu.sh b/scripts/install/ubuntu.sh index 5c935fadc6..550644787a 100644 --- a/scripts/install/ubuntu.sh +++ b/scripts/install/ubuntu.sh @@ -1,4 +1,4 @@ -# Production-ready Wagtail installation, tested on Ubuntu 13.04 +# Production-ready Wagtail installation, tested on Ubuntu 13.10 # Tom Dyson and Neal Todd PROJECT=mywagtail From a19459e1b2e409666d07efaa4f5591f2f215d531 Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Wed, 5 Mar 2014 13:58:39 +0000 Subject: [PATCH 12/13] Updated ubuntu and adapted debian install scripts for Wagtail production configuration instance on a fresh box. Accompanying uwsgi init.d script for debian. npm/less to be removed and init.d script location to be updated. --- scripts/install/debian.sh | 129 +++++++++++++++++++++++++++++++++++ scripts/install/ubuntu.sh | 23 ++++--- scripts/install/uwsgi-init.d | 113 ++++++++++++++++++++++++++++++ 3 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 scripts/install/debian.sh create mode 100644 scripts/install/uwsgi-init.d diff --git a/scripts/install/debian.sh b/scripts/install/debian.sh new file mode 100644 index 0000000000..cd9f7b4152 --- /dev/null +++ b/scripts/install/debian.sh @@ -0,0 +1,129 @@ +# Production-configured Wagtail installation +# (secure services/account for full production use). +# Tested on Debian 7.0. +# Tom Dyson and Neal Todd + +# NB: Ensure the system locale is okay before running (dpkg-reconfigure locales). + +PROJECT=mywagtail +PROJECT_ROOT=/usr/local/django + +echo "This script overwrites key files, and should only be run on a new box." +read -p "Type 'yes' to confirm: " CONFIRM +[ “$CONFIRM” == “yes” ] || exit + +read -p "Enter a name for your project [$PROJECT]: " U_PROJECT +if [ ! -z "$U_PROJECT" ]; then + PROJECT=$U_PROJECT +fi + +read -p "Enter the root of your project, without trailing slash [$PROJECT_ROOT]: " U_PROJECT_ROOT +if [ ! -z "$U_PROJECT_ROOT" ]; then + PROJECT_ROOT=$U_PROJECT_ROOT +fi + +if [ ! -z "$PROJECT_ROOT" ]; then + mkdir -p $PROJECT_ROOT || exit +fi + +echo -e "\nPlease come back in a few minutes, when we'll need you to create an admin account." +sleep 5 + +SERVER_IP=`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1` + +aptitude update +aptitude -y install git python-pip nginx postgresql redis-server +aptitude -y install postgresql-server-dev-all python-dev libxml2-dev libxslt-dev libjpeg62-dev + +wget -nv http://nodejs.org/dist/v0.10.20/node-v0.10.20.tar.gz +tar xzf node-v0.10.20.tar.gz +cd node-v0.10.20 +./configure && make -s && make -s install +cd .. +rm -r node-v0.10.20 node-v0.10.20.tar.gz +npm install -g less + +perl -pi -e "s/^(local\s+all\s+postgres\s+)peer$/\1trust/" /etc/postgresql/9.1/main/pg_hba.conf +service postgresql reload + +aptitude -y install openjdk-7-jre-headless +curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb +dpkg -i elasticsearch-1.0.0.deb +rm elasticsearch-1.0.0.deb +update-rc.d elasticsearch defaults 95 10 +service elasticsearch start + +cd $PROJECT_ROOT +git clone https://github.com/torchbox/wagtaildemo.git $PROJECT +cd $PROJECT +mv wagtaildemo $PROJECT +perl -pi -e"s/wagtaildemo/$PROJECT/" manage.py $PROJECT/wsgi.py $PROJECT/settings/*.py +rm -r etc README.md Vagrantfile* .git .gitignore + +dd if=/dev/zero of=/tmpswap bs=1024 count=524288 +mkswap /tmpswap +swapon /tmpswap +pip install -r requirements/production.txt +swapoff -v /tmpswap +rm /tmpswap + +echo SECRET_KEY = \"`python -c 'import random; print "".join([random.SystemRandom().choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50)])'`\" > $PROJECT/settings.local.py +echo ALLOWED_HOSTS = [\'$SERVER_IP\',] >> $PROJECT/settings/local.py +createdb -Upostgres $PROJECT +./manage.py syncdb --settings=$PROJECT.settings.production +./manage.py migrate --settings=$PROJECT.settings.production +./manage.py update_index --settings=$PROJECT.settings.production +./manage.py collectstatic --settings=$PROJECT.settings.production --noinput + +pip install uwsgi +cp $PROJECT/wsgi.py $PROJECT/wsgi_production.py +perl -pi -e"s/($PROJECT.settings)/\1.production/" $PROJECT/wsgi_production.py + +curl -O https://raw2.github.com/nginx/nginx/master/conf/uwsgi_params +cat << EOF > /etc/nginx/sites-enabled/default +upstream django { + server unix://$PROJECT_ROOT/$PROJECT/uwsgi.sock; +} +server { + listen 80; + charset utf-8; + client_max_body_size 75M; # max upload size + location /media { + alias $PROJECT_ROOT/$PROJECT/media; + } + location /static { + alias $PROJECT_ROOT/$PROJECT/static; + } + location / { + uwsgi_pass django; + include $PROJECT_ROOT/$PROJECT/uwsgi_params; + } +} +EOF + +cat << EOF > $PROJECT_ROOT/$PROJECT/uwsgi_conf.ini +[uwsgi] +chdir = $PROJECT_ROOT/$PROJECT +module = $PROJECT.wsgi_production +master = true +processes = 10 +socket = $PROJECT_ROOT/$PROJECT/uwsgi.sock +chmod-socket = 666 +vacuum = true +EOF + +mkdir -p /etc/uwsgi/vassals/ +ln -s $PROJECT_ROOT/$PROJECT/uwsgi_conf.ini /etc/uwsgi/vassals/ + +curl -o /etc/init.d/uwsgi https://gist.githubusercontent.com/nealtodd/9364691/raw/43f0bdb1304995ab73e3d22e62e14111d40d4e90/uwsgi-init.d +mkdir /var/log/uwsgi +chmod 755 /etc/init.d/uwsgi +update-rc.d uwsgi defaults + +service uwsgi start +service nginx restart + +URL="http://$SERVER_IP" +echo -e "\n\nWagtail lives!\n\n" +echo "The public site is at $URL/" +echo "and the admin interface is at $URL/admin/" diff --git a/scripts/install/ubuntu.sh b/scripts/install/ubuntu.sh index 550644787a..ac38d59106 100644 --- a/scripts/install/ubuntu.sh +++ b/scripts/install/ubuntu.sh @@ -1,4 +1,6 @@ -# Production-ready Wagtail installation, tested on Ubuntu 13.10 +# Production-configured Wagtail installation +# (secure services/account for full production use). +# Tested on Ubuntu 13.10. # Tom Dyson and Neal Todd PROJECT=mywagtail @@ -22,9 +24,12 @@ if [ ! -z "$PROJECT_ROOT" ]; then mkdir -p $PROJECT_ROOT || exit fi -echo "Please come back in a few minutes, when we'll need you to create an admin account." +echo -e "\nPlease come back in a few minutes, when we'll need you to create an admin account." sleep 5 +SERVER_IP=`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1` + +aptitude update aptitude -y install git python-pip nginx postgresql redis-server aptitude -y install postgresql-server-dev-all python-dev libxml2-dev libxslt-dev libjpeg62-dev @@ -36,7 +41,7 @@ perl -pi -e "s/^(local\s+all\s+postgres\s+)peer$/\1trust/" /etc/postgresql/9.1/m service postgresql reload aptitude -y install openjdk-7-jre-headless -curl -o elasticsearch-1.0.0.deb https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb +curl -O https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.0.0.deb dpkg -i elasticsearch-1.0.0.deb rm elasticsearch-1.0.0.deb update-rc.d elasticsearch defaults 95 10 @@ -57,7 +62,7 @@ swapoff -v /tmpswap rm /tmpswap echo SECRET_KEY = \"`python -c 'import random; print "".join([random.SystemRandom().choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50)])'`\" > $PROJECT/settings.local.py -echo ALLOWED_HOSTS = [\'`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1`\',] >> $PROJECT/settings/local.py +echo ALLOWED_HOSTS = [\'$SERVER_IP\',] >> $PROJECT/settings/local.py createdb -Upostgres $PROJECT ./manage.py syncdb --settings=$PROJECT.settings.production ./manage.py migrate --settings=$PROJECT.settings.production @@ -78,13 +83,15 @@ server { charset utf-8; client_max_body_size 75M; # max upload size location /media { - alias $PROJECT_ROOT/$PROJECT/media; } + alias $PROJECT_ROOT/$PROJECT/media; + } location /static { alias $PROJECT_ROOT/$PROJECT/static; } location / { uwsgi_pass django; - include $PROJECT_ROOT/$PROJECT/uwsgi_params; } + include $PROJECT_ROOT/$PROJECT/uwsgi_params; + } } EOF @@ -112,7 +119,7 @@ EOF service uwsgi start service nginx restart -URL="http://`ifconfig eth0 |grep "inet addr" | cut -d: -f2 | cut -d" " -f1`" -echo "Wagtail lives!" +URL="http://$SERVER_IP" +echo -e "\n\nWagtail lives!\n\n" echo "The public site is at $URL/" echo "and the admin interface is at $URL/admin/" diff --git a/scripts/install/uwsgi-init.d b/scripts/install/uwsgi-init.d new file mode 100644 index 0000000000..43f0bdb130 --- /dev/null +++ b/scripts/install/uwsgi-init.d @@ -0,0 +1,113 @@ +#!/usr/bin/env bash + +### BEGIN INIT INFO +# Provides: emperor +# Required-Start: $all +# Required-Stop: $all +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: uwsgi for wagtail +# Description: uwsgi for wagtail +### END INIT INFO +set -e + + +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin +DAEMON=/usr/local/bin/uwsgi +RUN=/var/run/uwsgi +CONFIG_DIR=/etc/uwsgi/vassals +NAME=uwsgi +DESC=emperor +OWNER=root +GROUP=root +OP=$1 + +[[ -x $DAEMON ]] || exit 0 +[[ -d $RUN ]] || mkdir $RUN && chown $OWNER.$GROUP $RUN + + +do_pid_check() +{ + local PIDFILE=$1 + [[ -f $PIDFILE ]] || return 0 + local PID=$(cat $PIDFILE) + for p in $(pgrep $NAME); do + [[ $p == $PID ]] && return 1 + done + return 0 +} + + +do_start() +{ + local PIDFILE=$RUN/$NAME.pid + local START_OPTS=" \ + --emperor $CONFIG_DIR \ + --pidfile $PIDFILE \ + --uid $OWNER --gid $GROUP \ + --daemonize /var/log/$NAME/uwsgi-emperor.log" + if do_pid_check $PIDFILE; then + $NAME $START_OPTS + else + echo "Already running!" + fi +} + +send_sig() +{ + local PIDFILE=$RUN/$NAME.pid + set +e + [[ -f $PIDFILE ]] && kill $1 $(cat $PIDFILE) > /dev/null 2>&1 + set -e +} + +wait_and_clean_pidfile() +{ + local PIDFILE=$RUN/uwsgi.pid + until do_pid_check $PIDFILE; do + echo -n ""; + done + rm -f $PIDFILE +} + +do_stop() +{ + send_sig -3 + wait_and_clean_pidfile +} + +do_reload() +{ + send_sig -1 +} + +case "$OP" in + start) + echo "Starting $DESC: " + do_start + echo "$NAME." + ;; + stop) + echo -n "Stopping $DESC: " + do_stop + echo "$NAME." + ;; + reload) + echo -n "Reloading $DESC: " + do_reload + echo "$NAME." + ;; + restart) + echo "Restarting $DESC: " + do_stop + sleep 1 + do_start + echo "$NAME." + ;; + *) + N=/etc/init.d/$NAME + echo "Usage: $N {start|stop|restart|reload}">&2 + exit 1 + ;; +esac +exit 0 \ No newline at end of file From cff198089126cb806d6e59ff381cca4208f0ec4c Mon Sep 17 00:00:00 2001 From: Neal Todd Date: Wed, 5 Mar 2014 14:10:06 +0000 Subject: [PATCH 13/13] Updating init.d script link now that it's in the repo. --- scripts/install/debian.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install/debian.sh b/scripts/install/debian.sh index cd9f7b4152..2b1c04326e 100644 --- a/scripts/install/debian.sh +++ b/scripts/install/debian.sh @@ -115,7 +115,7 @@ EOF mkdir -p /etc/uwsgi/vassals/ ln -s $PROJECT_ROOT/$PROJECT/uwsgi_conf.ini /etc/uwsgi/vassals/ -curl -o /etc/init.d/uwsgi https://gist.githubusercontent.com/nealtodd/9364691/raw/43f0bdb1304995ab73e3d22e62e14111d40d4e90/uwsgi-init.d +curl -o /etc/init.d/uwsgi https://raw.github.com/torchbox/wagtail/master/scripts/install/uwsgi-init.d mkdir /var/log/uwsgi chmod 755 /etc/init.d/uwsgi update-rc.d uwsgi defaults