From 46b135059052f57e3105ee5236059350e456a80d Mon Sep 17 00:00:00 2001 From: Andrei Date: Fri, 15 Apr 2011 19:07:48 -0400 Subject: added an send_unanswered_question_reminders management command, untested --- askbot/conf/email.py | 37 ++++++++ askbot/const/__init__.py | 5 ++ askbot/management/commands/send_email_alerts.py | 98 ++++------------------ .../commands/send_unanswered_question_reminders.py | 91 ++++++++++++++++++++ askbot/models/__init__.py | 43 +++++++++- askbot/models/question.py | 38 ++++++++- askbot/tests/email_alert_tests.py | 3 +- 7 files changed, 230 insertions(+), 85 deletions(-) create mode 100644 askbot/management/commands/send_unanswered_question_reminders.py diff --git a/askbot/conf/email.py b/askbot/conf/email.py index f18a554d..b86b7330 100644 --- a/askbot/conf/email.py +++ b/askbot/conf/email.py @@ -54,6 +54,43 @@ 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)' + ) + ) +) + +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, 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/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..adef4a2d --- /dev/null +++ b/askbot/management/commands/send_unanswered_question_reminders.py @@ -0,0 +1,91 @@ +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_UANSWERED_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 = False + ).exclude( + added_at__lt = cutoff_date + ).filter( + answer_count__gt = 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(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 + activity, created = models.Activity.objects.get_or_create( + user = user, + question = question, + activity_type = activity_type + ) + + now = datetime.datetime.now() + recurrence_delay = datetime.timedelta( + askbot_settings.UNANSWERED_REMINDER_FREQUENCY + ) + if created == False: + if activity.active_at >= now + recurrence_delay: + continue + + activity.active_at = datetime.datetime.now() + activity.save() + + 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 = '' + + 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..75426cf5 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -1492,6 +1492,43 @@ 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 == 'ignored': + + ignored_tags = Tag.objects.filter( + user_selections__reason = 'bad', + user_selections__user = self + ) + + wk = user.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 + ) + else: + selected_tags = Tag.objects.filter( + user_selections__reason = 'good', + user_selections__user = self + ) + + 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)) + + return questions.filter( tag_filter ) + def get_messages(self): messages = [] for m in self.message_set.all(): @@ -1831,10 +1868,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 d1570877..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` """ @@ -112,7 +148,7 @@ class QuestionQuerySet(models.query.QuerySet): return self.extra(**extra_kwargs) else: #fallback to dumb title match search - return extra( + return self.extra( where=['title like %s'], params=['%' + search_query + '%'] ) diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index de801967..84d9857b 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 @@ -686,7 +687,7 @@ class DelayedAlertSubjectLineTests(TestCase): 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) -- cgit v1.2.3-1-g7c22