diff options
author | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2011-04-03 19:11:01 -0400 |
---|---|---|
committer | Evgeny Fadeev <evgeny.fadeev@gmail.com> | 2011-04-03 19:11:01 -0400 |
commit | 8e57f310d1da605c31d30d7392c26deab73f2619 (patch) | |
tree | 08d9fa0aae37360fc0290b42d89333b79ecbec7f | |
parent | 7da87a124353c9fea3bc26d50f54bcd677e5c112 (diff) | |
download | askbot-8e57f310d1da605c31d30d7392c26deab73f2619.tar.gz askbot-8e57f310d1da605c31d30d7392c26deab73f2619.tar.bz2 askbot-8e57f310d1da605c31d30d7392c26deab73f2619.zip |
added support to receive email updates on interesting and exclude ignored wildcards
-rw-r--r-- | askbot/models/__init__.py | 20 | ||||
-rw-r--r-- | askbot/models/base.py | 32 | ||||
-rw-r--r-- | askbot/models/content.py | 111 | ||||
-rw-r--r-- | askbot/models/question.py | 20 | ||||
-rw-r--r-- | askbot/models/tag.py | 24 | ||||
-rw-r--r-- | askbot/tests/db_api_tests.py | 96 | ||||
-rw-r--r-- | askbot/tests/email_alert_tests.py | 1 | ||||
-rw-r--r-- | askbot/tests/utils.py | 2 | ||||
-rw-r--r-- | askbot/views/commands.py | 1 |
9 files changed, 269 insertions, 38 deletions
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 78a98192..6ef9e4a1 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -861,15 +861,31 @@ def user_post_comment( ) return comment -def user_mark_tags(self, tagnames, wildcards, reason = None, action = None): - """subscribe for or ignore a list of tags""" +def user_mark_tags( + self, + tagnames = None, + wildcards = None, + reason = None, + action = None + ): + """subscribe for or ignore a list of tags + + * ``tagnames`` and ``wildcards`` are lists of + pure tags and wildcard tags, respectively + * ``reason`` - either "good" or "bad" + * ``action`` - eitrer "add" or "remove" + """ cleaned_wildcards = list() + assert(reason in ('good', 'bad')) + assert(action in ('add', 'remove')) if wildcards: cleaned_wildcards = self.update_wildcard_tag_selections( action = action, reason = reason, wildcards = wildcards ) + if tagnames is None: + tagnames = list() #below we update normal tag selections marked_ts = MarkedTag.objects.filter( diff --git a/askbot/models/base.py b/askbot/models/base.py index 8b1bb5eb..361ae5df 100644 --- a/askbot/models/base.py +++ b/askbot/models/base.py @@ -5,6 +5,7 @@ from django.utils.html import strip_tags from django.contrib.auth.models import User from django.contrib.contenttypes import generic from django.contrib.contenttypes.models import ContentType +from django.contrib.sitemaps import ping_google #todo: maybe merge askbot.utils.markup and forum.utils.html from askbot.utils import markup from askbot.utils.html import sanitize_html @@ -156,6 +157,37 @@ def parse_and_save_post(post, author = None, **kwargs): except Exception: logging.debug('cannot ping google - did you register with them?') +class BaseQuerySetManager(models.Manager): + """a base class that allows chainable qustom filters + on the query sets + + pattern from http://djangosnippets.org/snippets/562/ + + Usage (the most basic example, all imports explicit for clarity): + + >>>import django.db.models.QuerySet + >>>import django.db.models.Model + >>>import askbot.models.base.BaseQuerySetManager + >>> + >>>class SomeQuerySet(django.db.models.QuerySet): + >>> def some_custom_filter(self, *args, **kwargs): + >>> return self #or any custom code + >>> #add more custom filters here + >>> + >>>class SomeManager(askbot.models.base.BaseQuerySetManager) + >>> def get_query_set(self): + >>> return SomeQuerySet(self.model) + >>> + >>>class SomeModel(django.db.models.Model) + >>> #add fields here + >>> objects = SomeManager() + """ + def __getattr__(self, attr, *args): + try: + return getattr(self.__class__, attr, *args) + except AttributeError: + return getattr(self.get_query_set(), attr, *args) + class UserContent(models.Model): user = models.ForeignKey(User, related_name='%(class)ss') diff --git a/askbot/models/content.py b/askbot/models/content.py index 5fa387ce..3ce219ed 100644 --- a/askbot/models/content.py +++ b/askbot/models/content.py @@ -7,6 +7,7 @@ from askbot import const from askbot.models.meta import Comment, Vote from askbot.models.user import EmailFeedSetting from askbot.models.tag import Tag, MarkedTag +from askbot.conf import settings as askbot_settings class Content(models.Model): """ @@ -93,10 +94,85 @@ class Content(models.Model): return comment + def get_global_tag_based_subscribers( + self, + tags = None, + tag_mark_reason = None, + subscription_records = None + ): + """returns a list of users who either follow or "do not ignore" + the given set of tags, depending on the tag_mark_reason + + ``subscription_records`` - query set of ``~askbot.models.EmailFeedSetting`` + this argument is used to reduce number of database queries + """ + if tag_mark_reason == 'good': + email_tag_filter_strategy = const.INCLUDE_INTERESTING + user_set_getter = User.objects.filter + elif tag_mark_reason == 'bad': + email_tag_filter_strategy = const.EXCLUDE_IGNORED + user_set_getter = User.objects.exclude + else: + raise ValueError('Uknown value of tag mark reason %s' % tag_mark_reason) + + #part 1 - find users who follow or not ignore the set of tags + tag_selections = MarkedTag.objects.filter( + tag__in = tags, + reason = tag_mark_reason + ) + subscribers = set( + user_set_getter( + tag_selections__in = tag_selections + ).filter( + notification_subscriptions__in = subscription_records + ).filter( + email_tag_filter_strategy = email_tag_filter_strategy + ) + ) + + #part 2 - find users who follow or not ignore tags via wildcard selections + #inside there is a potentially time consuming loop + if askbot_settings.USE_WILDCARD_TAGS: + #todo: fix this + #this branch will not scale well + #because we have to loop through the list of users + #in python + if tag_mark_reason == 'good': + empty_wildcard_filter = {'interesting_tags__exact': ''} + wildcard_tags_attribute = 'interesting_tags' + update_subscribers = lambda the_set, item: the_set.add(item) + elif tag_mark_reason == 'bad': + empty_wildcard_filter = {'ignored_tags__exact': ''} + wildcard_tags_attribute = 'ignored_tags' + update_subscribers = lambda the_set, item: the_set.remove(item) + + potential_wildcard_subscribers = User.objects.filter( + notification_subscriptions__in = subscription_records + ).filter( + email_tag_filter_strategy = email_tag_filter_strategy + ).exclude( + **empty_wildcard_filter #need this to limit size of the loop + ) + for potential_subscriber in potential_wildcard_subscribers: + wildcard_tags = getattr( + potential_subscriber, + wildcard_tags_attribute + ).split(' ') + + if tags.tags_match_some_wildcard(wildcard_tags): + update_subscribers(subscribers, potential_subscriber) + + return subscribers + def get_global_instant_notification_subscribers(self): """returns a set of subscribers to post according to tag filters both - subscribers who ignore tags or who follow only specific tags + + this method in turn calls several more specialized + subscriber retrieval functions + todo: retrieval of wildcard tag followers ignorers + won't scale at all """ tags = self.tags.all() @@ -114,33 +190,22 @@ class Content(models.Model): subscriber_set.update(global_subscribers) #segment of users who want emails on selected questions only - interesting_tag_selections = MarkedTag.objects.filter( - tag__in = tags, - reason = 'good' - ) - global_interested_subscribers = User.objects.filter( - tag_selections__in = interesting_tag_selections - ).filter( - notification_subscriptions__in = global_subscriptions - ).filter( - email_tag_filter_strategy = const.INCLUDE_INTERESTING + subscriber_set.update( + self.get_global_tag_based_subscribers( + tags = tags, + subscription_records = global_subscriptions, + tag_mark_reason = 'good' + ) ) - subscriber_set.update(global_interested_subscribers) #segment of users who want to exclude ignored tags - ignored_tag_selections = MarkedTag.objects.filter( - tag__in = tags, - reason = 'bad' - ) - - global_non_ignoring_subscribers = User.objects.exclude( - tag_selections__in = ignored_tag_selections - ).filter( - notification_subscriptions__in = global_subscriptions, - ).filter( - email_tag_filter_strategy = const.EXCLUDE_IGNORED + subscriber_set.update( + self.get_global_tag_based_subscribers( + tags = tags, + subscription_records = global_subscriptions, + tag_mark_reason = 'bad' + ) ) - subscriber_set.update(global_non_ignoring_subscribers) return subscriber_set diff --git a/askbot/models/question.py b/askbot/models/question.py index 0059f37d..bf4ab0d3 100644 --- a/askbot/models/question.py +++ b/askbot/models/question.py @@ -13,8 +13,12 @@ import askbot import askbot.conf from askbot import exceptions from askbot.models.tag import Tag -from askbot.models.base import AnonymousContent, DeletableContent, ContentRevision -from askbot.models.base import parse_post_text, parse_and_save_post +from askbot.models.base import AnonymousContent +from askbot.models.base import DeletableContent +from askbot.models.base import ContentRevision +from askbot.models.base import BaseQuerySetManager +from askbot.models.base import parse_post_text +from askbot.models.base import parse_and_save_post from askbot.models import content from askbot.models import signals from askbot import const @@ -331,19 +335,13 @@ class QuestionQuerySet(models.query.QuerySet): self.filter(id=question.id).update(view_count = question.view_count + 1) -class QuestionManager(models.Manager): - """pattern from http://djangosnippets.org/snippets/562/ - todo: turn this into a generic manager +class QuestionManager(BaseQuerySetManager): + """chainable custom query set manager for + questions """ def get_query_set(self): return QuestionQuerySet(self.model) - def __getattr__(self, attr, *args): - try: - return getattr(self.__class__, attr, *args) - except AttributeError: - return getattr(self.get_query_set(), attr, *args) - class Question(content.Content, DeletableContent): post_type = 'question' diff --git a/askbot/models/tag.py b/askbot/models/tag.py index dfd93879..acfc9413 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -3,9 +3,9 @@ from django.db import connection, transaction from django.contrib.auth.models import User from django.utils.translation import ugettext as _ from askbot.models.base import DeletableContent +from askbot.models.base import BaseQuerySetManager - -class TagManager(models.Manager): +class TagQuerySet(models.query.QuerySet): UPDATE_USED_COUNTS_QUERY = """ UPDATE tag SET used_count = ( @@ -30,6 +30,18 @@ class TagManager(models.Manager): transaction.commit_unless_managed() + def tags_match_some_wildcard(self, wildcard_tags = None): + """True if any one of the tags in the query set + matches a wildcard + + :arg:`wildcard_tags` is an iterable of wildcard tag strings + """ + for tag in self.all(): + for wildcard_tag in sorted(wildcard_tags): + if tag.name.startswith(wildcard_tag[:-1]): + return True + return False + def get_by_wildcards(self, wildcards = None): """returns query set of tags that match the wildcard tags wildcard tag is guaranteed to end with an asterisk and has @@ -86,6 +98,14 @@ class TagManager(models.Manager): return tags + +class TagManager(BaseQuerySetManager): + """chainable custom filter query set manager + for :class:``~askbot.models.Tag`` objects + """ + def get_query_set(self): + return TagQuerySet(self.model) + class Tag(DeletableContent): name = models.CharField(max_length=255, unique=True) created_by = models.ForeignKey(User, related_name='created_tags') diff --git a/askbot/tests/db_api_tests.py b/askbot/tests/db_api_tests.py index 818d1596..a6b2dfc4 100644 --- a/askbot/tests/db_api_tests.py +++ b/askbot/tests/db_api_tests.py @@ -5,6 +5,7 @@ e.g. ``some_user.do_something(...)`` """ from askbot.tests.utils import AskbotTestCase from askbot import models +from askbot import const from askbot.conf import settings as askbot_settings import datetime @@ -226,3 +227,98 @@ class UserLikeTests(AskbotTestCase): self.assert_affinity_is('like', False) self.assert_affinity_is('dislike', False) +class GlobalTagSubscriberGetterTests(AskbotTestCase): + """tests for the :meth:`~askbot.models.Question.get_global_tag_based_subscribers` + """ + def setUp(self): + """create two users""" + schedule = {'q_all': 'i'} + self.u1 = self.create_user( + username = 'user1', + notification_schedule = schedule + ) + self.u2 = self.create_user( + username = 'user2', + notification_schedule = schedule + ) + self.question = self.post_question( + user = self.u1, + tags = "good day" + ) + + def set_email_tag_filter_strategy(self, strategy): + self.u1.email_tag_filter_strategy = strategy + self.u1.save() + self.u2.email_tag_filter_strategy = strategy + self.u2.save() + + def assert_subscribers_are(self, expected_subscribers = None, reason = None): + """a special assertion that compares the subscribers + on the question with the given set""" + subscriptions = models.EmailFeedSetting.objects.filter( + feed_type = 'q_all', + frequency = 'i' + ) + actual_subscribers = self.question.get_global_tag_based_subscribers( + tags = self.question.tags.all(), + tag_mark_reason = reason, + subscription_records = subscriptions + ) + self.assertEquals(actual_subscribers, expected_subscribers) + + def test_nobody_likes_any_tags(self): + """no-one had marked tags, so the set + of subscribers must be empty + """ + self.assert_subscribers_are( + expected_subscribers = set(), + reason = 'good' + ) + + def test_nobody_dislikes_any_tags(self): + """since nobody dislikes tags - therefore + the set must contain two users""" + self.assert_subscribers_are( + expected_subscribers = set([self.u1, self.u2]), + reason = 'bad' + ) + + def test_user_likes_tag(self): + """user set must contain one person who likes the tag""" + self.set_email_tag_filter_strategy(const.INCLUDE_INTERESTING) + self.u1.mark_tags(tagnames = ('day',), reason = 'good', action = 'add') + self.assert_subscribers_are( + expected_subscribers = set([self.u1,]), + reason = 'good' + ) + + def test_user_dislikes_tag(self): + """user set must have one user who does not dislike a tag""" + self.set_email_tag_filter_strategy(const.EXCLUDE_IGNORED) + self.u1.mark_tags(tagnames = ('day',), reason = 'bad', action = 'add') + self.assert_subscribers_are( + expected_subscribers = set([self.u2,]), + reason = 'bad' + ) + + def test_user_likes_wildcard(self): + """user set must contain one person who likes the tag via wildcard""" + self.set_email_tag_filter_strategy(const.INCLUDE_INTERESTING) + askbot_settings.update('USE_WILDCARD_TAGS', True) + self.u1.mark_tags(wildcards = ('da*',), reason = 'good', action = 'add') + self.u1.save() + self.assert_subscribers_are( + expected_subscribers = set([self.u1,]), + reason = 'good' + ) + + def test_user_dislikes_wildcard(self): + """user set must have one user who does not dislike the tag via wildcard""" + self.set_email_tag_filter_strategy(const.EXCLUDE_IGNORED) + askbot_settings.update('USE_WILDCARD_TAGS', True) + self.u1.mark_tags(wildcards = ('da*',), reason = 'bad', action = 'add') + self.u1.save() + self.assert_subscribers_are( + expected_subscribers = set([self.u2,]), + reason = 'bad' + ) diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py index e8ef3ba5..5ec598d7 100644 --- a/askbot/tests/email_alert_tests.py +++ b/askbot/tests/email_alert_tests.py @@ -612,6 +612,7 @@ class LiveInstantSelectedQuestionsEmailAlertTests(EmailAlertTests): @setup_email_alert_tests def setUp(self): self.notification_schedule['q_sel'] = 'i' + #first posts yesterday self.setup_timestamp = datetime.datetime.now() - datetime.timedelta(1) self.follow_question = True diff --git a/askbot/tests/utils.py b/askbot/tests/utils.py index 76eda149..85599682 100644 --- a/askbot/tests/utils.py +++ b/askbot/tests/utils.py @@ -84,6 +84,8 @@ class AskbotTestCase(TestCase): """posts and returns question on behalf of user. If user is not given, it will be self.user + ``tags`` is a string with tagnames + if follow is True, question is followed by the poster """ diff --git a/askbot/views/commands.py b/askbot/views/commands.py index 2801384f..8d16c35f 100644 --- a/askbot/views/commands.py +++ b/askbot/views/commands.py @@ -393,6 +393,7 @@ def get_tag_list(request): def subscribe_for_tags(request): """process subscription of users by tags""" + #todo - use special separator to split tags tag_names = request.REQUEST.get('tags','').strip().split() pure_tag_names, wildcards = forms.clean_marked_tagnames(tag_names) if request.user.is_authenticated(): |