From c39e0ff7682ecfb860c81c07baf0950d5d59727d Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 09:53:39 -0300 Subject: [PATCH 01/22] running test with nose --- publish/actions.py | 1 - publish/models.py | 92 -- publish/tests.py | 1505 ------------------------ publish/tests/__init__.py | 1 + publish/tests/example_app/__init__.py | 0 publish/tests/example_app/models.py | 99 ++ publish/tests/example_app/views.py | 1 + publish/tests/settings_for_test.py | 46 + publish/tests/test_all.py | 1515 +++++++++++++++++++++++++ tests/test_settings.py | 52 - 10 files changed, 1662 insertions(+), 1650 deletions(-) delete mode 100644 publish/tests.py create mode 100644 publish/tests/__init__.py create mode 100644 publish/tests/example_app/__init__.py create mode 100644 publish/tests/example_app/models.py create mode 100644 publish/tests/example_app/views.py create mode 100644 publish/tests/settings_for_test.py create mode 100644 publish/tests/test_all.py delete mode 100644 tests/test_settings.py diff --git a/publish/actions.py b/publish/actions.py index c889925..763702e 100644 --- a/publish/actions.py +++ b/publish/actions.py @@ -97,7 +97,6 @@ def publish_selected(modeladmin, request, queryset): perms_needed = [] _check_permissions(modeladmin, all_published, request, perms_needed) - if request.POST.get('post'): if perms_needed: raise PermissionDenied diff --git a/publish/models.py b/publish/models.py index 7efaae3..548a412 100644 --- a/publish/models.py +++ b/publish/models.py @@ -2,7 +2,6 @@ from django.db.models.query import QuerySet, Q from django.db.models.base import ModelBase from django.db.models.fields.related import RelatedField -from django.conf import settings from utils import NestedSet from signals import pre_publish, post_publish @@ -392,96 +391,5 @@ def publish_deletions(self, all_published=None, parent=None, dry_run=False): self._post_publish(dry_run, all_published, deleted=True) -if getattr(settings, 'TESTING_PUBLISH', False): - # classes to test that publishing etc work ok - from datetime import datetime - - class Site(models.Model): - title = models.CharField(max_length=100) - domain = models.CharField(max_length=100) - - class FlatPage(Publishable): - url = models.CharField(max_length=100, db_index=True) - title = models.CharField(max_length=200) - content = models.TextField(blank=True) - enable_comments = models.BooleanField() - template_name = models.CharField(max_length=70, blank=True) - registration_required = models.BooleanField() - sites = models.ManyToManyField(Site) - - class Meta: - ordering = ['url'] - - def get_absolute_url(self): - if self.is_public: - return self.url - return '%s*' % self.url - - class Author(Publishable): - name = models.CharField(max_length=100) - profile = models.TextField(blank=True) - - class PublishMeta(Publishable.PublishMeta): - publish_reverse_fields = ['authorprofile'] - - class AuthorProfile(Publishable): - author = models.OneToOneField(Author) - extra_profile = models.TextField(blank=True) - - class ChangeLog(models.Model): - changed = models.DateTimeField(db_index=True, auto_now_add=True) - message = models.CharField(max_length=200) - - class Tag(models.Model): - title = models.CharField(max_length=100, unique=True) - slug = models.CharField(max_length=100) - - # publishable model with a reverse relation to - # page (as a child) - class PageBlock(Publishable): - page=models.ForeignKey('Page') - content = models.TextField(blank=True) - - # non-publishable reverse relation to page (as a child) - class Comment(models.Model): - page=models.ForeignKey('Page') - comment = models.TextField() - - def update_pub_date(page, field_name, value): - # ignore value entirely and replace with now - setattr(page, field_name, update_pub_date.pub_date) - update_pub_date.pub_date = datetime.now() - - class Page(Publishable): - slug = models.CharField(max_length=100, db_index=True) - title = models.CharField(max_length=200) - content = models.TextField(blank=True) - pub_date = models.DateTimeField(default=datetime.now) - - parent = models.ForeignKey('self', blank=True, null=True) - - authors = models.ManyToManyField(Author, blank=True) - log = models.ManyToManyField(ChangeLog, blank=True) - tags = models.ManyToManyField(Tag, through='PageTagOrder', blank=True) - - class Meta: - ordering = ['slug'] - - class PublishMeta(Publishable.PublishMeta): - publish_exclude_fields = ['log'] - publish_reverse_fields = ['pageblock_set'] - publish_functions = { 'pub_date': update_pub_date } - - def get_absolute_url(self): - if not self.parent: - return u'/%s/' % self.slug - return '%s%s/' % (self.parent.get_absolute_url(), self.slug) - - class PageTagOrder(Publishable): - # note these are named in non-standard way to - # ensure we are getting correct names - tagged_page=models.ForeignKey(Page) - page_tag=models.ForeignKey(Tag) - tag_order=models.IntegerField() diff --git a/publish/tests.py b/publish/tests.py deleted file mode 100644 index 87728fc..0000000 --- a/publish/tests.py +++ /dev/null @@ -1,1505 +0,0 @@ -from django.conf import settings - -if getattr(settings, 'TESTING_PUBLISH', False): - import unittest - from django.test import TransactionTestCase - from django.contrib.admin.sites import AdminSite - from django.contrib.auth.models import User - from django.forms.models import ModelChoiceField, ModelMultipleChoiceField - from django.conf.urls.defaults import * - from django.core.exceptions import PermissionDenied - from django.http import Http404 - - from publish.models import Publishable, FlatPage, Site, Page, PageBlock, \ - Author, AuthorProfile, Tag, PageTagOrder, Comment, update_pub_date, \ - PublishException - - from publish.admin import PublishableAdmin, PublishableStackedInline - from publish.actions import publish_selected, delete_selected, \ - _convert_all_published_to_html, undelete_selected - from publish.utils import NestedSet - from publish.signals import pre_publish, post_publish - from publish.filters import PublishableRelatedFieldListFilter - - - def _get_rendered_content(response): - content = getattr(response, 'rendered_content', None) - if content is not None: - return content - return response.content - - - class TestNestedSet(unittest.TestCase): - - def setUp(self): - super(TestNestedSet, self).setUp() - self.nested = NestedSet() - - def test_len(self): - self.failUnlessEqual(0, len(self.nested)) - self.nested.add('one') - self.failUnlessEqual(1, len(self.nested)) - self.nested.add('two') - self.failUnlessEqual(2, len(self.nested)) - self.nested.add('one2', parent='one') - self.failUnlessEqual(3, len(self.nested)) - - def test_contains(self): - self.failIf('one' in self.nested) - self.nested.add('one') - self.failUnless('one' in self.nested) - self.nested.add('one2', parent='one') - self.failUnless('one2' in self.nested) - - def test_nested_items(self): - self.failUnlessEqual([], self.nested.nested_items()) - self.nested.add('one') - self.failUnlessEqual(['one'], self.nested.nested_items()) - self.nested.add('two') - self.nested.add('one2', parent='one') - self.failUnlessEqual(['one', ['one2'], 'two'], self.nested.nested_items()) - self.nested.add('one2-1', parent='one2') - self.nested.add('one2-2', parent='one2') - self.failUnlessEqual(['one', ['one2', ['one2-1', 'one2-2']], 'two'], self.nested.nested_items()) - - def test_iter(self): - self.failUnlessEqual(set(), set(self.nested)) - - self.nested.add('one') - self.failUnlessEqual(set(['one']), set(self.nested)) - - self.nested.add('two', parent='one') - self.failUnlessEqual(set(['one', 'two']), set(self.nested)) - - items = set(['one', 'two']) - - for item in self.nested: - self.failUnless(item in items) - items.remove(item) - - self.failUnlessEqual(set(), items) - - def test_original(self): - class MyObject(object): - def __init__(self, obj): - self.obj = obj - - def __eq__(self, other): - return self.obj == other.obj - - def __hash__(self): - return hash(self.obj) - - # should always return an item at least - self.failUnlessEqual(MyObject('hi there'), self.nested.original(MyObject('hi there'))) - - m1 = MyObject('m1') - self.nested.add(m1) - - self.failUnlessEqual(id(m1), id(self.nested.original(m1))) - self.failUnlessEqual(id(m1), id(self.nested.original(MyObject('m1')))) - - - - - class TestBasicPublishable(TransactionTestCase): - - def setUp(self): - super(TestBasicPublishable, self).setUp() - self.flat_page = FlatPage(url='/my-page', title='my page', - content='here is some content', - enable_comments=False, - registration_required=True) - - def test_get_public_absolute_url(self): - self.failUnlessEqual('/my-page*', self.flat_page.get_absolute_url()) - # public absolute url doesn't exist until published - self.assertTrue(self.flat_page.get_public_absolute_url() is None) - self.flat_page.save() - self.flat_page.publish() - self.failUnlessEqual('/my-page', self.flat_page.get_public_absolute_url()) - - def test_save_marks_changed(self): - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) - self.flat_page.save(mark_changed=False) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) - self.flat_page.save() - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) - - def test_publish_excludes_fields(self): - self.flat_page.save() - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failIfEqual(self.flat_page.id, self.flat_page.public.id) - self.failUnless(self.flat_page.public.is_public) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.public.publish_state) - - def test_publish_check_is_not_public(self): - try: - self.flat_page.is_public = True - self.flat_page.publish() - self.fail("Should not be able to publish public models") - except PublishException: - pass - - def test_publish_check_has_id(self): - try: - self.flat_page.publish() - self.fail("Should not be able to publish unsaved models") - except PublishException: - pass - - def test_publish_simple_fields(self): - self.flat_page.save() - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) - self.failIf(self.flat_page.public) # should not be a public version yet - - self.flat_page.publish() - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) - self.failUnless(self.flat_page.public) - - for field in 'url', 'title', 'content', 'enable_comments', 'registration_required': - self.failUnlessEqual(getattr(self.flat_page, field), getattr(self.flat_page.public, field)) - - def test_published_simple_field_repeated(self): - self.flat_page.save() - self.flat_page.publish() - - public = self.flat_page.public - self.failUnless(public) - - self.flat_page.title = 'New Title' - self.flat_page.save() - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) - - self.failUnlessEqual(public, self.flat_page.public) - self.failIfEqual(public.title, self.flat_page.title) - - self.flat_page.publish() - self.failUnlessEqual(public, self.flat_page.public) - self.failUnlessEqual(public.title, self.flat_page.title) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) - - def test_publish_records_published(self): - all_published = NestedSet() - self.flat_page.save() - self.flat_page.publish(all_published=all_published) - self.failUnlessEqual(1, len(all_published)) - self.failUnless(self.flat_page in all_published) - self.failUnless(self.flat_page.public) - - def test_publish_dryrun(self): - all_published = NestedSet() - self.flat_page.save() - self.flat_page.publish(dry_run=True, all_published=all_published) - self.failUnlessEqual(1, len(all_published)) - self.failUnless(self.flat_page in all_published) - self.failIf(self.flat_page.public) - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) - - def test_delete_after_publish(self): - self.flat_page.save() - self.flat_page.publish() - public = self.flat_page.public - self.failUnless(public) - - self.flat_page.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.flat_page.publish_state) - - self.failUnlessEqual(set([self.flat_page, self.flat_page.public]), set(FlatPage.objects.all())) - - def test_delete_before_publish(self): - self.flat_page.save() - self.flat_page.delete() - self.failUnlessEqual([], list(FlatPage.objects.all())) - - def test_publish_deletions(self): - self.flat_page.save() - self.flat_page.publish() - public = self.flat_page.public - - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) - - self.flat_page.delete() - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) - - self.flat_page.publish() - self.failUnlessEqual([], list(FlatPage.objects.all())) - - def test_publish_deletions_checks_all_published(self): - # make sure publish_deletions looks at all_published arg - # to see if we need to actually publish the deletion - self.flat_page.save() - self.flat_page.publish() - public = self.flat_page.public - - self.flat_page.delete() - - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) - - # this should effectively stop the deletion happening - all_published = NestedSet() - all_published.add(self.flat_page) - - self.flat_page.publish(all_published=all_published) - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) - - - class TestPublishableManager(TransactionTestCase): - - def setUp(self): - super(TransactionTestCase, self).setUp() - self.flat_page1 = FlatPage.objects.create(url='/url1/', title='title 1') - self.flat_page2 = FlatPage.objects.create(url='/url2/', title='title 2') - - def test_all(self): - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.all())) - - # publishing will produce extra copies - self.flat_page1.publish() - self.failUnlessEqual(3, FlatPage.objects.count()) - - self.flat_page2.publish() - self.failUnlessEqual(4, FlatPage.objects.count()) - - - def test_changed(self): - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.changed())) - - self.flat_page1.publish() - self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.changed())) - - self.flat_page2.publish() - self.failUnlessEqual([], list(FlatPage.objects.changed())) - - def test_draft(self): - # draft should stay the same pretty much always - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) - - self.flat_page1.publish() - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) - - self.flat_page2.publish() - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) - - self.flat_page2.delete() - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft())) - - - def test_published(self): - self.failUnlessEqual([], list(FlatPage.objects.published())) - - self.flat_page1.publish() - self.failUnlessEqual([self.flat_page1.public], list(FlatPage.objects.published())) - - self.flat_page2.publish() - self.failUnlessEqual([self.flat_page1.public, self.flat_page2.public], list(FlatPage.objects.published())) - - def test_deleted(self): - self.failUnlessEqual([], list(FlatPage.objects.deleted())) - - self.flat_page1.publish() - self.failUnlessEqual([], list(FlatPage.objects.deleted())) - - self.flat_page1.delete() - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) - - def test_draft_and_deleted(self): - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) - - self.flat_page1.publish() - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft())) - - self.flat_page1.delete() - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) - self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.draft())) - - - def test_delete(self): - # delete is overriden, so it marks the public instances - self.flat_page1.publish() - public1 = self.flat_page1.public - - FlatPage.objects.draft().delete() - - self.failUnlessEqual([], list(FlatPage.objects.draft())) - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) - self.failUnlessEqual([public1], list(FlatPage.objects.published())) - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft_and_deleted())) - - def test_publish(self): - self.failUnlessEqual([], list(FlatPage.objects.published())) - - FlatPage.objects.draft().publish() - - flat_page1 = FlatPage.objects.get(id=self.flat_page1.id) - flat_page2 = FlatPage.objects.get(id=self.flat_page2.id) - - self.failUnlessEqual(set([flat_page1.public, flat_page2.public]), set(FlatPage.objects.published())) - - - - class TestPublishableManyToMany(TransactionTestCase): - - def setUp(self): - super(TestPublishableManyToMany, self).setUp() - self.flat_page = FlatPage.objects.create( - url='/my-page', title='my page', - content='here is some content', - enable_comments=False, - registration_required=True) - self.site1 = Site.objects.create(title='my site', domain='mysite.com') - self.site2 = Site.objects.create(title='a site', domain='asite.com') - - def test_publish_no_sites(self): - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([], list(self.flat_page.public.sites.all())) - - def test_publish_add_site(self): - self.flat_page.sites.add(self.site1) - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) - - def test_publish_repeated_add_site(self): - self.flat_page.sites.add(self.site1) - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) - - self.flat_page.sites.add(self.site2) - self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) - - self.flat_page.publish() - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - def test_publish_remove_site(self): - self.flat_page.sites.add(self.site1, self.site2) - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - self.flat_page.sites.remove(self.site1) - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - self.flat_page.publish() - self.failUnlessEqual([self.site2], list(self.flat_page.public.sites.all())) - - def test_publish_clear_sites(self): - self.flat_page.sites.add(self.site1, self.site2) - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - self.flat_page.sites.clear() - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - self.flat_page.publish() - self.failUnlessEqual([], list(self.flat_page.public.sites.all())) - - def test_publish_sites_cleared_not_deleted(self): - self.flat_page.sites.add(self.site1, self.site2) - self.flat_page.publish() - self.flat_page.sites.clear() - self.flat_page.publish() - - self.failUnlessEqual([], list(self.flat_page.public.sites.all())) - - self.failIfEqual([], list(Site.objects.all())) - - - - - class TestPublishableRecursiveForeignKey(TransactionTestCase): - - def setUp(self): - super(TestPublishableRecursiveForeignKey, self).setUp() - self.page1 = Page.objects.create(slug='page1', title='page 1', content='some content') - self.page2 = Page.objects.create(slug='page2', title='page 2', content='other content', parent=self.page1) - - def test_publish_parent(self): - # this shouldn't publish the child page - self.page1.publish() - self.failUnless(self.page1.public) - self.failIf(self.page1.public.parent) - - page2 = Page.objects.get(id=self.page2.id) - self.failIf(page2.public) - - def test_publish_child_parent_already_published(self): - self.page1.publish() - self.page2.publish() - - self.failUnless(self.page1.public) - self.failUnless(self.page2.public) - - self.failIf(self.page1.public.parent) - self.failUnless(self.page2.public.parent) - - self.failIfEqual(self.page1, self.page2.public.parent) - - self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) - self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) - - def test_publish_child_parent_not_already_published(self): - self.page2.publish() - - page1 = Page.objects.get(id=self.page1.id) - self.failUnless(page1.public) - self.failUnless(self.page2.public) - - self.failIf(page1.public.parent) - self.failUnless(self.page2.public.parent) - - self.failIfEqual(page1, self.page2.public.parent) - - self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) - self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) - - def test_publish_repeated(self): - self.page1.publish() - self.page2.publish() - - self.page1.slug='main' - self.page1.save() - - self.failUnlessEqual('/main/', self.page1.get_absolute_url()) - - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - self.failUnlessEqual('/page1/', page1.public.get_absolute_url()) - self.failUnlessEqual('/page1/page2/', page2.public.get_absolute_url()) - - page1.publish() - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - self.failUnlessEqual('/main/', page1.public.get_absolute_url()) - self.failUnlessEqual('/main/page2/', page2.public.get_absolute_url()) - - page1.slug='elsewhere' - page1.save() - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - page2.slug='meanwhile' - page2.save() - page2.publish() - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - - # only page2 should be published, not page1, as page1 already published - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page2.publish_state) - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, page1.publish_state) - - self.failUnlessEqual('/main/', page1.public.get_absolute_url()) - self.failUnlessEqual('/main/meanwhile/', page2.public.get_absolute_url()) - - page1.publish() - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page2.publish_state) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page1.publish_state) - - self.failUnlessEqual('/elsewhere/', page1.public.get_absolute_url()) - self.failUnlessEqual('/elsewhere/meanwhile/', page2.public.get_absolute_url()) - - def test_publish_deletions(self): - self.page1.publish() - self.page2.publish() - - self.page2.delete() - self.failUnlessEqual([self.page2], list(Page.objects.deleted())) - - self.page2.publish() - self.failUnlessEqual([self.page1.public], list(Page.objects.published())) - self.failUnlessEqual([], list(Page.objects.deleted())) - - def test_publish_reverse_fields(self): - page_block = PageBlock.objects.create(page=self.page1, content='here we are') - - self.page1.publish() - - public = self.page1.public - self.failUnless(public) - - blocks = list(public.pageblock_set.all()) - self.failUnlessEqual(1, len(blocks)) - self.failUnlessEqual(page_block.content, blocks[0].content) - - def test_publish_deletions_reverse_fields(self): - page_block = PageBlock.objects.create(page=self.page1, content='here we are') - - self.page1.publish() - public = self.page1.public - self.failUnless(public) - - self.page1.delete() - - self.failUnlessEqual([self.page1], list(Page.objects.deleted())) - - self.page1.publish() - self.failUnlessEqual([], list(Page.objects.deleted())) - self.failUnlessEqual([], list(Page.objects.all())) - - def test_publish_reverse_fields_deleted(self): - # make sure child elements get removed - page_block = PageBlock.objects.create(page=self.page1, content='here we are') - - self.page1.publish() - - public = self.page1.public - page_block = PageBlock.objects.get(id=page_block.id) - page_block_public = page_block.public - self.failIf(page_block_public is None) - - self.failUnlessEqual([page_block_public], list(public.pageblock_set.all())) - - # now delete the page block and publish the parent - # to make sure that deletion gets copied over properly - page_block.delete() - page1 = Page.objects.get(id=self.page1.id) - page1.publish() - public = page1.public - - self.failUnlessEqual([], list(public.pageblock_set.all())) - - def test_publish_delections_with_non_publishable_children(self): - self.page1.publish() - - comment = Comment.objects.create(page=self.page1.public, comment='This is a comment') - - self.failUnlessEqual(1, Comment.objects.count()) - - self.page1.delete() - - self.failUnlessEqual([self.page1], list(Page.objects.deleted())) - self.failIf(self.page1 in Page.objects.draft()) - - self.page1.publish() - self.failUnlessEqual([], list(Page.objects.deleted())) - self.failUnlessEqual([], list(Page.objects.all())) - self.failUnlessEqual([], list(Comment.objects.all())) - - class TestPublishableRecursiveManyToManyField(TransactionTestCase): - - def setUp(self): - super(TestPublishableRecursiveManyToManyField, self).setUp() - self.page = Page.objects.create(slug='page1', title='page 1', content='some content') - self.author1 = Author.objects.create(name='author1', profile='a profile') - self.author2 = Author.objects.create(name='author2', profile='something else') - - def test_publish_add_author(self): - self.page.authors.add(self.author1) - self.page.publish() - self.failUnless(self.page.public) - - author1 = Author.objects.get(id=self.author1.id) - self.failUnless(author1.public) - self.failIfEqual(author1.id, author1.public.id) - self.failUnlessEqual(author1.name, author1.public.name) - self.failUnlessEqual(author1.profile, author1.public.profile) - - self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) - - def test_publish_repeated_add_author(self): - self.page.authors.add(self.author1) - self.page.publish() - - self.failUnless(self.page.public) - - self.page.authors.add(self.author2) - author1 = Author.objects.get(id=self.author1.id) - self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) - - self.page.publish() - author1 = Author.objects.get(id=self.author1.id) - author2 = Author.objects.get(id=self.author2.id) - self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) - - def test_publish_clear_authors(self): - self.page.authors.add(self.author1, self.author2) - self.page.publish() - - author1 = Author.objects.get(id=self.author1.id) - author2 = Author.objects.get(id=self.author2.id) - self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) - - self.page.authors.clear() - self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) - - self.page.publish() - self.failUnlessEqual([], list(self.page.public.authors.all())) - - class TestInfiniteRecursion(TransactionTestCase): - - def setUp(self): - super(TestInfiniteRecursion, self).setUp() - - self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2', parent=self.page1) - self.page1.parent = self.page2 - self.page1.save() - - def test_publish_recursion_breaks(self): - self.page1.publish() # this should simple run without an error - - class TestOverlappingPublish(TransactionTestCase): - - def setUp(self): - self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2') - self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') - self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') - self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') - - def test_publish_with_overlapping_models(self): - # make sure when we publish we don't accidentally create - # multiple published versions - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(0, Page.objects.published().count()) - - Page.objects.draft().publish() - - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(5, Page.objects.published().count()) - - def test_publish_with_overlapping_models_published(self): - # make sure when we publish we don't accidentally create - # multiple published versions - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(0, Page.objects.published().count()) - - all_published = NestedSet() - Page.objects.draft().publish(all_published) - - self.failUnlessEqual(5, len(all_published)) - - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(5, Page.objects.published().count()) - - def test_publish_after_dry_run_handles_caching(self): - # if we do a dry tun publish in the same queryset - # before publishing for real, we have to make - # sure we don't run into issues with the instance - # caching parent's as None - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(0, Page.objects.published().count()) - - draft = Page.objects.draft() - - all_published = NestedSet() - for p in draft: - p.publish(dry_run=True, all_published=all_published) - - # nothing published yet - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(0, Page.objects.published().count()) - - # now publish (using same queryset, as this will have cached the instances) - draft.publish() - - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(5, Page.objects.published().count()) - - # now actually check the public parent's are setup right - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - child1 = Page.objects.get(id=self.child1.id) - child2 = Page.objects.get(id=self.child2.id) - child3 = Page.objects.get(id=self.child3.id) - - self.failUnlessEqual(None, page1.public.parent) - self.failUnlessEqual(None, page2.public.parent) - self.failUnlessEqual(page1.public, child1.public.parent) - self.failUnlessEqual(page1.public, child2.public.parent) - self.failUnlessEqual(page2.public, child3.public.parent) - - class TestPublishableAdmin(TransactionTestCase): - - def setUp(self): - super(TestPublishableAdmin, self).setUp() - self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2') - self.page1.publish() - self.page2.publish() - - self.author1 = Author.objects.create(name='a1') - self.author2 = Author.objects.create(name='a2') - self.author1.publish() - self.author2.publish() - - self.admin_site = AdminSite('Test Admin') - - class PageBlockInline(PublishableStackedInline): - model = PageBlock - - class PageAdmin(PublishableAdmin): - inlines = [PageBlockInline] - - self.admin_site.register(Page, PageAdmin) - self.page_admin = PageAdmin(Page, self.admin_site) - - # override urls, so reverse works - settings.ROOT_URLCONF=patterns('', - ('^admin/', include(self.admin_site.urls)), - ) - - def test_get_publish_status_display(self): - page = Page.objects.create(slug="hhkkk", title="hjkhjkh") - self.failUnlessEqual('Changed - not yet published', self.page_admin.get_publish_status_display(page)) - page.publish() - self.failUnlessEqual('Published', self.page_admin.get_publish_status_display(page)) - page.save() - self.failUnlessEqual('Changed', self.page_admin.get_publish_status_display(page)) - - page.delete() - self.failUnlessEqual('To be deleted', self.page_admin.get_publish_status_display(page)) - - def test_queryset(self): - # make sure we only get back draft objects - request = None - - self.failUnlessEqual( - set([self.page1, self.page1.public, self.page2, self.page2.public]), - set(Page.objects.all()) - ) - self.failUnlessEqual( - set([self.page1, self.page2]), - set(self.page_admin.queryset(request)) - ) - - def test_get_actions_global_delete_replaced(self): - from publish.actions import delete_selected - - class request(object): - GET = {} - - actions = self.page_admin.get_actions(request) - - - self.failUnless('delete_selected' in actions) - action, name, description = actions['delete_selected'] - self.failUnlessEqual(delete_selected, action) - self.failUnlessEqual('delete_selected', name) - self.failUnlessEqual(delete_selected.short_description, description) - - def test_formfield_for_foreignkey(self): - # foreign key forms fields in admin - # for publishable models should be filtered - # to hide public object - - request = None - parent_field = None - for field in Page._meta.fields: - if field.name == 'parent': - parent_field = field - break - self.failUnless(parent_field) - - choice_field = self.page_admin.formfield_for_foreignkey(parent_field, request) - self.failUnless(choice_field) - self.failUnless(isinstance(choice_field, ModelChoiceField)) - - self.failUnlessEqual( - set([self.page1, self.page1.public, self.page2, self.page2.public]), - set(Page.objects.all()) - ) - self.failUnlessEqual( - set([self.page1, self.page2]), - set(choice_field.queryset) - ) - - def test_formfield_for_manytomany(self): - request = None - authors_field = None - for field in Page._meta.many_to_many: - if field.name == 'authors': - authors_field = field - break - self.failUnless(authors_field) - - choice_field = self.page_admin.formfield_for_manytomany(authors_field, request) - self.failUnless(choice_field) - self.failUnless(isinstance(choice_field, ModelMultipleChoiceField)) - - self.failUnlessEqual( - set([self.author1, self.author1.public, self.author2, self.author2.public]), - set(Author.objects.all()) - ) - self.failUnlessEqual( - set([self.author1, self.author2]), - set(choice_field.queryset) - ) - - def test_has_change_permission(self): - class dummy_request(object): - method = 'GET' - REQUEST = {} - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - self.failUnless(self.page_admin.has_change_permission(dummy_request)) - self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) - self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1.public)) - - # can view deleted items - self.page1.publish_state = Publishable.PUBLISH_DELETE - self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) - - # but cannot modify them - dummy_request.method = 'POST' - self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1)) - - def test_has_delete_permission(self): - class dummy_request(object): - method = 'GET' - REQUEST = {} - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - self.failUnless(self.page_admin.has_delete_permission(dummy_request)) - self.failUnless(self.page_admin.has_delete_permission(dummy_request, self.page1)) - self.failIf(self.page_admin.has_delete_permission(dummy_request, self.page1.public)) - - def test_change_view_normal(self): - class dummy_request(object): - method = 'GET' - REQUEST = {} - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - response = self.page_admin.change_view(dummy_request, str(self.page1.id)) - self.failUnless(response is not None) - self.failIf('deleted' in _get_rendered_content(response)) - - def test_change_view_not_deleted(self): - class dummy_request(object): - method = 'GET' - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - try: - self.page_admin.change_view(dummy_request, unicode(self.page1.public.id)) - self.fail() - except Http404: - pass - - def test_change_view_deleted(self): - class dummy_request(object): - method = 'GET' - REQUEST = {} - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - self.page1.delete() - - response = self.page_admin.change_view(dummy_request, str(self.page1.id)) - self.failUnless(response is not None) - self.failUnless('deleted' in _get_rendered_content(response)) - - def test_change_view_deleted_POST(self): - class dummy_request(object): - csrf_processing_done = True # stop csrf check - method = 'POST' - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - self.page1.delete() - - try: - self.page_admin.change_view(dummy_request, str(self.page1.id)) - self.fail() - except PermissionDenied: - pass - - def test_change_view_delete_inline(self): - block = PageBlock.objects.create(page=self.page1, content='some content') - page1 = Page.objects.get(pk=self.page1.pk) - page1.publish() - - user1 = User.objects.create_user('test1', 'test@example.com', 'jkljkl') - - # fake selecting the delete tickbox for the block - - class dummy_request(object): - csrf_processing_done = True - method = 'POST' - - POST = { - 'slug': page1.slug, - 'title': page1.title, - 'content': page1.content, - 'pub_date_0': '2010-02-12', - 'pub_date_1': '17:40:00', - 'pageblock_set-TOTAL_FORMS': '2', - 'pageblock_set-INITIAL_FORMS': '1', - 'pageblock_set-0-id': str(block.id), - 'pageblock_set-0-page': str(page1.id), - 'pageblock_set-0-DELETE': 'yes' - } - REQUEST = POST - FILES = {} - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - class user(object): - pk = user1.pk - - @classmethod - def is_authenticated(self): - return True - - @classmethod - def has_perm(cls, permission): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - class message_set(object): - @classmethod - def create(cls, message=''): - pass - - class _messages(object): - @classmethod - def add(cls, *message): - pass - - - block = PageBlock.objects.get(id=block.id) - public_block = block.public - - response = self.page_admin.change_view(dummy_request, str(page1.id)) - self.assertEqual(302, response.status_code) - - # the block should have been deleted (but not the public one) - self.failUnlessEqual([public_block], list(PageBlock.objects.all())) - - - class TestPublishSelectedAction(TransactionTestCase): - - def setUp(self): - super(TestPublishSelectedAction, self).setUp() - self.fp1 = Page.objects.create(slug='fp1', title='FP1') - self.fp2 = Page.objects.create(slug='fp2', title='FP2') - self.fp3 = Page.objects.create(slug='fp3', title='FP3') - - self.admin_site = AdminSite('Test Admin') - self.page_admin = PublishableAdmin(Page, self.admin_site) - - # override urls, so reverse works - settings.ROOT_URLCONF=patterns('', - ('^admin/', include(self.admin_site.urls)), - ) - - def test_publish_selected_confirm(self): - pages = Page.objects.exclude(id=self.fp3.id) - - class dummy_request(object): - META = {} - POST = {} - - class user(object): - @classmethod - def has_perm(cls, *arg): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - response = publish_selected(self.page_admin, dummy_request, pages) - - self.failIf(Page.objects.published().count() > 0) - self.failUnless(response is not None) - self.failUnlessEqual(200, response.status_code) - - def test_publish_selected_confirmed(self): - pages = Page.objects.exclude(id=self.fp3.id) - - class dummy_request(object): - POST = {'post': True} - - class user(object): - @classmethod - def is_authenticated(cls): - return True - - @classmethod - def has_perm(cls, *arg): - return True - - class message_set(object): - @classmethod - def create(cls, message=None): - self._message = message - - class _messages(object): - @classmethod - def add(cls, *message): - self._message = message - - - response = publish_selected(self.page_admin, dummy_request, pages) - - - self.failUnlessEqual(2, Page.objects.published().count()) - self.failUnless( getattr(self, '_message', None) is not None ) - self.failUnless( response is None ) - - def test_convert_all_published_to_html(self): - self.admin_site.register(Page, PublishableAdmin) - - all_published = NestedSet() - - page = Page.objects.create(slug='here', title='title') - block = PageBlock.objects.create(page=page, content='stuff here') - - all_published.add(page) - all_published.add(block, parent=page) - - converted = _convert_all_published_to_html(self.admin_site, all_published) - - expected = [u'Page: Page object (Changed - not yet published)' % page.id, [u'Page block: PageBlock object']] - - self.failUnlessEqual(expected, converted) - - def test_publish_selected_does_not_have_permission(self): - self.admin_site.register(Page, PublishableAdmin) - pages = Page.objects.exclude(id=self.fp3.id) - - class dummy_request(object): - POST = {} - - class user(object): - @classmethod - def has_perm(cls, *arg): - return False - - @classmethod - def get_and_delete_messages(cls): - return [] - - response = publish_selected(self.page_admin, dummy_request, pages) - self.failIf(response is None) - # publish button should not be in response - self.failIf('value="publish_selected"' in response.content) - self.failIf('value="Yes, Publish"' in response.content) - self.failIf('form' in response.content) - - self.failIf(Page.objects.published().count() > 0) - - def test_publish_selected_does_not_have_related_permission(self): - # check we can't publish when we don't have permission - # for a related model (in this case authors) - self.admin_site.register(Author, PublishableAdmin) - - author = Author.objects.create(name='John') - self.fp1.authors.add(author) - - pages = Page.objects.draft() - - class dummy_request(object): - POST = { 'post': True } - - class user(object): - pk = 1 - - @classmethod - def is_authenticated(cls): - return True - - @classmethod - def has_perm(cls, perm): - return perm != 'publish.publish_author' - - try: - publish_selected(self.page_admin, dummy_request, pages) - self.fail() - except PermissionDenied: - pass - - self.failIf(Page.objects.published().count() > 0) - - def test_publish_selected_logs_publication(self): - self.admin_site.register(Page, PublishableAdmin) - - pages = Page.objects.exclude(id=self.fp3.id) - - class dummy_request(object): - POST = { 'post': True } - - class user(object): - pk = 1 - - @classmethod - def is_authenticated(cls): - return True - - @classmethod - def has_perm(cls, perm): - return perm != 'publish.publish_author' - - class message_set(object): - @classmethod - def create(cls, message=None): - pass - - class _messages(object): - @classmethod - def add(cls, *message): - pass - - publish_selected(self.page_admin, dummy_request, pages) - - # should have logged two publications - from django.contrib.admin.models import LogEntry - from django.contrib.contenttypes.models import ContentType - - content_type_id = ContentType.objects.get_for_model(self.fp1).pk - self.failUnlessEqual(2, LogEntry.objects.filter().count()) - - - class TestDeleteSelected(TransactionTestCase): - - def setUp(self): - super(TestDeleteSelected, self).setUp() - self.fp1 = FlatPage.objects.create(url='/fp1', title='FP1') - self.fp2 = FlatPage.objects.create(url='/fp2', title='FP2') - self.fp3 = FlatPage.objects.create(url='/fp3', title='FP3') - - self.fp1.publish() - self.fp2.publish() - self.fp3.publish() - - self.admin_site = AdminSite('Test Admin') - self.page_admin = PublishableAdmin(FlatPage, self.admin_site) - - # override urls, so reverse works - settings.ROOT_URLCONF=patterns('', - ('^admin/', include(self.admin_site.urls)), - ) - - def test_delete_selected_check_cannot_delete_public(self): - # delete won't work (via admin) for public instances - request = None - try: - delete_selected(self.page_admin, request, FlatPage.objects.published()) - fail() - except PermissionDenied: - pass - - def test_delete_selected(self): - class dummy_request(object): - POST = {} - META = {} - - class user(object): - @classmethod - def has_perm(cls, *arg): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - response = delete_selected(self.page_admin, dummy_request, FlatPage.objects.draft()) - self.failUnless(response is not None) - - class TestUndeleteSelected(TransactionTestCase): - - def setUp(self): - super(TestUndeleteSelected, self).setUp() - self.fp1 = FlatPage.objects.create(url='/fp1', title='FP1') - - self.fp1.publish() - - self.admin_site = AdminSite('Test Admin') - self.page_admin = PublishableAdmin(FlatPage, self.admin_site) - - def test_undelete_selected(self): - class dummy_request(object): - - class user(object): - @classmethod - def has_perm(cls, *arg): - return True - - self.fp1.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) - - response = undelete_selected(self.page_admin, dummy_request, FlatPage.objects.deleted()) - self.failUnless(response is None) - - # publish state should no longer be delete - fp1 = FlatPage.objects.get(pk=self.fp1.pk) - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, fp1.publish_state) - - def test_undelete_selected_no_permission(self): - class dummy_request(object): - - class user(object): - @classmethod - def has_perm(cls, *arg): - return False - - self.fp1.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) - - try: - undelete_selected(self.page_admin, dummy_request, FlatPage.objects.deleted()) - fail() - except PermissionDenied: - pass - - class TestManyToManyThrough(TransactionTestCase): - - def setUp(self): - super(TestManyToManyThrough, self).setUp() - self.page = Page.objects.create(slug='p1', title='P 1') - self.tag1 = Tag.objects.create(slug='tag1', title='Tag 1') - self.tag2 = Tag.objects.create(slug='tag2', title='Tag 2') - PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag1, tag_order=2) - PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag2, tag_order=1) - - def test_publish_copies_tags(self): - self.page.publish() - - self.failUnlessEqual(set([self.tag1, self.tag2]), set(self.page.public.tags.all())) - - class TestPublishFunction(TransactionTestCase): - - def setUp(self): - super(TestPublishFunction, self).setUp() - self.page = Page.objects.create(slug='page', title='Page') - - def test_publish_function_invoked(self): - # check we can override default copy behaviour - - from datetime import datetime - - pub_date = datetime(2000, 1, 1) - update_pub_date.pub_date = pub_date - - self.failIfEqual(pub_date, self.page.pub_date) - - self.page.publish() - self.failIfEqual(pub_date, self.page.pub_date) - self.failUnlessEqual(pub_date, self.page.public.pub_date) - - - class TestPublishSignals(TransactionTestCase): - - def setUp(self): - self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2') - self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') - self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') - self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') - - self.failUnlessEqual(5, Page.objects.draft().count()) - - def _check_pre_publish(self, queryset): - pre_published = [] - def pre_publish_handler(sender, instance, **kw): - pre_published.append(instance) - - pre_publish.connect(pre_publish_handler, sender=Page) - - queryset.draft().publish() - - self.failUnlessEqual(queryset.draft().count(), len(pre_published)) - self.failUnlessEqual(set(queryset.draft()), set(pre_published)) - - def test_pre_publish(self): - # page order shouldn't matter when publishing - # should always get the right number of signals - self._check_pre_publish(Page.objects.order_by('id')) - self._check_pre_publish(Page.objects.order_by('-id')) - self._check_pre_publish(Page.objects.order_by('?')) - - def _check_post_publish(self, queryset): - published = [] - def post_publish_handler(sender, instance, **kw): - published.append(instance) - - post_publish.connect(post_publish_handler, sender=Page) - - queryset.draft().publish() - - self.failUnlessEqual(queryset.draft().count(), len(published)) - self.failUnlessEqual(set(queryset.draft()), set(published)) - - def test_post_publish(self): - self._check_post_publish(Page.objects.order_by('id')) - self._check_post_publish(Page.objects.order_by('-id')) - self._check_post_publish(Page.objects.order_by('?')) - - def test_signals_sent_for_followed(self): - pre_published = [] - def pre_publish_handler(sender, instance, **kw): - pre_published.append(instance) - - pre_publish.connect(pre_publish_handler, sender=Page) - - published = [] - def post_publish_handler(sender, instance, **kw): - published.append(instance) - - post_publish.connect(post_publish_handler, sender=Page) - - # publishing just children will also publish it's parent (if needed) - # which should also fire signals - - self.child1.publish() - - self.failUnlessEqual(set([self.page1, self.child1]), set(pre_published)) - self.failUnlessEqual(set([self.page1, self.child1]), set(published)) - - def test_deleted_flag_false_when_publishing_change(self): - def pre_publish_handler(sender, instance, deleted, **kw): - self.failIf(deleted) - - pre_publish.connect(pre_publish_handler, sender=Page) - - def post_publish_handler(sender, instance, deleted, **kw): - self.failIf(deleted) - - post_publish.connect(post_publish_handler, sender=Page) - - self.page1.publish() - - def test_deleted_flag_true_when_publishing_deletion(self): - self.child1.publish() - public = self.child1.public - - self.child1.delete() - - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.child1.publish_state) - - def pre_publish_handler(sender, instance, deleted, **kw): - self.failUnless(deleted) - - pre_publish.connect(pre_publish_handler, sender=Page) - - def post_publish_handler(sender, instance, deleted, **kw): - self.failUnless(deleted) - - post_publish.connect(post_publish_handler, sender=Page) - - self.child1.publish() - - - try: - from django.contrib.admin.filters import FieldListFilter - except ImportError: - # pre 1.4 - from django.contrib.admin.filterspecs import FilterSpec - class FieldListFilter(object): - @classmethod - def create(cls, field, request, params, model, model_admin, *arg, **kw): - return FilterSpec.create(field, request, params, model, model_admin) - - - class TestPublishableRelatedFilterSpec(TransactionTestCase): - - def test_overridden_spec(self): - # make sure the publishable filter spec - # gets used when we use a publishable field - class dummy_request(object): - GET = {} - - spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) - self.failUnless(isinstance(spec, PublishableRelatedFieldListFilter)) - - def test_only_draft_shown(self): - self.author = Author.objects.create(name='author') - self.author.publish() - - self.failUnless(2, Author.objects.count()) - - # make sure the publishable filter spec - # gets used when we use a publishable field - class dummy_request(object): - GET = {} - - spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) - - lookup_choices = spec.lookup_choices - self.failUnlessEqual(1, len(lookup_choices)) - pk, label = lookup_choices[0] - self.failUnlessEqual(self.author.id, pk) - diff --git a/publish/tests/__init__.py b/publish/tests/__init__.py new file mode 100644 index 0000000..ea35eb4 --- /dev/null +++ b/publish/tests/__init__.py @@ -0,0 +1 @@ +__author__ = 'petry' diff --git a/publish/tests/example_app/__init__.py b/publish/tests/example_app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/publish/tests/example_app/models.py b/publish/tests/example_app/models.py new file mode 100644 index 0000000..4054f7e --- /dev/null +++ b/publish/tests/example_app/models.py @@ -0,0 +1,99 @@ +from datetime import datetime +from django.db import models +from publish.models import Publishable + + +class Site(models.Model): + title = models.CharField(max_length=100) + domain = models.CharField(max_length=100) + +class FlatPage(Publishable): + url = models.CharField(max_length=100, db_index=True) + title = models.CharField(max_length=200) + content = models.TextField(blank=True) + enable_comments = models.BooleanField() + template_name = models.CharField(max_length=70, blank=True) + registration_required = models.BooleanField() + sites = models.ManyToManyField(Site) + + class Meta: + ordering = ['url'] + + def get_absolute_url(self): + if self.is_public: + return self.url + return '%s*' % self.url + +class Author(Publishable): + name = models.CharField(max_length=100) + profile = models.TextField(blank=True) + + class PublishMeta(Publishable.PublishMeta): + publish_reverse_fields = ['authorprofile'] + + def __unicode__(self): + return self.name + + +class AuthorProfile(Publishable): + author = models.OneToOneField(Author) + extra_profile = models.TextField(blank=True) + +class ChangeLog(models.Model): + changed = models.DateTimeField(db_index=True, auto_now_add=True) + message = models.CharField(max_length=200) + +class Tag(models.Model): + title = models.CharField(max_length=100, unique=True) + slug = models.CharField(max_length=100) + +# publishable model with a reverse relation to +# page (as a child) +class PageBlock(Publishable): + page=models.ForeignKey('Page') + content = models.TextField(blank=True) + +# non-publishable reverse relation to page (as a child) +class Comment(models.Model): + page=models.ForeignKey('Page') + comment = models.TextField() + +def update_pub_date(page, field_name, value): + # ignore value entirely and replace with now + setattr(page, field_name, update_pub_date.pub_date) +update_pub_date.pub_date = datetime.now() + +class Page(Publishable): + slug = models.CharField(max_length=100, db_index=True) + title = models.CharField(max_length=200) + content = models.TextField(blank=True) + pub_date = models.DateTimeField(default=datetime.now) + + parent = models.ForeignKey('self', blank=True, null=True) + + authors = models.ManyToManyField(Author, blank=True) + log = models.ManyToManyField(ChangeLog, blank=True) + tags = models.ManyToManyField(Tag, through='PageTagOrder', blank=True) + + class Meta: + ordering = ['slug'] + + class PublishMeta(Publishable.PublishMeta): + publish_exclude_fields = ['log'] + publish_reverse_fields = ['pageblock_set'] + publish_functions = { 'pub_date': update_pub_date } + + def get_absolute_url(self): + if not self.parent: + return u'/%s/' % self.slug + return '%s%s/' % (self.parent.get_absolute_url(), self.slug) + + def __unicode__(self): + return self.slug + +class PageTagOrder(Publishable): + # note these are named in non-standard way to + # ensure we are getting correct names + tagged_page=models.ForeignKey(Page) + page_tag=models.ForeignKey(Tag) + tag_order=models.IntegerField() diff --git a/publish/tests/example_app/views.py b/publish/tests/example_app/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/publish/tests/example_app/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/publish/tests/settings_for_test.py b/publish/tests/settings_for_test.py new file mode 100644 index 0000000..bcd36d0 --- /dev/null +++ b/publish/tests/settings_for_test.py @@ -0,0 +1,46 @@ +import logging +from django.conf.global_settings import * + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'test.db', + 'USER': '', + 'PASSWORD': '', + } +} +INSTALLED_APPS = ( + 'django.contrib.contenttypes', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.messages', + 'publish', + 'publish.tests.example_app', + 'django_nose', + +) + +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +) +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + + +TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' + +logging.disable(logging.CRITICAL) + +# enable this for coverage (using django test coverage +# http://pypi.python.org/pypi/django-test-coverage ) +#TEST_RUNNER = 'django-test-coverage.runner.run_tests' +#COVERAGE_MODULES = ('publish.models', 'publish.admin', 'publish.actions', 'publish.utils', 'publish.signals') diff --git a/publish/tests/test_all.py b/publish/tests/test_all.py new file mode 100644 index 0000000..8c679b0 --- /dev/null +++ b/publish/tests/test_all.py @@ -0,0 +1,1515 @@ +from django.conf import settings +from publish.tests import settings_for_test + +settings.configure(settings_for_test) + +from django.core.management import call_command +call_command('syncdb', interactive=False) + +from django.test import TestCase + +from publish.models import Publishable, PublishException +from publish.tests.example_app.models import FlatPage, Page, Comment, PageBlock, Tag, PageTagOrder, Author, Site, update_pub_date + +from django.contrib.admin.sites import AdminSite +from django.contrib.auth.models import User +from django.forms.models import ModelChoiceField, ModelMultipleChoiceField +from django.conf.urls.defaults import * +from django.core.exceptions import PermissionDenied +from django.http import Http404 + +from publish.admin import PublishableAdmin, PublishableStackedInline +from publish.actions import publish_selected, delete_selected, _convert_all_published_to_html, undelete_selected +from publish.utils import NestedSet +from publish.signals import pre_publish, post_publish +from publish.filters import PublishableRelatedFieldListFilter + + +def _get_rendered_content(response): + content = getattr(response, 'rendered_content', None) + if content is not None: + return content + return response.content + + +class TestNestedSet(TestCase): + + def setUp(self): + super(TestNestedSet, self).setUp() + self.nested = NestedSet() + + def test_len(self): + self.failUnlessEqual(0, len(self.nested)) + self.nested.add('one') + self.failUnlessEqual(1, len(self.nested)) + self.nested.add('two') + self.failUnlessEqual(2, len(self.nested)) + self.nested.add('one2', parent='one') + self.failUnlessEqual(3, len(self.nested)) + + def test_contains(self): + self.failIf('one' in self.nested) + self.nested.add('one') + self.failUnless('one' in self.nested) + self.nested.add('one2', parent='one') + self.failUnless('one2' in self.nested) + + def test_nested_items(self): + self.failUnlessEqual([], self.nested.nested_items()) + self.nested.add('one') + self.failUnlessEqual(['one'], self.nested.nested_items()) + self.nested.add('two') + self.nested.add('one2', parent='one') + self.failUnlessEqual(['one', ['one2'], 'two'], self.nested.nested_items()) + self.nested.add('one2-1', parent='one2') + self.nested.add('one2-2', parent='one2') + self.failUnlessEqual(['one', ['one2', ['one2-1', 'one2-2']], 'two'], self.nested.nested_items()) + + def test_iter(self): + self.failUnlessEqual(set(), set(self.nested)) + + self.nested.add('one') + self.failUnlessEqual(set(['one']), set(self.nested)) + + self.nested.add('two', parent='one') + self.failUnlessEqual(set(['one', 'two']), set(self.nested)) + + items = set(['one', 'two']) + + for item in self.nested: + self.failUnless(item in items) + items.remove(item) + + self.failUnlessEqual(set(), items) + + def test_original(self): + class MyObject(object): + def __init__(self, obj): + self.obj = obj + + def __eq__(self, other): + return self.obj == other.obj + + def __hash__(self): + return hash(self.obj) + + # should always return an item at least + self.failUnlessEqual(MyObject('hi there'), self.nested.original(MyObject('hi there'))) + + m1 = MyObject('m1') + self.nested.add(m1) + + self.failUnlessEqual(id(m1), id(self.nested.original(m1))) + self.failUnlessEqual(id(m1), id(self.nested.original(MyObject('m1')))) + + + + +class TestBasicPublishable(TestCase): + + def setUp(self): + super(TestBasicPublishable, self).setUp() + self.flat_page = FlatPage(url='/my-page', title='my page', + content='here is some content', + enable_comments=False, + registration_required=True) + + def test_get_public_absolute_url(self): + self.failUnlessEqual('/my-page*', self.flat_page.get_absolute_url()) + # public absolute url doesn't exist until published + self.assertTrue(self.flat_page.get_public_absolute_url() is None) + self.flat_page.save() + self.flat_page.publish() + self.failUnlessEqual('/my-page', self.flat_page.get_public_absolute_url()) + + def test_save_marks_changed(self): + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.flat_page.save(mark_changed=False) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.flat_page.save() + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + + def test_publish_excludes_fields(self): + self.flat_page.save() + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failIfEqual(self.flat_page.id, self.flat_page.public.id) + self.failUnless(self.flat_page.public.is_public) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.public.publish_state) + + def test_publish_check_is_not_public(self): + try: + self.flat_page.is_public = True + self.flat_page.publish() + self.fail("Should not be able to publish public models") + except PublishException: + pass + + def test_publish_check_has_id(self): + try: + self.flat_page.publish() + self.fail("Should not be able to publish unsaved models") + except PublishException: + pass + + def test_publish_simple_fields(self): + self.flat_page.save() + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + self.failIf(self.flat_page.public) # should not be a public version yet + + self.flat_page.publish() + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.failUnless(self.flat_page.public) + + for field in 'url', 'title', 'content', 'enable_comments', 'registration_required': + self.failUnlessEqual(getattr(self.flat_page, field), getattr(self.flat_page.public, field)) + + def test_published_simple_field_repeated(self): + self.flat_page.save() + self.flat_page.publish() + + public = self.flat_page.public + self.failUnless(public) + + self.flat_page.title = 'New Title' + self.flat_page.save() + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + + self.failUnlessEqual(public, self.flat_page.public) + self.failIfEqual(public.title, self.flat_page.title) + + self.flat_page.publish() + self.failUnlessEqual(public, self.flat_page.public) + self.failUnlessEqual(public.title, self.flat_page.title) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + + def test_publish_records_published(self): + all_published = NestedSet() + self.flat_page.save() + self.flat_page.publish(all_published=all_published) + self.failUnlessEqual(1, len(all_published)) + self.failUnless(self.flat_page in all_published) + self.failUnless(self.flat_page.public) + + def test_publish_dryrun(self): + all_published = NestedSet() + self.flat_page.save() + self.flat_page.publish(dry_run=True, all_published=all_published) + self.failUnlessEqual(1, len(all_published)) + self.failUnless(self.flat_page in all_published) + self.failIf(self.flat_page.public) + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + + def test_delete_after_publish(self): + self.flat_page.save() + self.flat_page.publish() + public = self.flat_page.public + self.failUnless(public) + + self.flat_page.delete() + self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.flat_page.publish_state) + + self.failUnlessEqual(set([self.flat_page, self.flat_page.public]), set(FlatPage.objects.all())) + + def test_delete_before_publish(self): + self.flat_page.save() + self.flat_page.delete() + self.failUnlessEqual([], list(FlatPage.objects.all())) + + def test_publish_deletions(self): + self.flat_page.save() + self.flat_page.publish() + public = self.flat_page.public + + self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + + self.flat_page.delete() + self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + + self.flat_page.publish() + self.failUnlessEqual([], list(FlatPage.objects.all())) + + def test_publish_deletions_checks_all_published(self): + # make sure publish_deletions looks at all_published arg + # to see if we need to actually publish the deletion + self.flat_page.save() + self.flat_page.publish() + public = self.flat_page.public + + self.flat_page.delete() + + self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + + # this should effectively stop the deletion happening + all_published = NestedSet() + all_published.add(self.flat_page) + + self.flat_page.publish(all_published=all_published) + self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + + +class TestPublishableManager(TestCase): + + def setUp(self): + super(TestCase, self).setUp() + self.flat_page1 = FlatPage.objects.create(url='/url1/', title='title 1') + self.flat_page2 = FlatPage.objects.create(url='/url2/', title='title 2') + + def test_all(self): + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.all())) + + # publishing will produce extra copies + self.flat_page1.publish() + self.failUnlessEqual(3, FlatPage.objects.count()) + + self.flat_page2.publish() + self.failUnlessEqual(4, FlatPage.objects.count()) + + + def test_changed(self): + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.changed())) + + self.flat_page1.publish() + self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.changed())) + + self.flat_page2.publish() + self.failUnlessEqual([], list(FlatPage.objects.changed())) + + def test_draft(self): + # draft should stay the same pretty much always + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) + + self.flat_page1.publish() + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) + + self.flat_page2.publish() + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) + + self.flat_page2.delete() + self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft())) + + + def test_published(self): + self.failUnlessEqual([], list(FlatPage.objects.published())) + + self.flat_page1.publish() + self.failUnlessEqual([self.flat_page1.public], list(FlatPage.objects.published())) + + self.flat_page2.publish() + self.failUnlessEqual([self.flat_page1.public, self.flat_page2.public], list(FlatPage.objects.published())) + + def test_deleted(self): + self.failUnlessEqual([], list(FlatPage.objects.deleted())) + + self.flat_page1.publish() + self.failUnlessEqual([], list(FlatPage.objects.deleted())) + + self.flat_page1.delete() + self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) + + def test_draft_and_deleted(self): + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) + + self.flat_page1.publish() + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft())) + + self.flat_page1.delete() + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) + self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.draft())) + + + def test_delete(self): + # delete is overriden, so it marks the public instances + self.flat_page1.publish() + public1 = self.flat_page1.public + + FlatPage.objects.draft().delete() + + self.failUnlessEqual([], list(FlatPage.objects.draft())) + self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) + self.failUnlessEqual([public1], list(FlatPage.objects.published())) + self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft_and_deleted())) + + def test_publish(self): + self.failUnlessEqual([], list(FlatPage.objects.published())) + + FlatPage.objects.draft().publish() + + flat_page1 = FlatPage.objects.get(id=self.flat_page1.id) + flat_page2 = FlatPage.objects.get(id=self.flat_page2.id) + + self.failUnlessEqual(set([flat_page1.public, flat_page2.public]), set(FlatPage.objects.published())) + + + +class TestPublishableManyToMany(TestCase): + + def setUp(self): + super(TestPublishableManyToMany, self).setUp() + self.flat_page = FlatPage.objects.create( + url='/my-page', title='my page', + content='here is some content', + enable_comments=False, + registration_required=True) + self.site1 = Site.objects.create(title='my site', domain='mysite.com') + self.site2 = Site.objects.create(title='a site', domain='asite.com') + + def test_publish_no_sites(self): + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([], list(self.flat_page.public.sites.all())) + + def test_publish_add_site(self): + self.flat_page.sites.add(self.site1) + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) + + def test_publish_repeated_add_site(self): + self.flat_page.sites.add(self.site1) + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) + + self.flat_page.sites.add(self.site2) + self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) + + self.flat_page.publish() + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + def test_publish_remove_site(self): + self.flat_page.sites.add(self.site1, self.site2) + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + self.flat_page.sites.remove(self.site1) + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + self.flat_page.publish() + self.failUnlessEqual([self.site2], list(self.flat_page.public.sites.all())) + + def test_publish_clear_sites(self): + self.flat_page.sites.add(self.site1, self.site2) + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + self.flat_page.sites.clear() + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + self.flat_page.publish() + self.failUnlessEqual([], list(self.flat_page.public.sites.all())) + + def test_publish_sites_cleared_not_deleted(self): + self.flat_page.sites.add(self.site1, self.site2) + self.flat_page.publish() + self.flat_page.sites.clear() + self.flat_page.publish() + + self.failUnlessEqual([], list(self.flat_page.public.sites.all())) + + self.failIfEqual([], list(Site.objects.all())) + + + + +class TestPublishableRecursiveForeignKey(TestCase): + + def setUp(self): + super(TestPublishableRecursiveForeignKey, self).setUp() + self.page1 = Page.objects.create(slug='page1', title='page 1', content='some content') + self.page2 = Page.objects.create(slug='page2', title='page 2', content='other content', parent=self.page1) + + def test_publish_parent(self): + # this shouldn't publish the child page + self.page1.publish() + self.failUnless(self.page1.public) + self.failIf(self.page1.public.parent) + + page2 = Page.objects.get(id=self.page2.id) + self.failIf(page2.public) + + def test_publish_child_parent_already_published(self): + self.page1.publish() + self.page2.publish() + + self.failUnless(self.page1.public) + self.failUnless(self.page2.public) + + self.failIf(self.page1.public.parent) + self.failUnless(self.page2.public.parent) + + self.failIfEqual(self.page1, self.page2.public.parent) + + self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) + self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) + + def test_publish_child_parent_not_already_published(self): + self.page2.publish() + + page1 = Page.objects.get(id=self.page1.id) + self.failUnless(page1.public) + self.failUnless(self.page2.public) + + self.failIf(page1.public.parent) + self.failUnless(self.page2.public.parent) + + self.failIfEqual(page1, self.page2.public.parent) + + self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) + self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) + + def test_publish_repeated(self): + self.page1.publish() + self.page2.publish() + + self.page1.slug='main' + self.page1.save() + + self.failUnlessEqual('/main/', self.page1.get_absolute_url()) + + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + self.failUnlessEqual('/page1/', page1.public.get_absolute_url()) + self.failUnlessEqual('/page1/page2/', page2.public.get_absolute_url()) + + page1.publish() + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + self.failUnlessEqual('/main/', page1.public.get_absolute_url()) + self.failUnlessEqual('/main/page2/', page2.public.get_absolute_url()) + + page1.slug='elsewhere' + page1.save() + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + page2.slug='meanwhile' + page2.save() + page2.publish() + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + + # only page2 should be published, not page1, as page1 already published + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page2.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, page1.publish_state) + + self.failUnlessEqual('/main/', page1.public.get_absolute_url()) + self.failUnlessEqual('/main/meanwhile/', page2.public.get_absolute_url()) + + page1.publish() + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page2.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page1.publish_state) + + self.failUnlessEqual('/elsewhere/', page1.public.get_absolute_url()) + self.failUnlessEqual('/elsewhere/meanwhile/', page2.public.get_absolute_url()) + + def test_publish_deletions(self): + self.page1.publish() + self.page2.publish() + + self.page2.delete() + self.failUnlessEqual([self.page2], list(Page.objects.deleted())) + + self.page2.publish() + self.failUnlessEqual([self.page1.public], list(Page.objects.published())) + self.failUnlessEqual([], list(Page.objects.deleted())) + + def test_publish_reverse_fields(self): + page_block = PageBlock.objects.create(page=self.page1, content='here we are') + + self.page1.publish() + + public = self.page1.public + self.failUnless(public) + + blocks = list(public.pageblock_set.all()) + self.failUnlessEqual(1, len(blocks)) + self.failUnlessEqual(page_block.content, blocks[0].content) + + def test_publish_deletions_reverse_fields(self): + page_block = PageBlock.objects.create(page=self.page1, content='here we are') + + self.page1.publish() + public = self.page1.public + self.failUnless(public) + + self.page1.delete() + + self.failUnlessEqual([self.page1], list(Page.objects.deleted())) + + self.page1.publish() + self.failUnlessEqual([], list(Page.objects.deleted())) + self.failUnlessEqual([], list(Page.objects.all())) + + def test_publish_reverse_fields_deleted(self): + # make sure child elements get removed + page_block = PageBlock.objects.create(page=self.page1, content='here we are') + + self.page1.publish() + + public = self.page1.public + page_block = PageBlock.objects.get(id=page_block.id) + page_block_public = page_block.public + self.failIf(page_block_public is None) + + self.failUnlessEqual([page_block_public], list(public.pageblock_set.all())) + + # now delete the page block and publish the parent + # to make sure that deletion gets copied over properly + page_block.delete() + page1 = Page.objects.get(id=self.page1.id) + page1.publish() + public = page1.public + + self.failUnlessEqual([], list(public.pageblock_set.all())) + + def test_publish_delections_with_non_publishable_children(self): + self.page1.publish() + + comment = Comment.objects.create(page=self.page1.public, comment='This is a comment') + + self.failUnlessEqual(1, Comment.objects.count()) + + self.page1.delete() + + self.failUnlessEqual([self.page1], list(Page.objects.deleted())) + self.failIf(self.page1 in Page.objects.draft()) + + self.page1.publish() + self.failUnlessEqual([], list(Page.objects.deleted())) + self.failUnlessEqual([], list(Page.objects.all())) + self.failUnlessEqual([], list(Comment.objects.all())) + +class TestPublishableRecursiveManyToManyField(TestCase): + + def setUp(self): + super(TestPublishableRecursiveManyToManyField, self).setUp() + self.page = Page.objects.create(slug='page1', title='page 1', content='some content') + self.author1 = Author.objects.create(name='author1', profile='a profile') + self.author2 = Author.objects.create(name='author2', profile='something else') + + def test_publish_add_author(self): + self.page.authors.add(self.author1) + self.page.publish() + self.failUnless(self.page.public) + + author1 = Author.objects.get(id=self.author1.id) + self.failUnless(author1.public) + self.failIfEqual(author1.id, author1.public.id) + self.failUnlessEqual(author1.name, author1.public.name) + self.failUnlessEqual(author1.profile, author1.public.profile) + + self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) + + def test_publish_repeated_add_author(self): + self.page.authors.add(self.author1) + self.page.publish() + + self.failUnless(self.page.public) + + self.page.authors.add(self.author2) + author1 = Author.objects.get(id=self.author1.id) + self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) + + self.page.publish() + author1 = Author.objects.get(id=self.author1.id) + author2 = Author.objects.get(id=self.author2.id) + self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) + + def test_publish_clear_authors(self): + self.page.authors.add(self.author1, self.author2) + self.page.publish() + + author1 = Author.objects.get(id=self.author1.id) + author2 = Author.objects.get(id=self.author2.id) + self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) + + self.page.authors.clear() + self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) + + self.page.publish() + self.failUnlessEqual([], list(self.page.public.authors.all())) + +class TestInfiniteRecursion(TestCase): + + def setUp(self): + super(TestInfiniteRecursion, self).setUp() + + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page2 = Page.objects.create(slug='page2', title='page 2', parent=self.page1) + self.page1.parent = self.page2 + self.page1.save() + + def test_publish_recursion_breaks(self): + self.page1.publish() # this should simple run without an error + +class TestOverlappingPublish(TestCase): + + def setUp(self): + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page2 = Page.objects.create(slug='page2', title='page 2') + self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') + self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') + self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') + + def test_publish_with_overlapping_models(self): + # make sure when we publish we don't accidentally create + # multiple published versions + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(0, Page.objects.published().count()) + + Page.objects.draft().publish() + + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(5, Page.objects.published().count()) + + def test_publish_with_overlapping_models_published(self): + # make sure when we publish we don't accidentally create + # multiple published versions + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(0, Page.objects.published().count()) + + all_published = NestedSet() + Page.objects.draft().publish(all_published) + + self.failUnlessEqual(5, len(all_published)) + + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(5, Page.objects.published().count()) + + def test_publish_after_dry_run_handles_caching(self): + # if we do a dry tun publish in the same queryset + # before publishing for real, we have to make + # sure we don't run into issues with the instance + # caching parent's as None + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(0, Page.objects.published().count()) + + draft = Page.objects.draft() + + all_published = NestedSet() + for p in draft: + p.publish(dry_run=True, all_published=all_published) + + # nothing published yet + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(0, Page.objects.published().count()) + + # now publish (using same queryset, as this will have cached the instances) + draft.publish() + + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(5, Page.objects.published().count()) + + # now actually check the public parent's are setup right + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + child1 = Page.objects.get(id=self.child1.id) + child2 = Page.objects.get(id=self.child2.id) + child3 = Page.objects.get(id=self.child3.id) + + self.failUnlessEqual(None, page1.public.parent) + self.failUnlessEqual(None, page2.public.parent) + self.failUnlessEqual(page1.public, child1.public.parent) + self.failUnlessEqual(page1.public, child2.public.parent) + self.failUnlessEqual(page2.public, child3.public.parent) + +class TestPublishableAdmin(TestCase): + + def setUp(self): + super(TestPublishableAdmin, self).setUp() + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page2 = Page.objects.create(slug='page2', title='page 2') + self.page1.publish() + self.page2.publish() + + self.author1 = Author.objects.create(name='a1') + self.author2 = Author.objects.create(name='a2') + self.author1.publish() + self.author2.publish() + + self.admin_site = AdminSite('Test Admin') + + class PageBlockInline(PublishableStackedInline): + model = PageBlock + + class PageAdmin(PublishableAdmin): + inlines = [PageBlockInline] + + self.admin_site.register(Page, PageAdmin) + self.page_admin = PageAdmin(Page, self.admin_site) + + # override urls, so reverse works + settings.ROOT_URLCONF=patterns('', + ('^admin/', include(self.admin_site.urls)), + ) + + def test_get_publish_status_display(self): + page = Page.objects.create(slug="hhkkk", title="hjkhjkh") + self.failUnlessEqual('Changed - not yet published', self.page_admin.get_publish_status_display(page)) + page.publish() + self.failUnlessEqual('Published', self.page_admin.get_publish_status_display(page)) + page.save() + self.failUnlessEqual('Changed', self.page_admin.get_publish_status_display(page)) + + page.delete() + self.failUnlessEqual('To be deleted', self.page_admin.get_publish_status_display(page)) + + def test_queryset(self): + # make sure we only get back draft objects + request = None + + self.failUnlessEqual( + set([self.page1, self.page1.public, self.page2, self.page2.public]), + set(Page.objects.all()) + ) + self.failUnlessEqual( + set([self.page1, self.page2]), + set(self.page_admin.queryset(request)) + ) + + def test_get_actions_global_delete_replaced(self): + from publish.actions import delete_selected + + class request(object): + GET = {} + + actions = self.page_admin.get_actions(request) + + + self.failUnless('delete_selected' in actions) + action, name, description = actions['delete_selected'] + self.failUnlessEqual(delete_selected, action) + self.failUnlessEqual('delete_selected', name) + self.failUnlessEqual(delete_selected.short_description, description) + + def test_formfield_for_foreignkey(self): + # foreign key forms fields in admin + # for publishable models should be filtered + # to hide public object + + request = None + parent_field = None + for field in Page._meta.fields: + if field.name == 'parent': + parent_field = field + break + self.failUnless(parent_field) + + choice_field = self.page_admin.formfield_for_foreignkey(parent_field, request) + self.failUnless(choice_field) + self.failUnless(isinstance(choice_field, ModelChoiceField)) + + self.failUnlessEqual( + set([self.page1, self.page1.public, self.page2, self.page2.public]), + set(Page.objects.all()) + ) + self.failUnlessEqual( + set([self.page1, self.page2]), + set(choice_field.queryset) + ) + + def test_formfield_for_manytomany(self): + request = None + authors_field = None + for field in Page._meta.many_to_many: + if field.name == 'authors': + authors_field = field + break + self.failUnless(authors_field) + + choice_field = self.page_admin.formfield_for_manytomany(authors_field, request) + self.failUnless(choice_field) + self.failUnless(isinstance(choice_field, ModelMultipleChoiceField)) + + self.failUnlessEqual( + set([self.author1, self.author1.public, self.author2, self.author2.public]), + set(Author.objects.all()) + ) + self.failUnlessEqual( + set([self.author1, self.author2]), + set(choice_field.queryset) + ) + + def test_has_change_permission(self): + class dummy_request(object): + method = 'GET' + REQUEST = {} + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + self.failUnless(self.page_admin.has_change_permission(dummy_request)) + self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) + self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1.public)) + + # can view deleted items + self.page1.publish_state = Publishable.PUBLISH_DELETE + self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) + + # but cannot modify them + dummy_request.method = 'POST' + self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1)) + + def test_has_delete_permission(self): + class dummy_request(object): + method = 'GET' + REQUEST = {} + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + self.failUnless(self.page_admin.has_delete_permission(dummy_request)) + self.failUnless(self.page_admin.has_delete_permission(dummy_request, self.page1)) + self.failIf(self.page_admin.has_delete_permission(dummy_request, self.page1.public)) + + def test_change_view_normal(self): + class dummy_request(object): + method = 'GET' + REQUEST = {} + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + response = self.page_admin.change_view(dummy_request, str(self.page1.id)) + self.failUnless(response is not None) + self.failIf('deleted' in _get_rendered_content(response)) + + def test_change_view_not_deleted(self): + class dummy_request(object): + method = 'GET' + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + try: + self.page_admin.change_view(dummy_request, unicode(self.page1.public.id)) + self.fail() + except Http404: + pass + + def test_change_view_deleted(self): + class dummy_request(object): + method = 'GET' + REQUEST = {} + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + self.page1.delete() + + response = self.page_admin.change_view(dummy_request, str(self.page1.id)) + self.failUnless(response is not None) + self.failUnless('deleted' in _get_rendered_content(response)) + + def test_change_view_deleted_POST(self): + class dummy_request(object): + csrf_processing_done = True # stop csrf check + method = 'POST' + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + self.page1.delete() + + try: + self.page_admin.change_view(dummy_request, str(self.page1.id)) + self.fail() + except PermissionDenied: + pass + + def test_change_view_delete_inline(self): + block = PageBlock.objects.create(page=self.page1, content='some content') + page1 = Page.objects.get(pk=self.page1.pk) + page1.publish() + + user1 = User.objects.create_user('test1', 'test@example.com', 'jkljkl') + + # fake selecting the delete tickbox for the block + + class dummy_request(object): + csrf_processing_done = True + method = 'POST' + + POST = { + 'slug': page1.slug, + 'title': page1.title, + 'content': page1.content, + 'pub_date_0': '2010-02-12', + 'pub_date_1': '17:40:00', + 'pageblock_set-TOTAL_FORMS': '2', + 'pageblock_set-INITIAL_FORMS': '1', + 'pageblock_set-0-id': str(block.id), + 'pageblock_set-0-page': str(page1.id), + 'pageblock_set-0-DELETE': 'yes' + } + REQUEST = POST + FILES = {} + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + class user(object): + pk = user1.pk + + @classmethod + def is_authenticated(self): + return True + + @classmethod + def has_perm(cls, permission): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + class message_set(object): + @classmethod + def create(cls, message=''): + pass + + class _messages(object): + @classmethod + def add(cls, *message): + pass + + + block = PageBlock.objects.get(id=block.id) + public_block = block.public + + response = self.page_admin.change_view(dummy_request, str(page1.id)) + self.assertEqual(302, response.status_code) + + # the block should have been deleted (but not the public one) + self.failUnlessEqual([public_block], list(PageBlock.objects.all())) + + +class TestPublishSelectedAction(TestCase): + + def setUp(self): + super(TestPublishSelectedAction, self).setUp() + self.fp1 = Page.objects.create(slug='fp1', title='FP1') + self.fp2 = Page.objects.create(slug='fp2', title='FP2') + self.fp3 = Page.objects.create(slug='fp3', title='FP3') + + self.admin_site = AdminSite('Test Admin') + self.page_admin = PublishableAdmin(Page, self.admin_site) + + # override urls, so reverse works + settings.ROOT_URLCONF=patterns('', + ('^admin/', include(self.admin_site.urls)), + ) + + def test_publish_selected_confirm(self): + pages = Page.objects.exclude(id=self.fp3.id) + + class dummy_request(object): + META = {} + POST = {} + + class user(object): + @classmethod + def has_perm(cls, *arg): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + response = publish_selected(self.page_admin, dummy_request, pages) + + self.failIf(Page.objects.published().count() > 0) + self.failUnless(response is not None) + self.failUnlessEqual(200, response.status_code) + + def test_publish_selected_confirmed(self): + pages = Page.objects.exclude(id=self.fp3.id) + + class dummy_request(object): + POST = {'post': True} + + class user(object): + @classmethod + def is_authenticated(cls): + return True + + @classmethod + def has_perm(cls, *arg): + return True + + class message_set(object): + @classmethod + def create(cls, message=None): + self._message = message + + class _messages(object): + @classmethod + def add(cls, *message): + self._message = message + + + response = publish_selected(self.page_admin, dummy_request, pages) + + + self.failUnlessEqual(2, Page.objects.published().count()) + self.failUnless( getattr(self, '_message', None) is not None ) + self.failUnless( response is None ) + + def test_convert_all_published_to_html(self): + self.admin_site.register(Page, PublishableAdmin) + + all_published = NestedSet() + + page = Page.objects.create(slug='here', title='title') + block = PageBlock.objects.create(page=page, content='stuff here') + + all_published.add(page) + all_published.add(block, parent=page) + + converted = _convert_all_published_to_html(self.admin_site, all_published) + + expected = [u'Page: here (Changed - not yet published)' % page.id, [u'Page block: PageBlock object']] + + self.failUnlessEqual(expected, converted) + + def test_publish_selected_does_not_have_permission(self): + self.admin_site.register(Page, PublishableAdmin) + pages = Page.objects.exclude(id=self.fp3.id) + + class dummy_request(object): + POST = {} + META = {} + + class user(object): + @classmethod + def has_perm(cls, *arg): + return False + + @classmethod + def get_and_delete_messages(cls): + return [] + + response = publish_selected(self.page_admin, dummy_request, pages) + self.failIf(response is None) + # publish button should not be in response + self.failIf('value="publish_selected"' in response.content) + self.failIf('value="Yes, Publish"' in response.content) + self.failIf('form' in response.content) + + self.failIf(Page.objects.published().count() > 0) + + def test_publish_selected_does_not_have_related_permission(self): + # check we can't publish when we don't have permission + # for a related model (in this case authors) + self.admin_site.register(Author, PublishableAdmin) + + author = Author.objects.create(name='John') + self.fp1.authors.add(author) + + pages = Page.objects.draft() + + class dummy_request(object): + POST = { 'post': True } + + class _messages(object): + @classmethod + def add(cls, *args): + return 'message' + + class user(object): + pk = 1 + + @classmethod + def is_authenticated(cls): + return True + + @classmethod + def has_perm(cls, perm): + return perm != 'example_app.publish_author' + + try: + publish_selected(self.page_admin, dummy_request, pages) + + self.fail() + except PermissionDenied: + pass + + self.failIf(Page.objects.published().count() > 0) + + def test_publish_selected_logs_publication(self): + self.admin_site.register(Page, PublishableAdmin) + + pages = Page.objects.exclude(id=self.fp3.id) + + class dummy_request(object): + POST = { 'post': True } + + class user(object): + pk = 1 + + @classmethod + def is_authenticated(cls): + return True + + @classmethod + def has_perm(cls, perm): + return perm != 'example_app.publish_author' + + class message_set(object): + @classmethod + def create(cls, message=None): + pass + + class _messages(object): + @classmethod + def add(cls, *message): + pass + + publish_selected(self.page_admin, dummy_request, pages) + + # should have logged two publications + from django.contrib.admin.models import LogEntry + from django.contrib.contenttypes.models import ContentType + + content_type_id = ContentType.objects.get_for_model(self.fp1).pk + self.failUnlessEqual(2, LogEntry.objects.filter().count()) + + +class TestDeleteSelected(TestCase): + + def setUp(self): + super(TestDeleteSelected, self).setUp() + self.fp1 = FlatPage.objects.create(url='/fp1', title='FP1') + self.fp2 = FlatPage.objects.create(url='/fp2', title='FP2') + self.fp3 = FlatPage.objects.create(url='/fp3', title='FP3') + + self.fp1.publish() + self.fp2.publish() + self.fp3.publish() + + self.admin_site = AdminSite('Test Admin') + self.page_admin = PublishableAdmin(FlatPage, self.admin_site) + + # override urls, so reverse works + settings.ROOT_URLCONF=patterns('', + ('^admin/', include(self.admin_site.urls)), + ) + + def test_delete_selected_check_cannot_delete_public(self): + # delete won't work (via admin) for public instances + request = None + try: + delete_selected(self.page_admin, request, FlatPage.objects.published()) + fail() + except PermissionDenied: + pass + + def test_delete_selected(self): + class dummy_request(object): + POST = {} + META = {} + + class user(object): + @classmethod + def has_perm(cls, *arg): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + response = delete_selected(self.page_admin, dummy_request, FlatPage.objects.draft()) + self.failUnless(response is not None) + +class TestUndeleteSelected(TestCase): + + def setUp(self): + super(TestUndeleteSelected, self).setUp() + self.fp1 = FlatPage.objects.create(url='/fp1', title='FP1') + + self.fp1.publish() + + self.admin_site = AdminSite('Test Admin') + self.page_admin = PublishableAdmin(FlatPage, self.admin_site) + + def test_undelete_selected(self): + class dummy_request(object): + + class user(object): + @classmethod + def has_perm(cls, *arg): + return True + + self.fp1.delete() + self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) + + response = undelete_selected(self.page_admin, dummy_request, FlatPage.objects.deleted()) + self.failUnless(response is None) + + # publish state should no longer be delete + fp1 = FlatPage.objects.get(pk=self.fp1.pk) + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, fp1.publish_state) + + def test_undelete_selected_no_permission(self): + class dummy_request(object): + + class user(object): + @classmethod + def has_perm(cls, *arg): + return False + + self.fp1.delete() + self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) + + try: + undelete_selected(self.page_admin, dummy_request, FlatPage.objects.deleted()) + fail() + except PermissionDenied: + pass + +class TestManyToManyThrough(TestCase): + + def setUp(self): + super(TestManyToManyThrough, self).setUp() + self.page = Page.objects.create(slug='p1', title='P 1') + self.tag1 = Tag.objects.create(slug='tag1', title='Tag 1') + self.tag2 = Tag.objects.create(slug='tag2', title='Tag 2') + PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag1, tag_order=2) + PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag2, tag_order=1) + + def test_publish_copies_tags(self): + self.page.publish() + + self.failUnlessEqual(set([self.tag1, self.tag2]), set(self.page.public.tags.all())) + +class TestPublishFunction(TestCase): + + def setUp(self): + super(TestPublishFunction, self).setUp() + self.page = Page.objects.create(slug='page', title='Page') + + def test_publish_function_invoked(self): + # check we can override default copy behaviour + + from datetime import datetime + + pub_date = datetime(2000, 1, 1) + update_pub_date.pub_date = pub_date + + self.failIfEqual(pub_date, self.page.pub_date) + + self.page.publish() + self.failIfEqual(pub_date, self.page.pub_date) + self.failUnlessEqual(pub_date, self.page.public.pub_date) + + +class TestPublishSignals(TestCase): + + def setUp(self): + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page2 = Page.objects.create(slug='page2', title='page 2') + self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') + self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') + self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') + + self.failUnlessEqual(5, Page.objects.draft().count()) + + def _check_pre_publish(self, queryset): + pre_published = [] + def pre_publish_handler(sender, instance, **kw): + pre_published.append(instance) + + pre_publish.connect(pre_publish_handler, sender=Page) + + queryset.draft().publish() + + self.failUnlessEqual(queryset.draft().count(), len(pre_published)) + self.failUnlessEqual(set(queryset.draft()), set(pre_published)) + + def test_pre_publish(self): + # page order shouldn't matter when publishing + # should always get the right number of signals + self._check_pre_publish(Page.objects.order_by('id')) + self._check_pre_publish(Page.objects.order_by('-id')) + self._check_pre_publish(Page.objects.order_by('?')) + + def _check_post_publish(self, queryset): + published = [] + def post_publish_handler(sender, instance, **kw): + published.append(instance) + + post_publish.connect(post_publish_handler, sender=Page) + + queryset.draft().publish() + + self.failUnlessEqual(queryset.draft().count(), len(published)) + self.failUnlessEqual(set(queryset.draft()), set(published)) + + def test_post_publish(self): + self._check_post_publish(Page.objects.order_by('id')) + self._check_post_publish(Page.objects.order_by('-id')) + self._check_post_publish(Page.objects.order_by('?')) + + def test_signals_sent_for_followed(self): + pre_published = [] + def pre_publish_handler(sender, instance, **kw): + pre_published.append(instance) + + pre_publish.connect(pre_publish_handler, sender=Page) + + published = [] + def post_publish_handler(sender, instance, **kw): + published.append(instance) + + post_publish.connect(post_publish_handler, sender=Page) + + # publishing just children will also publish it's parent (if needed) + # which should also fire signals + + self.child1.publish() + + self.failUnlessEqual(set([self.page1, self.child1]), set(pre_published)) + self.failUnlessEqual(set([self.page1, self.child1]), set(published)) + + def test_deleted_flag_false_when_publishing_change(self): + def pre_publish_handler(sender, instance, deleted, **kw): + self.failIf(deleted) + + pre_publish.connect(pre_publish_handler, sender=Page) + + def post_publish_handler(sender, instance, deleted, **kw): + self.failIf(deleted) + + post_publish.connect(post_publish_handler, sender=Page) + + self.page1.publish() + + def test_deleted_flag_true_when_publishing_deletion(self): + self.child1.publish() + public = self.child1.public + + self.child1.delete() + + self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.child1.publish_state) + + def pre_publish_handler(sender, instance, deleted, **kw): + self.failUnless(deleted) + + pre_publish.connect(pre_publish_handler, sender=Page) + + def post_publish_handler(sender, instance, deleted, **kw): + self.failUnless(deleted) + + post_publish.connect(post_publish_handler, sender=Page) + + self.child1.publish() + + +try: + from django.contrib.admin.filters import FieldListFilter +except ImportError: + # pre 1.4 + from django.contrib.admin.filterspecs import FilterSpec + class FieldListFilter(object): + @classmethod + def create(cls, field, request, params, model, model_admin, *arg, **kw): + return FilterSpec.create(field, request, params, model, model_admin) + + +class TestPublishableRelatedFilterSpec(TestCase): + + def test_overridden_spec(self): + # make sure the publishable filter spec + # gets used when we use a publishable field + class dummy_request(object): + GET = {} + + spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) + self.failUnless(isinstance(spec, PublishableRelatedFieldListFilter)) + + def test_only_draft_shown(self): + self.author = Author.objects.create(name='author') + self.author.publish() + + self.failUnless(2, Author.objects.count()) + + # make sure the publishable filter spec + # gets used when we use a publishable field + class dummy_request(object): + GET = {} + + spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) + + lookup_choices = spec.lookup_choices + self.failUnlessEqual(1, len(lookup_choices)) + pk, label = lookup_choices[0] + self.failUnlessEqual(self.author.id, pk) + diff --git a/tests/test_settings.py b/tests/test_settings.py deleted file mode 100644 index a38d7f7..0000000 --- a/tests/test_settings.py +++ /dev/null @@ -1,52 +0,0 @@ -DEBUG = True -TEMPLATE_DEBUG = DEBUG -DATABASE_ENGINE = 'sqlite3' -DATABASE_NAME = ':memory:' - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': ':memory:', # Or path to database file if using sqlite3. - 'USER': '', # Not used with sqlite3. - 'PASSWORD': '', # Not used with sqlite3. - } -} - -import django - -if django.VERSION < (1,4): - INSTALLED_APPS = ( - 'django.contrib.contenttypes', - 'django.contrib.admin', - 'django.contrib.auth', - 'publish', - ) - - TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', - ) -else: - INSTALLED_APPS = ( - 'django.contrib.contenttypes', - 'django.contrib.admin', - 'django.contrib.auth', - 'publish', - ) - - TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ) - MIDDLEWARE_CLASSES = [ - 'django.contrib.messages.middleware.MessageMiddleware', - ] - - - -TESTING_PUBLISH=True - -# enable this for coverage (using django test coverage -# http://pypi.python.org/pypi/django-test-coverage ) -#TEST_RUNNER = 'django-test-coverage.runner.run_tests' -#COVERAGE_MODULES = ('publish.models', 'publish.admin', 'publish.actions', 'publish.utils', 'publish.signals') From 54502aa3fe71a4543226a4d30c865151f2e622dc Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 10:23:35 -0300 Subject: [PATCH 02/22] documented test command and now setup.py get requiremtns from requirements.txt file --- Makefile | 14 ++++++++++++++ README.rst | 6 +++++- requirements.txt | 1 + requirements_test.txt | 2 ++ setup.cfg | 7 +++++++ setup.py | 31 +++++++++++++++++++++++++++++-- 6 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 Makefile create mode 100644 requirements.txt create mode 100644 requirements_test.txt create mode 100644 setup.cfg diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e6dcf93 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +help: + @grep '^[^#[:space:]].*:' Makefile | awk -F ":" '{print $$1}' + +clean: + @find . -name "*.pyc" -delete + +deps: + @pip install -r requirements.txt + @pip install -r requirements_test.txt + +setup: deps + +test: clean deps + @cd publish && nosetests -s -v --with-coverage --cover-package=publish \ No newline at end of file diff --git a/README.rst b/README.rst index 3636471..ca66a7e 100644 --- a/README.rst +++ b/README.rst @@ -187,7 +187,11 @@ To run the tests for this app use the script: :: - tests/run_tests.sh + $ make test + + +or simply ``$ nosetests`` on *publish* folder + .. _Django: http://www.djangoproject.com/ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5936dcf --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Django==1.4.5 diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..e5615c8 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,2 @@ +nose==1.3.0 +django-nose==1.1 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..7ba1618 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,7 @@ +[nosetests] +detailed-errors=1 +with-coverage=1 +cover-package=nose +debug=nose.loader +pdb=1 +pdb-failures=1 diff --git a/setup.py b/setup.py index 5b9a6a8..8919b90 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,31 @@ -from setuptools import setup, find_packages +import re +from setuptools import setup, find_packages, findall version=__import__('publish').__version__ +def parse_requirements(file_name): + requirements = [] + for line in open(file_name, 'r').read().split('\n'): + if re.match(r'(\s*#)|(\s*$)', line): + continue + if re.match(r'\s*-e\s+', line): + requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', line)) + elif re.match(r'\s*-f\s+', line): + pass + else: + requirements.append(line) + return requirements + + +def not_py(file_path): + return not(file_path.endswith('.py') or file_path.endswith('.pyc')) + +core_packages = find_packages() +core_package_data = {} +for package in core_packages: + package_path = package.replace('.', '/') + core_package_data[package] = filter(not_py, findall(package_path)) + setup( name='django-publish', version=version, @@ -12,7 +36,8 @@ url='http://github.com/johnsensible/django-publish', download_url='https://github.com/johnsensible/django-publish/archive/v%s.zip#egg=django-publish-%s' % (version, version), license='BSD', - packages=find_packages(exclude=['ez_setup']), + packages=core_packages, + package_data=core_package_data, include_package_data=True, zip_safe=True, classifiers=[ @@ -25,4 +50,6 @@ 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', ], + install_requires=parse_requirements('requirements.txt'), + setup_requires=parse_requirements('requirements_test.txt') ) From 93492f867c9c2f866536940d99d8c66567e442a3 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 10:23:59 -0300 Subject: [PATCH 03/22] ignoring pycharm --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b7de798..90bc727 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ *.pyc *.swp *.db +.idea From 08ec9f457fea3bcad4046d98a5b7146d11ca6436 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 10:53:49 -0300 Subject: [PATCH 04/22] put helper funciotn on separated file --- publish/tests/helpers.py | 8 ++++ publish/tests/{test_all.py => test_zzzz.py} | 45 +++++---------------- 2 files changed, 19 insertions(+), 34 deletions(-) create mode 100644 publish/tests/helpers.py rename publish/tests/{test_all.py => test_zzzz.py} (98%) diff --git a/publish/tests/helpers.py b/publish/tests/helpers.py new file mode 100644 index 0000000..fd99458 --- /dev/null +++ b/publish/tests/helpers.py @@ -0,0 +1,8 @@ +__author__ = 'petry' + + +def _get_rendered_content(response): + content = getattr(response, 'rendered_content', None) + if content is not None: + return content + return response.content \ No newline at end of file diff --git a/publish/tests/test_all.py b/publish/tests/test_zzzz.py similarity index 98% rename from publish/tests/test_all.py rename to publish/tests/test_zzzz.py index 8c679b0..a79c258 100644 --- a/publish/tests/test_all.py +++ b/publish/tests/test_zzzz.py @@ -1,5 +1,6 @@ from django.conf import settings from publish.tests import settings_for_test +from publish.tests.helpers import _get_rendered_content settings.configure(settings_for_test) @@ -22,14 +23,7 @@ from publish.actions import publish_selected, delete_selected, _convert_all_published_to_html, undelete_selected from publish.utils import NestedSet from publish.signals import pre_publish, post_publish -from publish.filters import PublishableRelatedFieldListFilter - - -def _get_rendered_content(response): - content = getattr(response, 'rendered_content', None) - if content is not None: - return content - return response.content +from publish.filters import PublishableRelatedFieldListFilter, FieldListFilter class TestNestedSet(TestCase): @@ -103,8 +97,6 @@ def __hash__(self): self.failUnlessEqual(id(m1), id(self.nested.original(MyObject('m1')))) - - class TestBasicPublishable(TestCase): def setUp(self): @@ -342,7 +334,6 @@ def test_publish(self): self.failUnlessEqual(set([flat_page1.public, flat_page2.public]), set(FlatPage.objects.published())) - class TestPublishableManyToMany(TestCase): def setUp(self): @@ -413,8 +404,6 @@ def test_publish_sites_cleared_not_deleted(self): self.failIfEqual([], list(Site.objects.all())) - - class TestPublishableRecursiveForeignKey(TestCase): def setUp(self): @@ -585,6 +574,7 @@ def test_publish_delections_with_non_publishable_children(self): self.failUnlessEqual([], list(Page.objects.all())) self.failUnlessEqual([], list(Comment.objects.all())) + class TestPublishableRecursiveManyToManyField(TestCase): def setUp(self): @@ -635,6 +625,7 @@ def test_publish_clear_authors(self): self.page.publish() self.failUnlessEqual([], list(self.page.public.authors.all())) + class TestInfiniteRecursion(TestCase): def setUp(self): @@ -648,6 +639,7 @@ def setUp(self): def test_publish_recursion_breaks(self): self.page1.publish() # this should simple run without an error + class TestOverlappingPublish(TestCase): def setUp(self): @@ -719,6 +711,7 @@ def test_publish_after_dry_run_handles_caching(self): self.failUnlessEqual(page1.public, child2.public.parent) self.failUnlessEqual(page2.public, child3.public.parent) + class TestPublishableAdmin(TestCase): def setUp(self): @@ -1266,11 +1259,7 @@ def setUp(self): def test_delete_selected_check_cannot_delete_public(self): # delete won't work (via admin) for public instances request = None - try: - delete_selected(self.page_admin, request, FlatPage.objects.published()) - fail() - except PermissionDenied: - pass + self.assertRaises(PermissionDenied, delete_selected, self.page_admin, request, FlatPage.objects.published()) def test_delete_selected(self): class dummy_request(object): @@ -1289,6 +1278,7 @@ def get_and_delete_messages(cls): response = delete_selected(self.page_admin, dummy_request, FlatPage.objects.draft()) self.failUnless(response is not None) + class TestUndeleteSelected(TestCase): def setUp(self): @@ -1329,11 +1319,8 @@ def has_perm(cls, *arg): self.fp1.delete() self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) - try: - undelete_selected(self.page_admin, dummy_request, FlatPage.objects.deleted()) - fail() - except PermissionDenied: - pass + self.assertRaises(PermissionDenied, undelete_selected, self.page_admin, dummy_request, FlatPage.objects.deleted()) + class TestManyToManyThrough(TestCase): @@ -1350,6 +1337,7 @@ def test_publish_copies_tags(self): self.failUnlessEqual(set([self.tag1, self.tag2]), set(self.page.public.tags.all())) + class TestPublishFunction(TestCase): def setUp(self): @@ -1473,17 +1461,6 @@ def post_publish_handler(sender, instance, deleted, **kw): self.child1.publish() -try: - from django.contrib.admin.filters import FieldListFilter -except ImportError: - # pre 1.4 - from django.contrib.admin.filterspecs import FilterSpec - class FieldListFilter(object): - @classmethod - def create(cls, field, request, params, model, model_admin, *arg, **kw): - return FilterSpec.create(field, request, params, model, model_admin) - - class TestPublishableRelatedFilterSpec(TestCase): def test_overridden_spec(self): From 31e80d082105d3de26d13febb98b1a16badcd74c Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 11:07:11 -0300 Subject: [PATCH 05/22] separated tests to easy to mantain them --- publish/tests/__init__.py | 8 +- publish/tests/test_basic_publishable.py | 149 ++ publish/tests/test_delete_selected.py | 53 + publish/tests/test_infinite_recursion.py | 18 + publish/tests/test_many_to_many_through.py | 20 + publish/tests/test_nested_set.py | 75 + publish/tests/test_overlapping_publish.py | 77 + publish/tests/test_publish_function.py | 25 + publish/tests/test_publish_selected_action.py | 202 +++ publish/tests/test_publish_signal.py | 108 ++ publish/tests/test_publishable_admin.py | 347 ++++ publish/tests/test_publishable_manager.py | 98 ++ .../tests/test_publishable_many_to_many.py | 74 + .../tests/test_publishable_recursive_fk.py | 176 ++ ...ublishable_recursive_many_to_many_field.py | 55 + .../test_publishable_related_filter_spec.py | 36 + publish/tests/test_undelete_selected.py | 52 + publish/tests/test_zzzz.py | 1492 ----------------- 18 files changed, 1572 insertions(+), 1493 deletions(-) create mode 100644 publish/tests/test_basic_publishable.py create mode 100644 publish/tests/test_delete_selected.py create mode 100644 publish/tests/test_infinite_recursion.py create mode 100644 publish/tests/test_many_to_many_through.py create mode 100644 publish/tests/test_nested_set.py create mode 100644 publish/tests/test_overlapping_publish.py create mode 100644 publish/tests/test_publish_function.py create mode 100644 publish/tests/test_publish_selected_action.py create mode 100644 publish/tests/test_publish_signal.py create mode 100644 publish/tests/test_publishable_admin.py create mode 100644 publish/tests/test_publishable_manager.py create mode 100644 publish/tests/test_publishable_many_to_many.py create mode 100644 publish/tests/test_publishable_recursive_fk.py create mode 100644 publish/tests/test_publishable_recursive_many_to_many_field.py create mode 100644 publish/tests/test_publishable_related_filter_spec.py create mode 100644 publish/tests/test_undelete_selected.py delete mode 100644 publish/tests/test_zzzz.py diff --git a/publish/tests/__init__.py b/publish/tests/__init__.py index ea35eb4..6829615 100644 --- a/publish/tests/__init__.py +++ b/publish/tests/__init__.py @@ -1 +1,7 @@ -__author__ = 'petry' +from django.conf import settings +from publish.tests import settings_for_test + +settings.configure(settings_for_test) + +from django.core.management import call_command +call_command('syncdb', interactive=False) diff --git a/publish/tests/test_basic_publishable.py b/publish/tests/test_basic_publishable.py new file mode 100644 index 0000000..52693a8 --- /dev/null +++ b/publish/tests/test_basic_publishable.py @@ -0,0 +1,149 @@ +from django.test import TestCase +from publish.models import Publishable, PublishException +from publish.tests.example_app.models import FlatPage +from publish.utils import NestedSet + +__author__ = 'petry' + + +class TestBasicPublishable(TestCase): + + def setUp(self): + super(TestBasicPublishable, self).setUp() + self.flat_page = FlatPage(url='/my-page', title='my page', + content='here is some content', + enable_comments=False, + registration_required=True) + + def test_get_public_absolute_url(self): + self.failUnlessEqual('/my-page*', self.flat_page.get_absolute_url()) + # public absolute url doesn't exist until published + self.assertTrue(self.flat_page.get_public_absolute_url() is None) + self.flat_page.save() + self.flat_page.publish() + self.failUnlessEqual('/my-page', self.flat_page.get_public_absolute_url()) + + def test_save_marks_changed(self): + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.flat_page.save(mark_changed=False) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.flat_page.save() + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + + def test_publish_excludes_fields(self): + self.flat_page.save() + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failIfEqual(self.flat_page.id, self.flat_page.public.id) + self.failUnless(self.flat_page.public.is_public) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.public.publish_state) + + def test_publish_check_is_not_public(self): + try: + self.flat_page.is_public = True + self.flat_page.publish() + self.fail("Should not be able to publish public models") + except PublishException: + pass + + def test_publish_check_has_id(self): + try: + self.flat_page.publish() + self.fail("Should not be able to publish unsaved models") + except PublishException: + pass + + def test_publish_simple_fields(self): + self.flat_page.save() + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + self.failIf(self.flat_page.public) # should not be a public version yet + + self.flat_page.publish() + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.failUnless(self.flat_page.public) + + for field in 'url', 'title', 'content', 'enable_comments', 'registration_required': + self.failUnlessEqual(getattr(self.flat_page, field), getattr(self.flat_page.public, field)) + + def test_published_simple_field_repeated(self): + self.flat_page.save() + self.flat_page.publish() + + public = self.flat_page.public + self.failUnless(public) + + self.flat_page.title = 'New Title' + self.flat_page.save() + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + + self.failUnlessEqual(public, self.flat_page.public) + self.failIfEqual(public.title, self.flat_page.title) + + self.flat_page.publish() + self.failUnlessEqual(public, self.flat_page.public) + self.failUnlessEqual(public.title, self.flat_page.title) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + + def test_publish_records_published(self): + all_published = NestedSet() + self.flat_page.save() + self.flat_page.publish(all_published=all_published) + self.failUnlessEqual(1, len(all_published)) + self.failUnless(self.flat_page in all_published) + self.failUnless(self.flat_page.public) + + def test_publish_dryrun(self): + all_published = NestedSet() + self.flat_page.save() + self.flat_page.publish(dry_run=True, all_published=all_published) + self.failUnlessEqual(1, len(all_published)) + self.failUnless(self.flat_page in all_published) + self.failIf(self.flat_page.public) + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + + def test_delete_after_publish(self): + self.flat_page.save() + self.flat_page.publish() + public = self.flat_page.public + self.failUnless(public) + + self.flat_page.delete() + self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.flat_page.publish_state) + + self.failUnlessEqual(set([self.flat_page, self.flat_page.public]), set(FlatPage.objects.all())) + + def test_delete_before_publish(self): + self.flat_page.save() + self.flat_page.delete() + self.failUnlessEqual([], list(FlatPage.objects.all())) + + def test_publish_deletions(self): + self.flat_page.save() + self.flat_page.publish() + public = self.flat_page.public + + self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + + self.flat_page.delete() + self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + + self.flat_page.publish() + self.failUnlessEqual([], list(FlatPage.objects.all())) + + def test_publish_deletions_checks_all_published(self): + # make sure publish_deletions looks at all_published arg + # to see if we need to actually publish the deletion + self.flat_page.save() + self.flat_page.publish() + public = self.flat_page.public + + self.flat_page.delete() + + self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + + # this should effectively stop the deletion happening + all_published = NestedSet() + all_published.add(self.flat_page) + + self.flat_page.publish(all_published=all_published) + self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) \ No newline at end of file diff --git a/publish/tests/test_delete_selected.py b/publish/tests/test_delete_selected.py new file mode 100644 index 0000000..2ad8670 --- /dev/null +++ b/publish/tests/test_delete_selected.py @@ -0,0 +1,53 @@ +from django.conf import settings +from django.conf.urls import patterns, include +from django.contrib.admin import AdminSite +from django.core.exceptions import PermissionDenied +from django.test import TestCase +from publish.actions import delete_selected +from publish.admin import PublishableAdmin +from publish.tests.example_app.models import FlatPage + +__author__ = 'petry' + + +class TestDeleteSelected(TestCase): + + def setUp(self): + super(TestDeleteSelected, self).setUp() + self.fp1 = FlatPage.objects.create(url='/fp1', title='FP1') + self.fp2 = FlatPage.objects.create(url='/fp2', title='FP2') + self.fp3 = FlatPage.objects.create(url='/fp3', title='FP3') + + self.fp1.publish() + self.fp2.publish() + self.fp3.publish() + + self.admin_site = AdminSite('Test Admin') + self.page_admin = PublishableAdmin(FlatPage, self.admin_site) + + # override urls, so reverse works + settings.ROOT_URLCONF=patterns('', + ('^admin/', include(self.admin_site.urls)), + ) + + def test_delete_selected_check_cannot_delete_public(self): + # delete won't work (via admin) for public instances + request = None + self.assertRaises(PermissionDenied, delete_selected, self.page_admin, request, FlatPage.objects.published()) + + def test_delete_selected(self): + class dummy_request(object): + POST = {} + META = {} + + class user(object): + @classmethod + def has_perm(cls, *arg): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + response = delete_selected(self.page_admin, dummy_request, FlatPage.objects.draft()) + self.failUnless(response is not None) \ No newline at end of file diff --git a/publish/tests/test_infinite_recursion.py b/publish/tests/test_infinite_recursion.py new file mode 100644 index 0000000..b081c9a --- /dev/null +++ b/publish/tests/test_infinite_recursion.py @@ -0,0 +1,18 @@ +from django.test import TestCase +from publish.tests.example_app.models import Page + +__author__ = 'petry' + + +class TestInfiniteRecursion(TestCase): + + def setUp(self): + super(TestInfiniteRecursion, self).setUp() + + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page2 = Page.objects.create(slug='page2', title='page 2', parent=self.page1) + self.page1.parent = self.page2 + self.page1.save() + + def test_publish_recursion_breaks(self): + self.page1.publish() # this should simple run without an error \ No newline at end of file diff --git a/publish/tests/test_many_to_many_through.py b/publish/tests/test_many_to_many_through.py new file mode 100644 index 0000000..934d750 --- /dev/null +++ b/publish/tests/test_many_to_many_through.py @@ -0,0 +1,20 @@ +from django.test import TestCase +from publish.tests.example_app.models import Page, Tag, PageTagOrder + +__author__ = 'petry' + + +class TestManyToManyThrough(TestCase): + + def setUp(self): + super(TestManyToManyThrough, self).setUp() + self.page = Page.objects.create(slug='p1', title='P 1') + self.tag1 = Tag.objects.create(slug='tag1', title='Tag 1') + self.tag2 = Tag.objects.create(slug='tag2', title='Tag 2') + PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag1, tag_order=2) + PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag2, tag_order=1) + + def test_publish_copies_tags(self): + self.page.publish() + + self.failUnlessEqual(set([self.tag1, self.tag2]), set(self.page.public.tags.all())) \ No newline at end of file diff --git a/publish/tests/test_nested_set.py b/publish/tests/test_nested_set.py new file mode 100644 index 0000000..7cf01f2 --- /dev/null +++ b/publish/tests/test_nested_set.py @@ -0,0 +1,75 @@ +from django.test import TestCase +from publish.utils import NestedSet + +__author__ = 'petry' + + +class TestNestedSet(TestCase): + + def setUp(self): + super(TestNestedSet, self).setUp() + self.nested = NestedSet() + + def test_len(self): + self.failUnlessEqual(0, len(self.nested)) + self.nested.add('one') + self.failUnlessEqual(1, len(self.nested)) + self.nested.add('two') + self.failUnlessEqual(2, len(self.nested)) + self.nested.add('one2', parent='one') + self.failUnlessEqual(3, len(self.nested)) + + def test_contains(self): + self.failIf('one' in self.nested) + self.nested.add('one') + self.failUnless('one' in self.nested) + self.nested.add('one2', parent='one') + self.failUnless('one2' in self.nested) + + def test_nested_items(self): + self.failUnlessEqual([], self.nested.nested_items()) + self.nested.add('one') + self.failUnlessEqual(['one'], self.nested.nested_items()) + self.nested.add('two') + self.nested.add('one2', parent='one') + self.failUnlessEqual(['one', ['one2'], 'two'], self.nested.nested_items()) + self.nested.add('one2-1', parent='one2') + self.nested.add('one2-2', parent='one2') + self.failUnlessEqual(['one', ['one2', ['one2-1', 'one2-2']], 'two'], self.nested.nested_items()) + + def test_iter(self): + self.failUnlessEqual(set(), set(self.nested)) + + self.nested.add('one') + self.failUnlessEqual(set(['one']), set(self.nested)) + + self.nested.add('two', parent='one') + self.failUnlessEqual(set(['one', 'two']), set(self.nested)) + + items = set(['one', 'two']) + + for item in self.nested: + self.failUnless(item in items) + items.remove(item) + + self.failUnlessEqual(set(), items) + + def test_original(self): + class MyObject(object): + def __init__(self, obj): + self.obj = obj + + def __eq__(self, other): + return self.obj == other.obj + + def __hash__(self): + return hash(self.obj) + + # should always return an item at least + self.failUnlessEqual(MyObject('hi there'), self.nested.original(MyObject('hi there'))) + + m1 = MyObject('m1') + self.nested.add(m1) + + self.failUnlessEqual(id(m1), id(self.nested.original(m1))) + self.failUnlessEqual(id(m1), id(self.nested.original(MyObject('m1')))) \ No newline at end of file diff --git a/publish/tests/test_overlapping_publish.py b/publish/tests/test_overlapping_publish.py new file mode 100644 index 0000000..55a0bbd --- /dev/null +++ b/publish/tests/test_overlapping_publish.py @@ -0,0 +1,77 @@ +from django.test import TestCase +from publish.tests.example_app.models import Page +from publish.utils import NestedSet + +__author__ = 'petry' + + +class TestOverlappingPublish(TestCase): + + def setUp(self): + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page2 = Page.objects.create(slug='page2', title='page 2') + self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') + self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') + self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') + + def test_publish_with_overlapping_models(self): + # make sure when we publish we don't accidentally create + # multiple published versions + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(0, Page.objects.published().count()) + + Page.objects.draft().publish() + + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(5, Page.objects.published().count()) + + def test_publish_with_overlapping_models_published(self): + # make sure when we publish we don't accidentally create + # multiple published versions + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(0, Page.objects.published().count()) + + all_published = NestedSet() + Page.objects.draft().publish(all_published) + + self.failUnlessEqual(5, len(all_published)) + + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(5, Page.objects.published().count()) + + def test_publish_after_dry_run_handles_caching(self): + # if we do a dry tun publish in the same queryset + # before publishing for real, we have to make + # sure we don't run into issues with the instance + # caching parent's as None + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(0, Page.objects.published().count()) + + draft = Page.objects.draft() + + all_published = NestedSet() + for p in draft: + p.publish(dry_run=True, all_published=all_published) + + # nothing published yet + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(0, Page.objects.published().count()) + + # now publish (using same queryset, as this will have cached the instances) + draft.publish() + + self.failUnlessEqual(5, Page.objects.draft().count()) + self.failUnlessEqual(5, Page.objects.published().count()) + + # now actually check the public parent's are setup right + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + child1 = Page.objects.get(id=self.child1.id) + child2 = Page.objects.get(id=self.child2.id) + child3 = Page.objects.get(id=self.child3.id) + + self.failUnlessEqual(None, page1.public.parent) + self.failUnlessEqual(None, page2.public.parent) + self.failUnlessEqual(page1.public, child1.public.parent) + self.failUnlessEqual(page1.public, child2.public.parent) + self.failUnlessEqual(page2.public, child3.public.parent) \ No newline at end of file diff --git a/publish/tests/test_publish_function.py b/publish/tests/test_publish_function.py new file mode 100644 index 0000000..9f7873f --- /dev/null +++ b/publish/tests/test_publish_function.py @@ -0,0 +1,25 @@ +from django.test import TestCase +from publish.tests.example_app.models import Page, update_pub_date + +__author__ = 'petry' + + +class TestPublishFunction(TestCase): + + def setUp(self): + super(TestPublishFunction, self).setUp() + self.page = Page.objects.create(slug='page', title='Page') + + def test_publish_function_invoked(self): + # check we can override default copy behaviour + + from datetime import datetime + + pub_date = datetime(2000, 1, 1) + update_pub_date.pub_date = pub_date + + self.failIfEqual(pub_date, self.page.pub_date) + + self.page.publish() + self.failIfEqual(pub_date, self.page.pub_date) + self.failUnlessEqual(pub_date, self.page.public.pub_date) \ No newline at end of file diff --git a/publish/tests/test_publish_selected_action.py b/publish/tests/test_publish_selected_action.py new file mode 100644 index 0000000..e2814b4 --- /dev/null +++ b/publish/tests/test_publish_selected_action.py @@ -0,0 +1,202 @@ +from django.conf import settings +from django.conf.urls import patterns, include +from django.contrib.admin import AdminSite +from django.core.exceptions import PermissionDenied +from django.test import TestCase +from publish.actions import publish_selected, _convert_all_published_to_html +from publish.admin import PublishableAdmin +from publish.tests.example_app.models import Page, PageBlock, Author +from publish.utils import NestedSet + +__author__ = 'petry' + + +class TestPublishSelectedAction(TestCase): + + def setUp(self): + super(TestPublishSelectedAction, self).setUp() + self.fp1 = Page.objects.create(slug='fp1', title='FP1') + self.fp2 = Page.objects.create(slug='fp2', title='FP2') + self.fp3 = Page.objects.create(slug='fp3', title='FP3') + + self.admin_site = AdminSite('Test Admin') + self.page_admin = PublishableAdmin(Page, self.admin_site) + + # override urls, so reverse works + settings.ROOT_URLCONF=patterns('', + ('^admin/', include(self.admin_site.urls)), + ) + + def test_publish_selected_confirm(self): + pages = Page.objects.exclude(id=self.fp3.id) + + class dummy_request(object): + META = {} + POST = {} + + class user(object): + @classmethod + def has_perm(cls, *arg): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + response = publish_selected(self.page_admin, dummy_request, pages) + + self.failIf(Page.objects.published().count() > 0) + self.failUnless(response is not None) + self.failUnlessEqual(200, response.status_code) + + def test_publish_selected_confirmed(self): + pages = Page.objects.exclude(id=self.fp3.id) + + class dummy_request(object): + POST = {'post': True} + + class user(object): + @classmethod + def is_authenticated(cls): + return True + + @classmethod + def has_perm(cls, *arg): + return True + + class message_set(object): + @classmethod + def create(cls, message=None): + self._message = message + + class _messages(object): + @classmethod + def add(cls, *message): + self._message = message + + + response = publish_selected(self.page_admin, dummy_request, pages) + + + self.failUnlessEqual(2, Page.objects.published().count()) + self.failUnless( getattr(self, '_message', None) is not None ) + self.failUnless( response is None ) + + def test_convert_all_published_to_html(self): + self.admin_site.register(Page, PublishableAdmin) + + all_published = NestedSet() + + page = Page.objects.create(slug='here', title='title') + block = PageBlock.objects.create(page=page, content='stuff here') + + all_published.add(page) + all_published.add(block, parent=page) + + converted = _convert_all_published_to_html(self.admin_site, all_published) + + expected = [u'Page: here (Changed - not yet published)' % page.id, [u'Page block: PageBlock object']] + + self.failUnlessEqual(expected, converted) + + def test_publish_selected_does_not_have_permission(self): + self.admin_site.register(Page, PublishableAdmin) + pages = Page.objects.exclude(id=self.fp3.id) + + class dummy_request(object): + POST = {} + META = {} + + class user(object): + @classmethod + def has_perm(cls, *arg): + return False + + @classmethod + def get_and_delete_messages(cls): + return [] + + response = publish_selected(self.page_admin, dummy_request, pages) + self.failIf(response is None) + # publish button should not be in response + self.failIf('value="publish_selected"' in response.content) + self.failIf('value="Yes, Publish"' in response.content) + self.failIf('form' in response.content) + + self.failIf(Page.objects.published().count() > 0) + + def test_publish_selected_does_not_have_related_permission(self): + # check we can't publish when we don't have permission + # for a related model (in this case authors) + self.admin_site.register(Author, PublishableAdmin) + + author = Author.objects.create(name='John') + self.fp1.authors.add(author) + + pages = Page.objects.draft() + + class dummy_request(object): + POST = { 'post': True } + + class _messages(object): + @classmethod + def add(cls, *args): + return 'message' + + class user(object): + pk = 1 + + @classmethod + def is_authenticated(cls): + return True + + @classmethod + def has_perm(cls, perm): + return perm != 'example_app.publish_author' + + try: + publish_selected(self.page_admin, dummy_request, pages) + + self.fail() + except PermissionDenied: + pass + + self.failIf(Page.objects.published().count() > 0) + + def test_publish_selected_logs_publication(self): + self.admin_site.register(Page, PublishableAdmin) + + pages = Page.objects.exclude(id=self.fp3.id) + + class dummy_request(object): + POST = { 'post': True } + + class user(object): + pk = 1 + + @classmethod + def is_authenticated(cls): + return True + + @classmethod + def has_perm(cls, perm): + return perm != 'example_app.publish_author' + + class message_set(object): + @classmethod + def create(cls, message=None): + pass + + class _messages(object): + @classmethod + def add(cls, *message): + pass + + publish_selected(self.page_admin, dummy_request, pages) + + # should have logged two publications + from django.contrib.admin.models import LogEntry + from django.contrib.contenttypes.models import ContentType + + content_type_id = ContentType.objects.get_for_model(self.fp1).pk + self.failUnlessEqual(2, LogEntry.objects.filter().count()) \ No newline at end of file diff --git a/publish/tests/test_publish_signal.py b/publish/tests/test_publish_signal.py new file mode 100644 index 0000000..223ca69 --- /dev/null +++ b/publish/tests/test_publish_signal.py @@ -0,0 +1,108 @@ +from django.test import TestCase +from publish.models import Publishable +from publish.signals import pre_publish, post_publish +from publish.tests.example_app.models import Page + +__author__ = 'petry' + + +class TestPublishSignals(TestCase): + + def setUp(self): + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page2 = Page.objects.create(slug='page2', title='page 2') + self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') + self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') + self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') + + self.failUnlessEqual(5, Page.objects.draft().count()) + + def _check_pre_publish(self, queryset): + pre_published = [] + def pre_publish_handler(sender, instance, **kw): + pre_published.append(instance) + + pre_publish.connect(pre_publish_handler, sender=Page) + + queryset.draft().publish() + + self.failUnlessEqual(queryset.draft().count(), len(pre_published)) + self.failUnlessEqual(set(queryset.draft()), set(pre_published)) + + def test_pre_publish(self): + # page order shouldn't matter when publishing + # should always get the right number of signals + self._check_pre_publish(Page.objects.order_by('id')) + self._check_pre_publish(Page.objects.order_by('-id')) + self._check_pre_publish(Page.objects.order_by('?')) + + def _check_post_publish(self, queryset): + published = [] + def post_publish_handler(sender, instance, **kw): + published.append(instance) + + post_publish.connect(post_publish_handler, sender=Page) + + queryset.draft().publish() + + self.failUnlessEqual(queryset.draft().count(), len(published)) + self.failUnlessEqual(set(queryset.draft()), set(published)) + + def test_post_publish(self): + self._check_post_publish(Page.objects.order_by('id')) + self._check_post_publish(Page.objects.order_by('-id')) + self._check_post_publish(Page.objects.order_by('?')) + + def test_signals_sent_for_followed(self): + pre_published = [] + def pre_publish_handler(sender, instance, **kw): + pre_published.append(instance) + + pre_publish.connect(pre_publish_handler, sender=Page) + + published = [] + def post_publish_handler(sender, instance, **kw): + published.append(instance) + + post_publish.connect(post_publish_handler, sender=Page) + + # publishing just children will also publish it's parent (if needed) + # which should also fire signals + + self.child1.publish() + + self.failUnlessEqual(set([self.page1, self.child1]), set(pre_published)) + self.failUnlessEqual(set([self.page1, self.child1]), set(published)) + + def test_deleted_flag_false_when_publishing_change(self): + def pre_publish_handler(sender, instance, deleted, **kw): + self.failIf(deleted) + + pre_publish.connect(pre_publish_handler, sender=Page) + + def post_publish_handler(sender, instance, deleted, **kw): + self.failIf(deleted) + + post_publish.connect(post_publish_handler, sender=Page) + + self.page1.publish() + + def test_deleted_flag_true_when_publishing_deletion(self): + self.child1.publish() + public = self.child1.public + + self.child1.delete() + + self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.child1.publish_state) + + def pre_publish_handler(sender, instance, deleted, **kw): + self.failUnless(deleted) + + pre_publish.connect(pre_publish_handler, sender=Page) + + def post_publish_handler(sender, instance, deleted, **kw): + self.failUnless(deleted) + + post_publish.connect(post_publish_handler, sender=Page) + + self.child1.publish() \ No newline at end of file diff --git a/publish/tests/test_publishable_admin.py b/publish/tests/test_publishable_admin.py new file mode 100644 index 0000000..466ae7b --- /dev/null +++ b/publish/tests/test_publishable_admin.py @@ -0,0 +1,347 @@ +from django.conf import settings +from django.conf.urls import patterns, include +from django.contrib.admin import AdminSite +from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied +from django.forms import ModelChoiceField, ModelMultipleChoiceField +from django.http import Http404 +from django.test import TestCase +from publish.admin import PublishableStackedInline, PublishableAdmin +from publish.models import Publishable +from publish.tests.example_app.models import Page, Author, PageBlock +from publish.tests.helpers import _get_rendered_content + +__author__ = 'petry' + + +class TestPublishableAdmin(TestCase): + + def setUp(self): + super(TestPublishableAdmin, self).setUp() + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page2 = Page.objects.create(slug='page2', title='page 2') + self.page1.publish() + self.page2.publish() + + self.author1 = Author.objects.create(name='a1') + self.author2 = Author.objects.create(name='a2') + self.author1.publish() + self.author2.publish() + + self.admin_site = AdminSite('Test Admin') + + class PageBlockInline(PublishableStackedInline): + model = PageBlock + + class PageAdmin(PublishableAdmin): + inlines = [PageBlockInline] + + self.admin_site.register(Page, PageAdmin) + self.page_admin = PageAdmin(Page, self.admin_site) + + # override urls, so reverse works + settings.ROOT_URLCONF=patterns('', + ('^admin/', include(self.admin_site.urls)), + ) + + def test_get_publish_status_display(self): + page = Page.objects.create(slug="hhkkk", title="hjkhjkh") + self.failUnlessEqual('Changed - not yet published', self.page_admin.get_publish_status_display(page)) + page.publish() + self.failUnlessEqual('Published', self.page_admin.get_publish_status_display(page)) + page.save() + self.failUnlessEqual('Changed', self.page_admin.get_publish_status_display(page)) + + page.delete() + self.failUnlessEqual('To be deleted', self.page_admin.get_publish_status_display(page)) + + def test_queryset(self): + # make sure we only get back draft objects + request = None + + self.failUnlessEqual( + set([self.page1, self.page1.public, self.page2, self.page2.public]), + set(Page.objects.all()) + ) + self.failUnlessEqual( + set([self.page1, self.page2]), + set(self.page_admin.queryset(request)) + ) + + def test_get_actions_global_delete_replaced(self): + from publish.actions import delete_selected + + class request(object): + GET = {} + + actions = self.page_admin.get_actions(request) + + + self.failUnless('delete_selected' in actions) + action, name, description = actions['delete_selected'] + self.failUnlessEqual(delete_selected, action) + self.failUnlessEqual('delete_selected', name) + self.failUnlessEqual(delete_selected.short_description, description) + + def test_formfield_for_foreignkey(self): + # foreign key forms fields in admin + # for publishable models should be filtered + # to hide public object + + request = None + parent_field = None + for field in Page._meta.fields: + if field.name == 'parent': + parent_field = field + break + self.failUnless(parent_field) + + choice_field = self.page_admin.formfield_for_foreignkey(parent_field, request) + self.failUnless(choice_field) + self.failUnless(isinstance(choice_field, ModelChoiceField)) + + self.failUnlessEqual( + set([self.page1, self.page1.public, self.page2, self.page2.public]), + set(Page.objects.all()) + ) + self.failUnlessEqual( + set([self.page1, self.page2]), + set(choice_field.queryset) + ) + + def test_formfield_for_manytomany(self): + request = None + authors_field = None + for field in Page._meta.many_to_many: + if field.name == 'authors': + authors_field = field + break + self.failUnless(authors_field) + + choice_field = self.page_admin.formfield_for_manytomany(authors_field, request) + self.failUnless(choice_field) + self.failUnless(isinstance(choice_field, ModelMultipleChoiceField)) + + self.failUnlessEqual( + set([self.author1, self.author1.public, self.author2, self.author2.public]), + set(Author.objects.all()) + ) + self.failUnlessEqual( + set([self.author1, self.author2]), + set(choice_field.queryset) + ) + + def test_has_change_permission(self): + class dummy_request(object): + method = 'GET' + REQUEST = {} + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + self.failUnless(self.page_admin.has_change_permission(dummy_request)) + self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) + self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1.public)) + + # can view deleted items + self.page1.publish_state = Publishable.PUBLISH_DELETE + self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) + + # but cannot modify them + dummy_request.method = 'POST' + self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1)) + + def test_has_delete_permission(self): + class dummy_request(object): + method = 'GET' + REQUEST = {} + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + self.failUnless(self.page_admin.has_delete_permission(dummy_request)) + self.failUnless(self.page_admin.has_delete_permission(dummy_request, self.page1)) + self.failIf(self.page_admin.has_delete_permission(dummy_request, self.page1.public)) + + def test_change_view_normal(self): + class dummy_request(object): + method = 'GET' + REQUEST = {} + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + response = self.page_admin.change_view(dummy_request, str(self.page1.id)) + self.failUnless(response is not None) + self.failIf('deleted' in _get_rendered_content(response)) + + def test_change_view_not_deleted(self): + class dummy_request(object): + method = 'GET' + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + try: + self.page_admin.change_view(dummy_request, unicode(self.page1.public.id)) + self.fail() + except Http404: + pass + + def test_change_view_deleted(self): + class dummy_request(object): + method = 'GET' + REQUEST = {} + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + class user(object): + @classmethod + def has_perm(cls, permission): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + self.page1.delete() + + response = self.page_admin.change_view(dummy_request, str(self.page1.id)) + self.failUnless(response is not None) + self.failUnless('deleted' in _get_rendered_content(response)) + + def test_change_view_deleted_POST(self): + class dummy_request(object): + csrf_processing_done = True # stop csrf check + method = 'POST' + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + self.page1.delete() + + try: + self.page_admin.change_view(dummy_request, str(self.page1.id)) + self.fail() + except PermissionDenied: + pass + + def test_change_view_delete_inline(self): + block = PageBlock.objects.create(page=self.page1, content='some content') + page1 = Page.objects.get(pk=self.page1.pk) + page1.publish() + + user1 = User.objects.create_user('test1', 'test@example.com', 'jkljkl') + + # fake selecting the delete tickbox for the block + + class dummy_request(object): + csrf_processing_done = True + method = 'POST' + + POST = { + 'slug': page1.slug, + 'title': page1.title, + 'content': page1.content, + 'pub_date_0': '2010-02-12', + 'pub_date_1': '17:40:00', + 'pageblock_set-TOTAL_FORMS': '2', + 'pageblock_set-INITIAL_FORMS': '1', + 'pageblock_set-0-id': str(block.id), + 'pageblock_set-0-page': str(page1.id), + 'pageblock_set-0-DELETE': 'yes' + } + REQUEST = POST + FILES = {} + COOKIES = {} + META = {} + + @classmethod + def is_ajax(cls): + return False + + @classmethod + def is_secure(cls): + return False + + class user(object): + pk = user1.pk + + @classmethod + def is_authenticated(self): + return True + + @classmethod + def has_perm(cls, permission): + return True + + @classmethod + def get_and_delete_messages(cls): + return [] + + class message_set(object): + @classmethod + def create(cls, message=''): + pass + + class _messages(object): + @classmethod + def add(cls, *message): + pass + + + block = PageBlock.objects.get(id=block.id) + public_block = block.public + + response = self.page_admin.change_view(dummy_request, str(page1.id)) + self.assertEqual(302, response.status_code) + + # the block should have been deleted (but not the public one) + self.failUnlessEqual([public_block], list(PageBlock.objects.all())) \ No newline at end of file diff --git a/publish/tests/test_publishable_manager.py b/publish/tests/test_publishable_manager.py new file mode 100644 index 0000000..32ca9fd --- /dev/null +++ b/publish/tests/test_publishable_manager.py @@ -0,0 +1,98 @@ +from django.test import TestCase +from publish.tests.example_app.models import FlatPage + +__author__ = 'petry' + + +class TestPublishableManager(TestCase): + + def setUp(self): + super(TestCase, self).setUp() + self.flat_page1 = FlatPage.objects.create(url='/url1/', title='title 1') + self.flat_page2 = FlatPage.objects.create(url='/url2/', title='title 2') + + def test_all(self): + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.all())) + + # publishing will produce extra copies + self.flat_page1.publish() + self.failUnlessEqual(3, FlatPage.objects.count()) + + self.flat_page2.publish() + self.failUnlessEqual(4, FlatPage.objects.count()) + + + def test_changed(self): + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.changed())) + + self.flat_page1.publish() + self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.changed())) + + self.flat_page2.publish() + self.failUnlessEqual([], list(FlatPage.objects.changed())) + + def test_draft(self): + # draft should stay the same pretty much always + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) + + self.flat_page1.publish() + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) + + self.flat_page2.publish() + self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) + + self.flat_page2.delete() + self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft())) + + + def test_published(self): + self.failUnlessEqual([], list(FlatPage.objects.published())) + + self.flat_page1.publish() + self.failUnlessEqual([self.flat_page1.public], list(FlatPage.objects.published())) + + self.flat_page2.publish() + self.failUnlessEqual([self.flat_page1.public, self.flat_page2.public], list(FlatPage.objects.published())) + + def test_deleted(self): + self.failUnlessEqual([], list(FlatPage.objects.deleted())) + + self.flat_page1.publish() + self.failUnlessEqual([], list(FlatPage.objects.deleted())) + + self.flat_page1.delete() + self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) + + def test_draft_and_deleted(self): + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) + + self.flat_page1.publish() + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft())) + + self.flat_page1.delete() + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) + self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.draft())) + + + def test_delete(self): + # delete is overriden, so it marks the public instances + self.flat_page1.publish() + public1 = self.flat_page1.public + + FlatPage.objects.draft().delete() + + self.failUnlessEqual([], list(FlatPage.objects.draft())) + self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) + self.failUnlessEqual([public1], list(FlatPage.objects.published())) + self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft_and_deleted())) + + def test_publish(self): + self.failUnlessEqual([], list(FlatPage.objects.published())) + + FlatPage.objects.draft().publish() + + flat_page1 = FlatPage.objects.get(id=self.flat_page1.id) + flat_page2 = FlatPage.objects.get(id=self.flat_page2.id) + + self.failUnlessEqual(set([flat_page1.public, flat_page2.public]), set(FlatPage.objects.published())) \ No newline at end of file diff --git a/publish/tests/test_publishable_many_to_many.py b/publish/tests/test_publishable_many_to_many.py new file mode 100644 index 0000000..800a934 --- /dev/null +++ b/publish/tests/test_publishable_many_to_many.py @@ -0,0 +1,74 @@ +from django.test import TestCase +from publish.tests.example_app.models import FlatPage, Site + +__author__ = 'petry' + + +class TestPublishableManyToMany(TestCase): + + def setUp(self): + super(TestPublishableManyToMany, self).setUp() + self.flat_page = FlatPage.objects.create( + url='/my-page', title='my page', + content='here is some content', + enable_comments=False, + registration_required=True) + self.site1 = Site.objects.create(title='my site', domain='mysite.com') + self.site2 = Site.objects.create(title='a site', domain='asite.com') + + def test_publish_no_sites(self): + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([], list(self.flat_page.public.sites.all())) + + def test_publish_add_site(self): + self.flat_page.sites.add(self.site1) + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) + + def test_publish_repeated_add_site(self): + self.flat_page.sites.add(self.site1) + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) + + self.flat_page.sites.add(self.site2) + self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) + + self.flat_page.publish() + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + def test_publish_remove_site(self): + self.flat_page.sites.add(self.site1, self.site2) + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + self.flat_page.sites.remove(self.site1) + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + self.flat_page.publish() + self.failUnlessEqual([self.site2], list(self.flat_page.public.sites.all())) + + def test_publish_clear_sites(self): + self.flat_page.sites.add(self.site1, self.site2) + self.flat_page.publish() + self.failUnless(self.flat_page.public) + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + self.flat_page.sites.clear() + self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + + self.flat_page.publish() + self.failUnlessEqual([], list(self.flat_page.public.sites.all())) + + def test_publish_sites_cleared_not_deleted(self): + self.flat_page.sites.add(self.site1, self.site2) + self.flat_page.publish() + self.flat_page.sites.clear() + self.flat_page.publish() + + self.failUnlessEqual([], list(self.flat_page.public.sites.all())) + + self.failIfEqual([], list(Site.objects.all())) \ No newline at end of file diff --git a/publish/tests/test_publishable_recursive_fk.py b/publish/tests/test_publishable_recursive_fk.py new file mode 100644 index 0000000..9f3f24a --- /dev/null +++ b/publish/tests/test_publishable_recursive_fk.py @@ -0,0 +1,176 @@ +from django.test import TestCase +from publish.models import Publishable +from publish.tests.example_app.models import Page, PageBlock, Comment + +__author__ = 'petry' + + +class TestPublishableRecursiveForeignKey(TestCase): + + def setUp(self): + super(TestPublishableRecursiveForeignKey, self).setUp() + self.page1 = Page.objects.create(slug='page1', title='page 1', content='some content') + self.page2 = Page.objects.create(slug='page2', title='page 2', content='other content', parent=self.page1) + + def test_publish_parent(self): + # this shouldn't publish the child page + self.page1.publish() + self.failUnless(self.page1.public) + self.failIf(self.page1.public.parent) + + page2 = Page.objects.get(id=self.page2.id) + self.failIf(page2.public) + + def test_publish_child_parent_already_published(self): + self.page1.publish() + self.page2.publish() + + self.failUnless(self.page1.public) + self.failUnless(self.page2.public) + + self.failIf(self.page1.public.parent) + self.failUnless(self.page2.public.parent) + + self.failIfEqual(self.page1, self.page2.public.parent) + + self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) + self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) + + def test_publish_child_parent_not_already_published(self): + self.page2.publish() + + page1 = Page.objects.get(id=self.page1.id) + self.failUnless(page1.public) + self.failUnless(self.page2.public) + + self.failIf(page1.public.parent) + self.failUnless(self.page2.public.parent) + + self.failIfEqual(page1, self.page2.public.parent) + + self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) + self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) + + def test_publish_repeated(self): + self.page1.publish() + self.page2.publish() + + self.page1.slug='main' + self.page1.save() + + self.failUnlessEqual('/main/', self.page1.get_absolute_url()) + + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + self.failUnlessEqual('/page1/', page1.public.get_absolute_url()) + self.failUnlessEqual('/page1/page2/', page2.public.get_absolute_url()) + + page1.publish() + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + self.failUnlessEqual('/main/', page1.public.get_absolute_url()) + self.failUnlessEqual('/main/page2/', page2.public.get_absolute_url()) + + page1.slug='elsewhere' + page1.save() + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + page2.slug='meanwhile' + page2.save() + page2.publish() + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + + # only page2 should be published, not page1, as page1 already published + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page2.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, page1.publish_state) + + self.failUnlessEqual('/main/', page1.public.get_absolute_url()) + self.failUnlessEqual('/main/meanwhile/', page2.public.get_absolute_url()) + + page1.publish() + page1 = Page.objects.get(id=self.page1.id) + page2 = Page.objects.get(id=self.page2.id) + + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page2.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page1.publish_state) + + self.failUnlessEqual('/elsewhere/', page1.public.get_absolute_url()) + self.failUnlessEqual('/elsewhere/meanwhile/', page2.public.get_absolute_url()) + + def test_publish_deletions(self): + self.page1.publish() + self.page2.publish() + + self.page2.delete() + self.failUnlessEqual([self.page2], list(Page.objects.deleted())) + + self.page2.publish() + self.failUnlessEqual([self.page1.public], list(Page.objects.published())) + self.failUnlessEqual([], list(Page.objects.deleted())) + + def test_publish_reverse_fields(self): + page_block = PageBlock.objects.create(page=self.page1, content='here we are') + + self.page1.publish() + + public = self.page1.public + self.failUnless(public) + + blocks = list(public.pageblock_set.all()) + self.failUnlessEqual(1, len(blocks)) + self.failUnlessEqual(page_block.content, blocks[0].content) + + def test_publish_deletions_reverse_fields(self): + page_block = PageBlock.objects.create(page=self.page1, content='here we are') + + self.page1.publish() + public = self.page1.public + self.failUnless(public) + + self.page1.delete() + + self.failUnlessEqual([self.page1], list(Page.objects.deleted())) + + self.page1.publish() + self.failUnlessEqual([], list(Page.objects.deleted())) + self.failUnlessEqual([], list(Page.objects.all())) + + def test_publish_reverse_fields_deleted(self): + # make sure child elements get removed + page_block = PageBlock.objects.create(page=self.page1, content='here we are') + + self.page1.publish() + + public = self.page1.public + page_block = PageBlock.objects.get(id=page_block.id) + page_block_public = page_block.public + self.failIf(page_block_public is None) + + self.failUnlessEqual([page_block_public], list(public.pageblock_set.all())) + + # now delete the page block and publish the parent + # to make sure that deletion gets copied over properly + page_block.delete() + page1 = Page.objects.get(id=self.page1.id) + page1.publish() + public = page1.public + + self.failUnlessEqual([], list(public.pageblock_set.all())) + + def test_publish_delections_with_non_publishable_children(self): + self.page1.publish() + + comment = Comment.objects.create(page=self.page1.public, comment='This is a comment') + + self.failUnlessEqual(1, Comment.objects.count()) + + self.page1.delete() + + self.failUnlessEqual([self.page1], list(Page.objects.deleted())) + self.failIf(self.page1 in Page.objects.draft()) + + self.page1.publish() + self.failUnlessEqual([], list(Page.objects.deleted())) + self.failUnlessEqual([], list(Page.objects.all())) + self.failUnlessEqual([], list(Comment.objects.all())) \ No newline at end of file diff --git a/publish/tests/test_publishable_recursive_many_to_many_field.py b/publish/tests/test_publishable_recursive_many_to_many_field.py new file mode 100644 index 0000000..84e8b56 --- /dev/null +++ b/publish/tests/test_publishable_recursive_many_to_many_field.py @@ -0,0 +1,55 @@ +from django.test import TestCase +from publish.tests.example_app.models import Page, Author + +__author__ = 'petry' + + +class TestPublishableRecursiveManyToManyField(TestCase): + + def setUp(self): + super(TestPublishableRecursiveManyToManyField, self).setUp() + self.page = Page.objects.create(slug='page1', title='page 1', content='some content') + self.author1 = Author.objects.create(name='author1', profile='a profile') + self.author2 = Author.objects.create(name='author2', profile='something else') + + def test_publish_add_author(self): + self.page.authors.add(self.author1) + self.page.publish() + self.failUnless(self.page.public) + + author1 = Author.objects.get(id=self.author1.id) + self.failUnless(author1.public) + self.failIfEqual(author1.id, author1.public.id) + self.failUnlessEqual(author1.name, author1.public.name) + self.failUnlessEqual(author1.profile, author1.public.profile) + + self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) + + def test_publish_repeated_add_author(self): + self.page.authors.add(self.author1) + self.page.publish() + + self.failUnless(self.page.public) + + self.page.authors.add(self.author2) + author1 = Author.objects.get(id=self.author1.id) + self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) + + self.page.publish() + author1 = Author.objects.get(id=self.author1.id) + author2 = Author.objects.get(id=self.author2.id) + self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) + + def test_publish_clear_authors(self): + self.page.authors.add(self.author1, self.author2) + self.page.publish() + + author1 = Author.objects.get(id=self.author1.id) + author2 = Author.objects.get(id=self.author2.id) + self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) + + self.page.authors.clear() + self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) + + self.page.publish() + self.failUnlessEqual([], list(self.page.public.authors.all())) \ No newline at end of file diff --git a/publish/tests/test_publishable_related_filter_spec.py b/publish/tests/test_publishable_related_filter_spec.py new file mode 100644 index 0000000..08b4592 --- /dev/null +++ b/publish/tests/test_publishable_related_filter_spec.py @@ -0,0 +1,36 @@ +from django.test import TestCase +from publish.admin import PublishableAdmin +from publish.filters import FieldListFilter, PublishableRelatedFieldListFilter +from publish.tests.example_app.models import Page, Author + +__author__ = 'petry' + + +class TestPublishableRelatedFilterSpec(TestCase): + + def test_overridden_spec(self): + # make sure the publishable filter spec + # gets used when we use a publishable field + class dummy_request(object): + GET = {} + + spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) + self.failUnless(isinstance(spec, PublishableRelatedFieldListFilter)) + + def test_only_draft_shown(self): + self.author = Author.objects.create(name='author') + self.author.publish() + + self.failUnless(2, Author.objects.count()) + + # make sure the publishable filter spec + # gets used when we use a publishable field + class dummy_request(object): + GET = {} + + spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) + + lookup_choices = spec.lookup_choices + self.failUnlessEqual(1, len(lookup_choices)) + pk, label = lookup_choices[0] + self.failUnlessEqual(self.author.id, pk) \ No newline at end of file diff --git a/publish/tests/test_undelete_selected.py b/publish/tests/test_undelete_selected.py new file mode 100644 index 0000000..fda2947 --- /dev/null +++ b/publish/tests/test_undelete_selected.py @@ -0,0 +1,52 @@ +from django.contrib.admin import AdminSite +from django.core.exceptions import PermissionDenied +from django.test import TestCase +from publish.actions import undelete_selected +from publish.admin import PublishableAdmin +from publish.models import Publishable +from publish.tests.example_app.models import FlatPage + +__author__ = 'petry' + + +class TestUndeleteSelected(TestCase): + + def setUp(self): + super(TestUndeleteSelected, self).setUp() + self.fp1 = FlatPage.objects.create(url='/fp1', title='FP1') + + self.fp1.publish() + + self.admin_site = AdminSite('Test Admin') + self.page_admin = PublishableAdmin(FlatPage, self.admin_site) + + def test_undelete_selected(self): + class dummy_request(object): + + class user(object): + @classmethod + def has_perm(cls, *arg): + return True + + self.fp1.delete() + self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) + + response = undelete_selected(self.page_admin, dummy_request, FlatPage.objects.deleted()) + self.failUnless(response is None) + + # publish state should no longer be delete + fp1 = FlatPage.objects.get(pk=self.fp1.pk) + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, fp1.publish_state) + + def test_undelete_selected_no_permission(self): + class dummy_request(object): + + class user(object): + @classmethod + def has_perm(cls, *arg): + return False + + self.fp1.delete() + self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) + + self.assertRaises(PermissionDenied, undelete_selected, self.page_admin, dummy_request, FlatPage.objects.deleted()) \ No newline at end of file diff --git a/publish/tests/test_zzzz.py b/publish/tests/test_zzzz.py deleted file mode 100644 index a79c258..0000000 --- a/publish/tests/test_zzzz.py +++ /dev/null @@ -1,1492 +0,0 @@ -from django.conf import settings -from publish.tests import settings_for_test -from publish.tests.helpers import _get_rendered_content - -settings.configure(settings_for_test) - -from django.core.management import call_command -call_command('syncdb', interactive=False) - -from django.test import TestCase - -from publish.models import Publishable, PublishException -from publish.tests.example_app.models import FlatPage, Page, Comment, PageBlock, Tag, PageTagOrder, Author, Site, update_pub_date - -from django.contrib.admin.sites import AdminSite -from django.contrib.auth.models import User -from django.forms.models import ModelChoiceField, ModelMultipleChoiceField -from django.conf.urls.defaults import * -from django.core.exceptions import PermissionDenied -from django.http import Http404 - -from publish.admin import PublishableAdmin, PublishableStackedInline -from publish.actions import publish_selected, delete_selected, _convert_all_published_to_html, undelete_selected -from publish.utils import NestedSet -from publish.signals import pre_publish, post_publish -from publish.filters import PublishableRelatedFieldListFilter, FieldListFilter - - -class TestNestedSet(TestCase): - - def setUp(self): - super(TestNestedSet, self).setUp() - self.nested = NestedSet() - - def test_len(self): - self.failUnlessEqual(0, len(self.nested)) - self.nested.add('one') - self.failUnlessEqual(1, len(self.nested)) - self.nested.add('two') - self.failUnlessEqual(2, len(self.nested)) - self.nested.add('one2', parent='one') - self.failUnlessEqual(3, len(self.nested)) - - def test_contains(self): - self.failIf('one' in self.nested) - self.nested.add('one') - self.failUnless('one' in self.nested) - self.nested.add('one2', parent='one') - self.failUnless('one2' in self.nested) - - def test_nested_items(self): - self.failUnlessEqual([], self.nested.nested_items()) - self.nested.add('one') - self.failUnlessEqual(['one'], self.nested.nested_items()) - self.nested.add('two') - self.nested.add('one2', parent='one') - self.failUnlessEqual(['one', ['one2'], 'two'], self.nested.nested_items()) - self.nested.add('one2-1', parent='one2') - self.nested.add('one2-2', parent='one2') - self.failUnlessEqual(['one', ['one2', ['one2-1', 'one2-2']], 'two'], self.nested.nested_items()) - - def test_iter(self): - self.failUnlessEqual(set(), set(self.nested)) - - self.nested.add('one') - self.failUnlessEqual(set(['one']), set(self.nested)) - - self.nested.add('two', parent='one') - self.failUnlessEqual(set(['one', 'two']), set(self.nested)) - - items = set(['one', 'two']) - - for item in self.nested: - self.failUnless(item in items) - items.remove(item) - - self.failUnlessEqual(set(), items) - - def test_original(self): - class MyObject(object): - def __init__(self, obj): - self.obj = obj - - def __eq__(self, other): - return self.obj == other.obj - - def __hash__(self): - return hash(self.obj) - - # should always return an item at least - self.failUnlessEqual(MyObject('hi there'), self.nested.original(MyObject('hi there'))) - - m1 = MyObject('m1') - self.nested.add(m1) - - self.failUnlessEqual(id(m1), id(self.nested.original(m1))) - self.failUnlessEqual(id(m1), id(self.nested.original(MyObject('m1')))) - - -class TestBasicPublishable(TestCase): - - def setUp(self): - super(TestBasicPublishable, self).setUp() - self.flat_page = FlatPage(url='/my-page', title='my page', - content='here is some content', - enable_comments=False, - registration_required=True) - - def test_get_public_absolute_url(self): - self.failUnlessEqual('/my-page*', self.flat_page.get_absolute_url()) - # public absolute url doesn't exist until published - self.assertTrue(self.flat_page.get_public_absolute_url() is None) - self.flat_page.save() - self.flat_page.publish() - self.failUnlessEqual('/my-page', self.flat_page.get_public_absolute_url()) - - def test_save_marks_changed(self): - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) - self.flat_page.save(mark_changed=False) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) - self.flat_page.save() - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) - - def test_publish_excludes_fields(self): - self.flat_page.save() - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failIfEqual(self.flat_page.id, self.flat_page.public.id) - self.failUnless(self.flat_page.public.is_public) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.public.publish_state) - - def test_publish_check_is_not_public(self): - try: - self.flat_page.is_public = True - self.flat_page.publish() - self.fail("Should not be able to publish public models") - except PublishException: - pass - - def test_publish_check_has_id(self): - try: - self.flat_page.publish() - self.fail("Should not be able to publish unsaved models") - except PublishException: - pass - - def test_publish_simple_fields(self): - self.flat_page.save() - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) - self.failIf(self.flat_page.public) # should not be a public version yet - - self.flat_page.publish() - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) - self.failUnless(self.flat_page.public) - - for field in 'url', 'title', 'content', 'enable_comments', 'registration_required': - self.failUnlessEqual(getattr(self.flat_page, field), getattr(self.flat_page.public, field)) - - def test_published_simple_field_repeated(self): - self.flat_page.save() - self.flat_page.publish() - - public = self.flat_page.public - self.failUnless(public) - - self.flat_page.title = 'New Title' - self.flat_page.save() - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) - - self.failUnlessEqual(public, self.flat_page.public) - self.failIfEqual(public.title, self.flat_page.title) - - self.flat_page.publish() - self.failUnlessEqual(public, self.flat_page.public) - self.failUnlessEqual(public.title, self.flat_page.title) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) - - def test_publish_records_published(self): - all_published = NestedSet() - self.flat_page.save() - self.flat_page.publish(all_published=all_published) - self.failUnlessEqual(1, len(all_published)) - self.failUnless(self.flat_page in all_published) - self.failUnless(self.flat_page.public) - - def test_publish_dryrun(self): - all_published = NestedSet() - self.flat_page.save() - self.flat_page.publish(dry_run=True, all_published=all_published) - self.failUnlessEqual(1, len(all_published)) - self.failUnless(self.flat_page in all_published) - self.failIf(self.flat_page.public) - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) - - def test_delete_after_publish(self): - self.flat_page.save() - self.flat_page.publish() - public = self.flat_page.public - self.failUnless(public) - - self.flat_page.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.flat_page.publish_state) - - self.failUnlessEqual(set([self.flat_page, self.flat_page.public]), set(FlatPage.objects.all())) - - def test_delete_before_publish(self): - self.flat_page.save() - self.flat_page.delete() - self.failUnlessEqual([], list(FlatPage.objects.all())) - - def test_publish_deletions(self): - self.flat_page.save() - self.flat_page.publish() - public = self.flat_page.public - - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) - - self.flat_page.delete() - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) - - self.flat_page.publish() - self.failUnlessEqual([], list(FlatPage.objects.all())) - - def test_publish_deletions_checks_all_published(self): - # make sure publish_deletions looks at all_published arg - # to see if we need to actually publish the deletion - self.flat_page.save() - self.flat_page.publish() - public = self.flat_page.public - - self.flat_page.delete() - - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) - - # this should effectively stop the deletion happening - all_published = NestedSet() - all_published.add(self.flat_page) - - self.flat_page.publish(all_published=all_published) - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) - - -class TestPublishableManager(TestCase): - - def setUp(self): - super(TestCase, self).setUp() - self.flat_page1 = FlatPage.objects.create(url='/url1/', title='title 1') - self.flat_page2 = FlatPage.objects.create(url='/url2/', title='title 2') - - def test_all(self): - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.all())) - - # publishing will produce extra copies - self.flat_page1.publish() - self.failUnlessEqual(3, FlatPage.objects.count()) - - self.flat_page2.publish() - self.failUnlessEqual(4, FlatPage.objects.count()) - - - def test_changed(self): - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.changed())) - - self.flat_page1.publish() - self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.changed())) - - self.flat_page2.publish() - self.failUnlessEqual([], list(FlatPage.objects.changed())) - - def test_draft(self): - # draft should stay the same pretty much always - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) - - self.flat_page1.publish() - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) - - self.flat_page2.publish() - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) - - self.flat_page2.delete() - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft())) - - - def test_published(self): - self.failUnlessEqual([], list(FlatPage.objects.published())) - - self.flat_page1.publish() - self.failUnlessEqual([self.flat_page1.public], list(FlatPage.objects.published())) - - self.flat_page2.publish() - self.failUnlessEqual([self.flat_page1.public, self.flat_page2.public], list(FlatPage.objects.published())) - - def test_deleted(self): - self.failUnlessEqual([], list(FlatPage.objects.deleted())) - - self.flat_page1.publish() - self.failUnlessEqual([], list(FlatPage.objects.deleted())) - - self.flat_page1.delete() - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) - - def test_draft_and_deleted(self): - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) - - self.flat_page1.publish() - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft())) - - self.flat_page1.delete() - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) - self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.draft())) - - - def test_delete(self): - # delete is overriden, so it marks the public instances - self.flat_page1.publish() - public1 = self.flat_page1.public - - FlatPage.objects.draft().delete() - - self.failUnlessEqual([], list(FlatPage.objects.draft())) - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) - self.failUnlessEqual([public1], list(FlatPage.objects.published())) - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft_and_deleted())) - - def test_publish(self): - self.failUnlessEqual([], list(FlatPage.objects.published())) - - FlatPage.objects.draft().publish() - - flat_page1 = FlatPage.objects.get(id=self.flat_page1.id) - flat_page2 = FlatPage.objects.get(id=self.flat_page2.id) - - self.failUnlessEqual(set([flat_page1.public, flat_page2.public]), set(FlatPage.objects.published())) - - -class TestPublishableManyToMany(TestCase): - - def setUp(self): - super(TestPublishableManyToMany, self).setUp() - self.flat_page = FlatPage.objects.create( - url='/my-page', title='my page', - content='here is some content', - enable_comments=False, - registration_required=True) - self.site1 = Site.objects.create(title='my site', domain='mysite.com') - self.site2 = Site.objects.create(title='a site', domain='asite.com') - - def test_publish_no_sites(self): - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([], list(self.flat_page.public.sites.all())) - - def test_publish_add_site(self): - self.flat_page.sites.add(self.site1) - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) - - def test_publish_repeated_add_site(self): - self.flat_page.sites.add(self.site1) - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) - - self.flat_page.sites.add(self.site2) - self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) - - self.flat_page.publish() - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - def test_publish_remove_site(self): - self.flat_page.sites.add(self.site1, self.site2) - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - self.flat_page.sites.remove(self.site1) - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - self.flat_page.publish() - self.failUnlessEqual([self.site2], list(self.flat_page.public.sites.all())) - - def test_publish_clear_sites(self): - self.flat_page.sites.add(self.site1, self.site2) - self.flat_page.publish() - self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - self.flat_page.sites.clear() - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) - - self.flat_page.publish() - self.failUnlessEqual([], list(self.flat_page.public.sites.all())) - - def test_publish_sites_cleared_not_deleted(self): - self.flat_page.sites.add(self.site1, self.site2) - self.flat_page.publish() - self.flat_page.sites.clear() - self.flat_page.publish() - - self.failUnlessEqual([], list(self.flat_page.public.sites.all())) - - self.failIfEqual([], list(Site.objects.all())) - - -class TestPublishableRecursiveForeignKey(TestCase): - - def setUp(self): - super(TestPublishableRecursiveForeignKey, self).setUp() - self.page1 = Page.objects.create(slug='page1', title='page 1', content='some content') - self.page2 = Page.objects.create(slug='page2', title='page 2', content='other content', parent=self.page1) - - def test_publish_parent(self): - # this shouldn't publish the child page - self.page1.publish() - self.failUnless(self.page1.public) - self.failIf(self.page1.public.parent) - - page2 = Page.objects.get(id=self.page2.id) - self.failIf(page2.public) - - def test_publish_child_parent_already_published(self): - self.page1.publish() - self.page2.publish() - - self.failUnless(self.page1.public) - self.failUnless(self.page2.public) - - self.failIf(self.page1.public.parent) - self.failUnless(self.page2.public.parent) - - self.failIfEqual(self.page1, self.page2.public.parent) - - self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) - self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) - - def test_publish_child_parent_not_already_published(self): - self.page2.publish() - - page1 = Page.objects.get(id=self.page1.id) - self.failUnless(page1.public) - self.failUnless(self.page2.public) - - self.failIf(page1.public.parent) - self.failUnless(self.page2.public.parent) - - self.failIfEqual(page1, self.page2.public.parent) - - self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) - self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) - - def test_publish_repeated(self): - self.page1.publish() - self.page2.publish() - - self.page1.slug='main' - self.page1.save() - - self.failUnlessEqual('/main/', self.page1.get_absolute_url()) - - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - self.failUnlessEqual('/page1/', page1.public.get_absolute_url()) - self.failUnlessEqual('/page1/page2/', page2.public.get_absolute_url()) - - page1.publish() - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - self.failUnlessEqual('/main/', page1.public.get_absolute_url()) - self.failUnlessEqual('/main/page2/', page2.public.get_absolute_url()) - - page1.slug='elsewhere' - page1.save() - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - page2.slug='meanwhile' - page2.save() - page2.publish() - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - - # only page2 should be published, not page1, as page1 already published - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page2.publish_state) - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, page1.publish_state) - - self.failUnlessEqual('/main/', page1.public.get_absolute_url()) - self.failUnlessEqual('/main/meanwhile/', page2.public.get_absolute_url()) - - page1.publish() - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page2.publish_state) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page1.publish_state) - - self.failUnlessEqual('/elsewhere/', page1.public.get_absolute_url()) - self.failUnlessEqual('/elsewhere/meanwhile/', page2.public.get_absolute_url()) - - def test_publish_deletions(self): - self.page1.publish() - self.page2.publish() - - self.page2.delete() - self.failUnlessEqual([self.page2], list(Page.objects.deleted())) - - self.page2.publish() - self.failUnlessEqual([self.page1.public], list(Page.objects.published())) - self.failUnlessEqual([], list(Page.objects.deleted())) - - def test_publish_reverse_fields(self): - page_block = PageBlock.objects.create(page=self.page1, content='here we are') - - self.page1.publish() - - public = self.page1.public - self.failUnless(public) - - blocks = list(public.pageblock_set.all()) - self.failUnlessEqual(1, len(blocks)) - self.failUnlessEqual(page_block.content, blocks[0].content) - - def test_publish_deletions_reverse_fields(self): - page_block = PageBlock.objects.create(page=self.page1, content='here we are') - - self.page1.publish() - public = self.page1.public - self.failUnless(public) - - self.page1.delete() - - self.failUnlessEqual([self.page1], list(Page.objects.deleted())) - - self.page1.publish() - self.failUnlessEqual([], list(Page.objects.deleted())) - self.failUnlessEqual([], list(Page.objects.all())) - - def test_publish_reverse_fields_deleted(self): - # make sure child elements get removed - page_block = PageBlock.objects.create(page=self.page1, content='here we are') - - self.page1.publish() - - public = self.page1.public - page_block = PageBlock.objects.get(id=page_block.id) - page_block_public = page_block.public - self.failIf(page_block_public is None) - - self.failUnlessEqual([page_block_public], list(public.pageblock_set.all())) - - # now delete the page block and publish the parent - # to make sure that deletion gets copied over properly - page_block.delete() - page1 = Page.objects.get(id=self.page1.id) - page1.publish() - public = page1.public - - self.failUnlessEqual([], list(public.pageblock_set.all())) - - def test_publish_delections_with_non_publishable_children(self): - self.page1.publish() - - comment = Comment.objects.create(page=self.page1.public, comment='This is a comment') - - self.failUnlessEqual(1, Comment.objects.count()) - - self.page1.delete() - - self.failUnlessEqual([self.page1], list(Page.objects.deleted())) - self.failIf(self.page1 in Page.objects.draft()) - - self.page1.publish() - self.failUnlessEqual([], list(Page.objects.deleted())) - self.failUnlessEqual([], list(Page.objects.all())) - self.failUnlessEqual([], list(Comment.objects.all())) - - -class TestPublishableRecursiveManyToManyField(TestCase): - - def setUp(self): - super(TestPublishableRecursiveManyToManyField, self).setUp() - self.page = Page.objects.create(slug='page1', title='page 1', content='some content') - self.author1 = Author.objects.create(name='author1', profile='a profile') - self.author2 = Author.objects.create(name='author2', profile='something else') - - def test_publish_add_author(self): - self.page.authors.add(self.author1) - self.page.publish() - self.failUnless(self.page.public) - - author1 = Author.objects.get(id=self.author1.id) - self.failUnless(author1.public) - self.failIfEqual(author1.id, author1.public.id) - self.failUnlessEqual(author1.name, author1.public.name) - self.failUnlessEqual(author1.profile, author1.public.profile) - - self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) - - def test_publish_repeated_add_author(self): - self.page.authors.add(self.author1) - self.page.publish() - - self.failUnless(self.page.public) - - self.page.authors.add(self.author2) - author1 = Author.objects.get(id=self.author1.id) - self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) - - self.page.publish() - author1 = Author.objects.get(id=self.author1.id) - author2 = Author.objects.get(id=self.author2.id) - self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) - - def test_publish_clear_authors(self): - self.page.authors.add(self.author1, self.author2) - self.page.publish() - - author1 = Author.objects.get(id=self.author1.id) - author2 = Author.objects.get(id=self.author2.id) - self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) - - self.page.authors.clear() - self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) - - self.page.publish() - self.failUnlessEqual([], list(self.page.public.authors.all())) - - -class TestInfiniteRecursion(TestCase): - - def setUp(self): - super(TestInfiniteRecursion, self).setUp() - - self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2', parent=self.page1) - self.page1.parent = self.page2 - self.page1.save() - - def test_publish_recursion_breaks(self): - self.page1.publish() # this should simple run without an error - - -class TestOverlappingPublish(TestCase): - - def setUp(self): - self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2') - self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') - self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') - self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') - - def test_publish_with_overlapping_models(self): - # make sure when we publish we don't accidentally create - # multiple published versions - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(0, Page.objects.published().count()) - - Page.objects.draft().publish() - - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(5, Page.objects.published().count()) - - def test_publish_with_overlapping_models_published(self): - # make sure when we publish we don't accidentally create - # multiple published versions - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(0, Page.objects.published().count()) - - all_published = NestedSet() - Page.objects.draft().publish(all_published) - - self.failUnlessEqual(5, len(all_published)) - - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(5, Page.objects.published().count()) - - def test_publish_after_dry_run_handles_caching(self): - # if we do a dry tun publish in the same queryset - # before publishing for real, we have to make - # sure we don't run into issues with the instance - # caching parent's as None - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(0, Page.objects.published().count()) - - draft = Page.objects.draft() - - all_published = NestedSet() - for p in draft: - p.publish(dry_run=True, all_published=all_published) - - # nothing published yet - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(0, Page.objects.published().count()) - - # now publish (using same queryset, as this will have cached the instances) - draft.publish() - - self.failUnlessEqual(5, Page.objects.draft().count()) - self.failUnlessEqual(5, Page.objects.published().count()) - - # now actually check the public parent's are setup right - page1 = Page.objects.get(id=self.page1.id) - page2 = Page.objects.get(id=self.page2.id) - child1 = Page.objects.get(id=self.child1.id) - child2 = Page.objects.get(id=self.child2.id) - child3 = Page.objects.get(id=self.child3.id) - - self.failUnlessEqual(None, page1.public.parent) - self.failUnlessEqual(None, page2.public.parent) - self.failUnlessEqual(page1.public, child1.public.parent) - self.failUnlessEqual(page1.public, child2.public.parent) - self.failUnlessEqual(page2.public, child3.public.parent) - - -class TestPublishableAdmin(TestCase): - - def setUp(self): - super(TestPublishableAdmin, self).setUp() - self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2') - self.page1.publish() - self.page2.publish() - - self.author1 = Author.objects.create(name='a1') - self.author2 = Author.objects.create(name='a2') - self.author1.publish() - self.author2.publish() - - self.admin_site = AdminSite('Test Admin') - - class PageBlockInline(PublishableStackedInline): - model = PageBlock - - class PageAdmin(PublishableAdmin): - inlines = [PageBlockInline] - - self.admin_site.register(Page, PageAdmin) - self.page_admin = PageAdmin(Page, self.admin_site) - - # override urls, so reverse works - settings.ROOT_URLCONF=patterns('', - ('^admin/', include(self.admin_site.urls)), - ) - - def test_get_publish_status_display(self): - page = Page.objects.create(slug="hhkkk", title="hjkhjkh") - self.failUnlessEqual('Changed - not yet published', self.page_admin.get_publish_status_display(page)) - page.publish() - self.failUnlessEqual('Published', self.page_admin.get_publish_status_display(page)) - page.save() - self.failUnlessEqual('Changed', self.page_admin.get_publish_status_display(page)) - - page.delete() - self.failUnlessEqual('To be deleted', self.page_admin.get_publish_status_display(page)) - - def test_queryset(self): - # make sure we only get back draft objects - request = None - - self.failUnlessEqual( - set([self.page1, self.page1.public, self.page2, self.page2.public]), - set(Page.objects.all()) - ) - self.failUnlessEqual( - set([self.page1, self.page2]), - set(self.page_admin.queryset(request)) - ) - - def test_get_actions_global_delete_replaced(self): - from publish.actions import delete_selected - - class request(object): - GET = {} - - actions = self.page_admin.get_actions(request) - - - self.failUnless('delete_selected' in actions) - action, name, description = actions['delete_selected'] - self.failUnlessEqual(delete_selected, action) - self.failUnlessEqual('delete_selected', name) - self.failUnlessEqual(delete_selected.short_description, description) - - def test_formfield_for_foreignkey(self): - # foreign key forms fields in admin - # for publishable models should be filtered - # to hide public object - - request = None - parent_field = None - for field in Page._meta.fields: - if field.name == 'parent': - parent_field = field - break - self.failUnless(parent_field) - - choice_field = self.page_admin.formfield_for_foreignkey(parent_field, request) - self.failUnless(choice_field) - self.failUnless(isinstance(choice_field, ModelChoiceField)) - - self.failUnlessEqual( - set([self.page1, self.page1.public, self.page2, self.page2.public]), - set(Page.objects.all()) - ) - self.failUnlessEqual( - set([self.page1, self.page2]), - set(choice_field.queryset) - ) - - def test_formfield_for_manytomany(self): - request = None - authors_field = None - for field in Page._meta.many_to_many: - if field.name == 'authors': - authors_field = field - break - self.failUnless(authors_field) - - choice_field = self.page_admin.formfield_for_manytomany(authors_field, request) - self.failUnless(choice_field) - self.failUnless(isinstance(choice_field, ModelMultipleChoiceField)) - - self.failUnlessEqual( - set([self.author1, self.author1.public, self.author2, self.author2.public]), - set(Author.objects.all()) - ) - self.failUnlessEqual( - set([self.author1, self.author2]), - set(choice_field.queryset) - ) - - def test_has_change_permission(self): - class dummy_request(object): - method = 'GET' - REQUEST = {} - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - self.failUnless(self.page_admin.has_change_permission(dummy_request)) - self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) - self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1.public)) - - # can view deleted items - self.page1.publish_state = Publishable.PUBLISH_DELETE - self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) - - # but cannot modify them - dummy_request.method = 'POST' - self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1)) - - def test_has_delete_permission(self): - class dummy_request(object): - method = 'GET' - REQUEST = {} - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - self.failUnless(self.page_admin.has_delete_permission(dummy_request)) - self.failUnless(self.page_admin.has_delete_permission(dummy_request, self.page1)) - self.failIf(self.page_admin.has_delete_permission(dummy_request, self.page1.public)) - - def test_change_view_normal(self): - class dummy_request(object): - method = 'GET' - REQUEST = {} - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - response = self.page_admin.change_view(dummy_request, str(self.page1.id)) - self.failUnless(response is not None) - self.failIf('deleted' in _get_rendered_content(response)) - - def test_change_view_not_deleted(self): - class dummy_request(object): - method = 'GET' - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - try: - self.page_admin.change_view(dummy_request, unicode(self.page1.public.id)) - self.fail() - except Http404: - pass - - def test_change_view_deleted(self): - class dummy_request(object): - method = 'GET' - REQUEST = {} - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - class user(object): - @classmethod - def has_perm(cls, permission): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - self.page1.delete() - - response = self.page_admin.change_view(dummy_request, str(self.page1.id)) - self.failUnless(response is not None) - self.failUnless('deleted' in _get_rendered_content(response)) - - def test_change_view_deleted_POST(self): - class dummy_request(object): - csrf_processing_done = True # stop csrf check - method = 'POST' - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - self.page1.delete() - - try: - self.page_admin.change_view(dummy_request, str(self.page1.id)) - self.fail() - except PermissionDenied: - pass - - def test_change_view_delete_inline(self): - block = PageBlock.objects.create(page=self.page1, content='some content') - page1 = Page.objects.get(pk=self.page1.pk) - page1.publish() - - user1 = User.objects.create_user('test1', 'test@example.com', 'jkljkl') - - # fake selecting the delete tickbox for the block - - class dummy_request(object): - csrf_processing_done = True - method = 'POST' - - POST = { - 'slug': page1.slug, - 'title': page1.title, - 'content': page1.content, - 'pub_date_0': '2010-02-12', - 'pub_date_1': '17:40:00', - 'pageblock_set-TOTAL_FORMS': '2', - 'pageblock_set-INITIAL_FORMS': '1', - 'pageblock_set-0-id': str(block.id), - 'pageblock_set-0-page': str(page1.id), - 'pageblock_set-0-DELETE': 'yes' - } - REQUEST = POST - FILES = {} - COOKIES = {} - META = {} - - @classmethod - def is_ajax(cls): - return False - - @classmethod - def is_secure(cls): - return False - - class user(object): - pk = user1.pk - - @classmethod - def is_authenticated(self): - return True - - @classmethod - def has_perm(cls, permission): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - class message_set(object): - @classmethod - def create(cls, message=''): - pass - - class _messages(object): - @classmethod - def add(cls, *message): - pass - - - block = PageBlock.objects.get(id=block.id) - public_block = block.public - - response = self.page_admin.change_view(dummy_request, str(page1.id)) - self.assertEqual(302, response.status_code) - - # the block should have been deleted (but not the public one) - self.failUnlessEqual([public_block], list(PageBlock.objects.all())) - - -class TestPublishSelectedAction(TestCase): - - def setUp(self): - super(TestPublishSelectedAction, self).setUp() - self.fp1 = Page.objects.create(slug='fp1', title='FP1') - self.fp2 = Page.objects.create(slug='fp2', title='FP2') - self.fp3 = Page.objects.create(slug='fp3', title='FP3') - - self.admin_site = AdminSite('Test Admin') - self.page_admin = PublishableAdmin(Page, self.admin_site) - - # override urls, so reverse works - settings.ROOT_URLCONF=patterns('', - ('^admin/', include(self.admin_site.urls)), - ) - - def test_publish_selected_confirm(self): - pages = Page.objects.exclude(id=self.fp3.id) - - class dummy_request(object): - META = {} - POST = {} - - class user(object): - @classmethod - def has_perm(cls, *arg): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - response = publish_selected(self.page_admin, dummy_request, pages) - - self.failIf(Page.objects.published().count() > 0) - self.failUnless(response is not None) - self.failUnlessEqual(200, response.status_code) - - def test_publish_selected_confirmed(self): - pages = Page.objects.exclude(id=self.fp3.id) - - class dummy_request(object): - POST = {'post': True} - - class user(object): - @classmethod - def is_authenticated(cls): - return True - - @classmethod - def has_perm(cls, *arg): - return True - - class message_set(object): - @classmethod - def create(cls, message=None): - self._message = message - - class _messages(object): - @classmethod - def add(cls, *message): - self._message = message - - - response = publish_selected(self.page_admin, dummy_request, pages) - - - self.failUnlessEqual(2, Page.objects.published().count()) - self.failUnless( getattr(self, '_message', None) is not None ) - self.failUnless( response is None ) - - def test_convert_all_published_to_html(self): - self.admin_site.register(Page, PublishableAdmin) - - all_published = NestedSet() - - page = Page.objects.create(slug='here', title='title') - block = PageBlock.objects.create(page=page, content='stuff here') - - all_published.add(page) - all_published.add(block, parent=page) - - converted = _convert_all_published_to_html(self.admin_site, all_published) - - expected = [u'Page: here (Changed - not yet published)' % page.id, [u'Page block: PageBlock object']] - - self.failUnlessEqual(expected, converted) - - def test_publish_selected_does_not_have_permission(self): - self.admin_site.register(Page, PublishableAdmin) - pages = Page.objects.exclude(id=self.fp3.id) - - class dummy_request(object): - POST = {} - META = {} - - class user(object): - @classmethod - def has_perm(cls, *arg): - return False - - @classmethod - def get_and_delete_messages(cls): - return [] - - response = publish_selected(self.page_admin, dummy_request, pages) - self.failIf(response is None) - # publish button should not be in response - self.failIf('value="publish_selected"' in response.content) - self.failIf('value="Yes, Publish"' in response.content) - self.failIf('form' in response.content) - - self.failIf(Page.objects.published().count() > 0) - - def test_publish_selected_does_not_have_related_permission(self): - # check we can't publish when we don't have permission - # for a related model (in this case authors) - self.admin_site.register(Author, PublishableAdmin) - - author = Author.objects.create(name='John') - self.fp1.authors.add(author) - - pages = Page.objects.draft() - - class dummy_request(object): - POST = { 'post': True } - - class _messages(object): - @classmethod - def add(cls, *args): - return 'message' - - class user(object): - pk = 1 - - @classmethod - def is_authenticated(cls): - return True - - @classmethod - def has_perm(cls, perm): - return perm != 'example_app.publish_author' - - try: - publish_selected(self.page_admin, dummy_request, pages) - - self.fail() - except PermissionDenied: - pass - - self.failIf(Page.objects.published().count() > 0) - - def test_publish_selected_logs_publication(self): - self.admin_site.register(Page, PublishableAdmin) - - pages = Page.objects.exclude(id=self.fp3.id) - - class dummy_request(object): - POST = { 'post': True } - - class user(object): - pk = 1 - - @classmethod - def is_authenticated(cls): - return True - - @classmethod - def has_perm(cls, perm): - return perm != 'example_app.publish_author' - - class message_set(object): - @classmethod - def create(cls, message=None): - pass - - class _messages(object): - @classmethod - def add(cls, *message): - pass - - publish_selected(self.page_admin, dummy_request, pages) - - # should have logged two publications - from django.contrib.admin.models import LogEntry - from django.contrib.contenttypes.models import ContentType - - content_type_id = ContentType.objects.get_for_model(self.fp1).pk - self.failUnlessEqual(2, LogEntry.objects.filter().count()) - - -class TestDeleteSelected(TestCase): - - def setUp(self): - super(TestDeleteSelected, self).setUp() - self.fp1 = FlatPage.objects.create(url='/fp1', title='FP1') - self.fp2 = FlatPage.objects.create(url='/fp2', title='FP2') - self.fp3 = FlatPage.objects.create(url='/fp3', title='FP3') - - self.fp1.publish() - self.fp2.publish() - self.fp3.publish() - - self.admin_site = AdminSite('Test Admin') - self.page_admin = PublishableAdmin(FlatPage, self.admin_site) - - # override urls, so reverse works - settings.ROOT_URLCONF=patterns('', - ('^admin/', include(self.admin_site.urls)), - ) - - def test_delete_selected_check_cannot_delete_public(self): - # delete won't work (via admin) for public instances - request = None - self.assertRaises(PermissionDenied, delete_selected, self.page_admin, request, FlatPage.objects.published()) - - def test_delete_selected(self): - class dummy_request(object): - POST = {} - META = {} - - class user(object): - @classmethod - def has_perm(cls, *arg): - return True - - @classmethod - def get_and_delete_messages(cls): - return [] - - response = delete_selected(self.page_admin, dummy_request, FlatPage.objects.draft()) - self.failUnless(response is not None) - - -class TestUndeleteSelected(TestCase): - - def setUp(self): - super(TestUndeleteSelected, self).setUp() - self.fp1 = FlatPage.objects.create(url='/fp1', title='FP1') - - self.fp1.publish() - - self.admin_site = AdminSite('Test Admin') - self.page_admin = PublishableAdmin(FlatPage, self.admin_site) - - def test_undelete_selected(self): - class dummy_request(object): - - class user(object): - @classmethod - def has_perm(cls, *arg): - return True - - self.fp1.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) - - response = undelete_selected(self.page_admin, dummy_request, FlatPage.objects.deleted()) - self.failUnless(response is None) - - # publish state should no longer be delete - fp1 = FlatPage.objects.get(pk=self.fp1.pk) - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, fp1.publish_state) - - def test_undelete_selected_no_permission(self): - class dummy_request(object): - - class user(object): - @classmethod - def has_perm(cls, *arg): - return False - - self.fp1.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) - - self.assertRaises(PermissionDenied, undelete_selected, self.page_admin, dummy_request, FlatPage.objects.deleted()) - - -class TestManyToManyThrough(TestCase): - - def setUp(self): - super(TestManyToManyThrough, self).setUp() - self.page = Page.objects.create(slug='p1', title='P 1') - self.tag1 = Tag.objects.create(slug='tag1', title='Tag 1') - self.tag2 = Tag.objects.create(slug='tag2', title='Tag 2') - PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag1, tag_order=2) - PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag2, tag_order=1) - - def test_publish_copies_tags(self): - self.page.publish() - - self.failUnlessEqual(set([self.tag1, self.tag2]), set(self.page.public.tags.all())) - - -class TestPublishFunction(TestCase): - - def setUp(self): - super(TestPublishFunction, self).setUp() - self.page = Page.objects.create(slug='page', title='Page') - - def test_publish_function_invoked(self): - # check we can override default copy behaviour - - from datetime import datetime - - pub_date = datetime(2000, 1, 1) - update_pub_date.pub_date = pub_date - - self.failIfEqual(pub_date, self.page.pub_date) - - self.page.publish() - self.failIfEqual(pub_date, self.page.pub_date) - self.failUnlessEqual(pub_date, self.page.public.pub_date) - - -class TestPublishSignals(TestCase): - - def setUp(self): - self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2') - self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') - self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') - self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') - - self.failUnlessEqual(5, Page.objects.draft().count()) - - def _check_pre_publish(self, queryset): - pre_published = [] - def pre_publish_handler(sender, instance, **kw): - pre_published.append(instance) - - pre_publish.connect(pre_publish_handler, sender=Page) - - queryset.draft().publish() - - self.failUnlessEqual(queryset.draft().count(), len(pre_published)) - self.failUnlessEqual(set(queryset.draft()), set(pre_published)) - - def test_pre_publish(self): - # page order shouldn't matter when publishing - # should always get the right number of signals - self._check_pre_publish(Page.objects.order_by('id')) - self._check_pre_publish(Page.objects.order_by('-id')) - self._check_pre_publish(Page.objects.order_by('?')) - - def _check_post_publish(self, queryset): - published = [] - def post_publish_handler(sender, instance, **kw): - published.append(instance) - - post_publish.connect(post_publish_handler, sender=Page) - - queryset.draft().publish() - - self.failUnlessEqual(queryset.draft().count(), len(published)) - self.failUnlessEqual(set(queryset.draft()), set(published)) - - def test_post_publish(self): - self._check_post_publish(Page.objects.order_by('id')) - self._check_post_publish(Page.objects.order_by('-id')) - self._check_post_publish(Page.objects.order_by('?')) - - def test_signals_sent_for_followed(self): - pre_published = [] - def pre_publish_handler(sender, instance, **kw): - pre_published.append(instance) - - pre_publish.connect(pre_publish_handler, sender=Page) - - published = [] - def post_publish_handler(sender, instance, **kw): - published.append(instance) - - post_publish.connect(post_publish_handler, sender=Page) - - # publishing just children will also publish it's parent (if needed) - # which should also fire signals - - self.child1.publish() - - self.failUnlessEqual(set([self.page1, self.child1]), set(pre_published)) - self.failUnlessEqual(set([self.page1, self.child1]), set(published)) - - def test_deleted_flag_false_when_publishing_change(self): - def pre_publish_handler(sender, instance, deleted, **kw): - self.failIf(deleted) - - pre_publish.connect(pre_publish_handler, sender=Page) - - def post_publish_handler(sender, instance, deleted, **kw): - self.failIf(deleted) - - post_publish.connect(post_publish_handler, sender=Page) - - self.page1.publish() - - def test_deleted_flag_true_when_publishing_deletion(self): - self.child1.publish() - public = self.child1.public - - self.child1.delete() - - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.child1.publish_state) - - def pre_publish_handler(sender, instance, deleted, **kw): - self.failUnless(deleted) - - pre_publish.connect(pre_publish_handler, sender=Page) - - def post_publish_handler(sender, instance, deleted, **kw): - self.failUnless(deleted) - - post_publish.connect(post_publish_handler, sender=Page) - - self.child1.publish() - - -class TestPublishableRelatedFilterSpec(TestCase): - - def test_overridden_spec(self): - # make sure the publishable filter spec - # gets used when we use a publishable field - class dummy_request(object): - GET = {} - - spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) - self.failUnless(isinstance(spec, PublishableRelatedFieldListFilter)) - - def test_only_draft_shown(self): - self.author = Author.objects.create(name='author') - self.author.publish() - - self.failUnless(2, Author.objects.count()) - - # make sure the publishable filter spec - # gets used when we use a publishable field - class dummy_request(object): - GET = {} - - spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) - - lookup_choices = spec.lookup_choices - self.failUnlessEqual(1, len(lookup_choices)) - pk, label = lookup_choices[0] - self.failUnlessEqual(self.author.id, pk) - From db879c5abaa2ad951c4bf5def4f12be58921041e Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 11:07:11 -0300 Subject: [PATCH 06/22] separated tests to easy to mantain them --- publish/tests/helpers.py | 3 --- publish/tests/test_basic_publishable.py | 3 --- publish/tests/test_delete_selected.py | 3 --- publish/tests/test_infinite_recursion.py | 2 +- publish/tests/test_many_to_many_through.py | 2 +- publish/tests/test_nested_set.py | 2 +- publish/tests/test_overlapping_publish.py | 2 +- publish/tests/test_publish_function.py | 2 +- publish/tests/test_publish_selected_action.py | 2 +- publish/tests/test_publish_signal.py | 2 +- publish/tests/test_publishable_admin.py | 2 +- publish/tests/test_publishable_manager.py | 2 +- publish/tests/test_publishable_many_to_many.py | 2 +- publish/tests/test_publishable_recursive_fk.py | 2 +- publish/tests/test_publishable_recursive_many_to_many_field.py | 2 +- publish/tests/test_publishable_related_filter_spec.py | 2 +- publish/tests/test_undelete_selected.py | 2 +- 17 files changed, 14 insertions(+), 23 deletions(-) diff --git a/publish/tests/helpers.py b/publish/tests/helpers.py index fd99458..e7eee94 100644 --- a/publish/tests/helpers.py +++ b/publish/tests/helpers.py @@ -1,6 +1,3 @@ -__author__ = 'petry' - - def _get_rendered_content(response): content = getattr(response, 'rendered_content', None) if content is not None: diff --git a/publish/tests/test_basic_publishable.py b/publish/tests/test_basic_publishable.py index 52693a8..35a7bad 100644 --- a/publish/tests/test_basic_publishable.py +++ b/publish/tests/test_basic_publishable.py @@ -3,9 +3,6 @@ from publish.tests.example_app.models import FlatPage from publish.utils import NestedSet -__author__ = 'petry' - - class TestBasicPublishable(TestCase): def setUp(self): diff --git a/publish/tests/test_delete_selected.py b/publish/tests/test_delete_selected.py index 2ad8670..54be1b2 100644 --- a/publish/tests/test_delete_selected.py +++ b/publish/tests/test_delete_selected.py @@ -7,9 +7,6 @@ from publish.admin import PublishableAdmin from publish.tests.example_app.models import FlatPage -__author__ = 'petry' - - class TestDeleteSelected(TestCase): def setUp(self): diff --git a/publish/tests/test_infinite_recursion.py b/publish/tests/test_infinite_recursion.py index b081c9a..ffee177 100644 --- a/publish/tests/test_infinite_recursion.py +++ b/publish/tests/test_infinite_recursion.py @@ -1,7 +1,7 @@ from django.test import TestCase from publish.tests.example_app.models import Page -__author__ = 'petry' + class TestInfiniteRecursion(TestCase): diff --git a/publish/tests/test_many_to_many_through.py b/publish/tests/test_many_to_many_through.py index 934d750..4efda22 100644 --- a/publish/tests/test_many_to_many_through.py +++ b/publish/tests/test_many_to_many_through.py @@ -1,7 +1,7 @@ from django.test import TestCase from publish.tests.example_app.models import Page, Tag, PageTagOrder -__author__ = 'petry' + class TestManyToManyThrough(TestCase): diff --git a/publish/tests/test_nested_set.py b/publish/tests/test_nested_set.py index 7cf01f2..c0a62cc 100644 --- a/publish/tests/test_nested_set.py +++ b/publish/tests/test_nested_set.py @@ -1,7 +1,7 @@ from django.test import TestCase from publish.utils import NestedSet -__author__ = 'petry' + class TestNestedSet(TestCase): diff --git a/publish/tests/test_overlapping_publish.py b/publish/tests/test_overlapping_publish.py index 55a0bbd..552b22e 100644 --- a/publish/tests/test_overlapping_publish.py +++ b/publish/tests/test_overlapping_publish.py @@ -2,7 +2,7 @@ from publish.tests.example_app.models import Page from publish.utils import NestedSet -__author__ = 'petry' + class TestOverlappingPublish(TestCase): diff --git a/publish/tests/test_publish_function.py b/publish/tests/test_publish_function.py index 9f7873f..95ba11d 100644 --- a/publish/tests/test_publish_function.py +++ b/publish/tests/test_publish_function.py @@ -1,7 +1,7 @@ from django.test import TestCase from publish.tests.example_app.models import Page, update_pub_date -__author__ = 'petry' + class TestPublishFunction(TestCase): diff --git a/publish/tests/test_publish_selected_action.py b/publish/tests/test_publish_selected_action.py index e2814b4..b215474 100644 --- a/publish/tests/test_publish_selected_action.py +++ b/publish/tests/test_publish_selected_action.py @@ -8,7 +8,7 @@ from publish.tests.example_app.models import Page, PageBlock, Author from publish.utils import NestedSet -__author__ = 'petry' + class TestPublishSelectedAction(TestCase): diff --git a/publish/tests/test_publish_signal.py b/publish/tests/test_publish_signal.py index 223ca69..3bad9ab 100644 --- a/publish/tests/test_publish_signal.py +++ b/publish/tests/test_publish_signal.py @@ -3,7 +3,7 @@ from publish.signals import pre_publish, post_publish from publish.tests.example_app.models import Page -__author__ = 'petry' + class TestPublishSignals(TestCase): diff --git a/publish/tests/test_publishable_admin.py b/publish/tests/test_publishable_admin.py index 466ae7b..276c531 100644 --- a/publish/tests/test_publishable_admin.py +++ b/publish/tests/test_publishable_admin.py @@ -11,7 +11,7 @@ from publish.tests.example_app.models import Page, Author, PageBlock from publish.tests.helpers import _get_rendered_content -__author__ = 'petry' + class TestPublishableAdmin(TestCase): diff --git a/publish/tests/test_publishable_manager.py b/publish/tests/test_publishable_manager.py index 32ca9fd..2d8fb9b 100644 --- a/publish/tests/test_publishable_manager.py +++ b/publish/tests/test_publishable_manager.py @@ -1,7 +1,7 @@ from django.test import TestCase from publish.tests.example_app.models import FlatPage -__author__ = 'petry' + class TestPublishableManager(TestCase): diff --git a/publish/tests/test_publishable_many_to_many.py b/publish/tests/test_publishable_many_to_many.py index 800a934..334745b 100644 --- a/publish/tests/test_publishable_many_to_many.py +++ b/publish/tests/test_publishable_many_to_many.py @@ -1,7 +1,7 @@ from django.test import TestCase from publish.tests.example_app.models import FlatPage, Site -__author__ = 'petry' + class TestPublishableManyToMany(TestCase): diff --git a/publish/tests/test_publishable_recursive_fk.py b/publish/tests/test_publishable_recursive_fk.py index 9f3f24a..7086b59 100644 --- a/publish/tests/test_publishable_recursive_fk.py +++ b/publish/tests/test_publishable_recursive_fk.py @@ -2,7 +2,7 @@ from publish.models import Publishable from publish.tests.example_app.models import Page, PageBlock, Comment -__author__ = 'petry' + class TestPublishableRecursiveForeignKey(TestCase): diff --git a/publish/tests/test_publishable_recursive_many_to_many_field.py b/publish/tests/test_publishable_recursive_many_to_many_field.py index 84e8b56..06d6b9b 100644 --- a/publish/tests/test_publishable_recursive_many_to_many_field.py +++ b/publish/tests/test_publishable_recursive_many_to_many_field.py @@ -1,7 +1,7 @@ from django.test import TestCase from publish.tests.example_app.models import Page, Author -__author__ = 'petry' + class TestPublishableRecursiveManyToManyField(TestCase): diff --git a/publish/tests/test_publishable_related_filter_spec.py b/publish/tests/test_publishable_related_filter_spec.py index 08b4592..f0bac83 100644 --- a/publish/tests/test_publishable_related_filter_spec.py +++ b/publish/tests/test_publishable_related_filter_spec.py @@ -3,7 +3,7 @@ from publish.filters import FieldListFilter, PublishableRelatedFieldListFilter from publish.tests.example_app.models import Page, Author -__author__ = 'petry' + class TestPublishableRelatedFilterSpec(TestCase): diff --git a/publish/tests/test_undelete_selected.py b/publish/tests/test_undelete_selected.py index fda2947..1633a5b 100644 --- a/publish/tests/test_undelete_selected.py +++ b/publish/tests/test_undelete_selected.py @@ -6,7 +6,7 @@ from publish.models import Publishable from publish.tests.example_app.models import FlatPage -__author__ = 'petry' + class TestUndeleteSelected(TestCase): From 88a00ad3c2875b00bc9dd23be054503cdcab4085 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 11:18:36 -0300 Subject: [PATCH 07/22] added travis --- .travis.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8edec24 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python + +python: + - 2.7 + +env: + - DJANGO_VERSION=1.4.5 + +install: + - make setup + - pip install django==$DJANGO_VERSION + +script: make test + From 5cd9d6b4db1a041102e14f12abb64c1ee11de61e Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 11:36:47 -0300 Subject: [PATCH 08/22] added travis badge --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index ca66a7e..eba1654 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,8 @@ Django Publish ============== +.. image:: https://travis-ci.org/petry/django-publish.png?branch=master + Handy mixin/abstract class for providing a "publisher workflow" to arbitrary Django_ models. Overview From af835c89b397b65767157715d33f1a73077004fd Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 14:02:24 -0300 Subject: [PATCH 09/22] pep8 compliance --- README.rst | 1 + examplecms/pubcms/admin.py | 5 +- examplecms/pubcms/models.py | 12 +- examplecms/pubcms/urls.py | 11 +- examplecms/pubcms/views.py | 7 +- examplecms/settings.py | 40 +--- examplecms/urls.py | 12 +- publish/actions.py | 56 ++++-- publish/admin.py | 80 +++++--- publish/filters.py | 37 ++-- publish/models.py | 188 ++++++++++-------- publish/signals.py | 6 +- publish/tests/example_app/models.py | 21 +- publish/tests/helpers.py | 2 +- publish/tests/settings_for_test.py | 5 - publish/tests/test_basic_publishable.py | 59 ++++-- publish/tests/test_delete_selected.py | 16 +- publish/tests/test_infinite_recursion.py | 8 +- publish/tests/test_many_to_many_through.py | 12 +- publish/tests/test_nested_set.py | 14 +- publish/tests/test_overlapping_publish.py | 17 +- publish/tests/test_publish_function.py | 5 +- publish/tests/test_publish_selected_action.py | 28 +-- publish/tests/test_publish_signal.py | 28 ++- publish/tests/test_publishable_admin.py | 72 ++++--- publish/tests/test_publishable_manager.py | 60 +++--- .../tests/test_publishable_many_to_many.py | 40 ++-- .../tests/test_publishable_recursive_fk.py | 55 +++-- ...ublishable_recursive_many_to_many_field.py | 28 ++- .../test_publishable_related_filter_spec.py | 12 +- publish/tests/test_undelete_selected.py | 16 +- publish/utils.py | 14 +- setup.py | 12 +- tests/run_tests.sh | 3 - 34 files changed, 561 insertions(+), 421 deletions(-) delete mode 100755 tests/run_tests.sh diff --git a/README.rst b/README.rst index eba1654..d28f023 100644 --- a/README.rst +++ b/README.rst @@ -3,6 +3,7 @@ Django Publish ============== .. image:: https://travis-ci.org/petry/django-publish.png?branch=master +.. image:: https://codeq.io/github/petry/django-publish/badges/master.png Handy mixin/abstract class for providing a "publisher workflow" to arbitrary Django_ models. diff --git a/examplecms/pubcms/admin.py b/examplecms/pubcms/admin.py index 1122c72..7200c67 100644 --- a/examplecms/pubcms/admin.py +++ b/examplecms/pubcms/admin.py @@ -3,19 +3,22 @@ from publish.admin import PublishableAdmin, PublishableStackedInline from pubcms.models import Page, PageBlock, Category, Image + class PageBlockInlineAdmin(PublishableStackedInline): model = PageBlock extra = 1 + class PageAdmin(PublishableAdmin): inlines = [PageBlockInlineAdmin] prepopulated_fields = {"slug": ("title",)} list_filter = ['publish_state', 'categories'] + class CategoryAdmin(PublishableAdmin): prepopulated_fields = {"slug": ("name",)} + admin.site.register(Page, PageAdmin) admin.site.register(Category, CategoryAdmin) admin.site.register(Image, PublishableAdmin) - diff --git a/examplecms/pubcms/models.py b/examplecms/pubcms/models.py index c46e74a..032f1e8 100644 --- a/examplecms/pubcms/models.py +++ b/examplecms/pubcms/models.py @@ -2,16 +2,17 @@ from django.core.urlresolvers import reverse as reverse_url from publish.models import Publishable + class Page(Publishable): title = models.CharField(max_length=200) - slug = models.CharField(max_length=100, db_index=True) - + slug = models.CharField(max_length=100, db_index=True) + parent = models.ForeignKey('self', blank=True, null=True) categories = models.ManyToManyField('Category', blank=True) class PublishMeta(Publishable.PublishMeta): - publish_reverse_fields=['pageblock_set'] + publish_reverse_fields = ['pageblock_set'] def __unicode__(self): return self.title @@ -30,18 +31,21 @@ def get_absolute_url(self): else: return reverse_url('draft_page_detail', args=[url]) + class PageBlock(Publishable): page = models.ForeignKey(Page) content = models.TextField(blank=True) image = models.ForeignKey('Image', blank=True, null=True) + class Image(Publishable): title = models.CharField(max_length=100) image = models.ImageField(upload_to='images/') - + def __unicode__(self): return self.title + class Category(Publishable): name = models.CharField(max_length=200) slug = models.CharField(max_length=100, db_index=True) diff --git a/examplecms/pubcms/urls.py b/examplecms/pubcms/urls.py index f22bf5b..8227edb 100644 --- a/examplecms/pubcms/urls.py +++ b/examplecms/pubcms/urls.py @@ -4,7 +4,12 @@ from views import page_detail from models import Page -urlpatterns = patterns('', - url('^(?P.*)\*$', page_detail, { 'queryset': Page.objects.draft() }, name='draft_page_detail'), - url('^(?P.*)$', page_detail, { 'queryset': Page.objects.published() }, name='public_page_detail'), +urlpatterns = patterns( + '', + url('^(?P.*)\*$', page_detail, + {'queryset': Page.objects.draft()}, + name='draft_page_detail'), + url('^(?P.*)$', page_detail, + {'queryset': Page.objects.published()}, + name='public_page_detail'), ) diff --git a/examplecms/pubcms/views.py b/examplecms/pubcms/views.py index 746e38c..9fccc93 100644 --- a/examplecms/pubcms/views.py +++ b/examplecms/pubcms/views.py @@ -2,6 +2,7 @@ from models import Page + def page_detail(request, page_url, queryset): parts = page_url.split('/') parts.reverse() @@ -10,6 +11,6 @@ def page_detail(request, page_url, queryset): for slug in parts: filter_params[field] = slug field = 'parent__%s' % field - page = get_object_or_404(queryset,**filter_params) - - return render_to_response("pubcms/page_detail.html", { 'page': page }) + page = get_object_or_404(queryset, **filter_params) + + return render_to_response("pubcms/page_detail.html", {'page': page}) diff --git a/examplecms/settings.py b/examplecms/settings.py index b2e1003..b83841d 100644 --- a/examplecms/settings.py +++ b/examplecms/settings.py @@ -1,77 +1,52 @@ import os PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) -DEBUG = True +DEBUG = True TEMPLATE_DEBUG = DEBUG ADMINS = ( - # ('Your Name', 'your_email@domain.com'), ) MANAGERS = ADMINS DATABASE_ENGINE = 'sqlite3' DATABASE_NAME = os.path.join(PROJECT_PATH, 'example.db') -DATABASE_USER = '' # Not used with sqlite3. -DATABASE_PASSWORD = '' # Not used with sqlite3. -DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. -DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# If running in a Windows environment this must be set to the same as your -# system time zone. +DATABASE_USER = '' +DATABASE_PASSWORD = '' +DATABASE_HOST = '' +DATABASE_PORT = '' + TIME_ZONE = 'America/Chicago' -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' SITE_ID = 1 -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. USE_I18N = True -# Relative file structure #3 -# Absolute path to the directory that holds media. -# Example: "/home/media/media.lawrence.com/" -# MEDIA_ROOT = "Z:/development/agfecms/media/" MEDIA_ROOT = os.path.join(PROJECT_PATH, 'media') - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash if there is a path component (optional in other cases). -# Examples: "http://media.lawrence.com" MEDIA_URL = '/media/' -# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a -# trailing slash. -# Examples: "http://foo.com/media/", "/media/". ADMIN_MEDIA_PREFIX = '/admin_media/' -# Make this unique, and don't share it with anybody. SECRET_KEY = '+9qi8mm2ddo&wb@0s(@9wfdhxmpe2cxh!cb3@7yd9ao13-s6ea' -# List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.load_template_source', 'django.template.loaders.app_directories.load_template_source', -# 'django.template.loaders.eggs.load_template_source', ) MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', #must be the last entry + 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', ) ROOT_URLCONF = 'urls' -# Relative file structure #2 TEMPLATE_DIRS = ( os.path.join(PROJECT_PATH, 'templates'), ) @@ -86,4 +61,3 @@ 'pubcms', 'publish', ) - diff --git a/examplecms/urls.py b/examplecms/urls.py index 12a870f..4d3d60b 100644 --- a/examplecms/urls.py +++ b/examplecms/urls.py @@ -3,12 +3,16 @@ # Uncomment the next two lines to enable the admin: from django.contrib import admin + admin.autodiscover() -urlpatterns = patterns('', +urlpatterns = patterns( + '', ('^admin/', include(admin.site.urls)), - - (r'^media/(?P.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}), - + + (r'^media/(?P.*)$', 'django.views.static.serve', + {'document_root': settings.MEDIA_ROOT, + 'show_indexes': True}), + ('^', include('pubcms.urls')), ) diff --git a/publish/actions.py b/publish/actions.py index 763702e..c87988e 100644 --- a/publish/actions.py +++ b/publish/actions.py @@ -13,9 +13,10 @@ from models import Publishable from utils import NestedSet + def _get_change_view_url(app_label, object_name, pk, levels_to_root): - return '%s%s/%s/%s/' % ('../'*levels_to_root, app_label, - object_name, quote(pk)) + return '%s%s/%s/%s/' % ('../' * levels_to_root, app_label, + object_name, quote(pk)) def delete_selected(modeladmin, request, queryset): @@ -24,7 +25,11 @@ def delete_selected(modeladmin, request, queryset): if not modeladmin.has_delete_permission(request, obj): raise PermissionDenied return django_delete_selected(modeladmin, request, queryset) -delete_selected.short_description = "Mark %(verbose_name_plural)s for deletion" + + +delete_selected.short_description = \ + "Mark %(verbose_name_plural)s for deletion" + def undelete_selected(modeladmin, request, queryset): for obj in queryset: @@ -33,35 +38,43 @@ def undelete_selected(modeladmin, request, queryset): for obj in queryset: obj.undelete() return None -undelete_selected.short_description = "Un-mark %(verbose_name_plural)s for deletion" + + +undelete_selected.short_description = \ + "Un-mark %(verbose_name_plural)s for deletion" + def _get_publishable_html(admin_site, levels_to_root, value): model = value.__class__ model_name = escape(capfirst(model._meta.verbose_name)) model_title = escape(force_unicode(value)) model_text = '%s: %s' % (model_name, model_title) - opts = model._meta + opts = model._meta has_admin = model in admin_site._registry if has_admin: modeladmin = admin_site._registry[model] - model_text = '%s (%s)' % (model_text, modeladmin.get_publish_status_display(value)) + model_text = '%s (%s)' % ( + model_text, modeladmin.get_publish_status_display(value) + ) url = _get_change_view_url(opts.app_label, opts.object_name.lower(), value._get_pk_val(), - levels_to_root) + levels_to_root) html_value = mark_safe(u'%s' % (url, model_text)) else: html_value = mark_safe(model_text) - + return html_value + def _to_html(admin_site, items): levels_to_root = 2 html_list = [] for value in items: if isinstance(value, Publishable): - html_value = _get_publishable_html(admin_site, levels_to_root, value) + html_value = _get_publishable_html(admin_site, levels_to_root, + value) else: html_value = _to_html(admin_site, value) html_list.append(html_value) @@ -71,12 +84,13 @@ def _to_html(admin_site, items): def _convert_all_published_to_html(admin_site, all_published): return _to_html(admin_site, all_published.nested_items()) + def _check_permissions(modeladmin, all_published, request, perms_needed): admin_site = modeladmin.admin_site for instance in all_published: model = instance.__class__ - other_modeladmin = admin_site._registry.get(model,None) + other_modeladmin = admin_site._registry.get(model, None) if other_modeladmin: if not other_modeladmin.has_publish_permission(request, instance): perms_needed.append(instance) @@ -90,7 +104,7 @@ def _root_path(admin_site): def publish_selected(modeladmin, request, queryset): opts = modeladmin.model._meta app_label = opts.app_label - + all_published = NestedSet() for obj in queryset: obj.publish(dry_run=True, all_published=all_published) @@ -107,19 +121,22 @@ def publish_selected(modeladmin, request, queryset): modeladmin.log_publication(request, object) queryset.publish() - - modeladmin.message_user(request, _("Successfully published %(count)d %(items)s.") % { - "count": n, "items": model_ngettext(modeladmin.opts, n) - }) + + message = _("Successfully published %(count)d %(items)s.") % { + "count": n, + "items": model_ngettext(modeladmin.opts, n) + } + modeladmin.message_user(request, message) # Return None to display the change list page again. return None - + admin_site = modeladmin.admin_site - + context = { "title": _("Publish?"), "object_name": force_unicode(opts.verbose_name), - "all_published": _convert_all_published_to_html(admin_site, all_published), + "all_published": _convert_all_published_to_html(admin_site, + all_published), "perms_lacking": _to_html(admin_site, perms_needed), 'queryset': queryset, "opts": opts, @@ -130,7 +147,8 @@ def publish_selected(modeladmin, request, queryset): # Display the confirmation page return render_to_response(modeladmin.publish_confirmation_template or [ - "admin/%s/%s/publish_selected_confirmation.html" % (app_label, opts.object_name.lower()), + "admin/%s/%s/publish_selected_confirmation.html" % ( + app_label, opts.object_name.lower()), "admin/%s/publish_selected_confirmation.html" % app_label, "admin/publish_selected_confirmation.html" ], context, context_instance=template.RequestContext(request)) diff --git a/publish/admin.py b/publish/admin.py index be48cbf..10629d9 100644 --- a/publish/admin.py +++ b/publish/admin.py @@ -1,11 +1,12 @@ from django.contrib import admin from django.forms.models import BaseInlineFormSet -from django.utils.encoding import force_unicode +from django.utils.encoding import force_unicode from .models import Publishable from .actions import publish_selected, delete_selected, undelete_selected from publish.filters import register_filters + register_filters() @@ -16,7 +17,7 @@ def _make_form_readonly(form): if hasattr(widget, 'widget'): widget = getattr(widget, 'widget') widget.attrs['disabled'] = 'disabled' - + def _make_adminform_readonly(adminform, inline_admin_formsets): _make_form_readonly(adminform.form) @@ -30,33 +31,38 @@ def _draft_queryset(db_field, kwargs): model = db_field.rel.to if issubclass(model, Publishable): kwargs['queryset'] = model._default_manager.draft() \ - .complex_filter(db_field.rel.limit_choices_to) + .complex_filter(db_field.rel.limit_choices_to) def attach_filtered_formfields(admin_class): - # class decorator to add in extra methods that + # class decorator to add in extra methods that # are common to several classes super_formfield_for_foreignkey = admin_class.formfield_for_foreignkey + def formfield_for_foreignkey(self, db_field, request=None, **kwargs): _draft_queryset(db_field, kwargs) - return super_formfield_for_foreignkey(self, db_field, request, **kwargs) + return super_formfield_for_foreignkey(self, db_field, request, + **kwargs) + admin_class.formfield_for_foreignkey = formfield_for_foreignkey - + super_formfield_for_manytomany = admin_class.formfield_for_manytomany + def formfield_for_manytomany(self, db_field, request=None, **kwargs): _draft_queryset(db_field, kwargs) - return super_formfield_for_manytomany(self, db_field, request, **kwargs) + return super_formfield_for_manytomany(self, db_field, request, + **kwargs) + admin_class.formfield_for_manytomany = formfield_for_manytomany return admin_class class PublishableAdmin(admin.ModelAdmin): - actions = [publish_selected, delete_selected, undelete_selected] change_form_template = 'admin/publish_change_form.html' publish_confirmation_template = None deleted_form_template = None - + list_display = ['__unicode__', 'publish_state'] list_filter = ['publish_state'] @@ -71,30 +77,37 @@ def get_actions(self, request): actions = super(PublishableAdmin, self).get_actions(request) # replace site-wide delete selected with our own version if 'delete_selected' in actions: - actions['delete_selected'] = (delete_selected, 'delete_selected', delete_selected.short_description) + actions['delete_selected'] = (delete_selected, 'delete_selected', + delete_selected.short_description) return actions def has_change_permission(self, request, obj=None): # user can never change public models directly # but can view old read-only copy of it if we are about to delete it if obj: - if obj.is_public or (request.method == 'POST' and obj.publish_state == Publishable.PUBLISH_DELETE): + if obj.is_public or ( + request.method == 'POST' + and obj.publish_state == Publishable.PUBLISH_DELETE + ): return False - return super(PublishableAdmin, self).has_change_permission(request, obj) - + return super(PublishableAdmin, self).has_change_permission(request, + obj) + def has_delete_permission(self, request, obj=None): # use can never delete models directly if obj and obj.is_public: return False - return super(PublishableAdmin, self).has_delete_permission(request, obj) - + return super(PublishableAdmin, self).has_delete_permission(request, + obj) + def has_undelete_permission(self, request, obj=None): return self.has_publish_permission(request, obj=obj) def has_publish_permission(self, request, obj=None): opts = self.opts - return request.user.has_perm(opts.app_label + '.' + opts.get_publish_permission()) - + return request.user.has_perm( + opts.app_label + '.' + opts.get_publish_permission()) + def get_publish_status_display(self, obj): state = obj.get_publish_state_display() if not obj.is_public and not obj.public: @@ -106,31 +119,39 @@ def log_publication(self, request, object): if isinstance(object, Publishable): model = object.__class__ other_modeladmin = self.admin_site._registry.get(model, None) - if other_modeladmin: - # just log as a change + if other_modeladmin: + # just log as a change self.log_change(request, object, 'Published') - def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): - context['has_publish_permission'] = self.has_publish_permission(request, obj) + def render_change_form(self, request, context, add=False, change=False, + form_url='', obj=None): + context['has_publish_permission'] = self.has_publish_permission( + request, obj) if obj and obj.publish_state == Publishable.PUBLISH_DELETE: - adminform, inline_admin_formsets = context['adminform'], context['inline_admin_formsets'] + adminform, inline_admin_formsets = context['adminform'], context[ + 'inline_admin_formsets'] _make_adminform_readonly(adminform, inline_admin_formsets) - + context.update({ - 'title': 'This %s will be deleted' % force_unicode(self.opts.verbose_name), + 'title': 'This %s will be deleted' % force_unicode( + self.opts.verbose_name), }) - - return super(PublishableAdmin, self).render_change_form(request, context, add, change, form_url, obj) + + return super(PublishableAdmin, self).render_change_form(request, + context, add, + change, + form_url, obj) class PublishableBaseInlineFormSet(BaseInlineFormSet): # we will actually delete inline objects, rather than # just marking them for deletion, as they are like # an edit to their parent - + def save_existing_objects(self, commit=True): - saved_instances = super(PublishableBaseInlineFormSet, self).save_existing_objects(commit=commit) + saved_instances = super(PublishableBaseInlineFormSet, + self).save_existing_objects(commit=commit) for obj in self.deleted_objects: if obj.pk is not None: obj.delete(mark_for_deletion=False) @@ -146,5 +167,6 @@ class PublishableTabularInline(admin.TabularInline): # add in extra methods -for admin_class in [PublishableAdmin, PublishableStackedInline, PublishableTabularInline]: +for admin_class in [PublishableAdmin, PublishableStackedInline, + PublishableTabularInline]: attach_filtered_formfields(admin_class) diff --git a/publish/filters.py b/publish/filters.py index 6cd0c65..0aee561 100644 --- a/publish/filters.py +++ b/publish/filters.py @@ -1,21 +1,7 @@ from django.utils.encoding import smart_unicode - from .models import Publishable - - -try: - from django.contrib.admin.filters import FieldListFilter, RelatedFieldListFilter -except ImportError: - # only using this code if on before Django 1.4 - from django.contrib.admin.filterspecs import FilterSpec, RelatedFilterSpec as RelatedFieldListFilter - - class FieldListFilter(object): - @classmethod - def register(cls, test, list_filter_class, take_priority=False): - if take_priority: - FilterSpec.filter_specs.insert(0, (test, list_filter_class)) - else: - FilterSpec.filter_specs.append((test, list_filter_class)) +from django.contrib.admin.filters import FieldListFilter, \ + RelatedFieldListFilter def is_publishable_filter(f): @@ -24,17 +10,24 @@ def is_publishable_filter(f): class PublishableRelatedFieldListFilter(RelatedFieldListFilter): def __init__(self, field, request, params, model, model_admin, *arg, **kw): - super(PublishableRelatedFieldListFilter, self).__init__(field, request, params, model, model_admin, *arg, **kw) - # to keep things simple we'll just remove all "non-draft" instance from list + super(PublishableRelatedFieldListFilter, self).__init__(field, request, + params, model, + model_admin, + *arg, **kw) + # to keep things simple we'll just remove all "non-draft" + # instance from list rel_model = field.rel.to - queryset = rel_model._default_manager.complex_filter(field.rel.limit_choices_to).draft_and_deleted() + queryset = rel_model._default_manager.complex_filter( + field.rel.limit_choices_to).draft_and_deleted() if hasattr(field.rel, 'get_related_field'): - lst = [(getattr(x, field.rel.get_related_field().attname), smart_unicode(x)) for x in queryset] + lst = [(getattr(x, field.rel.get_related_field().attname), + smart_unicode(x)) for x in queryset] else: lst = [(x._get_pk_val(), smart_unicode(x)) for x in queryset] self.lookup_choices = lst def register_filters(): - FieldListFilter.register(is_publishable_filter, PublishableRelatedFieldListFilter, take_priority=True) - + FieldListFilter.register(is_publishable_filter, + PublishableRelatedFieldListFilter, + take_priority=True) diff --git a/publish/models.py b/publish/models.py index 548a412..85b2229 100644 --- a/publish/models.py +++ b/publish/models.py @@ -10,18 +10,19 @@ # django-cms 2.0 # e.g. http://github.com/digi604/django-cms-2.0/blob/master/publisher/models.py # -# but we want this to be a reusable/standalone app and have a few different needs -# +# but we want this to be a reusable/standalone app and have a few different +# needs + class PublishException(Exception): pass -class PublishableQuerySet(QuerySet): +class PublishableQuerySet(QuerySet): def changed(self): '''all draft objects that have not been published yet''' return self.filter(Publishable.Q_CHANGED) - + def deleted(self): '''public objects that need deleting''' return self.filter(Publishable.Q_DELETED) @@ -29,16 +30,18 @@ def deleted(self): def draft(self): '''all draft objects''' return self.filter(Publishable.Q_DRAFT) - + def draft_and_deleted(self): return self.filter(Publishable.Q_DRAFT | Publishable.Q_DELETED) - + def published(self): '''all public/published objects''' return self.filter(Publishable.Q_PUBLISHED) def publish(self, all_published=None): - '''publish all models in this queryset''' + ''' + publish all models in this queryset + ''' if all_published is None: all_published = NestedSet() for p in self: @@ -46,79 +49,85 @@ def publish(self, all_published=None): def delete(self, mark_for_deletion=True): ''' - override delete so that we call delete on each object separately, as delete needs - to set some flags etc + override delete so that we call delete on each object separately, + as delete needs to set some flags etc ''' for p in self: p.delete(mark_for_deletion=mark_for_deletion) class PublishableManager(models.Manager): - def get_query_set(self): return PublishableQuerySet(self.model) def changed(self): '''all draft objects that have not been published yet''' return self.get_query_set().changed() - + def deleted(self): '''public objects that need deleting''' return self.get_query_set().deleted() - + def draft(self): '''all draft objects''' return self.get_query_set().draft() - + def draft_and_deleted(self): - return self.get_query_set().draft_and_deleted() - + return self.get_query_set().draft_and_deleted() + def published(self): '''all public/published objects''' return self.get_query_set().published() class PublishableBase(ModelBase): - def __new__(cls, name, bases, attrs): - new_class = super(PublishableBase, cls).__new__(cls, name, bases, attrs) - # insert an extra permission in for "Can publish" - # as well as a "method" to find name of publish_permission for this object + new_class = super(PublishableBase, cls).__new__(cls, name, bases, + attrs) + # insert an extra permission in for "Can publish" as well as a + # "method" to find name of publish_permission for this object opts = new_class._meta name = u'Can publish %s' % opts.verbose_name code = u'publish_%s' % opts.object_name.lower() opts.permissions = tuple(opts.permissions) + ((code, name), ) opts.get_publish_permission = lambda: code - + return new_class - + class Publishable(models.Model): __metaclass__ = PublishableBase PUBLISH_DEFAULT = 0 PUBLISH_CHANGED = 1 - PUBLISH_DELETE = 2 + PUBLISH_DELETE = 2 - PUBLISH_CHOICES = ((PUBLISH_DEFAULT, 'Published'), (PUBLISH_CHANGED, 'Changed'), (PUBLISH_DELETE, 'To be deleted')) + PUBLISH_CHOICES = ( + (PUBLISH_DEFAULT, 'Published'), (PUBLISH_CHANGED, 'Changed'), + (PUBLISH_DELETE, 'To be deleted')) # make these available here so can easily re-use them in other code Q_PUBLISHED = Q(is_public=True) - Q_DRAFT = Q(is_public=False) & ~Q(publish_state=PUBLISH_DELETE) - Q_CHANGED = Q(is_public=False, publish_state=PUBLISH_CHANGED) - Q_DELETED = Q(is_public=False, publish_state=PUBLISH_DELETE) - - is_public = models.BooleanField(default=False, editable=False, db_index=True) - publish_state = models.IntegerField('Publication status', editable=False, db_index=True, choices=PUBLISH_CHOICES, default=PUBLISH_DEFAULT) - public = models.OneToOneField('self', related_name='draft', null=True, editable=False) - + Q_DRAFT = Q(is_public=False) & ~Q(publish_state=PUBLISH_DELETE) + Q_CHANGED = Q(is_public=False, publish_state=PUBLISH_CHANGED) + Q_DELETED = Q(is_public=False, publish_state=PUBLISH_DELETE) + + is_public = models.BooleanField(default=False, editable=False, + db_index=True) + publish_state = models.IntegerField('Publication status', editable=False, + db_index=True, choices=PUBLISH_CHOICES, + default=PUBLISH_DEFAULT) + public = models.OneToOneField('self', related_name='draft', null=True, + editable=False) + class Meta: abstract = True class PublishMeta(object): - publish_exclude_fields = ['id', 'is_public', 'publish_state', 'public', 'draft'] + publish_exclude_fields = ['id', 'is_public', 'publish_state', 'public', + 'draft'] publish_reverse_fields = [] - publish_functions = {} + publish_functions = {} @classmethod def _combined_fields(cls, field_name): @@ -138,8 +147,8 @@ def reverse_fields_to_publish(cls): @classmethod def find_publish_function(cls, field_name, default_function): ''' - Search to see if there is a function to copy the given field over. - Function should take same params as setattr() + Search to see if there is a function to copy the given field over. + Function should take same params as setattr() ''' for clazz in cls.__mro__: publish_functions = getattr(clazz, 'publish_functions', {}) @@ -149,7 +158,7 @@ def find_publish_function(cls, field_name, default_function): return default_function objects = PublishableManager() - + def is_marked_for_deletion(self): return self.publish_state == Publishable.PUBLISH_DELETE @@ -161,11 +170,12 @@ def get_public_absolute_url(self): def save(self, mark_changed=True, *arg, **kw): if not self.is_public and mark_changed: if self.publish_state == Publishable.PUBLISH_DELETE: - raise PublishException("Attempting to save model marked for deletion") + raise PublishException( + "Attempting to save model marked for deletion") self.publish_state = Publishable.PUBLISH_CHANGED super(Publishable, self).save(*arg, **kw) - + def delete(self, mark_for_deletion=True): if self.public and mark_for_deletion: self.publish_state = Publishable.PUBLISH_DELETE @@ -188,28 +198,32 @@ def _post_publish(self, dry_run, all_published, deleted=False): # got published (in case it was indirectly published elsewhere) sender = self.__class__ instance = all_published.original(self) - post_publish.send(sender=sender, instance=instance, deleted=deleted) - + post_publish.send(sender=sender, instance=instance, + deleted=deleted) def publish(self, dry_run=False, all_published=None, parent=None): ''' either publish changes or deletions, depending on whether this model is public or draft. - public models will be examined to see if they need deleting and deleted if so. ''' if self.is_public: - raise PublishException("Cannot publish public model - publish should be called from draft model") + raise PublishException( + "Cannot publish public model - publish should be called from draft model") if self.pk is None: raise PublishException("Please save model before publishing") - + if self.publish_state == Publishable.PUBLISH_DELETE: - self.publish_deletions(dry_run=dry_run, all_published=all_published, parent=parent) + self.publish_deletions(dry_run=dry_run, + all_published=all_published, + parent=parent) return None else: - return self.publish_changes(dry_run=dry_run, all_published=all_published, parent=parent) - + return self.publish_changes(dry_run=dry_run, + all_published=all_published, + parent=parent) + def _get_public_or_publish(self, *arg, **kw): # only publish if we don't yet have an id for the # public model @@ -233,11 +247,11 @@ def _get_through_model(self, field_object): def publish_changes(self, dry_run=False, all_published=None, parent=None): ''' - publish changes to the model - basically copy all of it's content to another copy in the - database. - if you set dry_run=True nothing will be written to the database. combined with - the all_published value one can therefore get information about what other models - would be affected by this function + publish changes to the model - basically copy all of it's content to + another copy in the database. + if you set dry_run=True nothing will be written to the database. + combined with the all_published value one can therefore get + information about what other models would be affected by this function ''' assert not self.is_public, "Cannot publish public model - publish should be called from draft model" @@ -250,34 +264,37 @@ def publish_changes(self, dry_run=False, all_published=None, parent=None): if self in all_published: return all_published.original(self).public - all_published.add(self, parent=parent) + all_published.add(self, parent=parent) self._pre_publish(dry_run, all_published) public_version = self.public if not public_version: public_version = self.__class__(is_public=True) - + excluded_fields = self.PublishMeta.excluded_fields() reverse_fields_to_publish = self.PublishMeta.reverse_fields_to_publish() - + if self.publish_state == Publishable.PUBLISH_CHANGED: # copy over regular fields for field in self._meta.fields: if field.name in excluded_fields: continue - + value = getattr(self, field.name) if isinstance(field, RelatedField): related = field.rel.to if issubclass(related, Publishable): if value is not None: - value = value._get_public_or_publish(dry_run=dry_run, all_published=all_published, parent=self) - + value = value._get_public_or_publish( + dry_run=dry_run, all_published=all_published, + parent=self) + if not dry_run: - publish_function = self.PublishMeta.find_publish_function(field.name, setattr) + publish_function = self.PublishMeta.find_publish_function( + field.name, setattr) publish_function(public_version, field.name, value) - + # save the public version and update # state so we know everything is up-to-date if not dry_run: @@ -285,17 +302,18 @@ def publish_changes(self, dry_run=False, all_published=None, parent=None): self.public = public_version self.publish_state = Publishable.PUBLISH_DEFAULT self.save(mark_changed=False) - + # copy over many-to-many fields for field in self._meta.many_to_many: name = field.name if name in excluded_fields: continue - + m2m_manager = getattr(self, name) public_objs = list(m2m_manager.all()) - field_object, model, direct, m2m = self._meta.get_field_by_name(name) + field_object, model, direct, m2m = self._meta.get_field_by_name( + name) through_model = self._get_through_model(field_object) if through_model: # see if we can work out which reverse relationship this is @@ -306,19 +324,26 @@ def publish_changes(self, dry_run=False, all_published=None, parent=None): for reverse_field in through_model._meta.fields: if reverse_field.column == m2m_reverse_name: related_name = reverse_field.name - related_field = getattr(through_model, related_name).field + related_field = getattr(through_model, + related_name).field reverse_name = related_field.related.get_accessor_name() reverse_fields_to_publish.append(reverse_name) break - continue # m2m via through table won't be dealt with here + continue # m2m via through table won't be dealt with here related = field_object.rel.to if issubclass(related, Publishable): - public_objs = [p._get_public_or_publish(dry_run=dry_run, all_published=all_published, parent=self) for p in public_objs] - + public_objs = [ + p._get_public_or_publish( + dry_run=dry_run, + all_published=all_published, + parent=self) for p in public_objs + ] + if not dry_run: public_m2m_manager = getattr(public_version, name) - old_objs = public_m2m_manager.exclude(pk__in=[p.pk for p in public_objs]) + old_objs = public_m2m_manager.exclude( + pk__in=[p.pk for p in public_objs]) public_m2m_manager.remove(*old_objs) public_m2m_manager.add(*public_objs) @@ -339,32 +364,36 @@ def publish_changes(self, dry_run=False, all_published=None, parent=None): related_items = [] for related_item in related_items: - related_item.publish(dry_run=dry_run, all_published=all_published, parent=self) - + related_item.publish(dry_run=dry_run, + all_published=all_published, + parent=self) + # make sure we tidy up anything that needs deleting if self.public and not dry_run: if obj.field.rel.multiple: public_ids = [r.public_id for r in related_items] - deleted_items = getattr(self.public, name).exclude(pk__in=public_ids) + deleted_items = getattr(self.public, name).exclude( + pk__in=public_ids) deleted_items.delete(mark_for_deletion=False) - + self._post_publish(dry_run, all_published) return public_version - - def publish_deletions(self, all_published=None, parent=None, dry_run=False): + + def publish_deletions(self, all_published=None, parent=None, + dry_run=False): ''' actually delete models that have been marked for deletion ''' if self.publish_state != Publishable.PUBLISH_DELETE: - return + return if all_published is None: all_published = NestedSet() if self in all_published: return - + all_published.add(self, parent=parent) self._pre_publish(dry_run, all_published, deleted=True) @@ -380,8 +409,9 @@ def publish_deletions(self, all_published=None, parent=None, dry_run=False): except AttributeError: instances = [getattr(self, name)] for instance in instances: - instance.publish_deletions(all_published=all_published, parent=self, dry_run=dry_run) - + instance.publish_deletions(all_published=all_published, + parent=self, dry_run=dry_run) + if not dry_run: public = self.public self.delete(mark_for_deletion=False) @@ -389,7 +419,3 @@ def publish_deletions(self, all_published=None, parent=None, dry_run=False): public.delete(mark_for_deletion=False) self._post_publish(dry_run, all_published, deleted=True) - - - - diff --git a/publish/signals.py b/publish/signals.py index e190401..e6a72b1 100644 --- a/publish/signals.py +++ b/publish/signals.py @@ -1,6 +1,6 @@ import django.dispatch -# instance is the instance being published, deleted is a boolean to indicate whether the instance -# was being deleted (rather than changed) -pre_publish = django.dispatch.Signal(providing_args=['instance', 'deleted']) +# instance is the instance being published, deleted is a boolean to indicate +# whether the instance was being deleted (rather than changed) +pre_publish = django.dispatch.Signal(providing_args=['instance', 'deleted']) post_publish = django.dispatch.Signal(providing_args=['instance', 'deleted']) diff --git a/publish/tests/example_app/models.py b/publish/tests/example_app/models.py index 4054f7e..2a881f4 100644 --- a/publish/tests/example_app/models.py +++ b/publish/tests/example_app/models.py @@ -7,6 +7,7 @@ class Site(models.Model): title = models.CharField(max_length=100) domain = models.CharField(max_length=100) + class FlatPage(Publishable): url = models.CharField(max_length=100, db_index=True) title = models.CharField(max_length=200) @@ -24,6 +25,7 @@ def get_absolute_url(self): return self.url return '%s*' % self.url + class Author(Publishable): name = models.CharField(max_length=100) profile = models.TextField(blank=True) @@ -39,30 +41,36 @@ class AuthorProfile(Publishable): author = models.OneToOneField(Author) extra_profile = models.TextField(blank=True) + class ChangeLog(models.Model): changed = models.DateTimeField(db_index=True, auto_now_add=True) message = models.CharField(max_length=200) + class Tag(models.Model): title = models.CharField(max_length=100, unique=True) slug = models.CharField(max_length=100) + # publishable model with a reverse relation to # page (as a child) class PageBlock(Publishable): - page=models.ForeignKey('Page') + page = models.ForeignKey('Page') content = models.TextField(blank=True) + # non-publishable reverse relation to page (as a child) class Comment(models.Model): - page=models.ForeignKey('Page') + page = models.ForeignKey('Page') comment = models.TextField() + def update_pub_date(page, field_name, value): # ignore value entirely and replace with now setattr(page, field_name, update_pub_date.pub_date) update_pub_date.pub_date = datetime.now() + class Page(Publishable): slug = models.CharField(max_length=100, db_index=True) title = models.CharField(max_length=200) @@ -81,7 +89,7 @@ class Meta: class PublishMeta(Publishable.PublishMeta): publish_exclude_fields = ['log'] publish_reverse_fields = ['pageblock_set'] - publish_functions = { 'pub_date': update_pub_date } + publish_functions = {'pub_date': update_pub_date} def get_absolute_url(self): if not self.parent: @@ -91,9 +99,10 @@ def get_absolute_url(self): def __unicode__(self): return self.slug + class PageTagOrder(Publishable): # note these are named in non-standard way to # ensure we are getting correct names - tagged_page=models.ForeignKey(Page) - page_tag=models.ForeignKey(Tag) - tag_order=models.IntegerField() + tagged_page = models.ForeignKey(Page) + page_tag = models.ForeignKey(Tag) + tag_order = models.IntegerField() diff --git a/publish/tests/helpers.py b/publish/tests/helpers.py index e7eee94..736389c 100644 --- a/publish/tests/helpers.py +++ b/publish/tests/helpers.py @@ -2,4 +2,4 @@ def _get_rendered_content(response): content = getattr(response, 'rendered_content', None) if content is not None: return content - return response.content \ No newline at end of file + return response.content diff --git a/publish/tests/settings_for_test.py b/publish/tests/settings_for_test.py index bcd36d0..27660c8 100644 --- a/publish/tests/settings_for_test.py +++ b/publish/tests/settings_for_test.py @@ -39,8 +39,3 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' logging.disable(logging.CRITICAL) - -# enable this for coverage (using django test coverage -# http://pypi.python.org/pypi/django-test-coverage ) -#TEST_RUNNER = 'django-test-coverage.runner.run_tests' -#COVERAGE_MODULES = ('publish.models', 'publish.admin', 'publish.actions', 'publish.utils', 'publish.signals') diff --git a/publish/tests/test_basic_publishable.py b/publish/tests/test_basic_publishable.py index 35a7bad..0546e67 100644 --- a/publish/tests/test_basic_publishable.py +++ b/publish/tests/test_basic_publishable.py @@ -3,8 +3,8 @@ from publish.tests.example_app.models import FlatPage from publish.utils import NestedSet -class TestBasicPublishable(TestCase): +class TestBasicPublishable(TestCase): def setUp(self): super(TestBasicPublishable, self).setUp() self.flat_page = FlatPage(url='/my-page', title='my page', @@ -18,14 +18,18 @@ def test_get_public_absolute_url(self): self.assertTrue(self.flat_page.get_public_absolute_url() is None) self.flat_page.save() self.flat_page.publish() - self.failUnlessEqual('/my-page', self.flat_page.get_public_absolute_url()) + self.failUnlessEqual('/my-page', + self.flat_page.get_public_absolute_url()) def test_save_marks_changed(self): - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, + self.flat_page.publish_state) self.flat_page.save(mark_changed=False) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, + self.flat_page.publish_state) self.flat_page.save() - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, + self.flat_page.publish_state) def test_publish_excludes_fields(self): self.flat_page.save() @@ -33,7 +37,8 @@ def test_publish_excludes_fields(self): self.failUnless(self.flat_page.public) self.failIfEqual(self.flat_page.id, self.flat_page.public.id) self.failUnless(self.flat_page.public.is_public) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.public.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, + self.flat_page.public.publish_state) def test_publish_check_is_not_public(self): try: @@ -52,15 +57,20 @@ def test_publish_check_has_id(self): def test_publish_simple_fields(self): self.flat_page.save() - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) - self.failIf(self.flat_page.public) # should not be a public version yet + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, + self.flat_page.publish_state) + # should not be a public version yet + self.failIf(self.flat_page.public) self.flat_page.publish() - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, + self.flat_page.publish_state) self.failUnless(self.flat_page.public) - for field in 'url', 'title', 'content', 'enable_comments', 'registration_required': - self.failUnlessEqual(getattr(self.flat_page, field), getattr(self.flat_page.public, field)) + for field in 'url', 'title', 'content', 'enable_comments', \ + 'registration_required': + self.failUnlessEqual(getattr(self.flat_page, field), + getattr(self.flat_page.public, field)) def test_published_simple_field_repeated(self): self.flat_page.save() @@ -71,7 +81,8 @@ def test_published_simple_field_repeated(self): self.flat_page.title = 'New Title' self.flat_page.save() - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, + self.flat_page.publish_state) self.failUnlessEqual(public, self.flat_page.public) self.failIfEqual(public.title, self.flat_page.title) @@ -79,7 +90,8 @@ def test_published_simple_field_repeated(self): self.flat_page.publish() self.failUnlessEqual(public, self.flat_page.public) self.failUnlessEqual(public.title, self.flat_page.title) - self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, self.flat_page.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, + self.flat_page.publish_state) def test_publish_records_published(self): all_published = NestedSet() @@ -96,7 +108,8 @@ def test_publish_dryrun(self): self.failUnlessEqual(1, len(all_published)) self.failUnless(self.flat_page in all_published) self.failIf(self.flat_page.public) - self.failUnlessEqual(Publishable.PUBLISH_CHANGED, self.flat_page.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_CHANGED, + self.flat_page.publish_state) def test_delete_after_publish(self): self.flat_page.save() @@ -105,9 +118,11 @@ def test_delete_after_publish(self): self.failUnless(public) self.flat_page.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.flat_page.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DELETE, + self.flat_page.publish_state) - self.failUnlessEqual(set([self.flat_page, self.flat_page.public]), set(FlatPage.objects.all())) + self.failUnlessEqual(set([self.flat_page, self.flat_page.public]), + set(FlatPage.objects.all())) def test_delete_before_publish(self): self.flat_page.save() @@ -119,10 +134,12 @@ def test_publish_deletions(self): self.flat_page.publish() public = self.flat_page.public - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + self.failUnlessEqual(set([self.flat_page, public]), + set(FlatPage.objects.all())) self.flat_page.delete() - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + self.failUnlessEqual(set([self.flat_page, public]), + set(FlatPage.objects.all())) self.flat_page.publish() self.failUnlessEqual([], list(FlatPage.objects.all())) @@ -136,11 +153,13 @@ def test_publish_deletions_checks_all_published(self): self.flat_page.delete() - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) + self.failUnlessEqual(set([self.flat_page, public]), + set(FlatPage.objects.all())) # this should effectively stop the deletion happening all_published = NestedSet() all_published.add(self.flat_page) self.flat_page.publish(all_published=all_published) - self.failUnlessEqual(set([self.flat_page, public]), set(FlatPage.objects.all())) \ No newline at end of file + self.failUnlessEqual(set([self.flat_page, public]), + set(FlatPage.objects.all())) diff --git a/publish/tests/test_delete_selected.py b/publish/tests/test_delete_selected.py index 54be1b2..a97b810 100644 --- a/publish/tests/test_delete_selected.py +++ b/publish/tests/test_delete_selected.py @@ -7,8 +7,8 @@ from publish.admin import PublishableAdmin from publish.tests.example_app.models import FlatPage -class TestDeleteSelected(TestCase): +class TestDeleteSelected(TestCase): def setUp(self): super(TestDeleteSelected, self).setUp() self.fp1 = FlatPage.objects.create(url='/fp1', title='FP1') @@ -23,14 +23,17 @@ def setUp(self): self.page_admin = PublishableAdmin(FlatPage, self.admin_site) # override urls, so reverse works - settings.ROOT_URLCONF=patterns('', - ('^admin/', include(self.admin_site.urls)), + settings.ROOT_URLCONF = patterns( + '', + ('^admin/', + include(self.admin_site.urls)), ) def test_delete_selected_check_cannot_delete_public(self): # delete won't work (via admin) for public instances request = None - self.assertRaises(PermissionDenied, delete_selected, self.page_admin, request, FlatPage.objects.published()) + self.assertRaises(PermissionDenied, delete_selected, self.page_admin, + request, FlatPage.objects.published()) def test_delete_selected(self): class dummy_request(object): @@ -46,5 +49,6 @@ def has_perm(cls, *arg): def get_and_delete_messages(cls): return [] - response = delete_selected(self.page_admin, dummy_request, FlatPage.objects.draft()) - self.failUnless(response is not None) \ No newline at end of file + response = delete_selected(self.page_admin, dummy_request, + FlatPage.objects.draft()) + self.failUnless(response is not None) diff --git a/publish/tests/test_infinite_recursion.py b/publish/tests/test_infinite_recursion.py index ffee177..f743453 100644 --- a/publish/tests/test_infinite_recursion.py +++ b/publish/tests/test_infinite_recursion.py @@ -2,17 +2,15 @@ from publish.tests.example_app.models import Page - - class TestInfiniteRecursion(TestCase): - def setUp(self): super(TestInfiniteRecursion, self).setUp() self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2', parent=self.page1) + self.page2 = Page.objects.create(slug='page2', title='page 2', + parent=self.page1) self.page1.parent = self.page2 self.page1.save() def test_publish_recursion_breaks(self): - self.page1.publish() # this should simple run without an error \ No newline at end of file + self.page1.publish() # this should simple run without an error diff --git a/publish/tests/test_many_to_many_through.py b/publish/tests/test_many_to_many_through.py index 4efda22..a481064 100644 --- a/publish/tests/test_many_to_many_through.py +++ b/publish/tests/test_many_to_many_through.py @@ -2,19 +2,19 @@ from publish.tests.example_app.models import Page, Tag, PageTagOrder - - class TestManyToManyThrough(TestCase): - def setUp(self): super(TestManyToManyThrough, self).setUp() self.page = Page.objects.create(slug='p1', title='P 1') self.tag1 = Tag.objects.create(slug='tag1', title='Tag 1') self.tag2 = Tag.objects.create(slug='tag2', title='Tag 2') - PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag1, tag_order=2) - PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag2, tag_order=1) + PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag1, + tag_order=2) + PageTagOrder.objects.create(tagged_page=self.page, page_tag=self.tag2, + tag_order=1) def test_publish_copies_tags(self): self.page.publish() - self.failUnlessEqual(set([self.tag1, self.tag2]), set(self.page.public.tags.all())) \ No newline at end of file + self.failUnlessEqual(set([self.tag1, self.tag2]), + set(self.page.public.tags.all())) diff --git a/publish/tests/test_nested_set.py b/publish/tests/test_nested_set.py index c0a62cc..86a81a2 100644 --- a/publish/tests/test_nested_set.py +++ b/publish/tests/test_nested_set.py @@ -2,10 +2,7 @@ from publish.utils import NestedSet - - class TestNestedSet(TestCase): - def setUp(self): super(TestNestedSet, self).setUp() self.nested = NestedSet() @@ -32,10 +29,12 @@ def test_nested_items(self): self.failUnlessEqual(['one'], self.nested.nested_items()) self.nested.add('two') self.nested.add('one2', parent='one') - self.failUnlessEqual(['one', ['one2'], 'two'], self.nested.nested_items()) + self.failUnlessEqual(['one', ['one2'], 'two'], + self.nested.nested_items()) self.nested.add('one2-1', parent='one2') self.nested.add('one2-2', parent='one2') - self.failUnlessEqual(['one', ['one2', ['one2-1', 'one2-2']], 'two'], self.nested.nested_items()) + self.failUnlessEqual(['one', ['one2', ['one2-1', 'one2-2']], 'two'], + self.nested.nested_items()) def test_iter(self): self.failUnlessEqual(set(), set(self.nested)) @@ -66,10 +65,11 @@ def __hash__(self): return hash(self.obj) # should always return an item at least - self.failUnlessEqual(MyObject('hi there'), self.nested.original(MyObject('hi there'))) + self.failUnlessEqual(MyObject('hi there'), + self.nested.original(MyObject('hi there'))) m1 = MyObject('m1') self.nested.add(m1) self.failUnlessEqual(id(m1), id(self.nested.original(m1))) - self.failUnlessEqual(id(m1), id(self.nested.original(MyObject('m1')))) \ No newline at end of file + self.failUnlessEqual(id(m1), id(self.nested.original(MyObject('m1')))) diff --git a/publish/tests/test_overlapping_publish.py b/publish/tests/test_overlapping_publish.py index 552b22e..07165f9 100644 --- a/publish/tests/test_overlapping_publish.py +++ b/publish/tests/test_overlapping_publish.py @@ -3,16 +3,16 @@ from publish.utils import NestedSet - - class TestOverlappingPublish(TestCase): - def setUp(self): self.page1 = Page.objects.create(slug='page1', title='page 1') self.page2 = Page.objects.create(slug='page2', title='page 2') - self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') - self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') - self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') + self.child1 = Page.objects.create(parent=self.page1, slug='child1', + title='Child 1') + self.child2 = Page.objects.create(parent=self.page1, slug='child2', + title='Child 2') + self.child3 = Page.objects.create(parent=self.page2, slug='child3', + title='Child 3') def test_publish_with_overlapping_models(self): # make sure when we publish we don't accidentally create @@ -57,7 +57,8 @@ def test_publish_after_dry_run_handles_caching(self): self.failUnlessEqual(5, Page.objects.draft().count()) self.failUnlessEqual(0, Page.objects.published().count()) - # now publish (using same queryset, as this will have cached the instances) + # now publish (using same queryset, + # as this will have cached the instances) draft.publish() self.failUnlessEqual(5, Page.objects.draft().count()) @@ -74,4 +75,4 @@ def test_publish_after_dry_run_handles_caching(self): self.failUnlessEqual(None, page2.public.parent) self.failUnlessEqual(page1.public, child1.public.parent) self.failUnlessEqual(page1.public, child2.public.parent) - self.failUnlessEqual(page2.public, child3.public.parent) \ No newline at end of file + self.failUnlessEqual(page2.public, child3.public.parent) diff --git a/publish/tests/test_publish_function.py b/publish/tests/test_publish_function.py index 95ba11d..bb9bd29 100644 --- a/publish/tests/test_publish_function.py +++ b/publish/tests/test_publish_function.py @@ -2,10 +2,7 @@ from publish.tests.example_app.models import Page, update_pub_date - - class TestPublishFunction(TestCase): - def setUp(self): super(TestPublishFunction, self).setUp() self.page = Page.objects.create(slug='page', title='Page') @@ -22,4 +19,4 @@ def test_publish_function_invoked(self): self.page.publish() self.failIfEqual(pub_date, self.page.pub_date) - self.failUnlessEqual(pub_date, self.page.public.pub_date) \ No newline at end of file + self.failUnlessEqual(pub_date, self.page.public.pub_date) diff --git a/publish/tests/test_publish_selected_action.py b/publish/tests/test_publish_selected_action.py index b215474..1151012 100644 --- a/publish/tests/test_publish_selected_action.py +++ b/publish/tests/test_publish_selected_action.py @@ -9,10 +9,7 @@ from publish.utils import NestedSet - - class TestPublishSelectedAction(TestCase): - def setUp(self): super(TestPublishSelectedAction, self).setUp() self.fp1 = Page.objects.create(slug='fp1', title='FP1') @@ -23,8 +20,10 @@ def setUp(self): self.page_admin = PublishableAdmin(Page, self.admin_site) # override urls, so reverse works - settings.ROOT_URLCONF=patterns('', - ('^admin/', include(self.admin_site.urls)), + settings.ROOT_URLCONF = patterns( + '', + ('^admin/', + include(self.admin_site.urls)), ) def test_publish_selected_confirm(self): @@ -74,13 +73,11 @@ class _messages(object): def add(cls, *message): self._message = message - response = publish_selected(self.page_admin, dummy_request, pages) - self.failUnlessEqual(2, Page.objects.published().count()) - self.failUnless( getattr(self, '_message', None) is not None ) - self.failUnless( response is None ) + self.failUnless(getattr(self, '_message', None) is not None) + self.failUnless(response is None) def test_convert_all_published_to_html(self): self.admin_site.register(Page, PublishableAdmin) @@ -93,9 +90,12 @@ def test_convert_all_published_to_html(self): all_published.add(page) all_published.add(block, parent=page) - converted = _convert_all_published_to_html(self.admin_site, all_published) + converted = _convert_all_published_to_html(self.admin_site, + all_published) - expected = [u'Page: here (Changed - not yet published)' % page.id, [u'Page block: PageBlock object']] + expected = [ + u'Page: here (Changed - not yet published)' % page.id, + [u'Page block: PageBlock object']] self.failUnlessEqual(expected, converted) @@ -136,7 +136,7 @@ def test_publish_selected_does_not_have_related_permission(self): pages = Page.objects.draft() class dummy_request(object): - POST = { 'post': True } + POST = {'post': True} class _messages(object): @classmethod @@ -169,7 +169,7 @@ def test_publish_selected_logs_publication(self): pages = Page.objects.exclude(id=self.fp3.id) class dummy_request(object): - POST = { 'post': True } + POST = {'post': True} class user(object): pk = 1 @@ -199,4 +199,4 @@ def add(cls, *message): from django.contrib.contenttypes.models import ContentType content_type_id = ContentType.objects.get_for_model(self.fp1).pk - self.failUnlessEqual(2, LogEntry.objects.filter().count()) \ No newline at end of file + self.failUnlessEqual(2, LogEntry.objects.filter().count()) diff --git a/publish/tests/test_publish_signal.py b/publish/tests/test_publish_signal.py index 3bad9ab..4ebb0e0 100644 --- a/publish/tests/test_publish_signal.py +++ b/publish/tests/test_publish_signal.py @@ -4,21 +4,22 @@ from publish.tests.example_app.models import Page - - class TestPublishSignals(TestCase): - def setUp(self): - self.page1 = Page.objects.create(slug='page1', title='page 1') - self.page2 = Page.objects.create(slug='page2', title='page 2') - self.child1 = Page.objects.create(parent=self.page1, slug='child1', title='Child 1') - self.child2 = Page.objects.create(parent=self.page1, slug='child2', title='Child 2') - self.child3 = Page.objects.create(parent=self.page2, slug='child3', title='Child 3') + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page2 = Page.objects.create(slug='page2', title='page 2') + self.child1 = Page.objects.create(parent=self.page1, slug='child1', + title='Child 1') + self.child2 = Page.objects.create(parent=self.page1, slug='child2', + title='Child 2') + self.child3 = Page.objects.create(parent=self.page2, slug='child3', + title='Child 3') self.failUnlessEqual(5, Page.objects.draft().count()) def _check_pre_publish(self, queryset): pre_published = [] + def pre_publish_handler(sender, instance, **kw): pre_published.append(instance) @@ -38,6 +39,7 @@ def test_pre_publish(self): def _check_post_publish(self, queryset): published = [] + def post_publish_handler(sender, instance, **kw): published.append(instance) @@ -55,12 +57,14 @@ def test_post_publish(self): def test_signals_sent_for_followed(self): pre_published = [] + def pre_publish_handler(sender, instance, **kw): pre_published.append(instance) pre_publish.connect(pre_publish_handler, sender=Page) published = [] + def post_publish_handler(sender, instance, **kw): published.append(instance) @@ -71,7 +75,8 @@ def post_publish_handler(sender, instance, **kw): self.child1.publish() - self.failUnlessEqual(set([self.page1, self.child1]), set(pre_published)) + self.failUnlessEqual(set([self.page1, self.child1]), + set(pre_published)) self.failUnlessEqual(set([self.page1, self.child1]), set(published)) def test_deleted_flag_false_when_publishing_change(self): @@ -93,7 +98,8 @@ def test_deleted_flag_true_when_publishing_deletion(self): self.child1.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.child1.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DELETE, + self.child1.publish_state) def pre_publish_handler(sender, instance, deleted, **kw): self.failUnless(deleted) @@ -105,4 +111,4 @@ def post_publish_handler(sender, instance, deleted, **kw): post_publish.connect(post_publish_handler, sender=Page) - self.child1.publish() \ No newline at end of file + self.child1.publish() diff --git a/publish/tests/test_publishable_admin.py b/publish/tests/test_publishable_admin.py index 276c531..a85332b 100644 --- a/publish/tests/test_publishable_admin.py +++ b/publish/tests/test_publishable_admin.py @@ -12,10 +12,7 @@ from publish.tests.helpers import _get_rendered_content - - class TestPublishableAdmin(TestCase): - def setUp(self): super(TestPublishableAdmin, self).setUp() self.page1 = Page.objects.create(slug='page1', title='page 1') @@ -40,27 +37,34 @@ class PageAdmin(PublishableAdmin): self.page_admin = PageAdmin(Page, self.admin_site) # override urls, so reverse works - settings.ROOT_URLCONF=patterns('', - ('^admin/', include(self.admin_site.urls)), + settings.ROOT_URLCONF = patterns( + '', + ('^admin/', + include(self.admin_site.urls)), ) def test_get_publish_status_display(self): page = Page.objects.create(slug="hhkkk", title="hjkhjkh") - self.failUnlessEqual('Changed - not yet published', self.page_admin.get_publish_status_display(page)) + self.failUnlessEqual('Changed - not yet published', + self.page_admin.get_publish_status_display(page)) page.publish() - self.failUnlessEqual('Published', self.page_admin.get_publish_status_display(page)) + self.failUnlessEqual('Published', + self.page_admin.get_publish_status_display(page)) page.save() - self.failUnlessEqual('Changed', self.page_admin.get_publish_status_display(page)) + self.failUnlessEqual('Changed', + self.page_admin.get_publish_status_display(page)) page.delete() - self.failUnlessEqual('To be deleted', self.page_admin.get_publish_status_display(page)) + self.failUnlessEqual('To be deleted', + self.page_admin.get_publish_status_display(page)) def test_queryset(self): # make sure we only get back draft objects request = None self.failUnlessEqual( - set([self.page1, self.page1.public, self.page2, self.page2.public]), + set([self.page1, self.page1.public, self.page2, + self.page2.public]), set(Page.objects.all()) ) self.failUnlessEqual( @@ -76,7 +80,6 @@ class request(object): actions = self.page_admin.get_actions(request) - self.failUnless('delete_selected' in actions) action, name, description = actions['delete_selected'] self.failUnlessEqual(delete_selected, action) @@ -96,12 +99,14 @@ def test_formfield_for_foreignkey(self): break self.failUnless(parent_field) - choice_field = self.page_admin.formfield_for_foreignkey(parent_field, request) + choice_field = self.page_admin.formfield_for_foreignkey(parent_field, + request) self.failUnless(choice_field) self.failUnless(isinstance(choice_field, ModelChoiceField)) self.failUnlessEqual( - set([self.page1, self.page1.public, self.page2, self.page2.public]), + set([self.page1, self.page1.public, self.page2, + self.page2.public]), set(Page.objects.all()) ) self.failUnlessEqual( @@ -118,12 +123,14 @@ def test_formfield_for_manytomany(self): break self.failUnless(authors_field) - choice_field = self.page_admin.formfield_for_manytomany(authors_field, request) + choice_field = self.page_admin.formfield_for_manytomany(authors_field, + request) self.failUnless(choice_field) self.failUnless(isinstance(choice_field, ModelMultipleChoiceField)) self.failUnlessEqual( - set([self.author1, self.author1.public, self.author2, self.author2.public]), + set([self.author1, self.author1.public, self.author2, + self.author2.public]), set(Author.objects.all()) ) self.failUnlessEqual( @@ -142,16 +149,20 @@ def has_perm(cls, permission): return True self.failUnless(self.page_admin.has_change_permission(dummy_request)) - self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) - self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1.public)) + self.failUnless( + self.page_admin.has_change_permission(dummy_request, self.page1)) + self.failIf(self.page_admin.has_change_permission(dummy_request, + self.page1.public)) # can view deleted items self.page1.publish_state = Publishable.PUBLISH_DELETE - self.failUnless(self.page_admin.has_change_permission(dummy_request, self.page1)) + self.failUnless( + self.page_admin.has_change_permission(dummy_request, self.page1)) # but cannot modify them dummy_request.method = 'POST' - self.failIf(self.page_admin.has_change_permission(dummy_request, self.page1)) + self.failIf( + self.page_admin.has_change_permission(dummy_request, self.page1)) def test_has_delete_permission(self): class dummy_request(object): @@ -164,8 +175,10 @@ def has_perm(cls, permission): return True self.failUnless(self.page_admin.has_delete_permission(dummy_request)) - self.failUnless(self.page_admin.has_delete_permission(dummy_request, self.page1)) - self.failIf(self.page_admin.has_delete_permission(dummy_request, self.page1.public)) + self.failUnless( + self.page_admin.has_delete_permission(dummy_request, self.page1)) + self.failIf(self.page_admin.has_delete_permission(dummy_request, + self.page1.public)) def test_change_view_normal(self): class dummy_request(object): @@ -191,7 +204,8 @@ def has_perm(cls, permission): def get_and_delete_messages(cls): return [] - response = self.page_admin.change_view(dummy_request, str(self.page1.id)) + response = self.page_admin.change_view(dummy_request, + str(self.page1.id)) self.failUnless(response is not None) self.failIf('deleted' in _get_rendered_content(response)) @@ -215,7 +229,8 @@ def has_perm(cls, permission): return True try: - self.page_admin.change_view(dummy_request, unicode(self.page1.public.id)) + self.page_admin.change_view(dummy_request, + unicode(self.page1.public.id)) self.fail() except Http404: pass @@ -246,13 +261,14 @@ def get_and_delete_messages(cls): self.page1.delete() - response = self.page_admin.change_view(dummy_request, str(self.page1.id)) + response = self.page_admin.change_view(dummy_request, + str(self.page1.id)) self.failUnless(response is not None) self.failUnless('deleted' in _get_rendered_content(response)) def test_change_view_deleted_POST(self): class dummy_request(object): - csrf_processing_done = True # stop csrf check + csrf_processing_done = True method = 'POST' COOKIES = {} META = {} @@ -274,7 +290,8 @@ def is_secure(cls): pass def test_change_view_delete_inline(self): - block = PageBlock.objects.create(page=self.page1, content='some content') + block = PageBlock.objects.create(page=self.page1, + content='some content') page1 = Page.objects.get(pk=self.page1.pk) page1.publish() @@ -336,7 +353,6 @@ class _messages(object): def add(cls, *message): pass - block = PageBlock.objects.get(id=block.id) public_block = block.public @@ -344,4 +360,4 @@ def add(cls, *message): self.assertEqual(302, response.status_code) # the block should have been deleted (but not the public one) - self.failUnlessEqual([public_block], list(PageBlock.objects.all())) \ No newline at end of file + self.failUnlessEqual([public_block], list(PageBlock.objects.all())) diff --git a/publish/tests/test_publishable_manager.py b/publish/tests/test_publishable_manager.py index 2d8fb9b..fe52805 100644 --- a/publish/tests/test_publishable_manager.py +++ b/publish/tests/test_publishable_manager.py @@ -2,17 +2,17 @@ from publish.tests.example_app.models import FlatPage - - class TestPublishableManager(TestCase): - def setUp(self): super(TestCase, self).setUp() - self.flat_page1 = FlatPage.objects.create(url='/url1/', title='title 1') - self.flat_page2 = FlatPage.objects.create(url='/url2/', title='title 2') + self.flat_page1 = FlatPage.objects.create(url='/url1/', + title='title 1') + self.flat_page2 = FlatPage.objects.create(url='/url2/', + title='title 2') def test_all(self): - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.all())) + self.failUnlessEqual([self.flat_page1, self.flat_page2], + list(FlatPage.objects.all())) # publishing will produce extra copies self.flat_page1.publish() @@ -21,38 +21,43 @@ def test_all(self): self.flat_page2.publish() self.failUnlessEqual(4, FlatPage.objects.count()) - def test_changed(self): - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.changed())) + self.failUnlessEqual([self.flat_page1, self.flat_page2], + list(FlatPage.objects.changed())) self.flat_page1.publish() - self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.changed())) + self.failUnlessEqual([self.flat_page2], + list(FlatPage.objects.changed())) self.flat_page2.publish() self.failUnlessEqual([], list(FlatPage.objects.changed())) def test_draft(self): # draft should stay the same pretty much always - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) + self.failUnlessEqual([self.flat_page1, self.flat_page2], + list(FlatPage.objects.draft())) self.flat_page1.publish() - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) + self.failUnlessEqual([self.flat_page1, self.flat_page2], + list(FlatPage.objects.draft())) self.flat_page2.publish() - self.failUnlessEqual([self.flat_page1, self.flat_page2], list(FlatPage.objects.draft())) + self.failUnlessEqual([self.flat_page1, self.flat_page2], + list(FlatPage.objects.draft())) self.flat_page2.delete() self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft())) - def test_published(self): self.failUnlessEqual([], list(FlatPage.objects.published())) self.flat_page1.publish() - self.failUnlessEqual([self.flat_page1.public], list(FlatPage.objects.published())) + self.failUnlessEqual([self.flat_page1.public], + list(FlatPage.objects.published())) self.flat_page2.publish() - self.failUnlessEqual([self.flat_page1.public, self.flat_page2.public], list(FlatPage.objects.published())) + self.failUnlessEqual([self.flat_page1.public, self.flat_page2.public], + list(FlatPage.objects.published())) def test_deleted(self): self.failUnlessEqual([], list(FlatPage.objects.deleted())) @@ -61,20 +66,24 @@ def test_deleted(self): self.failUnlessEqual([], list(FlatPage.objects.deleted())) self.flat_page1.delete() - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) + self.failUnlessEqual([self.flat_page1], + list(FlatPage.objects.deleted())) def test_draft_and_deleted(self): - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), + set(FlatPage.objects.draft_and_deleted())) self.flat_page1.publish() - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft())) + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), + set(FlatPage.objects.draft_and_deleted())) + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), + set(FlatPage.objects.draft())) self.flat_page1.delete() - self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), set(FlatPage.objects.draft_and_deleted())) + self.failUnlessEqual(set([self.flat_page1, self.flat_page2]), + set(FlatPage.objects.draft_and_deleted())) self.failUnlessEqual([self.flat_page2], list(FlatPage.objects.draft())) - def test_delete(self): # delete is overriden, so it marks the public instances self.flat_page1.publish() @@ -83,9 +92,11 @@ def test_delete(self): FlatPage.objects.draft().delete() self.failUnlessEqual([], list(FlatPage.objects.draft())) - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.deleted())) + self.failUnlessEqual([self.flat_page1], + list(FlatPage.objects.deleted())) self.failUnlessEqual([public1], list(FlatPage.objects.published())) - self.failUnlessEqual([self.flat_page1], list(FlatPage.objects.draft_and_deleted())) + self.failUnlessEqual([self.flat_page1], + list(FlatPage.objects.draft_and_deleted())) def test_publish(self): self.failUnlessEqual([], list(FlatPage.objects.published())) @@ -95,4 +106,5 @@ def test_publish(self): flat_page1 = FlatPage.objects.get(id=self.flat_page1.id) flat_page2 = FlatPage.objects.get(id=self.flat_page2.id) - self.failUnlessEqual(set([flat_page1.public, flat_page2.public]), set(FlatPage.objects.published())) \ No newline at end of file + self.failUnlessEqual(set([flat_page1.public, flat_page2.public]), + set(FlatPage.objects.published())) diff --git a/publish/tests/test_publishable_many_to_many.py b/publish/tests/test_publishable_many_to_many.py index 334745b..59293c6 100644 --- a/publish/tests/test_publishable_many_to_many.py +++ b/publish/tests/test_publishable_many_to_many.py @@ -2,17 +2,14 @@ from publish.tests.example_app.models import FlatPage, Site - - class TestPublishableManyToMany(TestCase): - def setUp(self): super(TestPublishableManyToMany, self).setUp() self.flat_page = FlatPage.objects.create( - url='/my-page', title='my page', - content='here is some content', - enable_comments=False, - registration_required=True) + url='/my-page', title='my page', + content='here is some content', + enable_comments=False, + registration_required=True) self.site1 = Site.objects.create(title='my site', domain='mysite.com') self.site2 = Site.objects.create(title='a site', domain='asite.com') @@ -25,40 +22,49 @@ def test_publish_add_site(self): self.flat_page.sites.add(self.site1) self.flat_page.publish() self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) + self.failUnlessEqual([self.site1], + list(self.flat_page.public.sites.all())) def test_publish_repeated_add_site(self): self.flat_page.sites.add(self.site1) self.flat_page.publish() self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) + self.failUnlessEqual([self.site1], + list(self.flat_page.public.sites.all())) self.flat_page.sites.add(self.site2) - self.failUnlessEqual([self.site1], list(self.flat_page.public.sites.all())) + self.failUnlessEqual([self.site1], + list(self.flat_page.public.sites.all())) self.flat_page.publish() - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + self.failUnlessEqual([self.site1, self.site2], + list(self.flat_page.public.sites.order_by('id'))) def test_publish_remove_site(self): self.flat_page.sites.add(self.site1, self.site2) self.flat_page.publish() self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + self.failUnlessEqual([self.site1, self.site2], + list(self.flat_page.public.sites.order_by('id'))) self.flat_page.sites.remove(self.site1) - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + self.failUnlessEqual([self.site1, self.site2], + list(self.flat_page.public.sites.order_by('id'))) self.flat_page.publish() - self.failUnlessEqual([self.site2], list(self.flat_page.public.sites.all())) + self.failUnlessEqual([self.site2], + list(self.flat_page.public.sites.all())) def test_publish_clear_sites(self): self.flat_page.sites.add(self.site1, self.site2) self.flat_page.publish() self.failUnless(self.flat_page.public) - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + self.failUnlessEqual([self.site1, self.site2], + list(self.flat_page.public.sites.order_by('id'))) self.flat_page.sites.clear() - self.failUnlessEqual([self.site1, self.site2], list(self.flat_page.public.sites.order_by('id'))) + self.failUnlessEqual([self.site1, self.site2], + list(self.flat_page.public.sites.order_by('id'))) self.flat_page.publish() self.failUnlessEqual([], list(self.flat_page.public.sites.all())) @@ -71,4 +77,4 @@ def test_publish_sites_cleared_not_deleted(self): self.failUnlessEqual([], list(self.flat_page.public.sites.all())) - self.failIfEqual([], list(Site.objects.all())) \ No newline at end of file + self.failIfEqual([], list(Site.objects.all())) diff --git a/publish/tests/test_publishable_recursive_fk.py b/publish/tests/test_publishable_recursive_fk.py index 7086b59..ad5dfb8 100644 --- a/publish/tests/test_publishable_recursive_fk.py +++ b/publish/tests/test_publishable_recursive_fk.py @@ -3,14 +3,15 @@ from publish.tests.example_app.models import Page, PageBlock, Comment - - class TestPublishableRecursiveForeignKey(TestCase): - def setUp(self): super(TestPublishableRecursiveForeignKey, self).setUp() - self.page1 = Page.objects.create(slug='page1', title='page 1', content='some content') - self.page2 = Page.objects.create(slug='page2', title='page 2', content='other content', parent=self.page1) + self.page1 = Page.objects.create(slug='page1', title='page 1', + content='some content') + self.page2 = Page.objects.create(slug='page2', + title='page 2', + content='other content', + parent=self.page1) def test_publish_parent(self): # this shouldn't publish the child page @@ -33,8 +34,10 @@ def test_publish_child_parent_already_published(self): self.failIfEqual(self.page1, self.page2.public.parent) - self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) - self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) + self.failUnlessEqual('/page1/', + self.page1.public.get_absolute_url()) + self.failUnlessEqual('/page1/page2/', + self.page2.public.get_absolute_url()) def test_publish_child_parent_not_already_published(self): self.page2.publish() @@ -48,14 +51,16 @@ def test_publish_child_parent_not_already_published(self): self.failIfEqual(page1, self.page2.public.parent) - self.failUnlessEqual('/page1/', self.page1.public.get_absolute_url()) - self.failUnlessEqual('/page1/page2/', self.page2.public.get_absolute_url()) + self.failUnlessEqual('/page1/', + self.page1.public.get_absolute_url()) + self.failUnlessEqual('/page1/page2/', + self.page2.public.get_absolute_url()) def test_publish_repeated(self): self.page1.publish() self.page2.publish() - self.page1.slug='main' + self.page1.slug = 'main' self.page1.save() self.failUnlessEqual('/main/', self.page1.get_absolute_url()) @@ -71,11 +76,11 @@ def test_publish_repeated(self): self.failUnlessEqual('/main/', page1.public.get_absolute_url()) self.failUnlessEqual('/main/page2/', page2.public.get_absolute_url()) - page1.slug='elsewhere' + page1.slug = 'elsewhere' page1.save() page1 = Page.objects.get(id=self.page1.id) page2 = Page.objects.get(id=self.page2.id) - page2.slug='meanwhile' + page2.slug = 'meanwhile' page2.save() page2.publish() page1 = Page.objects.get(id=self.page1.id) @@ -86,7 +91,8 @@ def test_publish_repeated(self): self.failUnlessEqual(Publishable.PUBLISH_CHANGED, page1.publish_state) self.failUnlessEqual('/main/', page1.public.get_absolute_url()) - self.failUnlessEqual('/main/meanwhile/', page2.public.get_absolute_url()) + self.failUnlessEqual('/main/meanwhile/', + page2.public.get_absolute_url()) page1.publish() page1 = Page.objects.get(id=self.page1.id) @@ -96,7 +102,8 @@ def test_publish_repeated(self): self.failUnlessEqual(Publishable.PUBLISH_DEFAULT, page1.publish_state) self.failUnlessEqual('/elsewhere/', page1.public.get_absolute_url()) - self.failUnlessEqual('/elsewhere/meanwhile/', page2.public.get_absolute_url()) + self.failUnlessEqual('/elsewhere/meanwhile/', + page2.public.get_absolute_url()) def test_publish_deletions(self): self.page1.publish() @@ -106,11 +113,13 @@ def test_publish_deletions(self): self.failUnlessEqual([self.page2], list(Page.objects.deleted())) self.page2.publish() - self.failUnlessEqual([self.page1.public], list(Page.objects.published())) + self.failUnlessEqual([self.page1.public], + list(Page.objects.published())) self.failUnlessEqual([], list(Page.objects.deleted())) def test_publish_reverse_fields(self): - page_block = PageBlock.objects.create(page=self.page1, content='here we are') + page_block = PageBlock.objects.create(page=self.page1, + content='here we are') self.page1.publish() @@ -122,7 +131,8 @@ def test_publish_reverse_fields(self): self.failUnlessEqual(page_block.content, blocks[0].content) def test_publish_deletions_reverse_fields(self): - page_block = PageBlock.objects.create(page=self.page1, content='here we are') + page_block = PageBlock.objects.create(page=self.page1, + content='here we are') self.page1.publish() public = self.page1.public @@ -138,7 +148,8 @@ def test_publish_deletions_reverse_fields(self): def test_publish_reverse_fields_deleted(self): # make sure child elements get removed - page_block = PageBlock.objects.create(page=self.page1, content='here we are') + page_block = PageBlock.objects.create(page=self.page1, + content='here we are') self.page1.publish() @@ -147,7 +158,8 @@ def test_publish_reverse_fields_deleted(self): page_block_public = page_block.public self.failIf(page_block_public is None) - self.failUnlessEqual([page_block_public], list(public.pageblock_set.all())) + self.failUnlessEqual([page_block_public], + list(public.pageblock_set.all())) # now delete the page block and publish the parent # to make sure that deletion gets copied over properly @@ -161,7 +173,8 @@ def test_publish_reverse_fields_deleted(self): def test_publish_delections_with_non_publishable_children(self): self.page1.publish() - comment = Comment.objects.create(page=self.page1.public, comment='This is a comment') + comment = Comment.objects.create(page=self.page1.public, + comment='This is a comment') self.failUnlessEqual(1, Comment.objects.count()) @@ -173,4 +186,4 @@ def test_publish_delections_with_non_publishable_children(self): self.page1.publish() self.failUnlessEqual([], list(Page.objects.deleted())) self.failUnlessEqual([], list(Page.objects.all())) - self.failUnlessEqual([], list(Comment.objects.all())) \ No newline at end of file + self.failUnlessEqual([], list(Comment.objects.all())) diff --git a/publish/tests/test_publishable_recursive_many_to_many_field.py b/publish/tests/test_publishable_recursive_many_to_many_field.py index 06d6b9b..f81ef3e 100644 --- a/publish/tests/test_publishable_recursive_many_to_many_field.py +++ b/publish/tests/test_publishable_recursive_many_to_many_field.py @@ -2,15 +2,16 @@ from publish.tests.example_app.models import Page, Author - - class TestPublishableRecursiveManyToManyField(TestCase): def setUp(self): super(TestPublishableRecursiveManyToManyField, self).setUp() - self.page = Page.objects.create(slug='page1', title='page 1', content='some content') - self.author1 = Author.objects.create(name='author1', profile='a profile') - self.author2 = Author.objects.create(name='author2', profile='something else') + self.page = Page.objects.create( + slug='page1', title='page 1', content='some content') + self.author1 = Author.objects.create( + name='author1', profile='a profile') + self.author2 = Author.objects.create( + name='author2', profile='something else') def test_publish_add_author(self): self.page.authors.add(self.author1) @@ -23,7 +24,8 @@ def test_publish_add_author(self): self.failUnlessEqual(author1.name, author1.public.name) self.failUnlessEqual(author1.profile, author1.public.profile) - self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) + self.failUnlessEqual([author1.public], + list(self.page.public.authors.all())) def test_publish_repeated_add_author(self): self.page.authors.add(self.author1) @@ -33,12 +35,14 @@ def test_publish_repeated_add_author(self): self.page.authors.add(self.author2) author1 = Author.objects.get(id=self.author1.id) - self.failUnlessEqual([author1.public], list(self.page.public.authors.all())) + self.failUnlessEqual([author1.public], + list(self.page.public.authors.all())) self.page.publish() author1 = Author.objects.get(id=self.author1.id) author2 = Author.objects.get(id=self.author2.id) - self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) + self.failUnlessEqual([author1.public, author2.public], + list(self.page.public.authors.order_by('name'))) def test_publish_clear_authors(self): self.page.authors.add(self.author1, self.author2) @@ -46,10 +50,12 @@ def test_publish_clear_authors(self): author1 = Author.objects.get(id=self.author1.id) author2 = Author.objects.get(id=self.author2.id) - self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) + self.failUnlessEqual([author1.public, author2.public], + list(self.page.public.authors.order_by('name'))) self.page.authors.clear() - self.failUnlessEqual([author1.public, author2.public], list(self.page.public.authors.order_by('name'))) + self.failUnlessEqual([author1.public, author2.public], + list(self.page.public.authors.order_by('name'))) self.page.publish() - self.failUnlessEqual([], list(self.page.public.authors.all())) \ No newline at end of file + self.failUnlessEqual([], list(self.page.public.authors.all())) diff --git a/publish/tests/test_publishable_related_filter_spec.py b/publish/tests/test_publishable_related_filter_spec.py index f0bac83..808329c 100644 --- a/publish/tests/test_publishable_related_filter_spec.py +++ b/publish/tests/test_publishable_related_filter_spec.py @@ -4,8 +4,6 @@ from publish.tests.example_app.models import Page, Author - - class TestPublishableRelatedFilterSpec(TestCase): def test_overridden_spec(self): @@ -14,7 +12,9 @@ def test_overridden_spec(self): class dummy_request(object): GET = {} - spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) + spec = FieldListFilter.create( + Page._meta.get_field('authors'), + dummy_request, {}, Page, PublishableAdmin, None) self.failUnless(isinstance(spec, PublishableRelatedFieldListFilter)) def test_only_draft_shown(self): @@ -28,9 +28,11 @@ def test_only_draft_shown(self): class dummy_request(object): GET = {} - spec = FieldListFilter.create(Page._meta.get_field('authors'), dummy_request, {}, Page, PublishableAdmin, None) + spec = FieldListFilter.create( + Page._meta.get_field('authors'), dummy_request, {}, + Page, PublishableAdmin, None) lookup_choices = spec.lookup_choices self.failUnlessEqual(1, len(lookup_choices)) pk, label = lookup_choices[0] - self.failUnlessEqual(self.author.id, pk) \ No newline at end of file + self.failUnlessEqual(self.author.id, pk) diff --git a/publish/tests/test_undelete_selected.py b/publish/tests/test_undelete_selected.py index 1633a5b..b396dc7 100644 --- a/publish/tests/test_undelete_selected.py +++ b/publish/tests/test_undelete_selected.py @@ -7,8 +7,6 @@ from publish.tests.example_app.models import FlatPage - - class TestUndeleteSelected(TestCase): def setUp(self): @@ -29,9 +27,11 @@ def has_perm(cls, *arg): return True self.fp1.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DELETE, + self.fp1.publish_state) - response = undelete_selected(self.page_admin, dummy_request, FlatPage.objects.deleted()) + response = undelete_selected(self.page_admin, dummy_request, + FlatPage.objects.deleted()) self.failUnless(response is None) # publish state should no longer be delete @@ -47,6 +47,10 @@ def has_perm(cls, *arg): return False self.fp1.delete() - self.failUnlessEqual(Publishable.PUBLISH_DELETE, self.fp1.publish_state) + self.failUnlessEqual(Publishable.PUBLISH_DELETE, + self.fp1.publish_state) - self.assertRaises(PermissionDenied, undelete_selected, self.page_admin, dummy_request, FlatPage.objects.deleted()) \ No newline at end of file + self.assertRaises(PermissionDenied, + undelete_selected, + self.page_admin, dummy_request, + FlatPage.objects.deleted()) diff --git a/publish/utils.py b/publish/utils.py index 4dfe2ee..f72c8e4 100644 --- a/publish/utils.py +++ b/publish/utils.py @@ -1,27 +1,26 @@ - class NestedSet(object): ''' a class that can be used a bit like a set, but will let us store hiearchy too ''' - + def __init__(self): self._root_elements = [] self._children = {} - + def add(self, item, parent=None): if parent is None: self._root_elements.append(item) else: self._children[parent].append(item) - self._children[item]=[] + self._children[item] = [] def __contains__(self, item): return item in self._children - + def __len__(self): return len(self._children) - + def __iter__(self): return iter(self._children) @@ -32,7 +31,6 @@ def original(self, item): if child == item: return child return item - def _add_nested_items(self, items, nested): for item in items: @@ -45,7 +43,7 @@ def _nested_children(self, item): children = [] self._add_nested_items(self._children[item], children) return children - + def nested_items(self): items = [] self._add_nested_items(self._root_elements, items) diff --git a/setup.py b/setup.py index 8919b90..3e4c910 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ import re from setuptools import setup, find_packages, findall -version=__import__('publish').__version__ +version = __import__('publish').__version__ + def parse_requirements(file_name): requirements = [] @@ -18,7 +19,8 @@ def parse_requirements(file_name): def not_py(file_path): - return not(file_path.endswith('.py') or file_path.endswith('.pyc')) + return not (file_path.endswith('.py') or file_path.endswith('.pyc')) + core_packages = find_packages() core_package_data = {} @@ -26,6 +28,10 @@ def not_py(file_path): package_path = package.replace('.', '/') core_package_data[package] = filter(not_py, findall(package_path)) +download_url = 'https://github.com/johnsensible/django-publish/archive/v%s.zip#egg=django-publish-%s' % ( + version, version +) + setup( name='django-publish', version=version, @@ -34,7 +40,7 @@ def not_py(file_path): author='John Montgomery', author_email='john@sensibledevelopment.com', url='http://github.com/johnsensible/django-publish', - download_url='https://github.com/johnsensible/django-publish/archive/v%s.zip#egg=django-publish-%s' % (version, version), + download_url=download_url, license='BSD', packages=core_packages, package_data=core_package_data, diff --git a/tests/run_tests.sh b/tests/run_tests.sh deleted file mode 100755 index 2a00333..0000000 --- a/tests/run_tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -# run from parent directory (e.g. tests/run_tests.sh) -django-admin.py test publish --pythonpath=. --pythonpath=tests --settings=test_settings From b654d59e864f13bf1695d7a16442fb718149e79a Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 14:07:01 -0300 Subject: [PATCH 10/22] Update README.rst --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index d28f023..89efa4f 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,13 @@ Django Publish ============== .. image:: https://travis-ci.org/petry/django-publish.png?branch=master + :target: https://travis-ci.org/petry/django-publish + :alt: CI status on Travis CI + .. image:: https://codeq.io/github/petry/django-publish/badges/master.png + :target: https://codeq.io/github/petry/django-publish/branches/master + :alt: Quality score on Codeq + Handy mixin/abstract class for providing a "publisher workflow" to arbitrary Django_ models. From 6773476e9b36a2a82899fa397e96b23e663cea2c Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 15:24:11 -0300 Subject: [PATCH 11/22] update project to django 1.5.1 --- .travis.yml | 1 + examplecms/__init__.py | 1 + examplecms/examplecms/__init__.py | 0 .../media/images/next_landmark_2012-02.jpg | Bin 0 -> 241639 bytes examplecms/examplecms/settings.py | 165 ++++++++++++++++++ examplecms/examplecms/urls.py | 24 +++ examplecms/examplecms/wsgi.py | 32 ++++ examplecms/manage.py | 10 ++ .../templates/pubcms/page_detail.html | 0 examplecms/pubcms/urls.py | 5 +- examplecms/pubcms/views.py | 2 - examplecms/runserver.sh | 3 - examplecms/settings.py | 63 ------- examplecms/shell.sh | 3 - examplecms/syncdb.sh | 3 - examplecms/urls.py | 18 -- publish/models.py | 12 +- .../templates/admin/publish_change_form.html | 2 +- publish/tests/example_app/models.py | 3 + requirements.txt | 2 +- 20 files changed, 247 insertions(+), 102 deletions(-) create mode 100644 examplecms/__init__.py create mode 100644 examplecms/examplecms/__init__.py create mode 100644 examplecms/examplecms/media/images/next_landmark_2012-02.jpg create mode 100644 examplecms/examplecms/settings.py create mode 100644 examplecms/examplecms/urls.py create mode 100644 examplecms/examplecms/wsgi.py create mode 100644 examplecms/manage.py rename examplecms/{ => pubcms}/templates/pubcms/page_detail.html (100%) delete mode 100755 examplecms/runserver.sh delete mode 100644 examplecms/settings.py delete mode 100755 examplecms/shell.sh delete mode 100755 examplecms/syncdb.sh delete mode 100644 examplecms/urls.py diff --git a/.travis.yml b/.travis.yml index 8edec24..d214bd7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: env: - DJANGO_VERSION=1.4.5 + - DJANGO_VERSION=1.5.1 install: - make setup diff --git a/examplecms/__init__.py b/examplecms/__init__.py new file mode 100644 index 0000000..ea35eb4 --- /dev/null +++ b/examplecms/__init__.py @@ -0,0 +1 @@ +__author__ = 'petry' diff --git a/examplecms/examplecms/__init__.py b/examplecms/examplecms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examplecms/examplecms/media/images/next_landmark_2012-02.jpg b/examplecms/examplecms/media/images/next_landmark_2012-02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e557b91de2391ab32b48987d69dfc35d5b171a39 GIT binary patch literal 241639 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

