diff --git a/apps/access/tests/__init__.py b/apps/access/tests/__init__.py index 5a4b3373254..1cb1eb1db9e 100644 --- a/apps/access/tests/__init__.py +++ b/apps/access/tests/__init__.py @@ -4,11 +4,18 @@ import test_utils import access +from authority.models import Permission from forums.models import Forum, Thread -from sumo.tests import TestCase +from sumo.tests import TestCase, with_save from sumo.urlresolvers import reverse +@with_save +def permission(**kwargs): + if 'approved' not in kwargs: + kwargs['approved'] = True + return Permission(**kwargs) + class AccessTests(TestCase): """Test stuff in access/__init__.py""" fixtures = ['users.json', 'posts.json', 'forums_permissions.json'] diff --git a/apps/forums/tasks.py b/apps/forums/tasks.py index 89f12e7b067..0f764ac8c71 100644 --- a/apps/forums/tasks.py +++ b/apps/forums/tasks.py @@ -41,4 +41,5 @@ def connector(sender, instance, created, **kw): if created: log_reply.delay(instance) + post_save.connect(connector, sender=Post, dispatch_uid='forum_post_activity') diff --git a/apps/forums/tests/__init__.py b/apps/forums/tests/__init__.py index d75fd72bdfe..f8db619569d 100644 --- a/apps/forums/tests/__init__.py +++ b/apps/forums/tests/__init__.py @@ -1,155 +1,31 @@ from datetime import datetime +import uuid -from django.conf import settings -from django.contrib.auth.models import User +from django.template.defaultfilters import slugify -from nose.tools import eq_ - -from forums.models import Forum, Thread, Post, ThreadLockedError -from forums.views import sort_threads -from sumo.tests import get, LocalizingClient, TestCase, with_save +from forums.models import Forum, Thread, Post from users.tests import user +from sumo.tests import LocalizingClient, TestCase, with_save class ForumTestCase(TestCase): - fixtures = ['users.json', 'posts.json', 'forums_permissions.json'] client_class = LocalizingClient -class PostTestCase(ForumTestCase): - - def test_new_post_updates_thread(self): - """Saving a new post in a thread should update the last_post key in - that thread to point to the new post.""" - t = Thread.objects.get(pk=2) - post = t.new_post(author=t.creator, content='an update') - post.save() - eq_(post.id, t.last_post_id) - - def test_new_post_updates_forum(self): - """Saving a new post should update the last_post key in the forum to - point to the new post.""" - t = Thread.objects.get(pk=2) - f = t.forum - post = t.new_post(author=t.creator, content='another update') - post.save() - eq_(post.id, f.last_post_id) - - def test_update_post_does_not_update_thread(self): - """Updating/saving an old post in a thread should _not_ update the - last_post key in that thread.""" - p = Post.objects.get(pk=2) - old = p.thread.last_post_id - p.content = 'updated content' - p.save() - eq_(old, p.thread.last_post_id) - - def test_update_forum_does_not_update_thread(self): - """Updating/saving an old post in a forum should _not_ update the - last_post key in that forum.""" - p = Post.objects.get(pk=2) - old = p.thread.forum.last_post_id - p.content = 'updated content' - p.save() - eq_(old, p.thread.forum.last_post_id) - - def test_replies_count(self): - """The Thread.replies value should remain one less than the number of - posts in the thread.""" - t = Thread.objects.get(pk=2) - old = t.replies - t.new_post(author=t.creator, content='test').save() - eq_(old + 1, t.replies) - - def test_sticky_threads_first(self): - """Sticky threads should come before non-sticky threads.""" - thread = Thread.objects.all()[0] - # Thread 2 is the only sticky thread. - eq_(2, thread.id) - - def test_thread_sorting(self): - """After the sticky threads, threads should be sorted by the created - date of the last post.""" - threads = Thread.objects.filter(is_sticky=False) - self.assert_(threads[0].last_post.created > - threads[1].last_post.created) - - def test_post_sorting(self): - """Posts should be sorted chronologically.""" - posts = Thread.objects.get(pk=1).post_set.all() - for i in range(len(posts) - 1): - self.assert_(posts[i].created <= posts[i + 1].created) - - def test_sorting_creator(self): - """Sorting threads by creator.""" - threads = sort_threads(Thread.objects, 3, 1) - self.assert_(threads[0].creator.username >= - threads[1].creator.username) - - def test_sorting_replies(self): - """Sorting threads by replies.""" - threads = sort_threads(Thread.objects, 4) - self.assert_(threads[0].replies <= threads[1].replies) - - def test_sorting_last_post_desc(self): - """Sorting threads by last_post descendingly.""" - threads = sort_threads(Thread.objects, 5, 1) - self.assert_(threads[0].last_post.created >= - threads[1].last_post.created) - - def test_thread_last_page(self): - """Thread's last_page property is accurate.""" - thread = Thread.objects.all()[0] - # Format: (# replies, # of pages to expect) - test_data = ((thread.replies, 1), # Test default - (50, 3), # Test a large number - (19, 1), # Test off-by-one error, low - (20, 2), # Test off-by-one error, high - ) - for replies, pages in test_data: - thread.replies = replies - eq_(thread.last_page, pages) - - def test_locked_thread(self): - """Trying to reply to a locked thread should raise an exception.""" - locked = Thread.objects.get(pk=3) - open = Thread.objects.get(pk=2) - user1 = User.objects.get(pk=118533) - fn = lambda: locked.new_post(author=user1, content='empty') - self.assertRaises(ThreadLockedError, fn) - - # This should not raise an exception. - open.new_post(author=user1, content='empty') - - def test_post_no_session(self): - r = get(self.client, 'forums.new_thread', - kwargs={'forum_slug': 'test-forum'}) - assert(settings.LOGIN_URL in r.redirect_chain[0][0]) - eq_(302, r.redirect_chain[0][1]) - - -class ThreadTestCase(ForumTestCase): - - def test_delete_no_session(self): - """Delete a thread while logged out redirects.""" - r = get(self.client, 'forums.delete_thread', - kwargs={'forum_slug': 'test-forum', 'thread_id': 1}) - assert(settings.LOGIN_URL in r.redirect_chain[0][0]) - eq_(302, r.redirect_chain[0][1]) - - @with_save def forum(**kwargs): if 'name' not in kwargs: - kwargs['name'] = u'test forum' + kwargs['name'] = str(uuid.uuid4()) if 'slug' not in kwargs: - kwargs['slug'] = u'testforum' + kwargs['slug'] = slugify(kwargs['name']) return Forum(**kwargs) @with_save def thread(**kwargs): - defaults = dict(created=datetime.now()) + defaults = dict( + created=datetime.now(), + title=str(uuid.uuid4())) defaults.update(kwargs) if 'creator' not in kwargs and 'creator_id' not in kwargs: defaults['creator'] = user(save=True) @@ -164,4 +40,13 @@ def post(**kwargs): defaults.update(kwargs) if 'author' not in kwargs and 'author_id' not in kwargs: defaults['author'] = user(save=True) + if 'thread' not in kwargs and 'thread_id' not in kwargs: + # The thread creator should match the post author if the + # thread doesn't exist yet. + kw = {} + if 'author' in defaults: + kw['creator'] = defaults['author'] + else: + kw['creator_id'] = defaults['author_id'] + defaults['thread'] = thread(save=True, **kw) return Post(**defaults) diff --git a/apps/forums/tests/test_activity.py b/apps/forums/tests/test_activity.py index 6d01cb38bfe..cb9f31c14fb 100644 --- a/apps/forums/tests/test_activity.py +++ b/apps/forums/tests/test_activity.py @@ -1,10 +1,9 @@ -from django.contrib.auth.models import User - from nose.tools import eq_ from activity.models import Action -from forums.models import Forum, Post, Thread -from forums.tests import ForumTestCase +from forums.tests import ForumTestCase, thread, post as forum_post +from sumo.tests import post +from users.tests import user class ReplyLoggingTests(ForumTestCase): @@ -14,13 +13,15 @@ def setUp(self): def test_activity_logged(self): assert not Action.uncached.exists(), 'Actions start empty.' - orig, replier = User.objects.all()[0:2] - f = Forum.objects.all()[0] - t = Thread.objects.create(creator=orig, title='foo', forum=f) - Post.objects.create(author=orig, content='foo', thread=t) + orig = user(save=True) + replier = user(save=True) + t = thread(creator=orig, title='foo', save=True) + forum_post(author=orig, content='foo', thread=t, save=True) assert not Action.uncached.exists(), 'No actions were logged.' - Post.objects.create(author=replier, content='foo2', thread=t) + self.client.login(username=replier.username, password='testpass') + post(self.client, 'forums.reply', {'content': 'foo bar'}, + args=[t.forum.slug, t.id]) eq_(1, Action.uncached.count(), 'One action was logged.') a = Action.uncached.all()[0] diff --git a/apps/forums/tests/test_feeds.py b/apps/forums/tests/test_feeds.py index 6cdb6884752..46402431522 100644 --- a/apps/forums/tests/test_feeds.py +++ b/apps/forums/tests/test_feeds.py @@ -1,9 +1,13 @@ +from datetime import datetime, timedelta from nose.tools import eq_ from pyquery import PyQuery as pq from forums.feeds import ThreadsFeed, PostsFeed -from forums.models import Forum, Thread -from forums.tests import ForumTestCase, get +from forums.tests import ForumTestCase, forum, thread, post +from sumo.tests import get + + +YESTERDAY = datetime.now() - timedelta(days=1) class ForumTestFeedSorting(ForumTestCase): @@ -13,23 +17,32 @@ def setUp(self): def test_threads_sort(self): """Ensure that threads are being sorted properly by date/time.""" - f = Forum.objects.get(pk=1) - given_ = ThreadsFeed().items(f)[0].id - exp_ = 4L - eq_(exp_, given_) + # Threads are sorted descending by last post date. + f = forum(save=True) + t1 = thread(forum=f, created=YESTERDAY, save=True) + post(thread=t1, created=YESTERDAY, save=True) + t2 = thread(forum=f, save=True) + post(thread=t2, save=True) + + eq_(t2.id, ThreadsFeed().items(f)[0].id) def test_posts_sort(self): """Ensure that posts are being sorted properly by date/time.""" - t = Thread.objects.get(pk=1) - given_ = PostsFeed().items(t)[0].id - exp_ = 24L - eq_(exp_, given_) + t = thread(save=True) + post(thread=t, created=YESTERDAY, save=True) + post(thread=t, created=YESTERDAY, save=True) + p = post(thread=t, save=True) + + # The newest post should be the first one listed. + eq_(p.id, PostsFeed().items(t)[0].id) def test_multi_feed_titling(self): """Ensure that titles are being applied properly to feeds.""" - forum = Forum.objects.filter()[0] + t = thread(save=True) + forum = t.forum + post(thread=t, save=True) + response = get(self.client, 'forums.threads', args=[forum.slug]) doc = pq(response.content) - given_ = doc('link[type="application/atom+xml"]')[0].attrib['title'] - exp_ = ThreadsFeed().title(forum) - eq_(exp_, given_) + eq_(ThreadsFeed().title(forum), + doc('link[type="application/atom+xml"]')[0].attrib['title']) diff --git a/apps/forums/tests/test_models.py b/apps/forums/tests/test_models.py index 7679d7a9a5a..c2280090cc0 100644 --- a/apps/forums/tests/test_models.py +++ b/apps/forums/tests/test_models.py @@ -1,127 +1,164 @@ -import datetime +from datetime import datetime, timedelta -from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType from nose.tools import eq_ +from access.tests import permission +from forums import POSTS_PER_PAGE from forums.events import NewPostEvent, NewThreadEvent from forums.models import Forum, Thread, Post -from forums.tests import ForumTestCase -from sumo.urlresolvers import reverse +from forums.tests import ForumTestCase, forum, thread, post from sumo.helpers import urlparams +from sumo.urlresolvers import reverse +from users.tests import user -class ForumModelTestCase(ForumTestCase): +YESTERDAY = datetime.now() - timedelta(days=1) - def setUp(self): - super(ForumModelTestCase, self).setUp() +class ForumModelTestCase(ForumTestCase): def test_forum_absolute_url(self): - f = Forum.objects.get(pk=1) - exp_ = reverse('forums.threads', kwargs={'forum_slug': f.slug}) - eq_(exp_, f.get_absolute_url()) + f = forum(save=True) + + eq_('/forums/%s' % f.slug, + f.get_absolute_url()) def test_thread_absolute_url(self): - t = Thread.objects.get(pk=1) - exp_ = reverse('forums.posts', kwargs={'forum_slug': t.forum.slug, - 'thread_id': t.id}) - eq_(exp_, t.get_absolute_url()) + t = thread(save=True) + + eq_('/forums/%s/%s' % (t.forum.slug, t.id), + t.get_absolute_url()) def test_post_absolute_url(self): - p = Post.objects.get(pk=1) - url_ = reverse('forums.posts', - kwargs={'forum_slug': p.thread.forum.slug, - 'thread_id': p.thread.id}) - exp_ = urlparams(url_, hash='post-%s' % p.id) - eq_(exp_, p.get_absolute_url()) - - p = Post.objects.get(pk=24) - url_ = reverse('forums.posts', - kwargs={'forum_slug': p.thread.forum.slug, - 'thread_id': p.thread.id}) - exp_ = urlparams(url_, hash='post-%s' % p.id, page=2) - eq_(exp_, p.get_absolute_url()) + t = thread(save=True) + + # Fill out the first page with posts from yesterday. + p1 = post(thread=t, created=YESTERDAY, save=True) + for i in range(POSTS_PER_PAGE - 1): + post(thread=t, created=YESTERDAY, save=True) + # Second page post from today. + p2 = post(thread=t, save=True) + + url = reverse('forums.posts', + kwargs={'forum_slug': p1.thread.forum.slug, + 'thread_id': p1.thread.id}) + eq_(urlparams(url, hash='post-%s' % p1.id), p1.get_absolute_url()) + + url = reverse('forums.posts', + kwargs={'forum_slug': p2.thread.forum.slug, + 'thread_id': p2.thread.id}) + exp_ = urlparams(url, hash='post-%s' % p2.id, page=2) + eq_(exp_, p2.get_absolute_url()) def test_post_page(self): - p = Post.objects.get(pk=1) - eq_(1, p.page) - p = Post.objects.get(pk=22) - eq_(1, p.page) - p = Post.objects.get(pk=24) - eq_(2, p.page) + t = thread(save=True) + # Fill out the first page with posts from yesterday. + page1 = [] + for i in range(POSTS_PER_PAGE): + page1.append(post(thread=t, created=YESTERDAY, save=True)) + # Second page post from today. + p2 = post(thread=t, save=True) + + for p in page1: + eq_(1, p.page) + eq_(2, p2.page) def test_thread_last_post_url(self): - p = Post.objects.get(pk=24) - t = p.thread + t = thread(save=True) + post(thread=t, save=True) lp = t.last_post f = t.forum - url_ = t.get_last_post_url() - assert f.slug in url_ - assert str(t.id) in url_ - assert '#post-%s' % lp.id in url_ - assert 'last=%s' % lp.id in url_ + url = t.get_last_post_url() + assert f.slug in url + assert str(t.id) in url + assert '#post-%s' % lp.id in url + assert 'last=%s' % lp.id in url def test_last_post_updated(self): """Adding/Deleting the last post in a thread and forum should update the last_post field """ - thread = Thread.objects.get(pk=4) - user = User.objects.get(pk=118533) + orig_post = post(created=YESTERDAY, save=True) + t = orig_post.thread # add a new post, then check that last_post is updated - new_post = Post(thread=thread, content="test", author=user) - new_post.save() - forum = Forum.objects.get(pk=1) - thread = Thread.objects.get(pk=thread.id) - eq_(forum.last_post.id, new_post.id) - eq_(thread.last_post.id, new_post.id) + new_post = post(thread=t, content="test", save=True) + f = Forum.objects.get(id=t.forum_id) + t = Thread.objects.get(id=t.id) + eq_(f.last_post.id, new_post.id) + eq_(t.last_post.id, new_post.id) # delete the new post, then check that last_post is updated new_post.delete() - forum = Forum.objects.get(pk=1) - thread = Thread.objects.get(pk=thread.id) - eq_(forum.last_post.id, 25) - eq_(thread.last_post.id, 25) + f = Forum.objects.get(id=f.id) + t = Thread.objects.get(id=t.id) + eq_(f.last_post.id, orig_post.id) + eq_(t.last_post.id, orig_post.id) def test_public_access(self): """Assert Forums think they're publicly viewable and postable at appropriate times.""" - forum = Forum.objects.get(pk=1) - unprivileged_user = User.objects.get(pk=118533) - assert forum.allows_viewing_by(unprivileged_user) - assert forum.allows_posting_by(unprivileged_user) + # By default, users have access to forums that aren't restricted. + u = user(save=True) + f = forum(save=True) + assert f.allows_viewing_by(u) + assert f.allows_posting_by(u) def test_access_restriction(self): """Assert Forums are inaccessible to the public when restricted.""" - forum = Forum.objects.get(pk=3) - unprivileged_user = User.objects.get(pk=118533) - assert not forum.allows_viewing_by(unprivileged_user) - assert not forum.allows_posting_by(unprivileged_user) + # If the a forum has 'forums_forum.view_in_forum' permission defined, + # then it isn't public by default. If it has + # 'forums_forum.post_in_forum', then it isn't postable to by default. + f = forum(save=True) + ct = ContentType.objects.get_for_model(f) + permission(codename='forums_forum.view_in_forum', content_type=ct, + object_id=f.id, save=True) + permission(codename='forums_forum.post_in_forum', content_type=ct, + object_id=f.id, save=True) + + unprivileged_user = user(save=True) + assert not f.allows_viewing_by(unprivileged_user) + assert not f.allows_posting_by(unprivileged_user) def test_move_updates_last_posts(self): """Moving the thread containing a forum's last post to a new forum should update the last_post of both forums. Consequently, deleting the last post shouldn't delete the old forum. [bug 588994]""" - old_forum = Forum.objects.get(pk=1) # forum 1 has newest post - new_forum = Forum.objects.get(pk=2) - last_post = old_forum.last_post - thread = last_post.thread - thread.forum = new_forum - thread.save() + # Setup forum to move latest thread from. + old_forum = forum(save=True) + t1 = thread(forum=old_forum, save=True) + p1 = post(thread=t1, created=YESTERDAY, save=True) + t2 = thread(forum=old_forum, save=True) + p2 = post(thread=t2, save=True) # Newest post of all. + + # Setup forum to move latest thread to. + new_forum = forum(save=True) + t3 = thread(forum=new_forum, save=True) + p3 = post(thread=t3, created=YESTERDAY, save=True) + + # Verify the last_post's are correct. + eq_(p2, Forum.objects.get(id=old_forum.id).last_post) + eq_(p3, Forum.objects.get(id=new_forum.id).last_post) + + # Move the t2 thread. + t2 = Thread.objects.get(id=t2.id) + t2.forum = new_forum + t2.save() # Old forum's last_post updated? - self.assertNotEqual(Forum.objects.get(pk=1).last_post, last_post) + eq_(p1.id, Forum.objects.get(id=old_forum.id).last_post_id) # New forum's last_post updated? - eq_(Forum.objects.get(pk=2).last_post, last_post) + eq_(p2.id, Forum.objects.get(id=new_forum.id).last_post_id) # Delete the post, and both forums should still exist: - last_post.delete() - eq_(1, Forum.objects.filter(pk=1).count()) - eq_(1, Forum.objects.filter(pk=2).count()) + p2.delete() + eq_(1, Forum.objects.filter(id=old_forum.id).count()) + eq_(1, Forum.objects.filter(id=new_forum.id).count()) def test_delete_removes_watches(self): - f = Forum.objects.get(pk=1) + f = forum(save=True) NewThreadEvent.notify('me@me.com', f) assert NewThreadEvent.is_notifying('me@me.com', f) f.delete() @@ -129,34 +166,29 @@ def test_delete_removes_watches(self): class ThreadModelTestCase(ForumTestCase): - - def setUp(self): - super(ThreadModelTestCase, self).setUp() - self.fixtures = self.fixtures + ['notifications.json'] - def test_delete_thread_with_last_forum_post(self): """Deleting the thread with a forum's last post should update the last_post field on the forum """ - forum = Forum.objects.get(pk=1) - last_post = forum.last_post + t = thread(save=True) + post(thread=t, save=True) + f = t.forum + last_post = f.last_post # add a new thread and post, verify last_post updated - thread = Thread(title="test", forum=forum, creator_id=118533) - thread.save() - post = Post(thread=thread, content="test", author=thread.creator) - post.save() - forum = Forum.objects.get(pk=1) - eq_(forum.last_post.id, post.id) + t = thread(title="test", forum=f, save=True) + p = post(thread=t, content="test", author=t.creator, save=True) + f = Forum.objects.get(id=f.id) + eq_(f.last_post.id, p.id) # delete the post, verify last_post updated - thread.delete() - forum = Forum.objects.get(pk=1) - eq_(forum.last_post.id, last_post.id) - eq_(Thread.objects.filter(pk=thread.id).count(), 0) + t.delete() + f = Forum.objects.get(id=f.id) + eq_(f.last_post.id, last_post.id) + eq_(Thread.objects.filter(pk=t.id).count(), 0) def test_delete_removes_watches(self): - t = Thread.objects.get(pk=1) + t = thread(save=True) NewPostEvent.notify('me@me.com', t) assert NewPostEvent.is_notifying('me@me.com', t) t.delete() @@ -164,14 +196,12 @@ def test_delete_removes_watches(self): def test_delete_last_and_only_post_in_thread(self): """Deleting the only post in a thread should delete the thread""" - forum = Forum.objects.get(pk=1) - thread = Thread(title="test", forum=forum, creator_id=118533) - thread.save() - post = Post(thread=thread, content="test", author=thread.creator) - post.save() - eq_(1, thread.post_set.count()) - post.delete() - eq_(0, Thread.uncached.filter(pk=thread.id).count()) + t = thread(save=True) + post(thread=t, save=True) + + eq_(1, t.post_set.count()) + t.delete() + eq_(0, Thread.uncached.filter(pk=t.id).count()) class SaveDateTestCase(ForumTestCase): @@ -180,14 +210,14 @@ class SaveDateTestCase(ForumTestCase): and updated dates. """ - delta = datetime.timedelta(milliseconds=300) + delta = timedelta(milliseconds=300) def setUp(self): super(SaveDateTestCase, self).setUp() - self.user = User.objects.get(pk=118533) - self.forum = Forum.objects.get(pk=1) - self.thread = Thread.objects.get(pk=2) + self.user = user(save=True) + self.thread = thread(save=True) + self.forum = self.thread.forum def assertDateTimeAlmostEqual(self, a, b, delta, msg=None): """ @@ -198,9 +228,10 @@ def assertDateTimeAlmostEqual(self, a, b, delta, msg=None): def test_save_thread_no_created(self): """Saving a new thread should behave as if auto_add_now was set.""" - t = self.forum.thread_set.create(title='foo', creator=self.user) + t = thread(forum=self.forum, title='foo', creator=self.user, + save=True) t.save() - now = datetime.datetime.now() + now = datetime.now() self.assertDateTimeAlmostEqual(now, t.created, self.delta) def test_save_thread_created(self): @@ -208,18 +239,23 @@ def test_save_thread_created(self): Saving a new thread that already has a created date should respect that created date. """ - - created = datetime.datetime(1992, 1, 12, 9, 48, 23) - t = self.forum.thread_set.create(title='foo', creator=self.user, - created=created) + created = datetime(1992, 1, 12, 9, 48, 23) + t = thread(forum=self.forum, title='foo', creator=self.user, + created=created, save=True) t.save() eq_(created, t.created) def test_save_old_thread_created(self): """Saving an old thread should not change its created date.""" - t = Thread.objects.get(pk=3) + t = thread(created=YESTERDAY, save=True) + t = Thread.objects.get(id=t.id) created = t.created + + # Now make an update to the thread and resave. Created shouldn't + # change. + t.title = 'new title' t.save() + t = Thread.objects.get(id=t.id) eq_(created, t.created) def test_save_new_post_no_timestamps(self): @@ -227,9 +263,9 @@ def test_save_new_post_no_timestamps(self): Saving a new post should behave as if auto_add_now was set on created and auto_now set on updated. """ - p = Post(thread=self.thread, content='bar', author=self.user) - p.save() - now = datetime.datetime.now() + p = post(thread=self.thread, content='bar', author=self.user, + save=True) + now = datetime.now() self.assertDateTimeAlmostEqual(now, p.created, self.delta) self.assertDateTimeAlmostEqual(now, p.updated, self.delta) @@ -237,16 +273,17 @@ def test_save_old_post_no_timestamps(self): """ Saving an existing post should update the updated date. """ - p = Post.objects.get(pk=4) + created = datetime(2010, 5, 4, 14, 4, 22) + updated = datetime(2010, 5, 4, 14, 4, 31) + p = post(thread=self.thread, created=created, updated=updated, + save=True) - updated = datetime.datetime(2010, 5, 4, 14, 4, 31) eq_(updated, p.updated) p.content = 'baz' p.updated_by = self.user p.save() - now = datetime.datetime.now() - created = datetime.datetime(2010, 5, 4, 14, 4, 22) + now = datetime.now() self.assertDateTimeAlmostEqual(now, p.updated, self.delta) eq_(created, p.created) @@ -256,7 +293,7 @@ def test_save_new_post_timestamps(self): Saving a new post should allow you to override auto_add_now- and auto_now-like functionality. """ - created_ = datetime.datetime(1992, 1, 12, 10, 12, 32) + created_ = datetime(1992, 1, 12, 10, 12, 32) p = Post(thread=self.thread, content='bar', author=self.user, created=created_, updated=created_) p.save() @@ -265,5 +302,5 @@ def test_save_new_post_timestamps(self): def test_content_parsed_sanity(self): """The content_parsed field is populated.""" - p = Post.objects.get(pk=4) + p = post(thread=self.thread, content='yet another post', save=True) eq_('

