diff options
50 files changed, 503 insertions, 175 deletions
diff --git a/MANIFEST.in b/MANIFEST.in index 05201977..556620c4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,5 +13,7 @@ exclude urls.py exclude askbot/upfiles/*.* recursive-exclude avatar * recursive-exclude adzone * +recursive-exclude categories * +recursive-exclude follow * recursive-exclude env recursive-exclude .tox diff --git a/askbot/__init__.py b/askbot/__init__.py index 8d907571..cec0e1ba 100644 --- a/askbot/__init__.py +++ b/askbot/__init__.py @@ -11,7 +11,7 @@ import logging from askbot import patches from askbot.deployment.assertions import assert_package_compatibility -VERSION = (0, 6, 76) +VERSION = (0, 6, 78) #necessary for interoperability of django and coffin assert_package_compatibility() diff --git a/askbot/conf/email.py b/askbot/conf/email.py index f18a554d..953aac3d 100644 --- a/askbot/conf/email.py +++ b/askbot/conf/email.py @@ -57,6 +57,44 @@ settings.register( settings.register( livesettings.BooleanValue( EMAIL, + 'ENABLE_UNANSWERED_REMINDERS', + default = False, + description = _('Send periodic reminders about unanswered questions'), + help_text = _( + 'NOTE: in order to use this feature, it is necessary to ' + 'run the management command "send_unanswered_question_reminders" ' + '(for example, via a cron job - with an appropriate frequency) ' + 'and an IMAP server with a dedicated inbox must be configured ' + ) + ) +) + +settings.register( + livesettings.IntegerValue( + EMAIL, + 'DAYS_BEFORE_SENDING_UNANSWERED_REMINDER', + default = 1, + description = _( + 'Days before starting to send reminders about unanswered questions' + ), + ) +) + +settings.register( + livesettings.IntegerValue( + EMAIL, + 'UNANSWERED_REMINDER_FREQUENCY', + default = 1, + description = _( + 'How often to send unanswered question reminders ' + '(in days between the reminders sent).' + ) + ) +) + +settings.register( + livesettings.BooleanValue( + EMAIL, 'EMAIL_VALIDATION', default=False, hidden=True, diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 5a0781f0..ea9236b6 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -105,6 +105,7 @@ TYPE_ACTIVITY_FAVORITE=16 TYPE_ACTIVITY_USER_FULL_UPDATED = 17 TYPE_ACTIVITY_EMAIL_UPDATE_SENT = 18 TYPE_ACTIVITY_MENTION = 19 +TYPE_ACTIVITY_UNANSWERED_REMINDER_SENT = 20 #TYPE_ACTIVITY_EDIT_QUESTION=17 #TYPE_ACTIVITY_EDIT_ANSWER=18 @@ -128,6 +129,10 @@ TYPE_ACTIVITY = ( (TYPE_ACTIVITY_FAVORITE, _('selected favorite')), (TYPE_ACTIVITY_USER_FULL_UPDATED, _('completed user profile')), (TYPE_ACTIVITY_EMAIL_UPDATE_SENT, _('email update sent to user')), + ( + TYPE_ACTIVITY_UNANSWERED_REMINDER_SENT, + _('reminder about unanswered questions sent'), + ), (TYPE_ACTIVITY_MENTION, _('mentioned in the post')), ) diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py index 2ec423c7..14def205 100644 --- a/askbot/deps/django_authopenid/views.py +++ b/askbot/deps/django_authopenid/views.py @@ -40,6 +40,7 @@ from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required from django.contrib.auth import authenticate from django.core.urlresolvers import reverse +from django.views.decorators import csrf from django.utils.encoding import smart_unicode from django.utils.html import escape from django.utils.translation import ugettext as _ @@ -258,6 +259,7 @@ def complete_oauth_signin(request): return HttpResponseRedirect(next_url) #@not_authenticated +@csrf.csrf_protect def signin( request, newquestion = False,#todo: not needed @@ -447,6 +449,7 @@ def signin( view_subtype = view_subtype ) +@csrf.csrf_protect def show_signin_view( request, login_form = None, @@ -690,6 +693,7 @@ def finalize_generic_signin( return HttpResponseRedirect(redirect_url) @not_authenticated +@csrf.csrf_protect def register(request, login_provider_name=None, user_identifier=None): """ this function is used via it's own url with request.method=POST @@ -833,6 +837,7 @@ def signin_failure(request, message): @not_authenticated @decorators.valid_password_login_provider_required +@csrf.csrf_protect def signup_with_password(request): """Create a password-protected account template: authopenid/signup_with_password.html @@ -1024,6 +1029,7 @@ def send_new_email_key(user,nomessage=False): set_email_validation_message(user) @login_required +@csrf.csrf_protect def send_email_key(request): """ url = /email/sendkey/ diff --git a/askbot/doc/source/contributors.rst b/askbot/doc/source/contributors.rst index 67d8fdeb..f9617fc1 100644 --- a/askbot/doc/source/contributors.rst +++ b/askbot/doc/source/contributors.rst @@ -11,10 +11,13 @@ Programming * Mike Chen & Sailing Cai - original authors of CNPROG forum * Evgeny Fadeev - founder of askbot * Benoit Lavine (with Windriver Software, Inc.) -* Adolfo Fitoria +* Alex Robbins (celery support) +* `Adolfo Fitoria <http://fitoria.net>`_ * Andrei Mamoutkine * Ramiro Morales (with Machinalis) * Andy Knotts +* `Gael Pasgrimaud <http://www.gawel.org/>`_ (bearstech) +* Jeff Madynski Translations ------------ diff --git a/askbot/doc/source/index.rst b/askbot/doc/source/index.rst index a7fb8122..981af741 100644 --- a/askbot/doc/source/index.rst +++ b/askbot/doc/source/index.rst @@ -21,6 +21,8 @@ at the forum_ or by email at admin@askbot.org Deploy on a webserver <deployment> Import data (StackExchange) <import-data> Appendix A: Maintenance procedures <management-commands> + Appendix B: Sending email to askbot <sending-email-to-askbot> + Apperdix C: Optional modules <optional-modules> Contributors <contributors> Some background information: Askbot is written in Python on top of the Django platform. diff --git a/askbot/doc/source/initial-configuration.rst b/askbot/doc/source/initial-configuration.rst index 3e15dde4..cb871a7e 100644 --- a/askbot/doc/source/initial-configuration.rst +++ b/askbot/doc/source/initial-configuration.rst @@ -31,8 +31,9 @@ Within settings.py, at the very minimum you will need to provide correct values DATABASE_NAME = '' DATABASE_USER = '' DATABASE_PASSWORD = '' + CSRF_COOKIE_DOMAIN = ''#e.g. example.com (localhost/IP address for tests) -within single quotes - login credentials to your mysql database. +within single quotes - login credentials to your database. .. _urls.py: http://github.com/ASKBOT/askbot-devel/blob/master/askbot/setup_templates/urls.py .. _settings.py: http://github.com/ASKBOT/askbot-devel/blob/master/askbot/setup_templates/settings.py diff --git a/askbot/doc/source/live-settings.rst b/askbot/doc/source/live-settings.rst new file mode 100644 index 00000000..e154a257 --- /dev/null +++ b/askbot/doc/source/live-settings.rst @@ -0,0 +1,21 @@ +.. _live-settings: +============= +Live settings +============= + +Many of the configuration settings in askbot are accessible +to the site administators via link "settings" in the site header. + +Any change to the "live settings" will be reflected on the site +immediately. + +No-one but the site administrators can change those settings. + +.. note:: + Any user can be turned into an administrator via running a command. + + python manage.py add_admin <user_id> + + At the moment this command is not available from the web-interface + but this will be fixed in the future. + diff --git a/askbot/doc/source/management-commands.rst b/askbot/doc/source/management-commands.rst index 188654bf..a91c0286 100644 --- a/askbot/doc/source/management-commands.rst +++ b/askbot/doc/source/management-commands.rst @@ -73,22 +73,41 @@ The bulk of the management commands fall into this group and will probably be th | | marked as deleted. | +---------------------------------+-------------------------------------------------------------+ -Batch jobs -========== - -Batch jobs are those that should be run periodically. A program called `cron` can run these commands at the specified times (please look up futher information about `cron` elsewhere). - -+----------------------+-------------------------------------------------------------+ -| command | purpose | -+======================+=============================================================+ -| `send_email_alerts` | Dispatches email alerts to the users according to | -| | their subscription settings. This command does not | -| | send iinstant" alerts because those are sent automatically | -| | and do not require a separate command. | -| | The most frequent alert setting that can be served by this | -| | command is "daily", therefore running `send_email_alerts` | -| | more than twice a day is not necessary. | -+----------------------+-------------------------------------------------------------+ +.. _email-related-commands: + +Email-related commands +====================== + +These commands deal with the periodic tasks related to sending and receiving email by askbot. +A UNIX program called `cron` can run these commands at the specified times +(please look up futher information about `cron` elsewhere). + +Any configurable options, related to these commands are accessible via "Email" section of the +:ref:`live settings <live-settings>`. + ++-------------------------------------+-------------------------------------------------------------+ +| command | purpose | ++=====================================+=============================================================+ +| `send_email_alerts` | Dispatches email alerts to the users according to | +| | their subscription settings. This command does not | +| | send instant" alerts because those are sent automatically | +| | and do not require a separate command. | +| | The most frequent alert setting that can be served by this | +| | command is "daily", therefore running `send_email_alerts` | +| | more than twice a day is not necessary. | ++-------------------------------------+-------------------------------------------------------------+ +| `post_emailed_questions` | (experimental feature) posts questions sent by email | +| | to enable this feature - please follow the instructions | +| | on :doc:`sending email to askbot <sending-email-to-askbot>`.| +| | This command uses :ref:`live settings <live-settings>` | +| | "allow posting by email" and "replace spaces in tags | +| | with dash". | ++-------------------------------------+-------------------------------------------------------------+ +| `send_unanswered_question_reminders`| Sends periodic reminders about unanswered questions. | +| | This command may be disabled from the "email" section | +| | of :ref:`live settings <live-settings>`, as well as | +| | an initial wait period and the recurrence delay may be set. | ++-------------------------------------+-------------------------------------------------------------+ Data repair commands ==================== diff --git a/askbot/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst new file mode 100644 index 00000000..d7ca2f24 --- /dev/null +++ b/askbot/doc/source/optional-modules.rst @@ -0,0 +1,26 @@ +================ +Optional modules +================ + +Askbot supports a number of optional modules, enabling certain features, not available +in askbot by default. + +Uploaded avatars +================ + +To enable uploadable avatars (in addition to :ref:`gravatars <gravatar>`), +please install development version of +application ``django-avatar``, with the following command: + + pip install -e git+git://github.com/ericflo/django-avatar.git#egg=django-avatar + +Then add ``avatar`` to the list of ``INSTALLED_APPS`` in your ``settings.py`` file +and run (to install database table used by the avatar app): + + python manage.py syncdb + +.. note:: + + Version of the ``avatar`` application available at pypi may not + be up to date, so please take the development version from the + github repository diff --git a/askbot/doc/source/sending-email-to-askbot.rst b/askbot/doc/source/sending-email-to-askbot.rst new file mode 100644 index 00000000..b50b8c8e --- /dev/null +++ b/askbot/doc/source/sending-email-to-askbot.rst @@ -0,0 +1,33 @@ +======================= +Sending email to askbot +======================= + +Askbot supports asking questions by email via the IMAP protocol, +answering by email is not yet supported. + +.. note:: + This feature is still experimental and some emails will not + be parsed, please report any issues at the askbot forum. + +To enable the feature, please: + +* set up an IMAP mailbox called "INBOX" +* in your ``settings.py`` file fill out values + ``IMAP_HOST``, ``IMAP_HOST_USER``, ``IMAP_HOST_PASSWORD``, + ``IMAP_PORT`` and ``IMAP_USE_TLS`` +* in the site :ref:`live settings <live-settings>`, enable the + feature +* set up a cron job to periodically run command + :ref:`post_emailed_questions <email-related-commands>`. + This command will connect to the inbox, and post questions, + based on the incoming messages. + +The email address to send the questions will be +``<IMAP_HOST_USER>@<IMAP_HOST>``. Also, there is a quite strict +requirement to the format of incoming messages - described +in a response to any incorrectly formatted emails. + +.. warning:: + the "INBOX" used to post messages to askbot must be dedicated + do not use any other mailbox as all messages + are **automatically deleted** after each processing. diff --git a/askbot/management/commands/post_emailed_questions.py b/askbot/management/commands/post_emailed_questions.py index b80eb188..73b5ed3e 100644 --- a/askbot/management/commands/post_emailed_questions.py +++ b/askbot/management/commands/post_emailed_questions.py @@ -125,7 +125,7 @@ class Command(NoArgsCommand): all messages are deleted thereafter """ - if askbot_settings.ALLOW_ASKING_BY_EMAIL: + if not askbot_settings.ALLOW_ASKING_BY_EMAIL: raise CommandError('Asking by email is not enabled') #open imap server and select the inbox @@ -146,6 +146,12 @@ class Command(NoArgsCommand): #get message ids status, ids = imap.search(None, 'ALL') + if len(ids[0].strip()) == 0: + #with empty inbox - close and exit + imap.close() + imap.logout() + return + #for each id - read a message, parse it and post a question for id in ids[0].split(' '): t, data = imap.fetch(id, '(RFC822)') @@ -166,8 +172,9 @@ class Command(NoArgsCommand): if form.is_valid(): email_address = form.cleaned_data['email'] try: - print 'looking for ' + email_address - user = models.User.objects.get(email = email_address) + user = models.User.objects.get( + email__iexact = email_address + ) except models.User.DoesNotExist: bounce_email(email_address, subject, reason = 'unknown_user') except models.User.MultipleObjectsReturned: @@ -178,10 +185,6 @@ class Command(NoArgsCommand): body_text = form.cleaned_data['body_text'] try: - print 'posting question' - print title - print tagnames - print body_text user.post_question( title = title, tags = tagnames, diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py index 56286981..5f14618b 100644 --- a/askbot/management/commands/send_email_alerts.py +++ b/askbot/management/commands/send_email_alerts.py @@ -5,6 +5,7 @@ from django.db.models import Q, F from askbot.models import User, Question, Answer, Tag, QuestionRevision from askbot.models import AnswerRevision, Activity, EmailFeedSetting from askbot.models import Comment +from askbot.models.question import get_tag_summary_from_questions from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from django.conf import settings as django_settings @@ -72,52 +73,6 @@ def format_action_count(string, number, output): if number > 0: output.append(_(string) % {'num':number}) -def get_update_subject_line(question_dict): - """forms a subject line based on up to five most - frequently used tags in the question_dict - - question_dict is an instance of SortedDict, - where questions are keys and values are meta_data - accumulated during the question filtering - """ - #todo: in python 2.6 there is collections.Counter() thing - #which would be very useful here - tag_counts = dict() - updated_questions = question_dict.keys() - for question in updated_questions: - tag_names = question.get_tag_names() - for tag_name in tag_names: - if tag_name in tag_counts: - tag_counts[tag_name] += 1 - else: - tag_counts[tag_name] = 1 - tag_list = tag_counts.keys() - #sort in descending order - tag_list.sort(lambda x, y: cmp(tag_counts[y], tag_counts[x])) - - question_count = len(updated_questions) - #note that double quote placement is important here - if len(tag_list) == 1: - last_topic = '"' - elif len(tag_list) <= 5: - last_topic = _('" and "%s"') % tag_list.pop() - else: - tag_list = tag_list[:5] - last_topic = _('" and more') - - topics = '"' + '", "'.join(tag_list) + last_topic - - subject_line = ungettext( - '%(question_count)d updated question about %(topics)s', - '%(question_count)d updated questions about %(topics)s', - question_count - ) % { - 'question_count': question_count, - 'topics': topics - } - - return mail.prefix_the_subject_line(subject_line) - class Command(NoArgsCommand): def handle_noargs(self, **options): try: @@ -244,41 +199,8 @@ class Command(NoArgsCommand): q_ans_B.cutoff_time = cutoff_time elif feed.feed_type == 'q_all': - if user.email_tag_filter_strategy == 'ignored': - - ignored_tags = Tag.objects.filter( - user_selections__reason='bad', - user_selections__user=user - ) - - wk = user.ignored_tags.strip().split() - ignored_by_wildcards = Tag.objects.get_by_wildcards(wk) - - q_all_A = Q_set_A.exclude( - tags__in = ignored_tags - ).exclude( - tags__in = ignored_by_wildcards - ) - - q_all_B = Q_set_B.exclude( - tags__in = ignored_tags - ).exclude( - tags__in = ignored_by_wildcards - ) - else: - selected_tags = Tag.objects.filter( - user_selections__reason='good', - user_selections__user=user - ) - - wk = user.interesting_tags.strip().split() - selected_by_wildcards = Tag.objects.get_by_wildcards(wk) - - tag_filter = Q(tags__in = list(selected_tags)) \ - | Q(tags__in = list(selected_by_wildcards)) - - q_all_A = Q_set_A.filter( tag_filter ) - q_all_B = Q_set_B.filter( tag_filter ) + q_all_A = user.get_tag_filtered_questions(Q_set_A) + q_all_B = user.get_tag_filtered_questions(Q_set_B) q_all_A = q_all_A[:askbot_settings.MAX_ALERTS_PER_EMAIL] q_all_B = q_all_B[:askbot_settings.MAX_ALERTS_PER_EMAIL] @@ -478,7 +400,19 @@ class Command(NoArgsCommand): num_q += 1 if num_q > 0: url_prefix = askbot_settings.APP_URL - subject_line = get_update_subject_line(q_list) + + tag_summary = get_tag_summary_from_questions(q_list.keys()) + question_count = len(q_list.keys()) + + subject_line = ungettext( + '%(question_count)d updated question about %(topics)s', + '%(question_count)d updated questions about %(topics)s', + question_count + ) % { + 'question_count': question_count, + 'topics': tag_summary + } + #todo: send this to special log #print 'have %d updated questions for %s' % (num_q, user.username) text = ungettext('%(name)s, this is an update message header for %(num)d question', diff --git a/askbot/management/commands/send_unanswered_question_reminders.py b/askbot/management/commands/send_unanswered_question_reminders.py new file mode 100644 index 00000000..50ae6451 --- /dev/null +++ b/askbot/management/commands/send_unanswered_question_reminders.py @@ -0,0 +1,97 @@ +import datetime +from django.core.management.base import NoArgsCommand +from django.conf import settings as django_settings +from askbot import models +from askbot import const +from askbot.conf import settings as askbot_settings +from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +from askbot.utils import mail +from askbot.models.question import get_tag_summary_from_questions + +DEBUG_THIS_COMMAND = False + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + if askbot_settings.ENABLE_UNANSWERED_REMINDERS == False: + return + #get questions without answers, excluding closed and deleted + #order it by descending added_at date + wait_period = datetime.timedelta( + askbot_settings.DAYS_BEFORE_SENDING_UNANSWERED_REMINDER + ) + cutoff_date = datetime.datetime.now() + wait_period + + questions = models.Question.objects.exclude( + closed = True + ).exclude( + deleted = True + ).filter( + added_at__lt = cutoff_date + ).filter( + answer_count = 0 + ).order_by('-added_at') + #for all users, excluding blocked + #for each user, select a tag filtered subset + #format the email reminder and send it + for user in models.User.objects.exclude(status = 'b'): + user_questions = questions.exclude(author = user) + user_questions = user.get_tag_filtered_questions(user_questions) + + final_question_list = list() + #todo: rewrite using query set filter + #may be a lot more efficient + for question in user_questions: + activity_type = const.TYPE_ACTIVITY_UNANSWERED_REMINDER_SENT + try: + activity = models.Activity.objects.get( + user = user, + question = question, + activity_type = activity_type + ) + now = datetime.datetime.now() + recurrence_delay = datetime.timedelta( + askbot_settings.UNANSWERED_REMINDER_FREQUENCY + ) + if now < activity.active_at + recurrence_delay: + continue + except models.Activity.DoesNotExist: + activity = models.Activity( + user = user, + question = question, + activity_type = activity_type, + content_object = question, + ) + activity.active_at = datetime.datetime.now() + activity.save() + final_question_list.append(question) + + question_count = len(final_question_list) + if question_count == 0: + continue + + tag_summary = get_tag_summary_from_questions(user_questions) + subject_line = ungettext( + '%(question_count)d unanswered question about %(topics)s', + '%(question_count)d unanswered questions about %(topics)s', + question_count + ) % { + 'question_count': question_count, + 'topics': tag_summary + } + + body_text = '<ul>' + for question in final_question_list: + body_text += '<li><a href="%s%s?sort=latest">%s</a></li>' \ + % ( + askbot_settings.APP_URL, + question.get_absolute_url(), + question.title + ) + body_text += '</ul>' + + mail.send_mail( + subject_line = subject_line, + body_text = body_text, + recipient_list = (user.email,) + ) diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index a4d21caa..983188a6 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -876,8 +876,9 @@ def user_mark_tags( * ``action`` - eitrer "add" or "remove" """ cleaned_wildcards = list() - assert(reason in ('good', 'bad')) assert(action in ('add', 'remove')) + if action == 'add': + assert(reason in ('good', 'bad')) if wildcards: cleaned_wildcards = self.update_wildcard_tag_selections( action = action, @@ -1492,6 +1493,45 @@ def user_get_q_sel_email_feed_frequency(self): raise e return feed_setting.frequency +def user_get_tag_filtered_questions(self, questions = None): + """Returns a query set of questions, tag filtered according + to the user choices. Parameter ``questions`` can be either ``None`` + or a starting query set. + """ + if questions == None: + questions = Question.objects.all() + + if self.email_tag_filter_strategy == const.EXCLUDE_IGNORED: + + ignored_tags = Tag.objects.filter( + user_selections__reason = 'bad', + user_selections__user = self + ) + + wk = self.ignored_tags.strip().split() + ignored_by_wildcards = Tag.objects.get_by_wildcards(wk) + + return questions.exclude( + tags__in = ignored_tags + ).exclude( + tags__in = ignored_by_wildcards + ) + elif self.email_tag_filter_strategy == const.INCLUDE_INTERESTING: + selected_tags = Tag.objects.filter( + user_selections__reason = 'good', + user_selections__user = self + ) + + wk = self.interesting_tags.strip().split() + selected_by_wildcards = Tag.objects.get_by_wildcards(wk) + + tag_filter = models.Q(tags__in = list(selected_tags)) \ + | models.Q(tags__in = list(selected_by_wildcards)) + + return questions.filter( tag_filter ) + else: + return questions + def get_messages(self): messages = [] for m in self.message_set.all(): @@ -1831,10 +1871,14 @@ User.add_to_class('downvote', downvote) User.add_to_class('flag_post', flag_post) User.add_to_class('receive_reputation', user_receive_reputation) User.add_to_class('get_flags', user_get_flags) -User.add_to_class('get_flag_count_posted_today', user_get_flag_count_posted_today) +User.add_to_class( + 'get_flag_count_posted_today', + user_get_flag_count_posted_today +) User.add_to_class('get_flags_for_post', user_get_flags_for_post) User.add_to_class('get_profile_url', get_profile_url) User.add_to_class('get_profile_link', get_profile_link) +User.add_to_class('get_tag_filtered_questions', user_get_tag_filtered_questions) User.add_to_class('get_messages', get_messages) User.add_to_class('delete_messages', delete_messages) User.add_to_class('toggle_favorite_question', toggle_favorite_question) diff --git a/askbot/models/question.py b/askbot/models/question.py index bbfa4cfd..ccb587e7 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -40,6 +40,42 @@ QUESTION_ORDER_BY_MAP = { 'relevance-desc': None#this is a special case for postges only } +def get_tag_summary_from_questions(questions): + """returns a humanized string containing up to + five most frequently used + unique tags coming from the ``questions``. + Variable ``questions`` is an iterable of + :class:`~askbot.models.Question` model objects. + + This is not implemented yet as a query set method, + because it is used on a list. + """ + #todo: in python 2.6 there is collections.Counter() thing + #which would be very useful here + tag_counts = dict() + for question in questions: + tag_names = question.get_tag_names() + for tag_name in tag_names: + if tag_name in tag_counts: + tag_counts[tag_name] += 1 + else: + tag_counts[tag_name] = 1 + tag_list = tag_counts.keys() + #sort in descending order + tag_list.sort(lambda x, y: cmp(tag_counts[y], tag_counts[x])) + + #note that double quote placement is important here + if len(tag_list) == 1: + last_topic = '"' + elif len(tag_list) <= 5: + last_topic = _('" and "%s"') % tag_list.pop() + else: + tag_list = tag_list[:5] + last_topic = _('" and more') + + return '"' + '", "'.join(tag_list) + last_topic + + class QuestionQuerySet(models.query.QuerySet): """Custom query set subclass for :class:`~askbot.models.Question` """ diff --git a/askbot/setup_templates/settings.py b/askbot/setup_templates/settings.py index 5ded3e17..4e22346a 100644 --- a/askbot/setup_templates/settings.py +++ b/askbot/setup_templates/settings.py @@ -139,6 +139,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( #'django.core.context_processors.i18n', 'askbot.user_messages.context_processors.user_messages',#must be before auth 'django.core.context_processors.auth', #this is required for admin + 'django.core.context_processors.csrf', #necessary for csrf protection ) @@ -210,3 +211,6 @@ CELERY_ALWAYS_EAGER = True import djcelery djcelery.setup_loader() + +CSRF_COOKIE_NAME = 'askbot_scrf' +CSRF_COOKIE_DOMAIN = ''#enter domain name here - e.g. example.com diff --git a/askbot/skins/default/media/js/live_search.js b/askbot/skins/default/media/js/live_search.js index b94ecf07..39ed2d14 100644 --- a/askbot/skins/default/media/js/live_search.js +++ b/askbot/skins/default/media/js/live_search.js @@ -398,40 +398,43 @@ var liveSearch = function(){ var render_ask_page_result = function(data, text_status, xhr){ var container = $('#' + q_list_sel); - container.children().remove(); - if (data.length > 5){ - container.css('overflow-y', 'scroll'); - container.css('height', '120px'); - } else { - container.css('height', data.length * 24 + 'px'); - container.css('overflow-y', 'hidden'); - } - $.each(data, function(idx, question){ - var url = question['url']; - var title = question['title']; - var answer_count = question['answer_count']; - var list_item = $('<h2></h2>'); - var count_element = $('<span class="item-count"></span>'); - count_element.html(answer_count); - list_item.append(count_element); - var link = $('<a></a>'); - link.attr('href', url); - list_item.append(link); - title_element = $('<span class="title"></span>'); - title_element.html(title); - link.append(title) - container.append(list_item); + container.fadeOut(200, function() { + container.children().remove(); + if (data.length > 5){ + container.css('overflow-y', 'scroll'); + container.css('height', '120px'); + } else { + container.css('height', data.length * 24 + 'px'); + container.css('overflow-y', 'hidden'); + } + $.each(data, function(idx, question){ + var url = question['url']; + var title = question['title']; + var answer_count = question['answer_count']; + var list_item = $('<h2></h2>'); + var count_element = $('<span class="item-count"></span>'); + count_element.html(answer_count); + list_item.append(count_element); + var link = $('<a></a>'); + link.attr('href', url); + list_item.append(link); + title_element = $('<span class="title"></span>'); + title_element.html(title); + link.append(title) + container.append(list_item); + }); + container.fadeIn(); }); }; var render_main_page_result = function(data, text_status, xhr){ var old_list = $('#' + q_list_sel); - var new_list = $('<div></div>'); + var new_list = $('<div></div>').hide(); if (data['questions'].length > 0){ new_list.html(render_question_list(data['questions'])); - old_list.hide(); + //old_list.hide(); old_list.after(new_list); - old_list.remove(); + //old_list.remove(); //rename new div to old new_list.attr('id', q_list_sel); render_paginator(data['paginator']); @@ -442,8 +445,13 @@ var liveSearch = function(){ render_relevance_sort_tab(); set_active_sort_tab(sortMethod); query.focus(); + + //show new div with a fadeIn effect + old_list.fadeOut(200, function() { + old_list.remove(); + new_list.fadeIn(400); + }); } - //show new div } var try_again = function(){ @@ -485,6 +493,7 @@ var liveSearch = function(){ return { refresh: function(){ + query = $('input#keywords'); refresh_main_page(); }, init: function(mode){ diff --git a/askbot/skins/default/media/style/style.css b/askbot/skins/default/media/style/style.css index 34be550f..9671ec29 100755 --- a/askbot/skins/default/media/style/style.css +++ b/askbot/skins/default/media/style/style.css @@ -331,6 +331,7 @@ blockquote { #question-list { float: left; + position: relative; background-color: #FFF; padding: 0; width: 100%; @@ -474,6 +475,7 @@ ul#search-tags { .short-summary { position: relative; + filter: inherit; padding: 5px 2px 5px 2px; border-top: 1px dashed #ccccce; overflow: hidden; diff --git a/askbot/skins/default/templates/answer_edit.html b/askbot/skins/default/templates/answer_edit.html index 0dc137ae..0d8b40da 100644 --- a/askbot/skins/default/templates/answer_edit.html +++ b/askbot/skins/default/templates/answer_edit.html @@ -11,7 +11,7 @@ </h1> <div id="main-body" class="ask-body"> <div id="askform"> - <form id="fmedit" action="{% url edit_answer answer.id %}" method="post" > + <form id="fmedit" action="{% url edit_answer answer.id %}" method="post" >{% csrf_token %} <label for="id_revision" ><strong>{% trans %}revision{% endtrans %}:</strong></label> <br/> {% if revision_form.revision.errors %}{{ revision_form.revision.errors.as_ul() }}{% endif %} <div style="vertical-align:middle"> diff --git a/askbot/skins/default/templates/authopenid/changeemail.html b/askbot/skins/default/templates/authopenid/changeemail.html index 52dc6a0c..1316a048 100644 --- a/askbot/skins/default/templates/authopenid/changeemail.html +++ b/askbot/skins/default/templates/authopenid/changeemail.html @@ -21,7 +21,7 @@ <p class="error">{{ msg }}</p> {% endif %} <div class="aligned"> - <form action="." method="post" accept-charset="utf-8"> + <form action="." method="post" accept-charset="utf-8">{% csrf_token %} {% if next %} <input type="hidden" name="next" value="{{next}}"/> {% endif %} diff --git a/askbot/skins/default/templates/authopenid/complete.html b/askbot/skins/default/templates/authopenid/complete.html index ccaf753a..40ec4ccc 100644 --- a/askbot/skins/default/templates/authopenid/complete.html +++ b/askbot/skins/default/templates/authopenid/complete.html @@ -48,11 +48,11 @@ parameters: {% endif %} <div class="login"> {% if login_type=='openid' %} - <form name="fregister" action="{% url user_register %}" method="POST"> + <form name="fregister" action="{% url user_register %}" method="POST">{% csrf_token %} {% elif login_type=='facebook' %} - <form name="fregister" action="" method="POST"> + <form name="fregister" action="" method="POST">{% csrf_token %} {% else %} - <form name="fregister" action="{% url user_signin %}" method="POST"> + <form name="fregister" action="{% url user_signin %}" method="POST">{% csrf_token %} {% endif %} {{ openid_register_form.next }} <div class="form-row-vertical"> diff --git a/askbot/skins/default/templates/authopenid/signin.html b/askbot/skins/default/templates/authopenid/signin.html index aa67c95f..9316255a 100644 --- a/askbot/skins/default/templates/authopenid/signin.html +++ b/askbot/skins/default/templates/authopenid/signin.html @@ -44,7 +44,7 @@ <p class="warning">{{ openid_error_message }}</p>
{% endif %}
{% if view_subtype != 'email_sent' and view_subtype != 'bad_key' %}
- <form id="signin-form" method="post" action="{% url user_signin %}">
+ <form id="signin-form" method="post" action="{% url user_signin %}">{% csrf_token %}
{# in this branch - the real signin view we display the login icons
here we hide the local login button only if admin
wants to always show the password login form - then
@@ -157,7 +157,7 @@ {% endif %}
{% if view_subtype != 'email_sent' or view_subtype == 'bad_key' %}
{% if user.is_anonymous() %}
- <form id="account-recovery-form" action="{% url user_account_recover %}" method="post">
+ <form id="account-recovery-form" action="{% url user_account_recover %}" method="post">{% csrf_token %}
{% if view_subtype != 'bad_key' %}
<h2 id='account-recovery-heading'>{% trans %}Still have trouble signing in?{% endtrans %}</h2>
{% endif %}
diff --git a/askbot/skins/default/templates/authopenid/signup_with_password.html b/askbot/skins/default/templates/authopenid/signup_with_password.html index d85f8671..b5680806 100644 --- a/askbot/skins/default/templates/authopenid/signup_with_password.html +++ b/askbot/skins/default/templates/authopenid/signup_with_password.html @@ -8,7 +8,7 @@ {% block content %} {% if settings.PASSWORD_REGISTER_SHOW_PROVIDER_BUTTONS == True %} <h1>{% trans %}Please register by clicking on any of the icons below{% endtrans %}</h1> - <form id="signin-form" method="post" action="{% url user_signin %}"> + <form id="signin-form" method="post" action="{% url user_signin %}">{% csrf_token %} {# hide_local_login == True because it is password reg form #} {{ login_macros.provider_buttons( @@ -25,7 +25,7 @@ <h1>{% trans %}Create login name and password{% endtrans %}</h1> <p class="message">{% trans %}Traditional signup info{% endtrans %}</p> {%endif%} -<form action="{% url user_signup_with_password %}" method="post" accept-charset="utf-8"> +<form action="{% url user_signup_with_password %}" method="post" accept-charset="utf-8">{% csrf_token %} {{form.login_provider}} <ul class="form-horizontal-rows"> <li><label for="usename_id">{{form.username.label}}</label>{{form.username}}{{form.username.errors}}</li> diff --git a/askbot/skins/default/templates/avatar/add.html b/askbot/skins/default/templates/avatar/add.html index df700d0c..68a188ef 100644 --- a/askbot/skins/default/templates/avatar/add.html +++ b/askbot/skins/default/templates/avatar/add.html @@ -8,7 +8,7 @@ {% if not avatars %} <p>{% trans %}You haven't uploaded an avatar yet. Please upload one now.{% endtrans %}</p> {% endif %} - <form enctype="multipart/form-data" method="POST" action="{% url avatar_add %}"> + <form enctype="multipart/form-data" method="POST" action="{% url avatar_add %}">{% csrf_token %} {{ upload_avatar_form.as_p() }} <p><input type="submit" value="{% trans %}Upload New Image{% endtrans %}" /></p> </form> diff --git a/askbot/skins/default/templates/avatar/change.html b/askbot/skins/default/templates/avatar/change.html index 7a88ddef..7921a662 100644 --- a/askbot/skins/default/templates/avatar/change.html +++ b/askbot/skins/default/templates/avatar/change.html @@ -10,14 +10,14 @@ {% if not avatars %} <p>{% trans %}You haven't uploaded an avatar yet. Please upload one now.{% endtrans %}</p> {% else %} - <form method="POST" action="{% url avatar_change %}"> + <form method="POST" action="{% url avatar_change %}">{% csrf_token %} <ul> {{ primary_avatar_form.as_ul() }} </ul> <p><input type="submit" value="{% trans %}Choose new Default{% endtrans %}" /></p> </form> {% endif %} - <form enctype="multipart/form-data" method="POST" action="{% url avatar_add %}"> + <form enctype="multipart/form-data" method="POST" action="{% url avatar_add %}">{% csrf_token %} {{ upload_avatar_form.as_p() }} <p><input type="submit" value="{% trans %}Upload{% endtrans %}" /></p> </form> diff --git a/askbot/skins/default/templates/avatar/confirm_delete.html b/askbot/skins/default/templates/avatar/confirm_delete.html index 042d7c0d..282d72fa 100644 --- a/askbot/skins/default/templates/avatar/confirm_delete.html +++ b/askbot/skins/default/templates/avatar/confirm_delete.html @@ -6,7 +6,7 @@ {% if not avatars %} <p>{% trans avatar_change_url="avatar_change"|url %}You have no avatars to delete. Please <a href="{{ avatar_change_url }}">upload one</a> now.{% endtrans %}</p> {% else %} - <form method="POST" action="{% url avatar_delete %}"> + <form method="POST" action="{% url avatar_delete %}">{% csrf_token %} <ul> {{ delete_avatar_form.as_ul() }} </ul> diff --git a/askbot/skins/default/templates/blocks/ask_form.html b/askbot/skins/default/templates/blocks/ask_form.html index 8df6c019..9b61c7ce 100644 --- a/askbot/skins/default/templates/blocks/ask_form.html +++ b/askbot/skins/default/templates/blocks/ask_form.html @@ -1,6 +1,6 @@ {% import "macros.html" as macros %} <div id="askform"> - <form id="fmask" action="" method="post" > + <form id="fmask" action="" method="post" >{% csrf_token %} <div class="form-item"> <div id="askFormBar"> {% if not request.user.is_authenticated() %} diff --git a/askbot/skins/default/templates/close.html b/askbot/skins/default/templates/close.html index 57ff5780..d8160865 100644 --- a/askbot/skins/default/templates/close.html +++ b/askbot/skins/default/templates/close.html @@ -6,7 +6,7 @@ <p>{% trans %}Close the question{% endtrans %}: <a href="{{ question.get_absolute_url() }}"> <strong>{{ question.get_question_title() }}</strong></a> </p> - <form id="fmclose" action="{% url close question.id %}" method="post" > + <form id="fmclose" action="{% url close question.id %}" method="post" >{% csrf_token %} <p> <strong>{% trans %}Reasons{% endtrans %}:</strong> {{ form.reason }} diff --git a/askbot/skins/default/templates/feedback.html b/askbot/skins/default/templates/feedback.html index 258a85dc..d5e8b3a7 100644 --- a/askbot/skins/default/templates/feedback.html +++ b/askbot/skins/default/templates/feedback.html @@ -3,7 +3,7 @@ {% block title %}{% spaceless %}{% trans %}Feedback{% endtrans %}{% endspaceless %}{% endblock %} {% block content %} <h1>{% trans %}Give us your feedback!{% endtrans %}</h1> -<form method="post" action="{% url feedback %}" accept-charset="utf-8"> +<form method="post" action="{% url feedback %}" accept-charset="utf-8">{% csrf_token %} {% if user.is_authenticated() %} <p class="message"> {% trans user_name=user.username %} diff --git a/askbot/skins/default/templates/import_data.html b/askbot/skins/default/templates/import_data.html index 7bc370ab..affeaa73 100644 --- a/askbot/skins/default/templates/import_data.html +++ b/askbot/skins/default/templates/import_data.html @@ -18,7 +18,7 @@ Please note that feedback will be printed in plain text. {% endtrans %} </p> - <form id="load-dump-form" method="post" enctype="multipart/form-data"> + <form id="load-dump-form" method="post" enctype="multipart/form-data">{% csrf_token %} <table> {{dump_upload_form.as_table()}} </table> diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html index d593fec5..0eb0b303 100644 --- a/askbot/skins/default/templates/macros.html +++ b/askbot/skins/default/templates/macros.html @@ -528,7 +528,7 @@ poor design of the data or methods on data objects #} sortButtonData["{{key_name}}"] = { label: "{{label}}", asc_tooltip: "{{asc_tooltip}}", - desc_tooltip: "{{desc_tooltip}}", + desc_tooltip: "{{desc_tooltip}}" }; </script> {%- endmacro %} @@ -645,7 +645,7 @@ poor design of the data or methods on data objects #} useCache: true, matchInside: true, maxCacheLength: 100, - delay: 10, + delay: 10 }); tagAc.decorate($("{{ id }}")); {%- endmacro -%} diff --git a/askbot/skins/default/templates/main_page/tab_bar.html b/askbot/skins/default/templates/main_page/tab_bar.html index 9189db13..12096a3b 100644 --- a/askbot/skins/default/templates/main_page/tab_bar.html +++ b/askbot/skins/default/templates/main_page/tab_bar.html @@ -45,7 +45,7 @@ sortButtonData['relevance'] = { asc_tooltip: "{{asc_relevance_tooltip}}", desc_tooltip: "{{desc_relevance_tooltip}}", - label: "{{relevance_label}}", + label: "{{relevance_label}}" }; </script> {% endif %} diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html index d95fd6c0..ffab9bd1 100644 --- a/askbot/skins/default/templates/question.html +++ b/askbot/skins/default/templates/question.html @@ -304,7 +304,7 @@ {{ macros.paginator(paginator_context) }} </div><br/> {% endif %} -<form id="fmanswer" action="{% url answer question.id %}" method="post"> +<form id="fmanswer" action="{% url answer question.id %}" method="post">{% csrf_token %} {% if request.user.is_authenticated() %} <p style="padding-left:3px"> {{ answer.email_notify }} diff --git a/askbot/skins/default/templates/question_edit.html b/askbot/skins/default/templates/question_edit.html index c1a84426..6a55e58c 100644 --- a/askbot/skins/default/templates/question_edit.html +++ b/askbot/skins/default/templates/question_edit.html @@ -7,7 +7,7 @@ {% endblock %} {% block content %} <h1>{% trans %}Edit question{% endtrans %} [<a href="{{ question.get_absolute_url() }}">{% trans %}back{% endtrans %}</a>]</h1> -<form id="fmedit" action="{% url edit_question question.id %}" method="post" > +<form id="fmedit" action="{% url edit_question question.id %}" method="post" >{% csrf_token %} <label for="id_revision" ><strong>{% trans %}revision{% endtrans %}:</strong></label> <br/> {% if revision_form.revision.errors %}{{ revision_form.revision.errors.as_ul() }}{% endif %} <div style="vertical-align:middle"> diff --git a/askbot/skins/default/templates/question_retag.html b/askbot/skins/default/templates/question_retag.html index f521ccb3..79cbbbff 100644 --- a/askbot/skins/default/templates/question_retag.html +++ b/askbot/skins/default/templates/question_retag.html @@ -4,7 +4,7 @@ {% block content %} <h1>{% trans %}Change tags{% endtrans %} [<a href="{{ question.get_absolute_url() }}">{% trans %}back{% endtrans %}</a>]</h1> <div id="askform"> - <form id="fmretag" action="{% url retag_question question.id %}" method="post" > + <form id="fmretag" action="{% url retag_question question.id %}" method="post" >{% csrf_token %} <h2> {{ question.get_question_title() }} </h2> diff --git a/askbot/skins/default/templates/reopen.html b/askbot/skins/default/templates/reopen.html index 58d798a3..d68e8bdc 100644 --- a/askbot/skins/default/templates/reopen.html +++ b/askbot/skins/default/templates/reopen.html @@ -21,7 +21,7 @@ <p> {% trans %}Reopen this question?{% endtrans %} </p> -<form id="fmclose" action="{% url reopen question.id %}" method="post" > +<form id="fmclose" action="{% url reopen question.id %}" method="post" >{% csrf_token %} <div id="" style="padding:20px 0 20px 0"> <input type="submit" value="{% trans %}Reopen this question{% endtrans %}" class="submit" /> <input id="btBack" type="button" value="{% trans %}Cancel{% endtrans %}" class="submit" /> diff --git a/askbot/skins/default/templates/subscribe_for_tags.html b/askbot/skins/default/templates/subscribe_for_tags.html index 9a58ccbf..b436fb84 100644 --- a/askbot/skins/default/templates/subscribe_for_tags.html +++ b/askbot/skins/default/templates/subscribe_for_tags.html @@ -10,7 +10,7 @@ {% endfor %} </ul> <div style="clear:both;padding-top: 5px"> - <form method="post" action="{% url subscribe_for_tags %}"> + <form method="post" action="{% url subscribe_for_tags %}">{% csrf_token %} <input type="hidden" name="tags" value="{{tags|join(' ')|escape}}" /> <input type="submit" name="ok" value="{% trans %}Subscribe{% endtrans %}" /> <input type="submit" name="nope" value="{% trans %}Cancel{% endtrans %}" /> diff --git a/askbot/skins/default/templates/user_profile/user_edit.html b/askbot/skins/default/templates/user_profile/user_edit.html index 9308bf90..fe4ea35f 100644 --- a/askbot/skins/default/templates/user_profile/user_edit.html +++ b/askbot/skins/default/templates/user_profile/user_edit.html @@ -7,7 +7,7 @@ {{ request.user.username }} - {% trans %}edit profile{% endtrans %} </h1> <div id="main-body" style="width:100%;padding-top:10px"> - <form name="" action="{% url edit_user request.user.id %}" method="post"> + <form name="" action="{% url edit_user request.user.id %}" method="post">{% csrf_token %} <div id="left" style="float:left;width:180px"> {% if request.user.email %} {{ macros.gravatar(request.user, 128) }} diff --git a/askbot/skins/default/templates/user_profile/user_email_subscriptions.html b/askbot/skins/default/templates/user_profile/user_email_subscriptions.html index 896a77f0..e6a18dd3 100644 --- a/askbot/skins/default/templates/user_profile/user_email_subscriptions.html +++ b/askbot/skins/default/templates/user_profile/user_email_subscriptions.html @@ -10,7 +10,7 @@ {% if action_status %} <p class="action-status"><span>{{action_status}}</span></p> {% endif %} - <form method="post" action=""> + <form method="post" action="">{% csrf_token %} <table class='form-as-table ab-subscr-form'> {{email_feeds_form.as_table()}} </table> diff --git a/askbot/skins/default/templates/user_profile/user_moderate.html b/askbot/skins/default/templates/user_profile/user_moderate.html index b8070e50..563026a4 100644 --- a/askbot/skins/default/templates/user_profile/user_moderate.html +++ b/askbot/skins/default/templates/user_profile/user_moderate.html @@ -10,7 +10,7 @@ {% if user_status_changed %} <p class="action-status"><span>{% trans %}User status changed{% endtrans %}</span></p> {% endif %} - <form method="post"> + <form method="post">{% csrf_token %} <input type="hidden" name="sort" value="moderate"/> <table class="form-as-table"> {{ change_user_status_form.as_table() }} @@ -29,7 +29,7 @@ {% if user_rep_changed %} <p class="action-status"><span>{% trans %}User reputation changed{% endtrans %}</span></p> {% endif %} -<form method="post"> +<form method="post">{% csrf_token %} <input type="hidden" name="sort" value="moderate"/> <table class="form-as-table"> {{ change_user_reputation_form.as_table() }} @@ -44,7 +44,7 @@ {% if message_sent %} <p class="action-status"><span>{% trans %}Message sent{% endtrans %}</span></p> {% endif %} -<form method="post"> +<form method="post">{% csrf_token %} <input type="hidden" name="sort" value="moderate"/> <div class="form-row-vertical"> <label for="id_subject_line">{{ send_message_form.subject_line.label}}</label> diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index de801967..0c239be1 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -13,6 +13,7 @@ from askbot import models from askbot.utils import mail from askbot.conf import settings as askbot_settings from askbot import const +from askbot.models.question import get_tag_summary_from_questions def email_alert_test(test_func): """decorator for test methods in @@ -685,8 +686,7 @@ class DelayedAlertSubjectLineTests(TestCase): q1:'', q2:'', q3:'', q4:'', q5:'', q6:'', q7:'', q8:'', q9:'', q10:'', q11:'', } - from askbot.management.commands import send_email_alerts as cmd - subject = cmd.get_update_subject_line(q_dict) + subject = get_tag_summary_from_questions(q_dict.keys()) self.assertTrue('one' not in subject) self.assertTrue('two' in subject) @@ -701,9 +701,9 @@ class DelayedAlertSubjectLineTests(TestCase): i6 = subject.index('six') order = [i6, i5, i4, i3, i2] self.assertEquals( - order, - sorted(order) - ) + order, + sorted(order) + ) class FeedbackTests(utils.AskbotTestCase): def setUp(self): @@ -800,3 +800,22 @@ class TagFollowedInstantWholeForumEmailAlertTests(utils.AskbotTestCase): self.assertTrue( self.user1.email in outbox[0].recipients() ) + +class UnansweredReminderTests(utils.AskbotTestCase): + def setUp(self): + self.u1 = self.create_user(username = 'user1') + self.u2 = self.create_user(username = 'user2') + + def test_reminder_simple(self): + """a positive test - user must receive a reminder + """ + askbot_settings.update('ENABLE_UNANSWERED_REMINDERS', True) + days_ago = 5*askbot_settings.DAYS_BEFORE_SENDING_UNANSWERED_REMINDER + long_ago = datetime.datetime.now() - datetime.timedelta(days_ago) + self.post_question( + user = self.u1, + timestamp = long_ago + ) + management.call_command('send_unanswered_question_reminders') + outbox = django.core.mail.outbox + self.assertEqual(len(outbox), 1) diff --git a/askbot/views/avatar_views.py b/askbot/views/avatar_views.py index ea16380d..8ac30561 100644 --- a/askbot/views/avatar_views.py +++ b/askbot/views/avatar_views.py @@ -5,6 +5,7 @@ does not support jinja templates from django.http import HttpResponseRedirect from django.template import RequestContext from django.utils.translation import ugettext as _ +from django.views.decorators import csrf from django.conf import settings from django.contrib.auth.decorators import login_required @@ -74,6 +75,7 @@ def _get_avatars(user): return (avatar, avatars) @login_required +@csrf.csrf_protect def add(request, extra_context=None, next_override=None, upload_form=UploadAvatarForm, *args, **kwargs): if extra_context is None: @@ -109,6 +111,7 @@ def add(request, extra_context=None, next_override=None, return render_into_skin('avatar/add.html', data, request) @login_required +@csrf.csrf_protect def change(request, extra_context=None, next_override=None, upload_form=UploadAvatarForm, primary_form=PrimaryAvatarForm, *args, **kwargs): @@ -150,6 +153,7 @@ def change(request, extra_context=None, next_override=None, return render_into_skin('avatar/change.html', data, request) @login_required +@csrf.csrf_protect def delete(request, extra_context=None, next_override=None, *args, **kwargs): if extra_context is None: extra_context = {} diff --git a/askbot/views/commands.py b/askbot/views/commands.py index 8d16c35f..809d4249 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -12,6 +12,7 @@ from django.contrib.auth.decorators import login_required from django.http import HttpResponse, HttpResponseRedirect from django.forms import ValidationError from django.shortcuts import get_object_or_404 +from django.views.decorators import csrf from django.utils import simplejson from django.utils.translation import ugettext as _ from askbot import models @@ -391,6 +392,7 @@ def get_tag_list(request): output = '\n'.join(tag_names) return HttpResponse(output, mimetype = "text/plain") +@csrf.csrf_protect def subscribe_for_tags(request): """process subscription of users by tags""" #todo - use special separator to split tags @@ -471,6 +473,7 @@ def set_tag_filter_strategy(request): @login_required +@csrf.csrf_protect def close(request, id):#close question """view to initiate and process question close @@ -500,6 +503,7 @@ def close(request, id):#close question return HttpResponseRedirect(question.get_absolute_url()) @login_required +@csrf.csrf_protect def reopen(request, id):#re-open question """view to initiate and process question close diff --git a/askbot/views/meta.py b/askbot/views/meta.py index 0e67f08f..8953200b 100644 --- a/askbot/views/meta.py +++ b/askbot/views/meta.py @@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect, HttpResponse from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from django.views import static +from django.views.decorators import csrf from django.db.models import Max, Count from askbot.forms import FeedbackForm from askbot.utils.forms import get_next_url @@ -49,6 +50,7 @@ def faq(request): } return render_into_skin('faq.html', data, request) +@csrf.csrf_protect def feedback(request): data = {'page_class': 'meta'} form = None diff --git a/askbot/views/readers.py b/askbot/views/readers.py index 20a0df58..547addec 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -18,6 +18,7 @@ from django.utils import simplejson from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from django.utils import translation +from django.views.decorators import csrf from django.core.urlresolvers import reverse from django.core import exceptions as django_exceptions from django.contrib.humanize.templatetags import humanize @@ -354,6 +355,7 @@ def tags(request):#view showing a listing of available tags - plain list } return render_into_skin('tags.html', data, request) +@csrf.csrf_protect def question(request, id):#refactor - long subroutine. display question body, answers and comments """view that displays body of the question and all answers to it diff --git a/askbot/views/users.py b/askbot/views/users.py index 0db7124e..d96ceece 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -11,6 +11,7 @@ import functools import datetime import logging from django.db.models import Count +from django.conf import settings as django_settings from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator, EmptyPage, InvalidPage from django.contrib.contenttypes.models import ContentType @@ -19,7 +20,7 @@ from django.shortcuts import get_object_or_404 from django.http import HttpResponse from django.http import HttpResponseRedirect, Http404 from django.utils.translation import ugettext as _ -from django.conf import settings as django_settings +from django.views.decorators import csrf from askbot.utils.slug import slugify from askbot.utils.html import sanitize_html from askbot.utils.mail import send_mail @@ -129,6 +130,7 @@ def users(request): } return render_into_skin('users.html', data, request) +@csrf.csrf_protect def user_moderate(request, subject): """user subview for moderation """ @@ -232,6 +234,7 @@ def set_new_email(user, new_email, nomessage=False): # send_new_email_key(user,nomessage=nomessage) @login_required +@csrf.csrf_protect def edit_user(request, id): """View that allows to edit user profile. This view is accessible to profile owners or site administrators @@ -862,6 +865,7 @@ def user_favorites(request, user): return render_into_skin('user_profile/user_favorites.html', data, request) @owner_or_moderator_required +@csrf.csrf_protect def user_email_subscriptions(request, user): logging.debug(get_request_info(request)) diff --git a/askbot/views/writers.py b/askbot/views/writers.py index 56b37154..d103c776 100644 --- a/askbot/views/writers.py +++ b/askbot/views/writers.py @@ -22,6 +22,7 @@ from django.utils.translation import ugettext as _ from django.core.urlresolvers import reverse from django.core import exceptions from django.conf import settings +from django.views.decorators import csrf from askbot import forms from askbot import models @@ -147,7 +148,7 @@ def __import_se_data(dump_file): sys.stdout = real_stdout yield '<p>Done. Please, <a href="%s">Visit Your Forum</a></p></body></html>' % reverse('index') - +@csrf.csrf_protect def import_data(request): """a view allowing the site administrator upload stackexchange data @@ -185,6 +186,7 @@ def import_data(request): return render_into_skin('import_data.html', data, request) #@login_required #actually you can post anonymously, but then must register +@csrf.csrf_protect def ask(request):#view used to ask a new question """a view to ask a new question gives space for q title, body, tags and checkbox for to post as wiki @@ -260,6 +262,7 @@ def ask(request):#view used to ask a new question return render_into_skin('ask.html', data, request) @login_required +@csrf.csrf_protect def retag_question(request, id): """retag question view """ @@ -313,6 +316,7 @@ def retag_question(request, id): return HttpResponseRedirect(question.get_absolute_url()) @login_required +@csrf.csrf_protect def edit_question(request, id): """edit question view """ @@ -356,6 +360,7 @@ def edit_question(request, id): revision = latest_revision, user = request.user, ) + revision_form = forms.RevisionForm(question, latest_revision) if form.is_valid(): if form.has_changed(): @@ -397,6 +402,7 @@ def edit_question(request, id): return HttpResponseRedirect(question.get_absolute_url()) @login_required +@csrf.csrf_protect def edit_answer(request, id): answer = get_object_or_404(models.Answer, id=id) try: @@ -426,6 +432,8 @@ def edit_answer(request, id): ) else: form = forms.EditAnswerForm(answer, latest_revision, request.POST) + revision_form = forms.RevisionForm(answer, latest_revision) + if form.is_valid(): if form.has_changed(): request.user.edit_answer( @@ -34,7 +34,7 @@ if sys.platform not in WIN_PLATFORMS: setup( name = "askbot", - version = '0.6.76',#remember to manually set this correctly + version = '0.6.78',#remember to manually set this correctly description = 'Question and Answer forum, like StackOverflow, written in python and Django', packages = find_packages(), author = 'Evgeny.Fadeev', |