=waX>7vs^ZNuvc+( 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 z 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 literal 0 HcmV?d00001 diff --git a/examplecms/examplecms/settings.py b/examplecms/examplecms/settings.py new file mode 100644 index 0000000..a1b3aac --- /dev/null +++ b/examplecms/examplecms/settings.py @@ -0,0 +1,165 @@ +import os +PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + # ('Your Name', 'your_email@example.com'), +) + +MANAGERS = ADMINS + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'example.db', # Or path to database file if using sqlite3. + # The following settings are not used with sqlite3: + 'USER': '', + 'PASSWORD': '', + 'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. + 'PORT': '', # Set to empty string for default. + } +} + +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [] + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# In a Windows environment this must be set to your system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale. +USE_L10N = True + +# If you set this to False, Django will not use timezone-aware datetimes. +USE_TZ = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/var/www/example.com/media/" +MEDIA_ROOT = os.path.join(PROJECT_PATH, 'media') + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://example.com/media/", "http://media.example.com/" +MEDIA_URL = '/media/' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/var/www/example.com/static/" +STATIC_ROOT = '' + +# URL prefix for static files. +# Example: "http://example.com/static/", "http://static.example.com/" +STATIC_URL = '/static/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = '6ec5dt14-0cxe!hha)um#y10$9o#r&p&mf&h%y9oj8dc@_el-j' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', + + # Uncomment the next line for simple clickjacking protection: + # 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'examplecms.urls' + +# Python dotted path to the WSGI application used by Django's runserver. +WSGI_APPLICATION = 'examplecms.wsgi.application' + +TEMPLATE_DIRS = ( + os.path.join(PROJECT_PATH, 'templates'), + + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.flatpages', + + 'publish', + 'pubcms', + # Uncomment the next line to enable the admin: + 'django.contrib.admin', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', +) + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error when DEBUG=False. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'filters': { + 'require_debug_false': { + '()': 'django.utils.log.RequireDebugFalse' + } + }, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'filters': ['require_debug_false'], + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} diff --git a/examplecms/examplecms/urls.py b/examplecms/examplecms/urls.py new file mode 100644 index 0000000..3d2d1c2 --- /dev/null +++ b/examplecms/examplecms/urls.py @@ -0,0 +1,24 @@ +from django.conf import settings +from django.conf.urls import patterns, include, url + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns( + '', + # Examples: + # url(r'^$', 'examplecms.views.home', name='home'), + # url(r'^examplecms/', include('examplecms.foo.urls')), + + # Uncomment the admin/doc line below to enable admin documentation: + # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Uncomment the next line to enable the admin: + (r'^media/(?P.*)$', 'django.views.static.serve', + {'document_root': settings.MEDIA_ROOT, + 'show_indexes': True}), + + url(r'^admin/', include(admin.site.urls)), + ('^', include('pubcms.urls')), +) diff --git a/examplecms/examplecms/wsgi.py b/examplecms/examplecms/wsgi.py new file mode 100644 index 0000000..c9f08b2 --- /dev/null +++ b/examplecms/examplecms/wsgi.py @@ -0,0 +1,32 @@ +""" +WSGI config for examplecms project. + +This module contains the WSGI application used by Django's development server +and any production WSGI deployments. It should expose a module-level variable +named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover +this application via the ``WSGI_APPLICATION`` setting. + +Usually you will have the standard Django WSGI application here, but it also +might make sense to replace the whole Django WSGI application with a custom one +that later delegates to the Django one. For example, you could introduce WSGI +middleware here, or combine a Django application with an application of another +framework. + +""" +import os + +# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks +# if running multiple sites in the same mod_wsgi process. To fix this, use +# mod_wsgi daemon mode with each site in its own daemon process, or use +# os.environ["DJANGO_SETTINGS_MODULE"] = "examplecms.settings" +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "examplecms.settings") + +# This application object is used by any WSGI server configured to use this +# file. This includes Django's development server, if the WSGI_APPLICATION +# setting points here. +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() + +# Apply WSGI middleware here. +# from helloworld.wsgi import HelloWorldApplication +# application = HelloWorldApplication(application) diff --git a/examplecms/manage.py b/examplecms/manage.py new file mode 100644 index 0000000..806b60e --- /dev/null +++ b/examplecms/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "examplecms.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/examplecms/templates/pubcms/page_detail.html b/examplecms/pubcms/templates/pubcms/page_detail.html similarity index 100% rename from examplecms/templates/pubcms/page_detail.html rename to examplecms/pubcms/templates/pubcms/page_detail.html diff --git a/examplecms/pubcms/urls.py b/examplecms/pubcms/urls.py index 8227edb..9d9ab3c 100644 --- a/examplecms/pubcms/urls.py +++ b/examplecms/pubcms/urls.py @@ -1,8 +1,7 @@ from django.conf.urls.defaults import * -from django.conf import settings -from views import page_detail -from models import Page +from pubcms.views import page_detail +from pubcms.models import Page urlpatterns = patterns( '', diff --git a/examplecms/pubcms/views.py b/examplecms/pubcms/views.py index 9fccc93..a3e3bad 100644 --- a/examplecms/pubcms/views.py +++ b/examplecms/pubcms/views.py @@ -1,7 +1,5 @@ from django.shortcuts import render_to_response, get_object_or_404 -from models import Page - def page_detail(request, page_url, queryset): parts = page_url.split('/') diff --git a/examplecms/runserver.sh b/examplecms/runserver.sh deleted file mode 100755 index 3f2e58b..0000000 --- a/examplecms/runserver.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -# run from parent directory -django-admin.py runserver --pythonpath=. --pythonpath=examplecms --settings=settings diff --git a/examplecms/settings.py b/examplecms/settings.py deleted file mode 100644 index b83841d..0000000 --- a/examplecms/settings.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( -) - -MANAGERS = ADMINS - -DATABASE_ENGINE = 'sqlite3' -DATABASE_NAME = os.path.join(PROJECT_PATH, 'example.db') -DATABASE_USER = '' -DATABASE_PASSWORD = '' -DATABASE_HOST = '' -DATABASE_PORT = '' - -TIME_ZONE = 'America/Chicago' - -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -USE_I18N = True - -MEDIA_ROOT = os.path.join(PROJECT_PATH, 'media') - -MEDIA_URL = '/media/' - -ADMIN_MEDIA_PREFIX = '/admin_media/' - -SECRET_KEY = '+9qi8mm2ddo&wb@0s(@9wfdhxmpe2cxh!cb3@7yd9ao13-s6ea' - -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.load_template_source', - 'django.template.loaders.app_directories.load_template_source', -) - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', -) - -ROOT_URLCONF = 'urls' - - -TEMPLATE_DIRS = ( - os.path.join(PROJECT_PATH, 'templates'), -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.flatpages', - 'django.contrib.admin', - 'pubcms', - 'publish', -) diff --git a/examplecms/shell.sh b/examplecms/shell.sh deleted file mode 100755 index 999ef07..0000000 --- a/examplecms/shell.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -# run from parent directory -django-admin.py shell --pythonpath=. --pythonpath=examplecms --settings=settings diff --git a/examplecms/syncdb.sh b/examplecms/syncdb.sh deleted file mode 100755 index a04e4b8..0000000 --- a/examplecms/syncdb.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -# run from parent directory -django-admin.py syncdb --pythonpath=. --pythonpath=examplecms --settings=settings diff --git a/examplecms/urls.py b/examplecms/urls.py deleted file mode 100644 index 4d3d60b..0000000 --- a/examplecms/urls.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.conf.urls.defaults import * -from django.conf import settings - -# Uncomment the next two lines to enable the admin: -from django.contrib import admin - -admin.autodiscover() - -urlpatterns = patterns( - '', - ('^admin/', include(admin.site.urls)), - - (r'^media/(?P.*)$', 'django.views.static.serve', - {'document_root': settings.MEDIA_ROOT, - 'show_indexes': True}), - - ('^', include('pubcms.urls')), -) diff --git a/publish/models.py b/publish/models.py index 85b2229..fe137d0 100644 --- a/publish/models.py +++ b/publish/models.py @@ -239,11 +239,12 @@ def _get_through_model(self, field_object): In 1.2 through is the class ''' through = field_object.rel.through - if through: - if isinstance(through, basestring): - return field_object.rel.through_model - return through - return None + if not through: + return None + + if isinstance(through, basestring): + return field_object.rel.through_model + return through def publish_changes(self, dry_run=False, all_published=None, parent=None): ''' @@ -405,6 +406,7 @@ def publish_deletions(self, all_published=None, parent=None, if name in self.PublishMeta.excluded_fields(): continue try: + instances = getattr(self, name).all() except AttributeError: instances = [getattr(self, name)] diff --git a/publish/templates/admin/publish_change_form.html b/publish/templates/admin/publish_change_form.html index 387a0df..c2f6b19 100644 --- a/publish/templates/admin/publish_change_form.html +++ b/publish/templates/admin/publish_change_form.html @@ -1,5 +1,5 @@ {% extends "admin/change_form.html" %} -{% load i18n admin_modify adminmedia %} +{% load i18n admin_modify %} {% block content %}

{% block object-tools %} diff --git a/publish/tests/example_app/models.py b/publish/tests/example_app/models.py index 2a881f4..78873da 100644 --- a/publish/tests/example_app/models.py +++ b/publish/tests/example_app/models.py @@ -25,6 +25,9 @@ def get_absolute_url(self): return self.url return '%s*' % self.url + def __unicode__(self): + return "{0} - {1}".format(self.url, self.get_publish_state_display()) + class Author(Publishable): name = models.CharField(max_length=100) diff --git a/requirements.txt b/requirements.txt index 5936dcf..555708d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Django==1.4.5 +Django==1.5.1 From a01f069d0c03f446146c5f70f65d0daf241d1bb0 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 15:51:10 -0300 Subject: [PATCH 12/22] pep8 fix --- publish/models.py | 14 +++++++++----- setup.cfg | 6 ++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/publish/models.py b/publish/models.py index fe137d0..b1a93fe 100644 --- a/publish/models.py +++ b/publish/models.py @@ -210,7 +210,8 @@ def publish(self, dry_run=False, all_published=None, parent=None): ''' if self.is_public: raise PublishException( - "Cannot publish public model - publish should be called from draft model") + "Cannot publish public model - publish should be called " + "from draft model") if self.pk is None: raise PublishException("Please save model before publishing") @@ -255,7 +256,8 @@ def publish_changes(self, dry_run=False, all_published=None, parent=None): information about what other models would be affected by this function ''' - assert not self.is_public, "Cannot publish public model - publish should be called from draft model" + assert not self.is_public, "Cannot publish public model - publish " \ + "should be called from draft model" assert self.pk is not None, "Please save model before publishing" # avoid mutual recursion @@ -274,7 +276,8 @@ def publish_changes(self, dry_run=False, all_published=None, parent=None): public_version = self.__class__(is_public=True) excluded_fields = self.PublishMeta.excluded_fields() - reverse_fields_to_publish = self.PublishMeta.reverse_fields_to_publish() + reverse_fields_to_publish = \ + self.PublishMeta.reverse_fields_to_publish() if self.publish_state == Publishable.PUBLISH_CHANGED: # copy over regular fields @@ -327,10 +330,11 @@ def publish_changes(self, dry_run=False, all_published=None, parent=None): related_name = reverse_field.name related_field = getattr(through_model, related_name).field - reverse_name = related_field.related.get_accessor_name() + reverse_name = \ + related_field.related.get_accessor_name() reverse_fields_to_publish.append(reverse_name) break - continue # m2m via through table won't be dealt with here + continue # m2m via through table won't be dealt with here related = field_object.rel.to if issubclass(related, Publishable): diff --git a/setup.cfg b/setup.cfg index 7ba1618..d07b51f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,5 @@ [nosetests] detailed-errors=1 with-coverage=1 -cover-package=nose -debug=nose.loader -pdb=1 -pdb-failures=1 +cover-package=publish +debug=nose.loader \ No newline at end of file From 0f250bdcc14639ee168bcfa27dfbb98645610e28 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sun, 12 May 2013 15:57:52 -0300 Subject: [PATCH 13/22] improve code quality --- examplecms/examplecms/settings.py | 62 ++----------------- examplecms/pubcms/urls.py | 2 +- publish/actions.py | 3 +- publish/tests/test_publish_selected_action.py | 5 +- publish/tests/test_publish_signal.py | 2 +- .../tests/test_publishable_recursive_fk.py | 7 +-- 6 files changed, 15 insertions(+), 66 deletions(-) diff --git a/examplecms/examplecms/settings.py b/examplecms/examplecms/settings.py index a1b3aac..dfaa914 100644 --- a/examplecms/examplecms/settings.py +++ b/examplecms/examplecms/settings.py @@ -5,92 +5,56 @@ TEMPLATE_DEBUG = DEBUG ADMINS = ( - # ('Your Name', 'your_email@example.com'), ) MANAGERS = ADMINS DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': 'example.db', # Or path to database file if using sqlite3. - # The following settings are not used with sqlite3: + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': 'example.db', 'USER': '', 'PASSWORD': '', - 'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP. - 'PORT': '', # Set to empty string for default. + 'HOST': '', + 'PORT': '', } } -# Hosts/domain names that are valid for this site; required if DEBUG is False -# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts ALLOWED_HOSTS = [] -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# In a Windows environment this must be set to your system time zone. TIME_ZONE = 'America/Chicago' -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' SITE_ID = 1 -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. USE_I18N = True -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale. USE_L10N = True -# If you set this to False, Django will not use timezone-aware datetimes. USE_TZ = True -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/var/www/example.com/media/" MEDIA_ROOT = os.path.join(PROJECT_PATH, 'media') -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://example.com/media/", "http://media.example.com/" MEDIA_URL = '/media/' -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/var/www/example.com/static/" STATIC_ROOT = '' -# URL prefix for static files. -# Example: "http://example.com/static/", "http://static.example.com/" STATIC_URL = '/static/' -# Additional locations of static files STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. ) -# List of finder classes that know how to find static files in -# various locations. STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', ) -# Make this unique, and don't share it with anybody. SECRET_KEY = '6ec5dt14-0cxe!hha)um#y10$9o#r&p&mf&h%y9oj8dc@_el-j' -# List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', ) MIDDLEWARE_CLASSES = ( @@ -100,22 +64,14 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', - - # Uncomment the next line for simple clickjacking protection: - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) ROOT_URLCONF = 'examplecms.urls' -# Python dotted path to the WSGI application used by Django's runserver. WSGI_APPLICATION = 'examplecms.wsgi.application' TEMPLATE_DIRS = ( os.path.join(PROJECT_PATH, 'templates'), - - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. ) INSTALLED_APPS = ( @@ -126,20 +82,12 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.flatpages', + 'django.contrib.admin', 'publish', 'pubcms', - # Uncomment the next line to enable the admin: - 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', ) -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error when DEBUG=False. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. LOGGING = { 'version': 1, 'disable_existing_loggers': False, diff --git a/examplecms/pubcms/urls.py b/examplecms/pubcms/urls.py index 9d9ab3c..e658bd7 100644 --- a/examplecms/pubcms/urls.py +++ b/examplecms/pubcms/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import * +from django.conf.urls import patterns, url from pubcms.views import page_detail from pubcms.models import Page diff --git a/publish/actions.py b/publish/actions.py index c87988e..83080c4 100644 --- a/publish/actions.py +++ b/publish/actions.py @@ -8,7 +8,8 @@ from django.utils.safestring import mark_safe from django.utils.text import capfirst from django.utils.translation import ugettext as _ -from django.contrib.admin.actions import delete_selected as django_delete_selected +from django.contrib.admin.actions import delete_selected as \ + django_delete_selected from models import Publishable from utils import NestedSet diff --git a/publish/tests/test_publish_selected_action.py b/publish/tests/test_publish_selected_action.py index 1151012..eba4ee9 100644 --- a/publish/tests/test_publish_selected_action.py +++ b/publish/tests/test_publish_selected_action.py @@ -94,7 +94,8 @@ def test_convert_all_published_to_html(self): all_published) expected = [ - u'Page: here (Changed - not yet published)' % page.id, + u'Page: here (Changed - ' + u'not yet published)' % page.id, [u'Page block: PageBlock object']] self.failUnlessEqual(expected, converted) @@ -198,5 +199,5 @@ def add(cls, *message): from django.contrib.admin.models import LogEntry from django.contrib.contenttypes.models import ContentType - content_type_id = ContentType.objects.get_for_model(self.fp1).pk + ContentType.objects.get_for_model(self.fp1).pk self.failUnlessEqual(2, LogEntry.objects.filter().count()) diff --git a/publish/tests/test_publish_signal.py b/publish/tests/test_publish_signal.py index 4ebb0e0..cdea9c9 100644 --- a/publish/tests/test_publish_signal.py +++ b/publish/tests/test_publish_signal.py @@ -94,7 +94,7 @@ def post_publish_handler(sender, instance, deleted, **kw): def test_deleted_flag_true_when_publishing_deletion(self): self.child1.publish() - public = self.child1.public + self.child1.public self.child1.delete() diff --git a/publish/tests/test_publishable_recursive_fk.py b/publish/tests/test_publishable_recursive_fk.py index ad5dfb8..fbd6d17 100644 --- a/publish/tests/test_publishable_recursive_fk.py +++ b/publish/tests/test_publishable_recursive_fk.py @@ -131,8 +131,7 @@ def test_publish_reverse_fields(self): self.failUnlessEqual(page_block.content, blocks[0].content) def test_publish_deletions_reverse_fields(self): - page_block = PageBlock.objects.create(page=self.page1, - content='here we are') + PageBlock.objects.create(page=self.page1, content='here we are') self.page1.publish() public = self.page1.public @@ -173,8 +172,8 @@ def test_publish_reverse_fields_deleted(self): def test_publish_delections_with_non_publishable_children(self): self.page1.publish() - comment = Comment.objects.create(page=self.page1.public, - comment='This is a comment') + Comment.objects.create(page=self.page1.public, + comment='This is a comment') self.failUnlessEqual(1, Comment.objects.count()) From 3dc8baa441e9072cd2fbb2bb67381a2113efd766 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sat, 22 Jun 2013 12:20:00 -0300 Subject: [PATCH 14/22] added publish button on change_form page --- publish/admin.py | 32 ++++++++++++- .../templates/admin/publish_change_form.html | 6 +-- .../templates/admin/publish_submit_line.html | 9 ++++ publish/templatetags/__init__.py | 2 + publish/templatetags/publish_admin_tags.py | 42 +++++++++++++++++ publish/tests/test_publishable_admin.py | 45 ++++++++++++++++++- 6 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 publish/templates/admin/publish_submit_line.html create mode 100644 publish/templatetags/__init__.py create mode 100644 publish/templatetags/publish_admin_tags.py diff --git a/publish/admin.py b/publish/admin.py index 10629d9..d9dfa12 100644 --- a/publish/admin.py +++ b/publish/admin.py @@ -1,11 +1,17 @@ from django.contrib import admin +from django.contrib.admin.util import unquote +from django.contrib.auth.admin import csrf_protect_m +from django.db import transaction from django.forms.models import BaseInlineFormSet -from django.utils.encoding import force_unicode +from django.http import HttpResponseRedirect +from django.utils.encoding import force_unicode, force_text +from django.utils.translation import ugettext as _ from .models import Publishable from .actions import publish_selected, delete_selected, undelete_selected from publish.filters import register_filters +from publish.utils import NestedSet register_filters() @@ -143,6 +149,30 @@ def render_change_form(self, request, context, add=False, change=False, change, form_url, obj) + def response_publish(self, request, obj): + opts = self.model._meta + msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} + + msg = _('The %(name)s "%(obj)s" was published successfully.') % msg_dict + self.message_user(request, msg, fail_silently=True) + return HttpResponseRedirect(request.path) + + + @transaction.commit_on_success + def change_view(self, request, object_id, form_url='', extra_context=None): + if request.method == "POST" and "_publish" in request.POST: + obj = self.get_object(request, unquote(object_id)) + + all_published = NestedSet() + obj.publish(all_published=all_published) + self.log_publication(request, obj) + + + return self.response_publish(request, obj) + return super(PublishableAdmin, self).change_view(request, object_id, + form_url, + extra_context) + class PublishableBaseInlineFormSet(BaseInlineFormSet): # we will actually delete inline objects, rather than diff --git a/publish/templates/admin/publish_change_form.html b/publish/templates/admin/publish_change_form.html index c2f6b19..5008d7a 100644 --- a/publish/templates/admin/publish_change_form.html +++ b/publish/templates/admin/publish_change_form.html @@ -1,5 +1,5 @@ {% extends "admin/change_form.html" %} -{% load i18n admin_modify %} +{% load i18n admin_modify publish_admin_tags%} {% block content %}
{% block object-tools %} @@ -46,15 +46,13 @@ {% block after_related_objects %}{% endblock %} -{% if not original or not original.is_marked_for_deletion %}{% submit_row %}{% endif %} - +{% if not original or not original.is_marked_for_deletion %}{% publish_submit_row %}{% endif %} {% if adminform and add %} {% endif %} {# JavaScript for prepopulated fields #} {% prepopulated_fields_js %} -
{% endblock %} diff --git a/publish/templates/admin/publish_submit_line.html b/publish/templates/admin/publish_submit_line.html new file mode 100644 index 0000000..eb0e9b7 --- /dev/null +++ b/publish/templates/admin/publish_submit_line.html @@ -0,0 +1,9 @@ +{% load i18n admin_urls %} +
+{% if show_publish %}{% endif %} +{% if show_save %}{% endif %} +{% if show_delete_link %}{% endif %} +{% if show_save_as_new %}{%endif%} +{% if show_save_and_add_another %}{% endif %} +{% if show_save_and_continue %}{% endif %} +
diff --git a/publish/templatetags/__init__.py b/publish/templatetags/__init__.py new file mode 100644 index 0000000..a5682fb --- /dev/null +++ b/publish/templatetags/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/publish/templatetags/publish_admin_tags.py b/publish/templatetags/publish_admin_tags.py new file mode 100644 index 0000000..954f584 --- /dev/null +++ b/publish/templatetags/publish_admin_tags.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from django import template +from django.contrib.admin.templatetags.admin_modify import submit_row + +register = template.Library() + + +@register.inclusion_tag('admin/publish_submit_line.html', takes_context=True) +def publish_submit_row(context): + ctx = submit_row(context) + ctx['show_publish'] = context.get('has_publish_permission') \ + and context.get('change') + + return ctx + +# @register.inclusion_tag('admin/publish_submit_line.html', takes_context=True) +# def publish_submit_row(context): +# """ +# Displays the row of buttons for delete and save. +# """ +# opts = context['opts'] +# change = context['change'] +# is_popup = context['is_popup'] +# save_as = context['save_as'] +# +# ctx = { +# 'opts': opts, +# 'onclick_attrib': (opts.get_ordered_objects() and change +# and 'onclick="submitOrderForm();"' or ''), +# 'show_delete_link': (not is_popup and context['has_delete_permission'] +# and change and context.get('show_delete', True)), +# 'show_save_as_new': not is_popup and change and save_as, +# 'show_save_and_add_another': context['has_add_permission'] and +# not is_popup and (not save_as or context['add']), +# 'show_save_and_continue': not is_popup and context['has_change_permission'], +# 'is_popup': is_popup, +# 'show_save': True +# } +# if context.get('original') is not None: +# ctx['original'] = context['original'] +# return ctx diff --git a/publish/tests/test_publishable_admin.py b/publish/tests/test_publishable_admin.py index a85332b..c715591 100644 --- a/publish/tests/test_publishable_admin.py +++ b/publish/tests/test_publishable_admin.py @@ -1,11 +1,11 @@ from django.conf import settings from django.conf.urls import patterns, include from django.contrib.admin import AdminSite -from django.contrib.auth.models import User +from django.contrib.auth.models import User, AnonymousUser from django.core.exceptions import PermissionDenied from django.forms import ModelChoiceField, ModelMultipleChoiceField from django.http import Http404 -from django.test import TestCase +from django.test import TestCase, RequestFactory from publish.admin import PublishableStackedInline, PublishableAdmin from publish.models import Publishable from publish.tests.example_app.models import Page, Author, PageBlock @@ -272,6 +272,7 @@ class dummy_request(object): method = 'POST' COOKIES = {} META = {} + POST = {} @classmethod def is_ajax(cls): @@ -361,3 +362,43 @@ def add(cls, *message): # the block should have been deleted (but not the public one) self.failUnlessEqual([public_block], list(PageBlock.objects.all())) + + +class PublishPage(TestCase): + + + def setUp(self): + super(PublishPage, self).setUp() + self.admin_site = AdminSite('Test Admin') + + class PageAdmin(PublishableAdmin): + pass + + self.admin_site.register(Page, PageAdmin) + self.page_admin = PageAdmin(Page, self.admin_site) + + def test_should_be_publish(self): + self.page1 = Page.objects.create(slug='page1', title='page 1') + + user1 = User.objects.create_superuser('test1', 'test@example.com', 'pass') + + self.factory = RequestFactory() + request = self.factory.post('/publish/change_view', data={'_publish':''}) + request.user = user1 + + self.page_admin.change_view(request, str(self.page1.id)) + self.assertEqual(Page.objects.filter(Page.Q_PUBLISHED, + slug=self.page1.slug).count(), 1) + + def test_should_be_republish(self): + self.page1 = Page.objects.create(slug='page1', title='page 1') + self.page1.publish() + user1 = User.objects.create_superuser('test1', 'test@example.com', 'pass') + + self.factory = RequestFactory() + request = self.factory.post('/publish/change_view', data={'_publish':''}) + request.user = user1 + + self.page_admin.change_view(request, str(self.page1.id)) + self.assertEqual(Page.objects.filter(Page.Q_PUBLISHED, + slug=self.page1.slug).count(), 1) From be18d4cd128904534ea9a7b191b8517640b2fc11 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sat, 22 Jun 2013 13:15:19 -0300 Subject: [PATCH 15/22] pep8 fixes --- publish/admin.py | 14 +++++------ publish/templatetags/__init__.py | 2 +- publish/templatetags/publish_admin_tags.py | 27 ---------------------- publish/tests/test_publishable_admin.py | 18 ++++++++------- setup.py | 3 ++- 5 files changed, 19 insertions(+), 45 deletions(-) diff --git a/publish/admin.py b/publish/admin.py index d9dfa12..0f721bd 100644 --- a/publish/admin.py +++ b/publish/admin.py @@ -91,10 +91,9 @@ def has_change_permission(self, request, obj=None): # user can never change public models directly # but can view old read-only copy of it if we are about to delete it if obj: - if obj.is_public or ( - request.method == 'POST' - and obj.publish_state == Publishable.PUBLISH_DELETE - ): + if obj.is_public \ + or (request.method == 'POST' + and obj.publish_state == Publishable.PUBLISH_DELETE): return False return super(PublishableAdmin, self).has_change_permission(request, obj) @@ -151,13 +150,13 @@ def render_change_form(self, request, context, add=False, change=False, def response_publish(self, request, obj): opts = self.model._meta - msg_dict = {'name': force_text(opts.verbose_name), 'obj': force_text(obj)} + msg_dict = {'name': force_text(opts.verbose_name), + 'obj': force_text(obj)} - msg = _('The %(name)s "%(obj)s" was published successfully.') % msg_dict + msg = _('The %(name)s "%(obj)s" was published successfully') % msg_dict self.message_user(request, msg, fail_silently=True) return HttpResponseRedirect(request.path) - @transaction.commit_on_success def change_view(self, request, object_id, form_url='', extra_context=None): if request.method == "POST" and "_publish" in request.POST: @@ -167,7 +166,6 @@ def change_view(self, request, object_id, form_url='', extra_context=None): obj.publish(all_published=all_published) self.log_publication(request, obj) - return self.response_publish(request, obj) return super(PublishableAdmin, self).change_view(request, object_id, form_url, diff --git a/publish/templatetags/__init__.py b/publish/templatetags/__init__.py index a5682fb..faa18be 100644 --- a/publish/templatetags/__init__.py +++ b/publish/templatetags/__init__.py @@ -1,2 +1,2 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- \ No newline at end of file +# -*- coding: utf-8 -*- diff --git a/publish/templatetags/publish_admin_tags.py b/publish/templatetags/publish_admin_tags.py index 954f584..62eb0ef 100644 --- a/publish/templatetags/publish_admin_tags.py +++ b/publish/templatetags/publish_admin_tags.py @@ -13,30 +13,3 @@ def publish_submit_row(context): and context.get('change') return ctx - -# @register.inclusion_tag('admin/publish_submit_line.html', takes_context=True) -# def publish_submit_row(context): -# """ -# Displays the row of buttons for delete and save. -# """ -# opts = context['opts'] -# change = context['change'] -# is_popup = context['is_popup'] -# save_as = context['save_as'] -# -# ctx = { -# 'opts': opts, -# 'onclick_attrib': (opts.get_ordered_objects() and change -# and 'onclick="submitOrderForm();"' or ''), -# 'show_delete_link': (not is_popup and context['has_delete_permission'] -# and change and context.get('show_delete', True)), -# 'show_save_as_new': not is_popup and change and save_as, -# 'show_save_and_add_another': context['has_add_permission'] and -# not is_popup and (not save_as or context['add']), -# 'show_save_and_continue': not is_popup and context['has_change_permission'], -# 'is_popup': is_popup, -# 'show_save': True -# } -# if context.get('original') is not None: -# ctx['original'] = context['original'] -# return ctx diff --git a/publish/tests/test_publishable_admin.py b/publish/tests/test_publishable_admin.py index c715591..4c09106 100644 --- a/publish/tests/test_publishable_admin.py +++ b/publish/tests/test_publishable_admin.py @@ -365,8 +365,6 @@ def add(cls, *message): class PublishPage(TestCase): - - def setUp(self): super(PublishPage, self).setUp() self.admin_site = AdminSite('Test Admin') @@ -380,25 +378,29 @@ class PageAdmin(PublishableAdmin): def test_should_be_publish(self): self.page1 = Page.objects.create(slug='page1', title='page 1') - user1 = User.objects.create_superuser('test1', 'test@example.com', 'pass') + user1 = User.objects.create_superuser('test1', 'test@example.com', + 'pass') self.factory = RequestFactory() - request = self.factory.post('/publish/change_view', data={'_publish':''}) + request = self.factory.post('/publish/change_view', + data={'_publish': ''}) request.user = user1 self.page_admin.change_view(request, str(self.page1.id)) self.assertEqual(Page.objects.filter(Page.Q_PUBLISHED, - slug=self.page1.slug).count(), 1) + slug=self.page1.slug).count(), 1) def test_should_be_republish(self): self.page1 = Page.objects.create(slug='page1', title='page 1') self.page1.publish() - user1 = User.objects.create_superuser('test1', 'test@example.com', 'pass') + user1 = User.objects.create_superuser('test1', 'test@example.com', + 'pass') self.factory = RequestFactory() - request = self.factory.post('/publish/change_view', data={'_publish':''}) + request = self.factory.post('/publish/change_view', + data={'_publish': ''}) request.user = user1 self.page_admin.change_view(request, str(self.page1.id)) self.assertEqual(Page.objects.filter(Page.Q_PUBLISHED, - slug=self.page1.slug).count(), 1) + slug=self.page1.slug).count(), 1) diff --git a/setup.py b/setup.py index 3e4c910..d5c9a0f 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,8 @@ def not_py(file_path): setup( name='django-publish', version=version, - description='Handy mixin/abstract class for providing a "publisher workflow" to arbitrary Django models.', + description='Handy mixin/abstract class for providing a "publisher ' + 'workflow" to arbitrary Django models.', long_description=open('README.rst').read(), author='John Montgomery', author_email='john@sensibledevelopment.com', From 1a10d7857c2508788c3914617f49bb1ee840e4a0 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sat, 22 Jun 2013 13:23:45 -0300 Subject: [PATCH 16/22] added coveralls --- .travis.yml | 2 ++ requirements_test.txt | 1 + 2 files changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index d214bd7..9d6f0cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,5 @@ install: script: make test +after_success: + - coveralls diff --git a/requirements_test.txt b/requirements_test.txt index e5615c8..e182035 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,2 +1,3 @@ nose==1.3.0 django-nose==1.1 +coveralls==0.2 From 8d093c22ce846542a4858613e17721b49603095a Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sat, 22 Jun 2013 19:17:52 -0300 Subject: [PATCH 17/22] converage on root folder --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e6dcf93..f990fde 100644 --- a/Makefile +++ b/Makefile @@ -11,4 +11,4 @@ deps: setup: deps test: clean deps - @cd publish && nosetests -s -v --with-coverage --cover-package=publish \ No newline at end of file + @nosetests -s -v --with-coverage --cover-package=publish \ No newline at end of file From bd9d5e447cde0406c74f914503b89c1cda6f27ba Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sat, 22 Jun 2013 20:34:37 -0300 Subject: [PATCH 18/22] added coveragerc --- .coveragerc | 3 +++ Makefile | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..8eaf548 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +include = + publish/* diff --git a/Makefile b/Makefile index f990fde..1e7414e 100644 --- a/Makefile +++ b/Makefile @@ -11,4 +11,4 @@ deps: setup: deps test: clean deps - @nosetests -s -v --with-coverage --cover-package=publish \ No newline at end of file + @nosetests -s -v --with-coverage \ No newline at end of file From 78f8a20751380bc95f7bedc7a151571746a54264 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Sat, 22 Jun 2013 20:38:57 -0300 Subject: [PATCH 19/22] added coveralls badge --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 89efa4f..e3fdd76 100644 --- a/README.rst +++ b/README.rst @@ -10,6 +10,10 @@ Django Publish :target: https://codeq.io/github/petry/django-publish/branches/master :alt: Quality score on Codeq +.. image:: https://coveralls.io/repos/petry/django-publish/badge.png?branch=master + :target: https://coveralls.io/r/petry/django-publish + :alt: Coverage Status + Handy mixin/abstract class for providing a "publisher workflow" to arbitrary Django_ models. From 7f73c657a1233b7cf4dd52286a0e21dc5a34697c Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Tue, 1 Oct 2013 00:40:17 -0300 Subject: [PATCH 20/22] change button order to save an item when press enter button --- publish/templates/admin/publish_submit_line.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publish/templates/admin/publish_submit_line.html b/publish/templates/admin/publish_submit_line.html index eb0e9b7..a4a11b4 100644 --- a/publish/templates/admin/publish_submit_line.html +++ b/publish/templates/admin/publish_submit_line.html @@ -1,7 +1,7 @@ {% load i18n admin_urls %}
-{% if show_publish %}{% endif %} {% if show_save %}{% endif %} +{% if show_publish %}{% endif %} {% if show_delete_link %}{% endif %} {% if show_save_as_new %}{%endif%} {% if show_save_and_add_another %}{% endif %} From 292409a7af97d4d49a49037dddf93bef64b7443f Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Tue, 1 Oct 2013 01:05:37 -0300 Subject: [PATCH 21/22] compatible with django 1.5 latest version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 555708d..057ab2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Django==1.5.1 +Django<=1.6 From 3851b10682be72ea77a1df034372c2c928784820 Mon Sep 17 00:00:00 2001 From: Marcos Daniel Petry Date: Tue, 1 Oct 2013 12:13:09 -0300 Subject: [PATCH 22/22] fix compatibility with django<1.5 --- .travis.yml | 2 +- publish/admin.py | 5 +++-- publish/templates/admin/publish_submit_line.html | 8 ++++++-- requirements.txt | 2 +- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9d6f0cd..dc4eb7f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - 2.7 env: - - DJANGO_VERSION=1.4.5 + - DJANGO_VERSION=1.4.8 - DJANGO_VERSION=1.5.1 install: diff --git a/publish/admin.py b/publish/admin.py index 0f721bd..40a89dc 100644 --- a/publish/admin.py +++ b/publish/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin +from django.contrib import messages from django.contrib.admin.util import unquote -from django.contrib.auth.admin import csrf_protect_m from django.db import transaction from django.forms.models import BaseInlineFormSet from django.http import HttpResponseRedirect @@ -154,7 +154,8 @@ def response_publish(self, request, obj): 'obj': force_text(obj)} msg = _('The %(name)s "%(obj)s" was published successfully') % msg_dict - self.message_user(request, msg, fail_silently=True) + + messages.success(request, msg, fail_silently=True) return HttpResponseRedirect(request.path) @transaction.commit_on_success diff --git a/publish/templates/admin/publish_submit_line.html b/publish/templates/admin/publish_submit_line.html index a4a11b4..0595370 100644 --- a/publish/templates/admin/publish_submit_line.html +++ b/publish/templates/admin/publish_submit_line.html @@ -1,8 +1,12 @@ -{% load i18n admin_urls %} + + + + +
{% if show_save %}{% endif %} {% if show_publish %}{% endif %} -{% if show_delete_link %}{% endif %} +{% if show_delete_link %}{% endif %} {% if show_save_as_new %}{%endif%} {% if show_save_and_add_another %}{% endif %} {% if show_save_and_continue %}{% endif %} diff --git a/requirements.txt b/requirements.txt index 057ab2c..bea531e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -Django<=1.6 +Django<1.6