diff options
author | Tomasz Zielinski <tomasz.zielinski@pyconsultant.eu> | 2012-01-24 18:15:21 +0100 |
---|---|---|
committer | Tomasz Zielinski <tomasz.zielinski@pyconsultant.eu> | 2012-01-24 18:15:21 +0100 |
commit | d1056fc8abcb09720704b4c4980818ccf5184534 (patch) | |
tree | de73bd28c3e1afd76c1624fc8fee7ebfc9bd1b1c | |
parent | 6145566d88d7e270ea6270d5ab2a21a194bb5a59 (diff) | |
download | askbot-d1056fc8abcb09720704b4c4980818ccf5184534.tar.gz askbot-d1056fc8abcb09720704b4c4980818ccf5184534.tar.bz2 askbot-d1056fc8abcb09720704b4c4980818ccf5184534.zip |
Main page thread summary caching BETA
-rw-r--r-- | askbot/const/__init__.py | 3 | ||||
-rw-r--r-- | askbot/models/post.py | 2 | ||||
-rw-r--r-- | askbot/models/question.py | 71 | ||||
-rw-r--r-- | askbot/search/state_manager.py | 8 | ||||
-rw-r--r-- | askbot/skins/default/templates/main_page/questions_loop.html | 3 | ||||
-rw-r--r-- | askbot/tests/page_load_tests.py | 3 | ||||
-rw-r--r-- | askbot/tests/post_model_tests.py | 368 | ||||
-rw-r--r-- | askbot/views/commands.py | 10 | ||||
-rw-r--r-- | askbot/views/readers.py | 5 |
9 files changed, 462 insertions, 11 deletions
diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 56ef89cf..0f981dee 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -84,7 +84,8 @@ UNANSWERED_QUESTION_MEANING_CHOICES = ( #correct regexes - plus this must be an anchored regex #to do full string match TAG_CHARS = r'\w+.#-' -TAG_REGEX = r'^[%s]+$' % TAG_CHARS +TAG_REGEX_BARE = r'[%s]+' % TAG_CHARS +TAG_REGEX = r'^%s$' % TAG_REGEX_BARE TAG_SPLIT_REGEX = r'[ ,]+' TAG_SEP = ',' # has to be valid TAG_SPLIT_REGEX char and MUST NOT be in const.TAG_CHARS EMAIL_REGEX = re.compile(r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b', re.I) diff --git a/askbot/models/post.py b/askbot/models/post.py index 6cd9f7eb..5cb9708f 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -174,9 +174,9 @@ class PostManager(BaseQuerySetManager): ) #update thread data - thread.set_last_activity(last_activity_at=added_at, last_activity_by=author) thread.answer_count +=1 thread.save() + thread.set_last_activity(last_activity_at=added_at, last_activity_by=author) # this should be here because it regenerates cached thread summary html #set notification/delete if email_notify: diff --git a/askbot/models/question.py b/askbot/models/question.py index a112830d..cd1ca184 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -1,11 +1,12 @@ import datetime import operator -from askbot.utils import mysql +import re from django.conf import settings from django.db import models from django.contrib.auth.models import User from django.utils.translation import ugettext as _ +from django.core import cache # import cache, not from cache import cache, to be able to monkey-patch cache.cache in test cases import askbot import askbot.conf @@ -16,6 +17,9 @@ from askbot.models import signals from askbot import const from askbot.utils.lists import LazyList from askbot.utils import mysql +from askbot.skins.loaders import get_template #jinja2 template loading enviroment +from askbot.search.state_manager import DummySearchState + class ThreadManager(models.Manager): def get_tag_summary_from_threads(self, threads): @@ -246,6 +250,13 @@ class ThreadManager(models.Manager): return qs, meta_data def precache_view_data_hack(self, threads): + # TODO: Re-enable this when we have a good test cases to verify that it works properly. + # + # E.g.: - make sure that not precaching give threads never increase # of db queries for the main page + # - make sure that it really works, i.e. stuff for non-cached threads is fetched properly + # Precache data only for non-cached threads - only those will be rendered + #threads = [thread for thread in threads if not thread.summary_html_cached()] + page_questions = Post.objects.filter(post_type='question', thread__in=[obj.id for obj in threads])\ .only('id', 'thread', 'score', 'is_anonymous', 'summary', 'post_type', 'deleted') # pick only the used fields page_question_map = {} @@ -281,6 +292,8 @@ class ThreadManager(models.Manager): class Thread(models.Model): + SUMMARY_CACHE_KEY_TPL = 'thread-question-summary-%d' + title = models.CharField(max_length=300) tags = models.ManyToManyField('Tag', related_name='threads') @@ -313,7 +326,9 @@ class Thread(models.Model): class Meta: app_label = 'askbot' - def _question_post(self): + def _question_post(self, refresh=False): + if refresh and hasattr(self, '_question_cache'): + delattr(self, '_question_cache') post = getattr(self, '_question_cache', None) if post: return post @@ -335,6 +350,9 @@ class Thread(models.Model): qset = Thread.objects.filter(id=self.id) qset.update(view_count=models.F('view_count') + increment) self.view_count = qset.values('view_count')[0]['view_count'] # get the new view_count back because other pieces of code relies on such behaviour + #################################################################### + self.update_summary_html() # regenerate question/thread summary html + #################################################################### def set_closed_status(self, closed, closed_by, closed_at, close_reason): self.closed = closed @@ -354,6 +372,9 @@ class Thread(models.Model): self.last_activity_at = last_activity_at self.last_activity_by = last_activity_by self.save() + #################################################################### + self.update_summary_html() # regenerate question/thread summary html + #################################################################### def get_tag_names(self): "Creates a list of Tag names from the ``tagnames`` attribute." @@ -529,6 +550,10 @@ class Thread(models.Model): self.tags.add(*added_tags) modified_tags.extend(added_tags) + #################################################################### + self.update_summary_html() # regenerate question/thread summary html + #################################################################### + #if there are any modified tags, update their use counts if modified_tags: Tag.objects.update_use_counts(modified_tags) @@ -593,7 +618,47 @@ class Thread(models.Model): return last_updated_at, last_updated_by - + def get_summary_html(self, search_state): + html = self.get_cached_summary_html() + if not html: + html = self.update_summary_html() + + # use `<<<` and `>>>` because they cannot be confused with user input + # - if user accidentialy types <<<tag-name>>> into question title or body, + # then in html it'll become escaped like this: <<<tag-name>>> + regex = re.compile(r'<<<(%s)>>>' % const.TAG_REGEX_BARE) + + while True: + match = regex.search(html) + if not match: + break + seq = match.group(0) # e.g "<<<my-tag>>>" + tag = match.group(1) # e.g "my-tag" + full_url = search_state.add_tag(tag).full_url() + html = html.replace(seq, full_url) + + return html + + def get_cached_summary_html(self): + return cache.cache.get(self.SUMMARY_CACHE_KEY_TPL % self.id) + + def update_summary_html(self): + context = { + 'thread': self, + 'question': self._question_post(refresh=True), # fetch new question post to make sure we're up-to-date + 'search_state': DummySearchState(), + } + html = get_template('widgets/question_summary.html').render(context) + # INFO: Timeout is set to 30 days: + # * timeout=0/None is not a reliable cross-backend way to set infinite timeout + # * We probably don't need to pollute the cache with threads older than 30 days + # * Additionally, Memcached treats timeouts > 30day as dates (https://code.djangoproject.com/browser/django/tags/releases/1.3/django/core/cache/backends/memcached.py#L36), + # which probably doesn't break anything but if we can stick to 30 days then let's stick to it + cache.cache.set(self.SUMMARY_CACHE_KEY_TPL % self.id, html, timeout=60*60*24*30) + return html + + def summary_html_cached(self): + return cache.cache.has_key(self.SUMMARY_CACHE_KEY_TPL % self.id) #class Question(content.Content): diff --git a/askbot/search/state_manager.py b/askbot/search/state_manager.py index 29e6484e..232d64e9 100644 --- a/askbot/search/state_manager.py +++ b/askbot/search/state_manager.py @@ -232,3 +232,11 @@ class SearchState(object): ss = self.deepcopy() ss.page = new_page return ss + + +class DummySearchState(object): # Used for caching question/thread summaries + def add_tag(self, tag): + self.tag = tag + return self + def full_url(self): + return '<<<%s>>>' % self.tag diff --git a/askbot/skins/default/templates/main_page/questions_loop.html b/askbot/skins/default/templates/main_page/questions_loop.html index 6d83032c..6a5e5e3d 100644 --- a/askbot/skins/default/templates/main_page/questions_loop.html +++ b/askbot/skins/default/templates/main_page/questions_loop.html @@ -1,7 +1,8 @@ {% import "macros.html" as macros %} {# cache 0 "questions" questions search_tags scope sort query context.page language_code #} {% for thread in threads.object_list %} - {{macros.question_summary(thread, thread._question_post(), search_state=search_state)}} + {# {{macros.question_summary(thread, thread._question_post(), search_state=search_state)}} #} + {{ thread.get_summary_html(search_state=search_state) }} {% endfor %} {% if threads.object_list|length == 0 %} {% include "main_page/nothing_found.html" %} diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py index 1a56f951..18d8d69c 100644 --- a/askbot/tests/page_load_tests.py +++ b/askbot/tests/page_load_tests.py @@ -108,7 +108,8 @@ class PageLoadTestCase(AskbotTestCase): 'one template') % url ) - self.assertEqual(r.template[0].name, template) + #self.assertEqual(r.template[0].name, template) + self.assertIn(template, [t.name for t in r.template]) else: raise Exception('unexpected error while runnig test') diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py index 824a4a8f..44bcb10a 100644 --- a/askbot/tests/post_model_tests.py +++ b/askbot/tests/post_model_tests.py @@ -1,11 +1,19 @@ +import copy import datetime from operator import attrgetter +import time from askbot.search.state_manager import SearchState +from askbot.skins.loaders import get_template from django.contrib.auth.models import User +from django.core import cache, urlresolvers +from django.core.cache.backends.dummy import DummyCache +from django.core.cache.backends.locmem import LocMemCache from django.core.exceptions import ValidationError from askbot.tests.utils import AskbotTestCase from askbot.models import Post, PostRevision, Thread, Tag +from askbot.search.state_manager import DummySearchState +from django.utils import simplejson class PostModelTests(AskbotTestCase): @@ -300,3 +308,363 @@ class ThreadTagModelsTests(AskbotTestCase): self.assertTrue(thread.last_activity_by is thread._last_activity_by_cache) +class ThreadRenderLowLevelCachingTests(AskbotTestCase): + def setUp(self): + self.create_user() + # INFO: title and body_text should contain tag placeholders so that we can check if they stay untouched + # - only real tag placeholders in tag widget should be replaced with search URLs + self.q = self.post_question(title="<<<tag1>>> fake title", body_text="<<<tag2>>> <<<tag3>>> cheating", tags='tag1 tag2 tag3') + + self.old_cache = cache.cache + + def tearDown(self): + cache.cache = self.old_cache # Restore caching + + def test_thread_summary_rendering_dummy_cache(self): + cache.cache = DummyCache('', {}) # Disable caching + + ss = SearchState.get_empty() + thread = self.q.thread + test_html = thread.get_summary_html(search_state=ss) + + context = { + 'thread': thread, + 'question': thread._question_post(), + 'search_state': ss, + } + proper_html = get_template('widgets/question_summary.html').render(context) + self.assertEqual(test_html, proper_html) + + # Make double-check that all tags are included + self.assertTrue(ss.add_tag('tag1').full_url() in test_html) + self.assertTrue(ss.add_tag('tag2').full_url() in test_html) + self.assertTrue(ss.add_tag('tag3').full_url() in test_html) + self.assertFalse(ss.add_tag('mini-mini').full_url() in test_html) + + # Make sure that title and body text are escaped properly. + # This should be obvious at this point, if the above test passes, but why not be explicit + # UPDATE: And voila, these tests catched double-escaping bug in template, where `<` was `&lt;` + # And indeed, post.summary is escaped before saving, in parse_and_save_post() + # UPDATE 2:Weird things happen with question summary (it's double escaped etc., really weird) so + # let's just make sure that there are no tag placeholders left + self.assertTrue('<<<tag1>>> fake title' in proper_html) + #self.assertTrue('<<<tag2>>> <<<tag3>>> cheating' in proper_html) + self.assertFalse('<<<tag1>>>' in proper_html) + self.assertFalse('<<<tag2>>>' in proper_html) + self.assertFalse('<<<tag3>>>' in proper_html) + + ### + + ss = ss.add_tag('mini-mini') + context['search_state'] = ss + test_html = thread.get_summary_html(search_state=ss) + proper_html = get_template('widgets/question_summary.html').render(context) + + self.assertEqual(test_html, proper_html) + + # Make double-check that all tags are included (along with `mini-mini` tag) + self.assertTrue(ss.add_tag('tag1').full_url() in test_html) + self.assertTrue(ss.add_tag('tag2').full_url() in test_html) + self.assertTrue(ss.add_tag('tag3').full_url() in test_html) + + def test_thread_summary_locmem_cache(self): + cache.cache = LocMemCache('', {}) # Enable local caching + + thread = self.q.thread + key = Thread.SUMMARY_CACHE_KEY_TPL % thread.id + + self.assertTrue(thread.summary_html_cached()) + self.assertIsNotNone(thread.get_cached_summary_html()) + + ### + cache.cache.delete(key) # let's start over + + self.assertFalse(thread.summary_html_cached()) + self.assertIsNone(thread.get_cached_summary_html()) + + context = { + 'thread': thread, + 'question': self.q, + 'search_state': DummySearchState(), + } + html = get_template('widgets/question_summary.html').render(context) + filled_html = html.replace('<<<tag1>>>', SearchState.get_empty().add_tag('tag1').full_url())\ + .replace('<<<tag2>>>', SearchState.get_empty().add_tag('tag2').full_url())\ + .replace('<<<tag3>>>', SearchState.get_empty().add_tag('tag3').full_url()) + + self.assertEqual(filled_html, thread.get_summary_html(search_state=SearchState.get_empty())) + self.assertTrue(thread.summary_html_cached()) + self.assertEqual(html, thread.get_cached_summary_html()) + + ### + cache.cache.set(key, 'Test <<<tag1>>>', timeout=100) + + self.assertTrue(thread.summary_html_cached()) + self.assertEqual('Test <<<tag1>>>', thread.get_cached_summary_html()) + self.assertEqual( + 'Test %s' % SearchState.get_empty().add_tag('tag1').full_url(), + thread.get_summary_html(search_state=SearchState.get_empty()) + ) + + ### + cache.cache.set(key, 'TestBBB <<<tag1>>>', timeout=100) + + self.assertTrue(thread.summary_html_cached()) + self.assertEqual('TestBBB <<<tag1>>>', thread.get_cached_summary_html()) + self.assertEqual( + 'TestBBB %s' % SearchState.get_empty().add_tag('tag1').full_url(), + thread.get_summary_html(search_state=SearchState.get_empty()) + ) + + ### + cache.cache.delete(key) + thread.update_summary_html = lambda: "Monkey-patched <<<tag2>>>" + + self.assertFalse(thread.summary_html_cached()) + self.assertIsNone(thread.get_cached_summary_html()) + self.assertEqual( + 'Monkey-patched %s' % SearchState.get_empty().add_tag('tag2').full_url(), + thread.get_summary_html(search_state=SearchState.get_empty()) + ) + + + +class ThreadRenderCacheUpdateTests(AskbotTestCase): + def setUp(self): + self.create_user() + self.user.set_password('pswd') + self.user.save() + assert self.client.login(username=self.user.username, password='pswd') + + self.create_user(username='user2') + self.user2.set_password('pswd') + self.user2.reputation = 10000 + self.user2.save() + + self.old_cache = cache.cache + cache.cache = LocMemCache('', {}) # Enable local caching + + def tearDown(self): + cache.cache = self.old_cache # Restore caching + + def _html_for_question(self, q): + context = { + 'thread': q.thread, + 'question': q, + 'search_state': DummySearchState(), + } + html = get_template('widgets/question_summary.html').render(context) + return html + + def test_post_question(self): + self.assertEqual(0, Post.objects.count()) + response = self.client.post(urlresolvers.reverse('ask'), data={ + 'title': 'test title', + 'text': 'test body text', + 'tags': 'tag1 tag2', + }) + self.assertEqual(1, Post.objects.count()) + question = Post.objects.all()[0] + self.assertRedirects(response=response, expected_url=question.get_absolute_url()) + + self.assertEqual('test title', question.thread.title) + self.assertEqual('test body text', question.text) + self.assertItemsEqual(['tag1', 'tag2'], list(question.thread.tags.values_list('name', flat=True))) + self.assertEqual(0, question.thread.answer_count) + + self.assertTrue(question.thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(question) + self.assertEqual(html, question.thread.get_cached_summary_html()) + + def test_edit_question(self): + self.assertEqual(0, Post.objects.count()) + question = self.post_question() + + thread = Thread.objects.all()[0] + self.assertEqual(0, thread.answer_count) + self.assertEqual(thread.last_activity_at, question.added_at) + self.assertEqual(thread.last_activity_by, question.author) + + time.sleep(1.5) # compensate for 1-sec time resolution in some databases + + response = self.client.post(urlresolvers.reverse('edit_question', kwargs={'id': question.id}), data={ + 'title': 'edited title', + 'text': 'edited body text', + 'tags': 'tag1 tag2', + 'summary': 'just some edit', + }) + self.assertEqual(1, Post.objects.count()) + question = Post.objects.all()[0] + self.assertRedirects(response=response, expected_url=question.get_absolute_url()) + + thread = question.thread + self.assertEqual(0, thread.answer_count) + self.assertTrue(thread.last_activity_at > question.added_at) + self.assertEqual(thread.last_activity_at, question.last_edited_at) + self.assertEqual(thread.last_activity_by, question.author) + + self.assertTrue(question.thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(question) + self.assertEqual(html, question.thread.get_cached_summary_html()) + + def test_retag_question(self): + self.assertEqual(0, Post.objects.count()) + question = self.post_question() + response = self.client.post(urlresolvers.reverse('retag_question', kwargs={'id': question.id}), data={ + 'tags': 'tag1 tag2', + }) + self.assertEqual(1, Post.objects.count()) + question = Post.objects.all()[0] + self.assertRedirects(response=response, expected_url=question.get_absolute_url()) + + self.assertItemsEqual(['tag1', 'tag2'], list(question.thread.tags.values_list('name', flat=True))) + + self.assertTrue(question.thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(question) + self.assertEqual(html, question.thread.get_cached_summary_html()) + + def test_answer_question(self): + self.assertEqual(0, Post.objects.count()) + question = self.post_question() + self.assertEqual(1, Post.objects.count()) + + thread = question.thread + self.assertEqual(0, thread.answer_count) + self.assertEqual(thread.last_activity_at, question.added_at) + self.assertEqual(thread.last_activity_by, question.author) + + self.client.logout() + self.client.login(username='user2', password='pswd') + time.sleep(1.5) # compensate for 1-sec time resolution in some databases + response = self.client.post(urlresolvers.reverse('answer', kwargs={'id': question.id}), data={ + 'text': 'answer longer than 10 chars', + }) + self.assertEqual(2, Post.objects.count()) + answer = Post.objects.get_answers()[0] + self.assertRedirects(response=response, expected_url=answer.get_absolute_url()) + + thread = answer.thread + self.assertEqual(1, thread.answer_count) + self.assertEqual(thread.last_activity_at, answer.added_at) + self.assertEqual(thread.last_activity_by, answer.author) + + self.assertTrue(question.added_at < answer.added_at) + self.assertNotEqual(question.author, answer.author) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + def test_edit_answer(self): + self.assertEqual(0, Post.objects.count()) + question = self.post_question() + self.assertEqual(question.thread.last_activity_at, question.added_at) + self.assertEqual(question.thread.last_activity_by, question.author) + + time.sleep(1.5) # compensate for 1-sec time resolution in some databases + question_thread = copy.deepcopy(question.thread) # INFO: in the line below question.thread is touched and it reloads its `last_activity_by` field so we preserve it here + answer = self.post_answer(user=self.user2, question=question) + self.assertEqual(2, Post.objects.count()) + + time.sleep(1.5) # compensate for 1-sec time resolution in some databases + self.client.logout() + self.client.login(username='user2', password='pswd') + response = self.client.post(urlresolvers.reverse('edit_answer', kwargs={'id': answer.id}), data={ + 'text': 'edited body text', + 'summary': 'just some edit', + }) + self.assertRedirects(response=response, expected_url=answer.get_absolute_url()) + + answer = Post.objects.get(id=answer.id) + thread = answer.thread + self.assertEqual(thread.last_activity_at, answer.last_edited_at) + self.assertEqual(thread.last_activity_by, answer.last_edited_by) + self.assertTrue(thread.last_activity_at > question_thread.last_activity_at) + self.assertNotEqual(thread.last_activity_by, question_thread.last_activity_by) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + def test_view_count(self): + question = self.post_question() + self.assertEqual(0, question.thread.view_count) + self.assertEqual(0, Thread.objects.all()[0].view_count) + self.client.logout() + # INFO: We need to pass some headers to make question() view believe we're not a robot + self.client.get( + urlresolvers.reverse('question', kwargs={'id': question.id}), + {}, + follow=True, # the first view redirects to the full question url (with slug in it), so we have to follow that redirect + HTTP_ACCEPT_LANGUAGE='en', + HTTP_USER_AGENT='Mozilla Gecko' + ) + thread = Thread.objects.all()[0] + self.assertEqual(1, thread.view_count) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + def test_question_upvote_downvote(self): + question = self.post_question() + question.score = 5 + question.vote_up_count = 7 + question.vote_down_count = 2 + question.save() + + self.client.logout() + self.client.login(username='user2', password='pswd') + response = self.client.post(urlresolvers.reverse('vote', kwargs={'id': question.id}), data={'type': '1'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') # use AJAX request + self.assertEqual(200, response.status_code) + data = simplejson.loads(response.content) + + self.assertEqual(1, data['success']) + self.assertEqual(6, data['count']) # 6 == question.score(5) + 1 + + thread = Thread.objects.get(id=question.thread.id) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + ### + + response = self.client.post(urlresolvers.reverse('vote', kwargs={'id': question.id}), data={'type': '2'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') # use AJAX request + self.assertEqual(200, response.status_code) + data = simplejson.loads(response.content) + + self.assertEqual(1, data['success']) + self.assertEqual(5, data['count']) # 6 == question.score(6) - 1 + + thread = Thread.objects.get(id=question.thread.id) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + def test_question_accept_answer(self): + question = self.post_question(user=self.user2) + answer = self.post_answer(question=question) + + self.client.logout() + self.client.login(username='user2', password='pswd') + response = self.client.post(urlresolvers.reverse('vote', kwargs={'id': question.id}), data={'type': '0', 'postId': answer.id}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') # use AJAX request + self.assertEqual(200, response.status_code) + data = simplejson.loads(response.content) + + self.assertEqual(1, data['success']) + + thread = Thread.objects.get(id=question.thread.id) + + self.assertTrue(thread.summary_html_cached()) # <<< make sure that caching backend is set up properly (i.e. it's not dummy) + html = self._html_for_question(thread._question_post()) + self.assertEqual(html, thread.get_cached_summary_html()) + + +# TODO: (in spare time, these cases should already pass but we shold have them eventually for completness) +# - Publishing anonymous questions / answers +# - Re-posting question as answer and vice versa diff --git a/askbot/views/commands.py b/askbot/views/commands.py index f596b6f6..7db27ef2 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -205,6 +205,11 @@ def vote(request, id): response_data['status'] = 1 #cancelation else: request.user.accept_best_answer(answer) + + #################################################################### + answer.thread.update_summary_html() # regenerate question/thread summary html + #################################################################### + else: raise exceptions.PermissionDenied( _('Sorry, but anonymous users cannot accept answers') @@ -235,6 +240,11 @@ def vote(request, id): post = post ) + #################################################################### + if vote_type in ('1', '2'): # up/down-vote question + post.thread.update_summary_html() # regenerate question/thread summary html + #################################################################### + elif vote_type in ['7', '8']: #flag question or answer if vote_type == '7': diff --git a/askbot/views/readers.py b/askbot/views/readers.py index 88d2bf7d..c243f99c 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -10,13 +10,10 @@ import datetime import logging import urllib import operator -from django.contrib.auth.models import User from django.shortcuts import get_object_or_404 from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseNotAllowed from django.core.paginator import Paginator, EmptyPage, InvalidPage from django.template import Context -from django.template.base import Template -from django.template.context import RequestContext from django.utils import simplejson from django.utils.translation import ugettext as _ from django.utils.translation import ungettext @@ -42,7 +39,7 @@ from askbot.search.state_manager import SearchState from askbot.templatetags import extra_tags import askbot.conf from askbot.conf import settings as askbot_settings -from askbot.skins.loaders import render_into_skin, get_template#jinja2 template loading enviroment +from askbot.skins.loaders import render_into_skin, get_template #jinja2 template loading enviroment # used in index page #todo: - take these out of const or settings |