yet another post\n

', p.content_parsed) diff --git a/apps/forums/tests/test_notifications.py b/apps/forums/tests/test_notifications.py index a4affba6b9f..45bcfb02e29 100644 --- a/apps/forums/tests/test_notifications.py +++ b/apps/forums/tests/test_notifications.py @@ -9,10 +9,10 @@ import test_utils from forums.events import NewPostEvent, NewThreadEvent -from forums.models import Thread, Forum, Post -from forums.tests import ForumTestCase +from forums.models import Thread, Post +from forums.tests import ForumTestCase, thread, forum, post as forum_post from sumo.urlresolvers import reverse -from sumo.tests import get, post, attrs_eq, starts_with +from sumo.tests import post, attrs_eq, starts_with from users.models import Setting from users.tests import user @@ -20,9 +20,9 @@ # Some of these contain a locale prefix on included links, while others don't. # This depends on whether the tests use them inside or outside the scope of a # request. See the long explanation in questions.tests.test_notifications. -REPLY_EMAIL = u"""Reply to thread: Sticky Thread +REPLY_EMAIL = u"""Reply to thread: {thread_title} -User jsocol has replied to a thread you're watching. Here +User {username} has replied to a thread you're watching. Here is their reply: ======== @@ -34,15 +34,15 @@ To view this post on the site, click the following link, or paste it into your browser's location bar: -https://testserver/en-US/forums/test-forum/2#post-%s +https://testserver/en-US/forums/{forum_slug}/{thread_id}#post-{post_id} -- Unsubscribe from these emails: https://testserver/en-US/unsubscribe/""" -NEW_THREAD_EMAIL = u"""New thread: a title +NEW_THREAD_EMAIL = u"""New thread: {thread_title} -User jsocol has posted a new thread in a forum you're watching. +User {username} has posted a new thread in a forum you're watching. Here is the thread: ======== @@ -54,7 +54,7 @@ To view this post on the site, click the following link, or paste it into your browser's location bar: -https://testserver/en-US/forums/test-forum/%s +https://testserver/en-US/forums/{forum_slug}/{thread_id} -- Unsubscribe from these emails: @@ -67,8 +67,9 @@ class NotificationsTests(ForumTestCase): @mock.patch.object(NewPostEvent, 'fire') def test_fire_on_reply(self, fire): """The event fires when there is a reply.""" - t = Thread.objects.get(pk=2) - self.client.login(username='jsocol', password='testpass') + u = user(save=True) + t = thread(save=True) + self.client.login(username=u.username, password='testpass') post(self.client, 'forums.reply', {'content': 'a post'}, args=[t.forum.slug, t.id]) # NewPostEvent.fire() is called. @@ -77,19 +78,18 @@ def test_fire_on_reply(self, fire): @mock.patch.object(NewThreadEvent, 'fire') def test_fire_on_new_thread(self, fire): """The event fires when there is a new thread.""" - f = Forum.objects.get(pk=1) - self.client.login(username='jsocol', password='testpass') + u = user(save=True) + f = forum(save=True) + self.client.login(username=u.username, password='testpass') post(self.client, 'forums.new_thread', {'title': 'a title', 'content': 'a post'}, args=[f.slug]) # NewThreadEvent.fire() is called. assert fire.called - def _toggle_watch_thread_as(self, username, turn_on=True, thread_id=2): + def _toggle_watch_thread_as(self, thread, user, turn_on=True): """Watch a thread and return it.""" - thread = Thread.objects.get(pk=thread_id) - self.client.login(username=username, password='testpass') - user = User.objects.get(username=username) + self.client.login(username=user.username, password='testpass') watch = 'yes' if turn_on else 'no' post(self.client, 'forums.watch_thread', {'watch': watch}, args=[thread.forum.slug, thread.id]) @@ -100,13 +100,10 @@ def _toggle_watch_thread_as(self, username, turn_on=True, thread_id=2): else: assert not NewPostEvent.is_notifying(user, thread), ( 'NewPostEvent should not be notifying.') - return thread - def _toggle_watch_forum_as(self, username, turn_on=True, forum_id=1): + def _toggle_watch_forum_as(self, forum, user, turn_on=True): """Watch a forum and return it.""" - forum = Forum.objects.get(pk=forum_id) - self.client.login(username=username, password='testpass') - user = User.objects.get(username=username) + self.client.login(username=user.username, password='testpass') watch = 'yes' if turn_on else 'no' post(self.client, 'forums.watch_forum', {'watch': watch}, args=[forum.slug]) @@ -117,31 +114,43 @@ def _toggle_watch_forum_as(self, username, turn_on=True, forum_id=1): else: assert not NewPostEvent.is_notifying(user, forum), ( 'NewThreadEvent should not be notifying.') - return forum @mock.patch.object(Site.objects, 'get_current') def test_watch_thread_then_reply(self, get_current): """The event fires and sends emails when watching a thread.""" get_current.return_value.domain = 'testserver' - t = self._toggle_watch_thread_as('pcraciunoiu', turn_on=True) - self.client.login(username='jsocol', password='testpass') + t = thread(save=True) + f = t.forum + poster = user(save=True) + watcher = user(save=True) + + self._toggle_watch_thread_as(t, watcher, turn_on=True) + self.client.login(username=poster.username, password='testpass') post(self.client, 'forums.reply', {'content': 'a post'}, args=[t.forum.slug, t.id]) p = Post.objects.all().order_by('-id')[0] - attrs_eq(mail.outbox[0], to=['user47963@nowhere'], - subject='Re: Test forum - Sticky Thread') - starts_with(mail.outbox[0].body, REPLY_EMAIL % p.id) - - self._toggle_watch_thread_as('pcraciunoiu', turn_on=False) + attrs_eq(mail.outbox[0], to=[watcher.email], + subject='Re: {f} - {t}'.format(f=f, t=t)) + body = REPLY_EMAIL.format( + username=poster.username, + forum_slug=f.slug, + thread_title=t.title, + thread_id=t.id, + post_id=p.id) + starts_with(mail.outbox[0].body, body) def test_watch_other_thread_then_reply(self): """Watching a different thread than the one we're replying to shouldn't notify.""" - t = self._toggle_watch_thread_as('pcraciunoiu', turn_on=True) - t2 = Thread.objects.exclude(pk=t.pk)[0] - self.client.login(username='jsocol', password='testpass') + t1 = thread(save=True) + t2 = thread(save=True) + poster = user(save=True) + watcher = user(save=True) + + self._toggle_watch_thread_as(t1, watcher, turn_on=True) + self.client.login(username=poster.username, password='testpass') post(self.client, 'forums.reply', {'content': 'a post'}, args=[t2.forum.slug, t2.id]) @@ -152,17 +161,24 @@ def test_watch_forum_then_new_thread(self, get_current): """Watching a forum and creating a new thread should send email.""" get_current.return_value.domain = 'testserver' - f = self._toggle_watch_forum_as('pcraciunoiu', turn_on=True) - self.client.login(username='jsocol', password='testpass') + f = forum(save=True) + poster = user(save=True) + watcher = user(save=True) + + self._toggle_watch_forum_as(f, watcher, turn_on=True) + self.client.login(username=poster.username, password='testpass') post(self.client, 'forums.new_thread', {'title': 'a title', 'content': 'a post'}, args=[f.slug]) t = Thread.objects.all().order_by('-id')[0] - attrs_eq(mail.outbox[0], to=['user47963@nowhere'], - subject='Test forum - a title') - starts_with(mail.outbox[0].body, NEW_THREAD_EMAIL % t.id) - - self._toggle_watch_forum_as('pcraciunoiu', turn_on=False) + attrs_eq(mail.outbox[0], to=[watcher.email], + subject='{f} - {t}'.format(f=f, t=t)) + body = NEW_THREAD_EMAIL.format( + username=poster.username, + forum_slug=f.slug, + thread_title=t.title, + thread_id=t.id) + starts_with(mail.outbox[0].body, body) @mock.patch.object(Site.objects, 'get_current') def test_watch_forum_then_new_thread_as_self(self, get_current): @@ -170,8 +186,11 @@ def test_watch_forum_then_new_thread_as_self(self, get_current): send email.""" get_current.return_value.domain = 'testserver' - f = self._toggle_watch_forum_as('pcraciunoiu', turn_on=True) - self.client.login(username='pcraciunoiu', password='testpass') + f = forum(save=True) + watcher = user(save=True) + + self._toggle_watch_forum_as(f, watcher, turn_on=True) + self.client.login(username=watcher.username, password='testpass') post(self.client, 'forums.new_thread', {'title': 'a title', 'content': 'a post'}, args=[f.slug]) # Assert no email is sent. @@ -182,25 +201,40 @@ def test_watch_forum_then_new_post(self, get_current): """Watching a forum and replying to a thread should send email.""" get_current.return_value.domain = 'testserver' - f = self._toggle_watch_forum_as('pcraciunoiu', turn_on=True) - t = f.thread_set.all()[0] - self.client.login(username='jsocol', password='testpass') + t = thread(save=True) + f = t.forum + forum_post(thread=t, save=True) + poster = user(save=True) + watcher = user(save=True) + + self._toggle_watch_forum_as(f, watcher, turn_on=True) + self.client.login(username=poster.username, password='testpass') post(self.client, 'forums.reply', {'content': 'a post'}, args=[f.slug, t.id]) p = Post.objects.all().order_by('-id')[0] - attrs_eq(mail.outbox[0], to=['user47963@nowhere'], - subject='Re: Test forum - Sticky Thread') - starts_with(mail.outbox[0].body, REPLY_EMAIL % p.id) + attrs_eq(mail.outbox[0], to=[watcher.email], + subject='Re: {f} - {t}'.format(f=f, t=t)) + body = REPLY_EMAIL.format( + username=poster.username, + forum_slug=f.slug, + thread_title=t.title, + thread_id=t.id, + post_id=p.id) + starts_with(mail.outbox[0].body, body) @mock.patch.object(Site.objects, 'get_current') def test_watch_forum_then_new_post_as_self(self, get_current): """Watching a forum and replying as myself should not send email.""" get_current.return_value.domain = 'testserver' - f = self._toggle_watch_forum_as('pcraciunoiu', turn_on=True) - t = f.thread_set.all()[0] - self.client.login(username='pcraciunoiu', password='testpass') + t = thread(save=True) + f = t.forum + forum_post(thread=t, save=True) + watcher = user(save=True) + + self._toggle_watch_forum_as(f, watcher, turn_on=True) + self.client.login(username=watcher.username, password='testpass') post(self.client, 'forums.reply', {'content': 'a post'}, args=[f.slug, t.id]) # Assert no email is sent. @@ -208,85 +242,103 @@ def test_watch_forum_then_new_post_as_self(self, get_current): @mock.patch.object(Site.objects, 'get_current') def test_watch_both_then_new_post(self, get_current): - """Watching both and replying to a thread should send ONE email.""" + """Watching both forum and thread. + + Replying to a thread should send ONE email.""" get_current.return_value.domain = 'testserver' - f = self._toggle_watch_forum_as('pcraciunoiu', turn_on=True) - t = f.thread_set.all()[0] - self._toggle_watch_thread_as('pcraciunoiu', turn_on=True, - thread_id=t.id) - self.client.login(username='jsocol', password='testpass') + t = thread(save=True) + f = t.forum + forum_post(thread=t, save=True) + poster = user(save=True) + watcher = user(save=True) + + self._toggle_watch_forum_as(f, watcher, turn_on=True) + self._toggle_watch_thread_as(t, watcher, turn_on=True) + self.client.login(username=poster.username, password='testpass') post(self.client, 'forums.reply', {'content': 'a post'}, args=[f.slug, t.id]) eq_(1, len(mail.outbox)) p = Post.objects.all().order_by('-id')[0] - attrs_eq(mail.outbox[0], to=['user47963@nowhere'], - subject='Re: Test forum - Sticky Thread') - starts_with(mail.outbox[0].body, REPLY_EMAIL % p.id) - - self._toggle_watch_forum_as('pcraciunoiu', turn_on=False) - self._toggle_watch_thread_as('pcraciunoiu', turn_on=False) + attrs_eq(mail.outbox[0], to=[watcher.email], + subject='Re: {f} - {t}'.format(f=f, t=t)) + body = REPLY_EMAIL.format( + username=poster.username, + forum_slug=f.slug, + thread_title=t.title, + thread_id=t.id, + post_id=p.id) + starts_with(mail.outbox[0].body, body) @mock.patch.object(Site.objects, 'get_current') def test_autowatch_new_thread(self, get_current): """Creating a new thread should email responses""" get_current.return_value.domain = 'testserver' - eq_(0, len(mail.outbox)) - f = Forum.objects.get(pk=1) - self.client.login(username='jsocol', password='testpass') - user = User.objects.get(username='jsocol') - s = Setting.objects.create(user=user, name='forums_watch_new_thread', + f = forum(save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') + s = Setting.objects.create(user=u, name='forums_watch_new_thread', value='False') data = {'title': 'a title', 'content': 'a post'} post(self.client, 'forums.new_thread', data, args=[f.slug]) t1 = Thread.objects.all().order_by('-id')[0] - assert not NewPostEvent.is_notifying(user, t1), ( + assert not NewPostEvent.is_notifying(u, t1), ( 'NewPostEvent should not be notifying.') s.value = 'True' s.save() post(self.client, 'forums.new_thread', data, args=[f.slug]) t2 = Thread.uncached.all().order_by('-id')[0] - assert NewPostEvent.is_notifying(user, t2), ( + assert NewPostEvent.is_notifying(u, t2), ( 'NewPostEvent should be notifying.') @mock.patch.object(Site.objects, 'get_current') def test_autowatch_reply(self, get_current): + """Replying to a thread creates a watch.""" get_current.return_value.domain = 'testserver' - user = User.objects.get(username='timw') - t1, t2 = Thread.objects.filter(is_locked=False)[0:2] - assert not NewPostEvent.is_notifying(user, t1) - assert not NewPostEvent.is_notifying(user, t2) + u = user(save=True) + t1 = thread(save=True) + t2 = thread(save=True) + + assert not NewPostEvent.is_notifying(u, t1) + assert not NewPostEvent.is_notifying(u, t2) + + self.client.login(username=u.username, password='testpass') - self.client.login(username='timw', password='testpass') - s = Setting.objects.create(user=user, name='forums_watch_after_reply', + # If the poster has the forums_watch_after_reply setting set to True, + # they will start watching threads they reply to. + s = Setting.objects.create(user=u, name='forums_watch_after_reply', value='True') data = {'content': 'some content'} post(self.client, 'forums.reply', data, args=[t1.forum.slug, t1.pk]) - assert NewPostEvent.is_notifying(user, t1) + assert NewPostEvent.is_notifying(u, t1) + # Setting forums_watch_after_reply back to False, now they shouldn't + # start watching threads they reply to. s.value = 'False' s.save() post(self.client, 'forums.reply', data, args=[t2.forum.slug, t2.pk]) - assert not NewPostEvent.is_notifying(user, t2) + assert not NewPostEvent.is_notifying(u, t2) @mock.patch.object(Site.objects, 'get_current') def test_admin_delete_user_with_watched_thread(self, get_current): """Test the admin delete view for a user with a watched thread.""" get_current.return_value.domain = 'testserver' - self.client.login(username='admin', password='testpass') - u = user(save=True) - f = Forum.objects.all()[0] - t = Thread(creator=u, forum=f, title='title') - t.save() - self._toggle_watch_thread_as('pcraciunoiu', thread_id=t.id, turn_on=True) + t = thread(save=True) + u = t.creator + watcher = user(save=True) + admin_user = user(is_staff=True, is_superuser=True, save=True) + + self.client.login(username=admin_user.username, password='testpass') + self._toggle_watch_thread_as(t, watcher, turn_on=True) url = reverse('admin:auth_user_delete', args=[u.id]) request = test_utils.RequestFactory().get(url) - request.user = User.objects.get(username='admin') + request.user = admin_user request.session = self.client.session # The following blows up without our monkeypatch. ModelAdmin(User, admin.site).delete_view(request, str(u.id)) diff --git a/apps/forums/tests/test_permissions.py b/apps/forums/tests/test_permissions.py index c85bb20d711..7dfcb8a6577 100644 --- a/apps/forums/tests/test_permissions.py +++ b/apps/forums/tests/test_permissions.py @@ -1,54 +1,73 @@ -from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType import test_utils from access.helpers import has_perm, has_perm_or_owns -from forums.models import Forum, Thread -from sumo.tests import TestCase +from access.tests import permission +from forums.tests import ForumTestCase, forum, thread from sumo.urlresolvers import reverse +from users.tests import user, group -class ForumTestPermissions(TestCase): +class ForumTestPermissions(ForumTestCase): """Make sure access helpers work on the forums.""" - fixtures = ['users.json', 'posts.json', 'forums_permissions.json'] - def setUp(self): url = reverse('forums.threads', args=[u'test-forum']) self.context = {'request': test_utils.RequestFactory().get(url)} - self.forum_1 = Forum.objects.get(pk=1) - self.forum_2 = Forum.objects.get(pk=2) + + self.group = group(save=True) + + # Set up forum_1 + f = self.forum_1 = forum(save=True) + ct = ContentType.objects.get_for_model(self.forum_1) + permission(codename='forums_forum.thread_edit_forum', content_type=ct, + object_id=f.id, group=self.group, save=True) + permission(codename='forums_forum.post_edit_forum', content_type=ct, + object_id=f.id, group=self.group, save=True) + permission(codename='forums_forum.post_delete_forum', content_type=ct, + object_id=f.id, group=self.group, save=True) + permission(codename='forums_forum.thread_delete_forum', + content_type=ct, object_id=f.id, group=self.group, + save=True) + permission(codename='forums_forum.thread_sticky_forum', + content_type=ct, object_id=f.id, group=self.group, + save=True) + permission(codename='forums_forum.thread_move_forum', content_type=ct, + object_id=f.id, group=self.group, save=True) + + # Set up forum_2 + f = self.forum_2 = forum(save=True) + permission(codename='forums_forum.thread_move_forum', content_type=ct, + object_id=f.id, group=self.group, save=True) def test_has_perm_thread_edit(self): - """ - User in ForumsModerator group can edit thread in forum_1, but not in - forum_2. - """ - self.context['request'].user = User.objects.get(pk=47963) + """User in group can edit thread in forum_1, but not in forum_2.""" + u = user(save=True) + self.group.user_set.add(u) + + self.context['request'].user = u assert has_perm(self.context, 'forums_forum.thread_edit_forum', self.forum_1) assert not has_perm(self.context, 'forums_forum.thread_edit_forum', self.forum_2) def test_has_perm_or_owns_thread_edit(self): - """ - User in ForumsModerator group can edit thread in forum_1, but not in - forum_2. - """ - me = User.objects.get(pk=118533) - my_t = Thread.objects.filter(creator=me)[0] - other_t = Thread.objects.exclude(creator=me)[0] + """Users can edit their own threads.""" + my_t = thread(save=True) + me = my_t.creator + other_t = thread(save=True) self.context['request'].user = me perm = 'forums_forum.thread_edit_forum' assert has_perm_or_owns(self.context, perm, my_t, self.forum_1) assert not has_perm_or_owns(self.context, perm, other_t, self.forum_1) def test_has_perm_thread_delete(self): - """ - User in ForumsModerator group can delete thread in forum_1, but not in - forum_2. - """ - self.context['request'].user = User.objects.get(pk=47963) + """User in group can delete thread in forum_1, but not in forum_2.""" + u = user(save=True) + self.group.user_set.add(u) + + self.context['request'].user = u assert has_perm(self.context, 'forums_forum.thread_delete_forum', self.forum_1) assert not has_perm(self.context, 'forums_forum.thread_delete_forum', @@ -56,10 +75,13 @@ def test_has_perm_thread_delete(self): def test_has_perm_thread_sticky(self): """ - User in ForumsModerator group can change sticky status of thread in - forum_1, but not in forum_2. + User in group can change sticky status of thread in forum_1, but not + in forum_2. """ - self.context['request'].user = User.objects.get(pk=47963) + u = user(save=True) + self.group.user_set.add(u) + + self.context['request'].user = u assert has_perm(self.context, 'forums_forum.thread_sticky_forum', self.forum_1) assert not has_perm(self.context, 'forums_forum.thread_sticky_forum', @@ -67,30 +89,33 @@ def test_has_perm_thread_sticky(self): def test_has_perm_thread_locked(self): """ - Sanity check: ForumsModerator group has no permission to change locked + Sanity check: user in group has no permission to change locked status in forum_1. """ - self.context['request'].user = User.objects.get(pk=47963) + u = user(save=True) + self.group.user_set.add(u) + + self.context['request'].user = u assert not has_perm(self.context, 'forums_forum.thread_locked_forum', self.forum_1) def test_has_perm_post_edit(self): - """ - User in ForumsModerator group can edit any post in forum_1, but not - in forum_2. - """ - self.context['request'].user = User.objects.get(pk=47963) + """User in group can edit any post in forum_1, but not in forum_2.""" + u = user(save=True) + self.group.user_set.add(u) + + self.context['request'].user = u assert has_perm(self.context, 'forums_forum.post_edit_forum', self.forum_1) assert not has_perm(self.context, 'forums_forum.post_edit_forum', self.forum_2) def test_has_perm_post_delete(self): - """ - User in ForumsModerator group can delete any post in forum_1, but not - in forum_2. - """ - self.context['request'].user = User.objects.get(pk=47963) + """User in group can delete posts in forum_1, but not in forum_2.""" + u = user(save=True) + self.group.user_set.add(u) + + self.context['request'].user = u assert has_perm(self.context, 'forums_forum.post_delete_forum', self.forum_1) assert not has_perm(self.context, 'forums_forum.post_delete_forum', @@ -98,9 +123,9 @@ def test_has_perm_post_delete(self): def test_no_perm_thread_delete(self): """ - User not in ForumsModerator group cannot delete thread in any forum. + User not in group cannot delete thread in any forum. """ - self.context['request'].user = User.objects.get(pk=118533) + self.context['request'].user = user(save=True) assert not has_perm(self.context, 'forums_forum.thread_delete_forum', self.forum_1) assert not has_perm(self.context, 'forums_forum.thread_delete_forum', diff --git a/apps/forums/tests/test_posts.py b/apps/forums/tests/test_posts.py new file mode 100644 index 00000000000..e440d817cde --- /dev/null +++ b/apps/forums/tests/test_posts.py @@ -0,0 +1,170 @@ +from datetime import datetime, timedelta + +from django.conf import settings + +from nose.tools import eq_ + +from forums.models import Thread, Forum, ThreadLockedError +from forums.tests import ForumTestCase, thread, post +from forums.views import sort_threads +from sumo.tests import get +from users.tests import user + + +class PostTestCase(ForumTestCase): + + def test_new_post_updates_thread(self): + """Saving a new post in a thread should update the last_post key in + that thread to point to the new post.""" + t = thread(save=True) + post(thread=t, save=True) + p = t.new_post(author=t.creator, content='an update') + p.save() + t = Thread.objects.get(id=t.id) + eq_(p.id, t.last_post_id) + + def test_new_post_updates_forum(self): + """Saving a new post should update the last_post key in the forum to + point to the new post.""" + t = thread(save=True) + post(thread=t, save=True) + p = t.new_post(author=t.creator, content='another update') + p.save() + f = Forum.objects.get(id=t.forum_id) + eq_(p.id, f.last_post_id) + + def test_update_post_does_not_update_thread(self): + """Updating/saving an old post in a thread should _not_ update the + last_post key in that thread.""" + t = thread(save=True) + old = post(thread=t, save=True) + last = post(thread=t, save=True) + old.content = 'updated content' + old.save() + eq_(last.id, old.thread.last_post_id) + + def test_update_forum_does_not_update_thread(self): + """Updating/saving an old post in a forum should _not_ update the + last_post key in that forum.""" + t = thread(save=True) + old = post(thread=t, save=True) + last = post(thread=t, save=True) + old.content = 'updated content' + old.save() + eq_(last.id, t.forum.last_post_id) + + def test_replies_count(self): + """The Thread.replies value should remain one less than the number of + posts in the thread.""" + t = thread(save=True) + post(thread=t, save=True) + post(thread=t, save=True) + post(thread=t, save=True) + old = t.replies + eq_(2, old) + t.new_post(author=t.creator, content='test').save() + eq_(old + 1, t.replies) + + def test_sticky_threads_first(self): + """Sticky threads should come before non-sticky threads.""" + t = post(save=True).thread + sticky = thread(forum=t.forum, is_sticky=True, save=True) + yesterday = datetime.now() - timedelta(days=1) + post(thread=sticky, created=yesterday, save=True) + + # The older sticky thread shows up first. + eq_(sticky.id, Thread.objects.all()[0].id) + + def test_thread_sorting(self): + """After the sticky threads, threads should be sorted by the created + date of the last post.""" + # Make sure the datetimes are different. + post(created=datetime.now() - timedelta(days=1), save=True) + post(save=True) + t = thread(is_sticky=True, save=True) + post(thread=t, save=True) + + threads = Thread.objects.filter(is_sticky=False) + self.assert_(threads[0].last_post.created > + threads[1].last_post.created) + + def test_post_sorting(self): + """Posts should be sorted chronologically.""" + t = thread(save=True) + post(thread=t, created=datetime.now() - timedelta(days=1), save=True) + post(thread=t, created=datetime.now() - timedelta(days=4), save=True) + post(thread=t, created=datetime.now() - timedelta(days=7), save=True) + post(thread=t, created=datetime.now() - timedelta(days=11), save=True) + post(thread=t, save=True) + posts = t.post_set.all() + for i in range(len(posts) - 1): + self.assert_(posts[i].created <= posts[i + 1].created) + + def test_sorting_creator(self): + """Sorting threads by creator.""" + thread(creator=user(username='aaa', save=True), save=True) + thread(creator=user(username='bbb', save=True), save=True) + threads = sort_threads(Thread.objects, 3, 1) + self.assert_(threads[0].creator.username >= + threads[1].creator.username) + + def test_sorting_replies(self): + """Sorting threads by replies.""" + t = thread(save=True) + post(thread=t, save=True) + post(thread=t, save=True) + post(thread=t, save=True) + post(save=True) + threads = sort_threads(Thread.objects, 4) + self.assert_(threads[0].replies <= threads[1].replies) + + def test_sorting_last_post_desc(self): + """Sorting threads by last_post descendingly.""" + t = thread(save=True) + post(thread=t, save=True) + post(thread=t, save=True) + post(thread=t, save=True) + post(created=datetime.now() - timedelta(days=1), save=True) + threads = sort_threads(Thread.objects, 5, 1) + self.assert_(threads[0].last_post.created >= + threads[1].last_post.created) + + def test_thread_last_page(self): + """Thread's last_page property is accurate.""" + t = post(save=True).thread + # Format: (# replies, # of pages to expect) + test_data = ((t.replies, 1), # Test default + (50, 3), # Test a large number + (19, 1), # Test off-by-one error, low + (20, 2), # Test off-by-one error, high + ) + for replies, pages in test_data: + t.replies = replies + eq_(t.last_page, pages) + + def test_locked_thread(self): + """Trying to reply to a locked thread should raise an exception.""" + locked = thread(is_locked=True, save=True) + unlocked = thread(save=True) + user1 = user(save=True) + fn = lambda: locked.new_post(author=user1, content='empty') + self.assertRaises(ThreadLockedError, fn) + + # This should not raise an exception. + unlocked.new_post(author=user1, content='empty') + + def test_post_no_session(self): + r = get(self.client, 'forums.new_thread', + kwargs={'forum_slug': 'test-forum'}) + assert(settings.LOGIN_URL in r.redirect_chain[0][0]) + eq_(302, r.redirect_chain[0][1]) + + +class ThreadTestCase(ForumTestCase): + + def test_delete_no_session(self): + """Delete a thread while logged out redirects.""" + r = get(self.client, 'forums.delete_thread', + kwargs={'forum_slug': 'test-forum', 'thread_id': 1}) + assert(settings.LOGIN_URL in r.redirect_chain[0][0]) + eq_(302, r.redirect_chain[0][1]) diff --git a/apps/forums/tests/test_templates.py b/apps/forums/tests/test_templates.py index bdee56e775d..7a172b73ea6 100644 --- a/apps/forums/tests/test_templates.py +++ b/apps/forums/tests/test_templates.py @@ -1,23 +1,25 @@ +from django.contrib.contenttypes.models import ContentType + from nose.tools import eq_ from pyquery import PyQuery as pq -from django.contrib.auth.models import User - -from forums.models import Forum, Thread, Post -from forums.tests import ForumTestCase +from access.tests import permission +from forums.models import Post +from forums.tests import ForumTestCase, forum, thread, post as forum_post from sumo.tests import get, post +from users.tests import user, group class PostsTemplateTests(ForumTestCase): def test_empty_reply_errors(self): """Posting an empty reply shows errors.""" - self.client.login(username='jsocol', password='testpass') + u = user(save=True) + t = forum_post(save=True).thread - f = Forum.objects.filter()[0] - t = f.thread_set.all()[0] + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.reply', {'content': ''}, - args=[f.slug, t.id]) + args=[t.forum.slug, t.id]) doc = pq(response.content) error_msg = doc('ul.errorlist li a')[0] @@ -25,14 +27,13 @@ def test_empty_reply_errors(self): def test_edit_post_errors(self): """Changing post content works.""" - self.client.login(username='jsocol', password='testpass') + p = forum_post(save=True) + t = p.thread + u = p.author - f = Forum.objects.filter()[0] - t = f.thread_set.all()[0] - p_author = User.objects.get(username='jsocol') - p = t.post_set.filter(author=p_author)[0] + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.edit_post', - {'content': 'wha?'}, args=[f.slug, t.id, p.id]) + {'content': 'wha?'}, args=[t.forum.slug, t.id, p.id]) doc = pq(response.content) errors = doc('ul.errorlist li a') @@ -42,10 +43,10 @@ def test_edit_post_errors(self): def test_edit_thread_template(self): """The edit-post template should render.""" - self.client.login(username='jsocol', password='testpass') + p = forum_post(save=True) + u = p.author - u = User.objects.get(username='jsocol') - p = Post.objects.filter(author=u, thread__is_locked=False)[0] + self.client.login(username=u.username, password='testpass') res = get(self.client, 'forums.edit_post', args=[p.thread.forum.slug, p.thread.id, p.id]) @@ -54,43 +55,54 @@ def test_edit_thread_template(self): def test_edit_post(self): """Changing post content works.""" - self.client.login(username='jsocol', password='testpass') + p = forum_post(save=True) + t = p.thread + u = p.author - f = Forum.objects.filter()[0] - t = f.thread_set.all()[0] - p_author = User.objects.get(username='jsocol') - p = t.post_set.filter(author=p_author)[0] + self.client.login(username=u.username, password='testpass') post(self.client, 'forums.edit_post', {'content': 'Some new content'}, - args=[f.slug, t.id, p.id]) - edited_p = t.post_set.get(pk=p.id) + args=[t.forum.slug, t.id, p.id]) + edited_p = Post.objects.get(id=p.id) eq_('Some new content', edited_p.content) def test_posts_fr(self): """Posts render for [fr] locale.""" - forum = Forum.objects.filter()[0] - response = get(self.client, 'forums.posts', args=[forum.slug, 4], + t = forum_post(save=True).thread + + response = get(self.client, 'forums.posts', args=[t.forum.slug, t.id], locale='fr') eq_(200, response.status_code) - eq_('/forums/test-forum/4', + eq_('/forums/{f}/{t}'.format(f=t.forum.slug, t=t.id), pq(response.content)('link[rel="canonical"]')[0].attrib['href']) def test_long_title_truncated_in_crumbs(self): """A very long thread title gets truncated in the breadcrumbs""" - forum = Forum.objects.filter()[0] - response = get(self.client, 'forums.posts', args=[forum.slug, 4]) + t = thread(title='A thread with a very very long title', save=True) + forum_post(thread=t, save=True) + + response = get(self.client, 'forums.posts', args=[t.forum.slug, t.id]) doc = pq(response.content) crumb = doc('ol.breadcrumbs li:last-child') eq_(crumb.text(), 'A thread with a very very ...') def test_edit_post_moderator(self): """Editing post as a moderator works.""" - self.client.login(username='pcraciunoiu', password='testpass') - - p = Post.objects.get(pk=4) + p = forum_post(save=True) t = p.thread f = t.forum + # Create the moderator group, give it the edit permission + # and add a moderator. + moderator_group = group(save=True) + ct = ContentType.objects.get_for_model(f) + permission(codename='forums_forum.post_edit_forum', content_type=ct, + object_id=f.id, group=moderator_group, save=True) + moderator = user(save=True) + moderator_group.user_set.add(moderator) + + self.client.login(username=moderator.username, password='testpass') + r = post(self.client, 'forums.edit_post', {'content': 'More new content'}, args=[f.slug, t.id, p.id]) eq_(200, r.status_code) @@ -100,24 +112,26 @@ def test_edit_post_moderator(self): def test_preview_reply(self): """Preview a reply.""" - self.client.login(username='rrosario', password='testpass') - f = Forum.objects.filter()[0] - t = f.thread_set.all()[0] - num_posts = t.post_set.count() + t = forum_post(save=True).thread + u = t.creator + content = 'Full of awesome.' + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.reply', {'content': content, 'preview': 'any string'}, - args=[f.slug, t.id]) + args=[t.forum.slug, t.id]) eq_(200, response.status_code) doc = pq(response.content) eq_(content, doc('#post-preview div.content').text()) - eq_(num_posts, t.post_set.count()) + eq_(1, t.post_set.count()) def test_watch_thread(self): """Watch and unwatch a thread.""" - self.client.login(username='rrosario', password='testpass') + t = forum_post(save=True).thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') - t = Thread.objects.filter()[1] response = post(self.client, 'forums.watch_thread', {'watch': 'yes'}, args=[t.forum.slug, t.id]) self.assertContains(response, 'Watching') @@ -128,29 +142,34 @@ def test_watch_thread(self): def test_show_reply_fields(self): """Reply fields show if user has permission to post.""" - self.client.login(username='jsocol', password='testpass') + t = forum_post(save=True).thread + u = user(save=True) - f = Forum.objects.filter()[0] - t = f.thread_set.all()[0] - response = get(self.client, 'forums.posts', args=[f.slug, t.pk]) + self.client.login(username=u.username, password='testpass') + response = get(self.client, 'forums.posts', args=[t.forum.slug, t.pk]) self.assertContains(response, 'thread-reply') def test_restricted_hide_reply(self): """Reply fields don't show if user has no permission to post.""" - self.client.login(username='jsocol', password='testpass') - - f = Forum.objects.get(slug='visible') - t = f.thread_set.all()[0] + t = forum_post(save=True).thread + f = t.forum + ct = ContentType.objects.get_for_model(f) + # If the forum has the permission and the user isn't assigned said + # permission, then they can't post. + permission(codename='forums_forum.post_in_forum', content_type=ct, + object_id=f.id, save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.posts', args=[f.slug, t.pk]) self.assertNotContains(response, 'thread-reply') def test_links_nofollow(self): """Links posted should have rel=nofollow.""" - f = Forum.objects.filter()[0] - t = f.thread_set.all()[0] - p = t.post_set.all()[0] - p.content = 'linking http://test.org' - p.save() + p = forum_post(content='linking http://test.org', save=True) + t = p.thread + f = t.forum + response = get(self.client, 'forums.posts', args=[f.slug, t.pk]) doc = pq(response.content) eq_('nofollow', doc('ol.posts div.content a')[0].attrib['rel']) @@ -160,20 +179,23 @@ class ThreadsTemplateTests(ForumTestCase): def test_last_thread_post_link_has_post_id(self): """Make sure the last post url links to the last post (#post-).""" - response = get(self.client, 'forums.threads', args=['test-forum']) + t = forum_post(save=True).thread + last = forum_post(thread=t, save=True) + + response = get(self.client, 'forums.threads', args=[t.forum.slug]) doc = pq(response.content) last_post_link = doc('ol.threads div.last-post a:not(.username)')[0] href = last_post_link.attrib['href'] - eq_(href.split('#')[1], 'post-4') + eq_(href.split('#')[1], 'post-%s' % last.id) def test_empty_thread_errors(self): """Posting an empty thread shows errors.""" - self.client.login(username='jsocol', password='testpass') + f = forum(save=True) + u = user(save=True) - f = Forum.objects.filter()[0] + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.new_thread', {'title': '', 'content': ''}, args=[f.slug]) - doc = pq(response.content) errors = doc('ul.errorlist li a') eq_(errors[0].text, 'Please provide a title.') @@ -181,9 +203,10 @@ def test_empty_thread_errors(self): def test_new_short_thread_errors(self): """Posting a short new thread shows errors.""" - self.client.login(username='jsocol', password='testpass') + f = forum(save=True) + u = user(save=True) - f = Forum.objects.filter()[0] + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.new_thread', {'title': 'wha?', 'content': 'wha?'}, args=[f.slug]) @@ -198,13 +221,12 @@ def test_new_short_thread_errors(self): def test_edit_thread_errors(self): """Editing thread with too short of a title shows errors.""" - self.client.login(username='jsocol', password='testpass') + t = forum_post(save=True).thread + creator = t.creator - f = Forum.objects.filter()[0] - t_creator = User.objects.get(username='jsocol') - t = f.thread_set.filter(creator=t_creator)[0] + self.client.login(username=creator.username, password='testpass') response = post(self.client, 'forums.edit_thread', - {'title': 'wha?'}, args=[f.slug, t.id]) + {'title': 'wha?'}, args=[t.forum.slug, t.id]) doc = pq(response.content) errors = doc('ul.errorlist li a') @@ -214,10 +236,10 @@ def test_edit_thread_errors(self): def test_edit_thread_template(self): """The edit-thread template should render.""" - self.client.login(username='jsocol', password='testpass') + t = forum_post(save=True).thread + creator = t.creator - u = User.objects.get(username='jsocol') - t = Thread.objects.filter(creator=u, is_locked=False)[0] + self.client.login(username=creator.username, password='testpass') res = get(self.client, 'forums.edit_thread', args=[t.forum.slug, t.id]) @@ -226,9 +248,10 @@ def test_edit_thread_template(self): def test_watch_forum(self): """Watch and unwatch a forum.""" - self.client.login(username='rrosario', password='testpass') + f = forum(save=True) + u = user(save=True) - f = Forum.objects.filter()[0] + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.watch_forum', {'watch': 'yes'}, args=[f.slug]) self.assertContains(response, 'Watching') @@ -238,52 +261,59 @@ def test_watch_forum(self): self.assertNotContains(response, 'Watching') def test_canonical_url(self): - response = get(self.client, 'forums.threads', args=['test-forum']) - eq_('/forums/test-forum', + """Verify the canonical URL is set correctly.""" + f = forum(save=True) + + response = get(self.client, 'forums.threads', args=[f.slug]) + eq_('/forums/%s' % f.slug, pq(response.content)('link[rel="canonical"]')[0].attrib['href']) def test_show_new_thread(self): """'Post new thread' shows if user has permission to post.""" - self.client.login(username='jsocol', password='testpass') + f = forum(save=True) + u = user(save=True) - response = get(self.client, 'forums.threads', args=['test-forum']) + self.client.login(username=u.username, password='testpass') + response = get(self.client, 'forums.threads', args=[f.slug]) self.assertContains(response, 'Post a new thread') def test_restricted_hide_new_thread(self): """'Post new thread' doesn't show if user has no permission to post.""" - self.client.login(username='jsocol', password='testpass') - - response = get(self.client, 'forums.threads', args=['visible']) + f = forum(save=True) + ct = ContentType.objects.get_for_model(f) + # If the forum has the permission and the user isn't assigned said + # permission, then they can't post. + permission(codename='forums_forum.post_in_forum', content_type=ct, + object_id=f.id, save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') + response = get(self.client, 'forums.threads', args=[f.slug]) self.assertNotContains(response, 'Post a new thread') class ForumsTemplateTests(ForumTestCase): - def setUp(self): - super(ForumsTemplateTests, self).setUp() - self.forum = Forum.objects.all()[0] - admin = User.objects.get(pk=1) - self.thread = self.forum.thread_set.filter(creator=admin)[0] - self.post = self.thread.post_set.all()[0] - # Login for testing 403s - self.client.login(username='jsocol', password='testpass') - - def tearDown(self): - self.client.logout() - super(ForumsTemplateTests, self).tearDown() - def test_last_post_link_has_post_id(self): """Make sure the last post url links to the last post (#post-).""" + p = forum_post(save=True) + response = get(self.client, 'forums.forums') doc = pq(response.content) last_post_link = doc('ol.forums div.last-post a:not(.username)')[0] href = last_post_link.attrib['href'] - eq_(href.split('#')[1], 'post-25') + eq_(href.split('#')[1], 'post-%s' % p.id) def test_restricted_is_invisible(self): """Forums with restricted view_in permission shouldn't show up.""" + restricted_forum = forum(save=True) + # Make it restricted. + ct = ContentType.objects.get_for_model(restricted_forum) + permission(codename='forums_forum.view_in_forum', content_type=ct, + object_id=restricted_forum.id, save=True) + response = get(self.client, 'forums.forums') - self.assertNotContains(response, 'restricted-forum') + self.assertNotContains(response, restricted_forum.slug) def test_canonical_url(self): response = get(self.client, 'forums.forums') @@ -295,9 +325,10 @@ class NewThreadTemplateTests(ForumTestCase): def test_preview(self): """Preview the thread post.""" - self.client.login(username='rrosario', password='testpass') - f = Forum.objects.filter()[0] - num_threads = f.thread_set.count() + f = forum(save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') content = 'Full of awesome.' response = post(self.client, 'forums.new_thread', {'title': 'Topic', 'content': content, @@ -305,4 +336,4 @@ def test_preview(self): eq_(200, response.status_code) doc = pq(response.content) eq_(content, doc('#post-preview div.content').text()) - eq_(num_threads, f.thread_set.count()) + eq_(0, f.thread_set.count()) # No thread was created. diff --git a/apps/forums/tests/test_urls.py b/apps/forums/tests/test_urls.py index 2319ee97175..0a19f6e944e 100644 --- a/apps/forums/tests/test_urls.py +++ b/apps/forums/tests/test_urls.py @@ -1,8 +1,11 @@ +from django.contrib.contenttypes.models import ContentType + from nose.tools import eq_ -from forums.models import Forum -from forums.tests import ForumTestCase +from access.tests import permission +from forums.tests import ForumTestCase, forum, thread, post as forum_post from sumo.tests import get, post +from users.tests import user, group class BelongsTestCase(ForumTestCase): @@ -10,52 +13,91 @@ class BelongsTestCase(ForumTestCase): Mixing and matching thread, forum, and post data in URLs should fail. """ - def setUp(self): - super(BelongsTestCase, self).setUp() - self.forum = Forum.objects.all()[0] - self.forum_2 = Forum.objects.all()[1] - self.thread = self.forum.thread_set.filter(is_locked=False)[0] - self.thread_2 = self.forum.thread_set.filter(is_locked=False)[1] - self.post = self.thread.post_set.all()[0] - # Login for testing 403s - self.client.login(username='admin', password='testpass') - def test_posts_thread_belongs_to_forum(self): - """Posts view - redirect if thread does notbelong to forum.""" - r = get(self.client, 'forums.posts', - args=[self.forum_2.slug, self.thread.id]) + """Posts view - redirect if thread does not belong to forum.""" + f = forum(save=True) + t = thread(save=True) # Thread belongs to a different forum + + r = get(self.client, 'forums.posts', args=[f.slug, t.id]) eq_(200, r.status_code) u = r.redirect_chain[0][0] - assert u.endswith(self.thread.get_absolute_url()) + assert u.endswith(t.get_absolute_url()) def test_reply_thread_belongs_to_forum(self): """Reply action - thread belongs to forum.""" - r = post(self.client, 'forums.reply', {}, - args=[self.forum_2.slug, self.thread.id]) + f = forum(save=True) + t = thread(save=True) # Thread belongs to a different forum + u = user(save=True) + + self.client.login(username=u.username, password='testpass') + r = post(self.client, 'forums.reply', {}, args=[f.slug, t.id]) eq_(404, r.status_code) def test_locked_thread_belongs_to_forum(self): """Lock action - thread belongs to forum.""" - r = post(self.client, 'forums.lock_thread', {}, - args=[self.forum_2.slug, self.thread.id]) + f = forum(save=True) + t = thread(save=True) # Thread belongs to a different forum + u = user(save=True) + + # Give the user the permission to lock threads. + g = group(save=True) + ct = ContentType.objects.get_for_model(f) + permission(codename='forums_forum.thread_locked_forum', + content_type=ct, object_id=f.id, group=g, save=True) + permission(codename='forums_forum.thread_locked_forum', + content_type=ct, object_id=t.forum.id, group=g, save=True) + g.user_set.add(u) + + self.client.login(username=u.username, password='testpass') + r = post(self.client, 'forums.lock_thread', {}, args=[f.slug, t.id]) eq_(404, r.status_code) def test_sticky_thread_belongs_to_forum(self): """Sticky action - thread belongs to forum.""" - r = post(self.client, 'forums.sticky_thread', {}, - args=[self.forum_2.slug, self.thread.id]) + f = forum(save=True) + t = thread(save=True) # Thread belongs to a different forum + u = user(save=True) + + # Give the user the permission to sticky threads. + g = group(save=True) + ct = ContentType.objects.get_for_model(f) + permission(codename='forums_forum.thread_sticky_forum', + content_type=ct, object_id=f.id, group=g, save=True) + permission(codename='forums_forum.thread_sticky_forum', + content_type=ct, object_id=t.forum.id, group=g, save=True) + g.user_set.add(u) + + self.client.login(username=u.username, password='testpass') + r = post(self.client, 'forums.sticky_thread', {}, args=[f.slug, t.id]) eq_(404, r.status_code) def test_edit_thread_belongs_to_forum(self): """Edit thread action - thread belongs to forum.""" - r = get(self.client, 'forums.edit_thread', - args=[self.forum_2.slug, self.thread.id]) + f = forum(save=True) + t = forum_post(save=True).thread # Thread belongs to a different forum + u = t.creator + + self.client.login(username=u.username, password='testpass') + r = get(self.client, 'forums.edit_thread', args=[f.slug, t.id]) eq_(404, r.status_code) def test_delete_thread_belongs_to_forum(self): """Delete thread action - thread belongs to forum.""" - r = get(self.client, 'forums.delete_thread', - args=[self.forum_2.slug, self.thread.id]) + f = forum(save=True) + t = thread(save=True) # Thread belongs to a different forum + u = user(save=True) + + # Give the user the permission to delete threads. + g = group(save=True) + ct = ContentType.objects.get_for_model(f) + permission(codename='forums_forum.thread_delete_forum', + content_type=ct, object_id=f.id, group=g, save=True) + permission(codename='forums_forum.thread_delete_forum', + content_type=ct, object_id=t.forum.id, group=g, save=True) + g.user_set.add(u) + + self.client.login(username=u.username, password='testpass') + r = get(self.client, 'forums.delete_thread', args=[f.slug, t.id]) eq_(404, r.status_code) def test_edit_post_belongs_to_thread_and_forum(self): @@ -63,12 +105,22 @@ def test_edit_post_belongs_to_thread_and_forum(self): Edit post action - post belongs to thread and thread belongs to forum. """ + f = forum(save=True) + t = thread(forum=f, save=True) + # Post belongs to a different forum and thread. + p = forum_post(save=True) + u = p.author + + self.client.login(username=u.username, password='testpass') + + # Post isn't in the passed forum: r = get(self.client, 'forums.edit_post', - args=[self.forum_2.slug, self.thread.id, self.post.id]) + args=[f.slug, p.thread.id, p.id]) eq_(404, r.status_code) + # Post isn't in the passed thread: r = get(self.client, 'forums.edit_post', - args=[self.forum.slug, self.thread_2.id, self.post.id]) + args=[p.thread.forum.slug, t.id, p.id]) eq_(404, r.status_code) def test_delete_post_belongs_to_thread_and_forum(self): @@ -76,10 +128,30 @@ def test_delete_post_belongs_to_thread_and_forum(self): Delete post action - post belongs to thread and thread belongs to forum. """ + f = forum(save=True) + t = thread(forum=f, save=True) + # Post belongs to a different forum and thread. + p = forum_post(save=True) + u = p.author + + # Give the user the permission to delete posts. + g = group(save=True) + ct = ContentType.objects.get_for_model(f) + permission(codename='forums_forum.post_delete_forum', + content_type=ct, object_id=p.thread.forum_id, group=g, + save=True) + permission(codename='forums_forum.post_delete_forum', + content_type=ct, object_id=f.id, group=g, save=True) + g.user_set.add(u) + + self.client.login(username=u.username, password='testpass') + + # Post isn't in the passed forum: r = get(self.client, 'forums.delete_post', - args=[self.forum_2.slug, self.thread.id, self.post.id]) + args=[f.slug, p.thread.id, p.id]) eq_(404, r.status_code) + # Post isn't in the passed thread: r = get(self.client, 'forums.delete_post', - args=[self.forum.slug, self.thread_2.id, self.post.id]) + args=[p.thread.forum.slug, t.id, p.id]) eq_(404, r.status_code) diff --git a/apps/forums/tests/test_views.py b/apps/forums/tests/test_views.py index 25350c93c91..e4235a6f2dd 100644 --- a/apps/forums/tests/test_views.py +++ b/apps/forums/tests/test_views.py @@ -1,13 +1,15 @@ from mock import patch, Mock from nose.tools import eq_ -from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType -from forums.models import Forum, Thread -from forums.tests import ForumTestCase +from access.tests import permission from forums.events import NewThreadEvent, NewPostEvent +from forums.models import Forum, Thread +from forums.tests import ForumTestCase, forum, thread, post as forum_post from sumo.tests import get, post from sumo.urlresolvers import reverse +from users.tests import user, group class PostPermissionsTests(ForumTestCase): @@ -15,32 +17,44 @@ class PostPermissionsTests(ForumTestCase): def test_read_without_permission(self): """Listing posts without the view_in_forum permission should 404.""" - response = get(self.client, 'forums.posts', - args=['restricted-forum', 6]) + restricted_forum = _restricted_forum() + t = thread(forum=restricted_forum, save=True) + + response = get(self.client, 'forums.posts', args=[t.forum.slug, t.id]) eq_(404, response.status_code) def test_reply_without_view_permission(self): """Posting without view_in_forum permission should 404.""" - self.client.login(username='jsocol', password='testpass') + restricted_forum = _restricted_forum() + t = thread(forum=restricted_forum, save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.reply', {'content': 'Blahs'}, - args=['restricted-forum', 6]) + args=[t.forum.slug, t.id]) eq_(404, response.status_code) def test_reply_without_post_permission(self): """Posting without post_in_forum permission should 403.""" - self.client.login(username='jsocol', password='testpass') + restricted_forum = _restricted_forum( + permission_code='forums_forum.post_in_forum') + t = thread(forum=restricted_forum, save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') with patch.object(Forum, 'allows_viewing_by', Mock(return_value=True)): response = post(self.client, 'forums.reply', {'content': 'Blahs'}, - args=['restricted-forum', 6]) + args=[t.forum.slug, t.id]) eq_(403, response.status_code) def test_reply_thread_405(self): """Replying to a thread via a GET instead of a POST request.""" - f = Forum.objects.all()[0] - t = f.thread_set.all()[0] - self.client.login(username='jsocol', password='testpass') - response = get(self.client, 'forums.lock_thread', - args=[f.slug, t.id]) + t = thread(save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') + response = get(self.client, 'forums.reply', + args=[t.forum.slug, t.id]) eq_(405, response.status_code) @@ -49,48 +63,67 @@ class ThreadAuthorityPermissionsTests(ForumTestCase): def test_new_thread_without_view_permission(self): """Making a new thread without view permission should 404.""" - self.client.login(username='jsocol', password='testpass') + restricted_forum = _restricted_forum() + thread(forum=restricted_forum, save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.new_thread', {'title': 'Blahs', 'content': 'Blahs'}, - args=['restricted-forum']) + args=[restricted_forum.slug]) eq_(404, response.status_code) def test_new_thread_without_post_permission(self): """Making a new thread without post permission should 403.""" - self.client.login(username='jsocol', password='testpass') + restricted_forum = _restricted_forum( + permission_code='forums_forum.post_in_forum') + u = user(save=True) + + self.client.login(username=u.username, password='testpass') with patch.object(Forum, 'allows_viewing_by', Mock(return_value=True)): response = post(self.client, 'forums.new_thread', {'title': 'Blahs', 'content': 'Blahs'}, - args=['restricted-forum']) + args=[restricted_forum.slug]) eq_(403, response.status_code) def test_watch_GET_405(self): """Watch forum with HTTP GET results in 405.""" - self.client.login(username='rrosario', password='testpass') - f = Forum.objects.filter()[0] + f = forum(save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.watch_forum', args=[f.id]) eq_(405, response.status_code) def test_watch_forum_without_permission(self): """Watching forums without the view_in_forum permission should 404.""" - self.client.login(username='jsocol', password='testpass') + restricted_forum = _restricted_forum() + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = self.client.post(reverse('forums.watch_forum', - args=['restricted-forum']), + args=[restricted_forum.slug]), {'watch': 'yes'}, follow=False) eq_(404, response.status_code) def test_watch_thread_without_permission(self): """Watching threads without the view_in_forum permission should 404.""" - self.client.login(username='jsocol', password='testpass') + restricted_forum = _restricted_forum() + t = thread(forum=restricted_forum, save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = self.client.post(reverse('forums.watch_thread', - args=['restricted-forum', 6]), + args=[t.forum.slug, t.id]), {'watch': 'yes'}, follow=False) eq_(404, response.status_code) def test_read_without_permission(self): """Listing threads without the view_in_forum permission should 404.""" + restricted_forum = _restricted_forum() + response = get(self.client, 'forums.threads', - args=['restricted-forum']) + args=[restricted_forum.slug]) eq_(404, response.status_code) @@ -99,70 +132,74 @@ class ThreadTests(ForumTestCase): def test_watch_forum(self): """Watch then unwatch a forum.""" - self.client.login(username='rrosario', password='testpass') - user = User.objects.get(username='rrosario') + f = forum(save=True) + forum_post(thread=thread(forum=f, save=True), save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') - f = Forum.objects.filter()[0] post(self.client, 'forums.watch_forum', {'watch': 'yes'}, args=[f.slug]) - assert NewThreadEvent.is_notifying(user, f) + assert NewThreadEvent.is_notifying(u, f) # NewPostEvent is not notifying. - assert not NewPostEvent.is_notifying(user, f.last_post) + assert not NewPostEvent.is_notifying(u, f.last_post) post(self.client, 'forums.watch_forum', {'watch': 'no'}, args=[f.slug]) - assert not NewThreadEvent.is_notifying(user, f) + assert not NewThreadEvent.is_notifying(u, f) def test_watch_thread(self): """Watch then unwatch a thread.""" - self.client.login(username='rrosario', password='testpass') - user = User.objects.get(username='rrosario') + t = thread(save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') - t = Thread.objects.filter()[1] post(self.client, 'forums.watch_thread', {'watch': 'yes'}, args=[t.forum.slug, t.id]) - assert NewPostEvent.is_notifying(user, t) + assert NewPostEvent.is_notifying(u, t) # NewThreadEvent is not notifying. - assert not NewThreadEvent.is_notifying(user, t.forum) + assert not NewThreadEvent.is_notifying(u, t.forum) post(self.client, 'forums.watch_thread', {'watch': 'no'}, args=[t.forum.slug, t.id]) - assert not NewPostEvent.is_notifying(user, t) + assert not NewPostEvent.is_notifying(u, t) - def test_edit_thread(self): - """Changing thread title works.""" - self.client.login(username='jsocol', password='testpass') + def test_edit_thread_creator(self): + """Changing thread title as the thread creator works.""" + t = forum_post(save=True).thread + u = t.creator - f = Forum.objects.filter()[0] - t_creator = User.objects.get(username='jsocol') - t = f.thread_set.filter(creator=t_creator)[0] + self.client.login(username=u.username, password='testpass') post(self.client, 'forums.edit_thread', {'title': 'A new title'}, - args=[f.slug, t.id]) - edited_t = f.thread_set.get(pk=t.id) - - eq_('Sticky Thread', t.title) + args=[t.forum.slug, t.id]) + edited_t = Thread.uncached.get(id=t.id) eq_('A new title', edited_t.title) def test_edit_thread_moderator(self): """Editing post as a moderator works.""" - self.client.login(username='pcraciunoiu', password='testpass') - - t = Thread.objects.get(pk=2) + t = forum_post(save=True).thread f = t.forum - - eq_('Sticky Thread', t.title) - + u = user(save=True) + g = group(save=True) + ct = ContentType.objects.get_for_model(f) + permission(codename='forums_forum.thread_edit_forum', content_type=ct, + object_id=f.id, group=g, save=True) + g.user_set.add(u) + + self.client.login(username=u.username, password='testpass') r = post(self.client, 'forums.edit_thread', {'title': 'new title'}, args=[f.slug, t.id]) eq_(200, r.status_code) - - edited_t = Thread.uncached.get(pk=2) + edited_t = Thread.uncached.get(id=t.id) eq_('new title', edited_t.title) def test_new_thread_redirect(self): """Posting a new thread should redirect.""" - self.client.login(username='pcraciunoiu', password='testpass') - f = Forum.objects.get(pk=1) + f = forum(save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') url = reverse('forums.new_thread', args=[f.slug]) data = {'title': 'some title', 'content': 'some content'} r = self.client.post(url, data, follow=False) @@ -172,8 +209,10 @@ def test_new_thread_redirect(self): def test_reply_redirect(self): """Posting a reply should redirect.""" - self.client.login(username='pcraciunoiu', password='testpass') - t = Thread.objects.get(pk=2) + t = thread(save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') url = reverse('forums.reply', args=[t.forum.slug, t.id]) data = {'content': 'some content'} r = self.client.post(url, data, follow=False) @@ -185,93 +224,151 @@ def test_reply_redirect(self): class ThreadPermissionsTests(ForumTestCase): - def setUp(self): - super(ThreadPermissionsTests, self).setUp() - self.forum = Forum.objects.all()[0] - admin = User.objects.get(pk=1) - self.thread = self.forum.thread_set.filter(creator=admin)[0] - self.post = self.thread.post_set.all()[0] - # Login for testing 403s - self.client.login(username='jsocol', password='testpass') - - def tearDown(self): - self.client.logout() - super(ThreadPermissionsTests, self).tearDown() - def test_edit_thread_403(self): """Editing a thread without permissions returns 403.""" + t = forum_post(save=True).thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.edit_thread', - args=[self.forum.slug, self.thread.id]) + args=[t.forum.slug, t.id]) eq_(403, response.status_code) def test_edit_locked_thread_403(self): """Editing a locked thread returns 403.""" - jsocol = User.objects.get(username='jsocol') - t = self.forum.thread_set.filter(creator=jsocol, is_locked=True)[0] + locked = thread(is_locked=True, save=True) + u = locked.creator + forum_post(thread=locked, author=u, save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.edit_thread', - args=[self.forum.slug, t.id]) + args=[locked.forum.slug, locked.id]) eq_(403, response.status_code) def test_delete_thread_403(self): """Deleting a thread without permissions returns 403.""" + t = forum_post(save=True).thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.delete_thread', - args=[self.forum.slug, self.thread.id]) + args=[t.forum.slug, t.id]) eq_(403, response.status_code) def test_sticky_thread_405(self): """Marking a thread sticky with a HTTP GET returns 405.""" + t = forum_post(save=True).thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.sticky_thread', - args=[self.forum.slug, self.thread.id]) + args=[t.forum.slug, t.id]) eq_(405, response.status_code) def test_sticky_thread_403(self): """Marking a thread sticky without permissions returns 403.""" + t = forum_post(save=True).thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.sticky_thread', - args=[self.forum.slug, self.thread.id]) + args=[t.forum.slug, t.id]) eq_(403, response.status_code) def test_locked_thread_403(self): """Marking a thread locked without permissions returns 403.""" + t = forum_post(save=True).thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.lock_thread', - args=[self.forum.slug, self.thread.id]) + args=[t.forum.slug, t.id]) eq_(403, response.status_code) def test_locked_thread_405(self): """Marking a thread locked via a GET instead of a POST request.""" + t = forum_post(save=True).thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.lock_thread', - args=[self.forum.slug, self.thread.id]) + args=[t.forum.slug, t.id]) eq_(405, response.status_code) def test_move_thread_403(self): """Moving a thread without permissions returns 403.""" - response = post(self.client, 'forums.move_thread', {'forum': 2}, - args=[self.forum.slug, self.thread.id]) + t = forum_post(save=True).thread + f = forum(save=True) + u = user(save=True) + + self.client.login(username=u.username, password='testpass') + response = post(self.client, 'forums.move_thread', {'forum': f.id}, + args=[t.forum.slug, t.id]) eq_(403, response.status_code) def test_move_thread_405(self): """Moving a thread via a GET instead of a POST request.""" + t = forum_post(save=True).thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.move_thread', - args=[self.forum.slug, self.thread.id]) + args=[t.forum.slug, t.id]) eq_(405, response.status_code) def test_move_thread(self): """Move a thread.""" - self.client.login(username='rrosario', password='testpass') + t = forum_post(save=True).thread + f = forum(save=True) + u = user(save=True) + g = group(save=True) + + # Give the user permission to move threads between the two forums. + ct = ContentType.objects.get_for_model(f) + permission(codename='forums_forum.thread_move_forum', content_type=ct, + object_id=f.id, group=g, save=True) + permission(codename='forums_forum.thread_move_forum', content_type=ct, + object_id=t.forum.id, group=g, save=True) + g.user_set.add(u) + + self.client.login(username=u.username, password='testpass') response = post(self.client, 'forums.move_thread', - {'forum': 2}, - args=[self.forum.slug, self.thread.id]) + {'forum': f.id}, + args=[t.forum.slug, t.id]) eq_(200, response.status_code) - thread = Thread.uncached.get(pk=self.thread.pk) - eq_(2, thread.forum.id) + t = Thread.uncached.get(pk=t.pk) + eq_(f.id, t.forum.id) def test_post_edit_403(self): """Editing a post without permissions returns 403.""" + p = forum_post(save=True) + t = p.thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.edit_post', - args=[self.forum.slug, self.thread.id, self.post.id]) + args=[t.forum.slug, t.id, p.id]) eq_(403, response.status_code) def test_post_delete_403(self): """Deleting a post without permissions returns 403.""" + p = forum_post(save=True) + t = p.thread + u = user(save=True) + + self.client.login(username=u.username, password='testpass') response = get(self.client, 'forums.delete_post', - args=[self.forum.slug, self.thread.id, self.post.id]) + args=[t.forum.slug, t.id, p.id]) eq_(403, response.status_code) + + +def _restricted_forum(permission_code='forums_forum.view_in_forum'): + """Return a forum with specified restriction.""" + restricted_forum = forum(save=True) + + # Make it restricted. + ct = ContentType.objects.get_for_model(restricted_forum) + permission(codename=permission_code, content_type=ct, + object_id=restricted_forum.id, save=True) + + return restricted_forum diff --git a/apps/users/tests/__init__.py b/apps/users/tests/__init__.py index f6316492e36..cdeb8643584 100644 --- a/apps/users/tests/__init__.py +++ b/apps/users/tests/__init__.py @@ -38,6 +38,9 @@ def user(**kwargs): if 'username' not in kwargs: defaults['username'] = ''.join(random.choice(letters) for x in xrange(15)) + if 'email not in kwargs': + defaults['email'] = ''.join( + random.choice(letters) for x in xrange(10)) + '@example.com' defaults.update(kwargs) user = User(**defaults) user.set_password(kwargs.get('password', 'testpass'))