summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2011-04-03 19:11:01 -0400
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2011-04-03 19:11:01 -0400
commit8e57f310d1da605c31d30d7392c26deab73f2619 (patch)
tree08d9fa0aae37360fc0290b42d89333b79ecbec7f
parent7da87a124353c9fea3bc26d50f54bcd677e5c112 (diff)
downloadaskbot-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__.py20
-rw-r--r--askbot/models/base.py32
-rw-r--r--askbot/models/content.py111
-rw-r--r--askbot/models/question.py20
-rw-r--r--askbot/models/tag.py24
-rw-r--r--askbot/tests/db_api_tests.py96
-rw-r--r--askbot/tests/email_alert_tests.py1
-rw-r--r--askbot/tests/utils.py2
-rw-r--r--askbot/views/commands.py1
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():