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-