summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEvgeny Fadeev <evgeny.fadeev@gmail.com>2010-06-05 17:35:56 -0400
committerEvgeny Fadeev <evgeny.fadeev@gmail.com>2010-06-05 17:35:56 -0400
commit056b2a1ffbdbe04dc42cd2719923398e0bf222ce (patch)
treec141b896ba5b2d79aab6658cde3608c1b2f69bbc
parente5ea67fd5ec4d15a217689faa216a01814f55748 (diff)
downloadaskbot-056b2a1ffbdbe04dc42cd2719923398e0bf222ce.tar.gz
askbot-056b2a1ffbdbe04dc42cd2719923398e0bf222ce.tar.bz2
askbot-056b2a1ffbdbe04dc42cd2719923398e0bf222ce.zip
mentions work in all kinds of posts, unified activity logging for post updates
-rw-r--r--forum/const/__init__.py7
-rw-r--r--forum/management/commands/send_email_alerts.py12
-rw-r--r--forum/middleware/pagesize.py2
-rw-r--r--forum/models/__init__.py196
-rw-r--r--forum/models/answer.py34
-rw-r--r--forum/models/base.py12
-rw-r--r--forum/models/content.py124
-rw-r--r--forum/models/meta.py80
-rw-r--r--forum/models/question.py56
-rw-r--r--forum/models/signals.py1
-rw-r--r--forum/models/user.py53
-rw-r--r--forum/skins/default/templates/instant_notification.html2
-rw-r--r--forum/skins/default/templates/user.html9
-rw-r--r--forum/skins/default/templates/user_info.html3
-rw-r--r--forum/skins/default/templates/user_stats.html2
-rw-r--r--forum/utils/markup.py12
-rw-r--r--forum/views/users.py203
17 files changed, 505 insertions, 303 deletions
diff --git a/forum/const/__init__.py b/forum/const/__init__.py
index 11a27ed9..f27229c8 100644
--- a/forum/const/__init__.py
+++ b/forum/const/__init__.py
@@ -97,7 +97,7 @@ TYPE_ACTIVITY_MARK_OFFENSIVE=14
TYPE_ACTIVITY_UPDATE_TAGS=15
TYPE_ACTIVITY_FAVORITE=16
TYPE_ACTIVITY_USER_FULL_UPDATED = 17
-TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT = 18
+TYPE_ACTIVITY_EMAIL_UPDATE_SENT = 18
TYPE_ACTIVITY_MENTION = 19
#TYPE_ACTIVITY_EDIT_QUESTION=17
#TYPE_ACTIVITY_EDIT_ANSWER=18
@@ -120,7 +120,7 @@ TYPE_ACTIVITY = (
(TYPE_ACTIVITY_UPDATE_TAGS, _('updated tags')),
(TYPE_ACTIVITY_FAVORITE, _('selected favorite')),
(TYPE_ACTIVITY_USER_FULL_UPDATED, _('completed user profile')),
- (TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT, _('email update sent to user')),
+ (TYPE_ACTIVITY_EMAIL_UPDATE_SENT, _('email update sent to user')),
(TYPE_ACTIVITY_MENTION, _('mentioned in the post')),
)
@@ -197,7 +197,8 @@ NOTIFICATION_DELIVERY_SCHEDULE_CHOICES= (
USERS_PAGE_SIZE = 28#todo: move it to settings?
USERNAME_REGEX_STRING = r'^[\w ]+$'
-TWITTER_STYLE_MENTION_TERMINATION_CHARS = ' ;,.!?'#chars that can go after @mention
+#chars that can go before or after @mention
+TWITTER_STYLE_MENTION_TERMINATION_CHARS = '\n ;,.!?<>'
COMMENT_HARD_MAX_LENGTH = 2048
diff --git a/forum/management/commands/send_email_alerts.py b/forum/management/commands/send_email_alerts.py
index cd434a34..2c39311f 100644
--- a/forum/management/commands/send_email_alerts.py
+++ b/forum/management/commands/send_email_alerts.py
@@ -16,6 +16,13 @@ from forum import const
DEBUG_THIS_COMMAND = False
+def get_all_origin_posts(mentions):
+ origin_posts = set()
+ for mention in mentions:
+ post = mention.content_object
+ origin_posts.add(post.get_origin_post())
+ return list(origin_posts)
+
#todo: refactor this as class
def extend_question_list(src, dst, limit=False, add_mention=False):
"""src is a query set with questions
@@ -244,7 +251,8 @@ class Command(NoArgsCommand):
mentioned_whom = user
)
- q_mentions_id = [q.id for q in mentions.get_all_origin_posts()]
+ mention_posts = get_all_origin_posts(mentions)
+ q_mentions_id = [q.id for q in mention_posts]
q_mentions_A = Q_set_A.filter(id__in = q_mentions_id)
q_mentions_A.cutoff_time = cutoff_time
@@ -269,7 +277,7 @@ class Command(NoArgsCommand):
extend_question_list(q_all_B, q_list, limit=True)
ctype = ContentType.objects.get_for_model(Question)
- EMAIL_UPDATE_ACTIVITY = const.TYPE_ACTIVITY_QUESTION_EMAIL_UPDATE_SENT
+ EMAIL_UPDATE_ACTIVITY = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT
#up to this point we still don't know if emails about
#collected questions were sent recently
diff --git a/forum/middleware/pagesize.py b/forum/middleware/pagesize.py
index 154c112f..20af6aa6 100644
--- a/forum/middleware/pagesize.py
+++ b/forum/middleware/pagesize.py
@@ -35,3 +35,5 @@ class QuestionsPageSizeMiddleware(object):
def process_exception(self, request, exception):
exc_type, exc_value, exc_traceback = sys.exc_info()
logging.debug(''.join(traceback.format_tb(exc_traceback)))
+ logging.debug(exc_type)
+ logging.debug(exc_value)
diff --git a/forum/models/__init__.py b/forum/models/__init__.py
index 168399f9..946e2a90 100644
--- a/forum/models/__init__.py
+++ b/forum/models/__init__.py
@@ -223,18 +223,6 @@ def flag_post(user, post, timestamp=None, cancel=False):
)
auth.onFlaggedItem(flag, post, user, timestamp=timestamp)
-def user_should_receive_instant_notification_about_post(
- user,
- post = None,
- newly_mentioned_users = []
- ):
- return EmailFeedSetting.objects.exists_match_to_post_and_subscriber(
- subscriber = user,
- post = post,
- frequency = 'i',
- newly_mentioned_users = newly_mentioned_users
- )
-
User.add_to_class('is_username_taken',classmethod(user_is_username_taken))
User.add_to_class('get_q_sel_email_feed_frequency',user_get_q_sel_email_feed_frequency)
User.add_to_class('get_absolute_url', user_get_absolute_url)
@@ -247,11 +235,8 @@ User.add_to_class('get_profile_link', get_profile_link)
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)
-User.add_to_class(
- 'should_receive_instant_notification_about_post',
- user_should_receive_instant_notification_about_post
- )
+#todo: move this to forum/utils ??
def format_instant_notification_body(
to_user = None,
from_user = None,
@@ -268,6 +253,7 @@ def format_instant_notification_body(
site_url = forum_settings.APP_URL
origin_post = post.get_origin_post()
+ #todo: create a better method to access "sub-urls" in user views
user_subscriptions_url = site_url + to_user.get_absolute_url() + \
'?sort=email_subscriptions'
@@ -285,16 +271,16 @@ def format_instant_notification_body(
update_data = {
'update_author_name': from_user.username,
'post_url': site_url + post.get_absolute_url(),
- 'origin_post_title': origin_post.title
+ 'origin_post_title': origin_post.title,
'user_subscriptions_url': user_subscriptions_url
}
return template.render(Context(update_data))
+#todo: action
def send_instant_notifications_about_activity_in_post(
- activity = None,
+ update_activity = None,
post = None,
receiving_users = [],
- newly_mentioned_users = []
):
"""
function called when posts are updated
@@ -302,75 +288,57 @@ def send_instant_notifications_about_activity_in_post(
database hits
"""
- update_type_map = const.RESPONSE_ACTIVITY_TYPE_MAP_FOR_TEMPLATES
+ acceptable_types = const.RESPONSE_ACTIVITY_TYPES_FOR_INSTANT_NOTIFICATIONS
- if activity.activity_type in update_type_map:
- update_type = update_type_map[activity.activity_type]
- else:
+ if update_activity.activity_type not in acceptable_types:
return
template = loader.get_template('instant_notification.html')
- for user in set(receiving_users) | set(newly_mentioned_users):
- if user.should_receive_instant_notification_about_post(
- post = post,
- newly_mentioned_users = newly_mentioned_users
- ):
- #send update
+ update_type_map = const.RESPONSE_ACTIVITY_TYPE_MAP_FOR_TEMPLATES
+ update_type = update_type_map[update_activity.activity_type]
+
+ for user in receiving_users:
+
subject = _('email update message subject')
text = format_instant_notification_body(
to_user = user,
- from_user = activity.user,
+ from_user = update_activity.user,
post = post,
update_type = update_type,
template = template,
)
+ #todo: this could be packaged as an "action" - a bundle
+ #of executive function with the corresponding activity log recording
msg = EmailMessage(
subject,
text,
django_settings.DEFAULT_FROM_EMAIL,
[user.email]
)
- msg.send()
-
-
+ #msg.send()
+ print text
+ EMAIL_UPDATE_ACTIVITY = const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT
+ email_activity = Activity(
+ user = user,
+ content_object = post.get_origin_post(),
+ activity_type = EMAIL_UPDATE_ACTIVITY
+ )
+ email_activity.save()
+
+
+#todo: move to utils
def calculate_gravatar_hash(instance, **kwargs):
"""Calculates a User's gravatar hash from their email address."""
if kwargs.get('raw', False):
return
instance.gravatar = hashlib.md5(instance.email).hexdigest()
-def record_ask_event(instance, created, **kwargs):
- if created:
- activity = Activity(
- user=instance.author,
- active_at=instance.added_at,
- content_object=instance,
- activity_type=const.TYPE_ACTIVITY_ASK_QUESTION
- )
- activity.save()
-
-def record_answer_event(instance, created, **kwargs):
- if created:
- activity = Activity(
- user = instance.author,
- active_at = instance.added_at,
- content_object = instance,
- activity_type = const.TYPE_ACTIVITY_ANSWER
- )
- activity.save()
- receiving_users = instance.question.get_author_list(
- include_comments = True,
- exclude_list = [instance.author],
- )
-
- activity.receiving_users.add(*receiving_users)
-
-#todo: change to more general post_update_activity
def record_post_update_activity(
- post,
+ post,
newly_mentioned_users = list(),
+ updated_by = None,
timestamp = None,
created = False,
**kwargs
@@ -378,93 +346,42 @@ def record_post_update_activity(
"""called upon signal forum.models.signals.post_updated
which is sent at the end of save() method in posts
"""
- #todo: take into account created == True case
- activity_type = post.get_updated_activity_type(created)
-
assert(timestamp != None)
+ assert(updated_by != None)
- #fields will depend on post type and maybe activity type
- #post has to be saved already, b/c Activity is in generic relation to post
- activity = Activity(
- user = post.get_last_author(),
+ #todo: take into account created == True case
+ (activity_type, update_object) = post.get_updated_activity_data(created)
+
+ update_activity = Activity(
+ user = updated_by,
active_at = timestamp,
content_object = post,
activity_type = activity_type
)
- activity.save()
+ update_activity.save()
#what users are included depends on the post type
#for example for question - all Q&A contributors
#are included, for comments only authors of comments and parent
#post are included
- receiving_users = post.get_potentially_interested_users()
-
- activity.receiving_users.add(*receiving_users)
-
- send_instant_notifications_about_activity_in_post(
- activity = activity,
- post = post,
- receiving_users = receiving_users,
- newly_mentioned_users = newly_mentioned_users
- )
-
-
-def record_revision_question_event(instance, created, **kwargs):
- if created and instance.revision != 1:
- activity = Activity(
- user=instance.author,
- active_at=instance.revised_at,
- content_object=instance,
- activity_type=const.TYPE_ACTIVITY_UPDATE_QUESTION
- )
- activity.save()
- receiving_users = set()
- receiving_users.update(
- instance.question.get_author_list(include_comments = True)
- )
-
- for a in instance.question.answers.all():
- receiving_users.update(a.get_author_list())
-
- receiving_users -= set([instance.author])#remove activity user
+ receiving_users = post.get_response_receivers(
+ exclude_list = [updated_by, ]
+ )
- receiving_users = list(receiving_users)
- activity.receiving_users.add(*receiving_users)
+ update_activity.receiving_users.add(*receiving_users)
- send_instant_notifications_about_activity_in_post(
- activity,
- instance.question,
- receiving_users
+ notification_subscribers = post.get_instant_notification_subscribers(
+ potential_subscribers = receiving_users,
+ mentioned_users = newly_mentioned_users,
+ exclude_list = [updated_by, ]
)
-
-def record_revision_answer_event(instance, created, **kwargs):
- if created and instance.revision != 1:
- activity = Activity(
- user=instance.author,
- active_at=instance.revised_at,
- content_object=instance,
- activity_type=const.TYPE_ACTIVITY_UPDATE_ANSWER
- )
- activity.save()
- receiving_users = set()
- receiving_users.update(
- instance.answer.get_author_list(
- include_comments = True
- )
+ send_instant_notifications_about_activity_in_post(
+ update_activity = update_activity,
+ post = post,
+ receiving_users = notification_subscribers,
)
- receiving_users.update(instance.answer.question.get_author_list())
- receiving_users -= set([instance.author])
- receiving_users = list(receiving_users)
-
- activity.receiving_users.add(*receiving_users)
-
- send_instant_notifications_about_activity_in_post(
- activity,
- instance.answer,
- receiving_users
- )
def record_award_event(instance, created, **kwargs):
"""
@@ -522,15 +439,21 @@ def record_answer_accepted(instance, created, **kwargs):
)
activity.receiving_users.add(*receiving_users)
+
def update_last_seen(instance, created, **kwargs):
"""
when user has activities, we update 'last_seen' time stamp for him
"""
- #todo: improve this
+ #todo: in reality author of this activity must not be the receiving user
+ #but for now just have this plug, so that last seen timestamp is not
+ #perturbed by the email update sender
+ if instance.activity_type == const.TYPE_ACTIVITY_EMAIL_UPDATE_SENT:
+ return
user = instance.user
user.last_seen = datetime.datetime.now()
user.save()
+
def record_vote(instance, created, **kwargs):
"""
when user have voted
@@ -550,6 +473,7 @@ def record_vote(instance, created, **kwargs):
#todo: problem cannot access receiving user here
activity.save()
+
def record_cancel_vote(instance, **kwargs):
"""
when user canceled vote, the vote will be deleted.
@@ -563,6 +487,7 @@ def record_cancel_vote(instance, **kwargs):
#todo: same problem - cannot access receiving user here
activity.save()
+
#todo: weird that there is no record delete answer or comment
#is this even necessary to keep track of?
def record_delete_question(instance, delete_by, **kwargs):
@@ -654,9 +579,6 @@ def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs)
#signal for User model save changes
django_signals.pre_save.connect(calculate_gravatar_hash, sender=User)
-django_signals.post_save.connect(record_ask_event, sender=Question)
-django_signals.post_save.connect(record_revision_question_event, sender=QuestionRevision)
-django_signals.post_save.connect(record_revision_answer_event, sender=AnswerRevision)
django_signals.post_save.connect(record_award_event, sender=Award)
django_signals.post_save.connect(notify_award_message, sender=Award)
django_signals.post_save.connect(record_answer_accepted, sender=Answer)
@@ -681,6 +603,10 @@ signals.post_updated.connect(
record_post_update_activity,
sender=Answer
)
+signals.post_updated.connect(
+ record_post_update_activity,
+ sender=Question
+ )
#post_syncdb.connect(create_fulltext_indexes)
#todo: wtf??? what is x=x about?
diff --git a/forum/models/answer.py b/forum/models/answer.py
index 4b35fd31..48b1c464 100644
--- a/forum/models/answer.py
+++ b/forum/models/answer.py
@@ -94,6 +94,15 @@ class Answer(content.Content, DeletableContent):
save = save_post
parse = parse_post_text
+ def get_updated_activity_data(self, created = False):
+ #todo: simplify this to always return latest revision for the second
+ #part
+ if created:
+ return const.TYPE_ACTIVITY_ANSWER, self
+ else:
+ latest_revision = self.get_latest_revision()
+ return const.TYPE_ACTIVITY_UPDATE_ANSWER, latest_revision
+
def apply_edit(self, edited_at=None, edited_by=None, text=None, comment=None, wiki=False):
if text is None:
@@ -142,6 +151,31 @@ class Answer(content.Content, DeletableContent):
def get_origin_post(self):
return self.question
+ def get_response_receivers(self, exclude_list = None):
+ """get list of users interested in this response
+ update based on their participation in the question
+ activity
+
+ exclude_list is required and normally should contain
+ author of the updated so that he/she is not notified of
+ the response
+ """
+ assert(exclude_list is not None)
+ receiving_users = set()
+ receiving_users.update(
+ self.get_author_list(
+ include_comments = True
+ )
+ )
+ receiving_users.update(
+ self.question.get_author_list(
+ include_comments = True
+ )
+ )
+ receiving_users -= set(exclude_list)
+
+ return list(receiving_users)
+
def get_user_vote(self, user):
if user.__class__.__name__ == "AnonymousUser":
return None
diff --git a/forum/models/base.py b/forum/models/base.py
index 1b3a4c2a..964c6142 100644
--- a/forum/models/base.py
+++ b/forum/models/base.py
@@ -25,7 +25,10 @@ def parse_post_text(post):
this metadata is limited by twitter style @mentions
but there may be more in the future
- so really it should be renamed into ..._and_get_meta_data
+ function returns a dictionary with the following keys
+ html
+ newly_mentioned_users - list of <User> objects
+ removed_mentions - list of mention <Activity> objects - for removed ones
"""
text = post.get_text()
@@ -134,6 +137,7 @@ def save_post(post, **kwargs):
from forum.models import signals
signals.post_updated.send(
post = post,
+ updated_by = last_author,
newly_mentioned_users = newly_mentioned_users,
timestamp = post.get_time_of_last_edit(),
created = created,
@@ -143,7 +147,7 @@ def save_post(post, **kwargs):
try:
ping_google()
except Exception:
- logging.debug('problem pinging google did you register you sitemap with google?')
+ logging.debug('problem pinging google did you register the sitemap with google?')
class UserContent(models.Model):
user = models.ForeignKey(User, related_name='%(class)ss')
@@ -153,6 +157,10 @@ class UserContent(models.Model):
app_label = 'forum'
def get_last_author(self):
+ """
+ get author who last edited the content
+ since here we don't have revisions, it will be the creator
+ """
return self.user
class MetaContent(models.Model):
diff --git a/forum/models/content.py b/forum/models/content.py
index 5f144f1e..441f7133 100644
--- a/forum/models/content.py
+++ b/forum/models/content.py
@@ -5,6 +5,7 @@ from django.contrib.contenttypes import generic
from django.contrib.sitemaps import ping_google
from django.db import models
from forum.models.meta import Comment, Vote, FlaggedItem
+from forum.models.user import EmailFeedSetting
class Content(models.Model):
"""
@@ -75,6 +76,115 @@ class Content(models.Model):
self.comment_count = self.comment_count + 1
self.save()
+ def get_instant_notification_subscribers(
+ self,
+ potential_subscribers = None,
+ mentioned_users = None,
+ exclude_list = None,
+ ):
+ """get list of users who have subscribed to
+ receive instant notifications for a given post
+ this method works for questions and answers
+
+ parameter "potential_subscribers" is not used here,
+ but left for the uniformity of the interface (Comment method does use it)
+
+ comment class has it's own variant which does have quite a bit
+ of duplicated code at the moment
+ """
+ subscriber_set = set()
+
+ #1) mention subscribers - common to questions and answers
+ if mentioned_users:
+ mention_subscribers = EmailFeedSetting.objects.filter(
+ subscriber__in = mentioned_users,
+ feed_type = 'm_and_c',
+ frequency = 'i'
+ ).values_list(
+ 'subscriber',
+ flat=True
+ )
+ subscriber_set.update(mention_subscribers)
+
+ origin_post = self.get_origin_post()#handy to make generic method
+
+ #2) individually selected - make sure that users
+ #are individual subscribers to this question
+ selective_subscribers = origin_post.followed_by.all()
+ if selective_subscribers:
+ selective_subscribers = EmailFeedSetting.objects.filter(
+ subscriber__in = selective_subscribers,
+ feed_type = 'q_sel',
+ frequency = 'i'
+ ).values_list(
+ 'subscriber',
+ flat=True
+ )
+ for subscriber in selective_subscribers:
+ if origin_post.passes_tag_filter_for_user(subscriber):
+ subscriber_set.add(subscriber)
+
+ subscriber_set.update(selective_subscribers)
+
+ #3) whole forum subscibers
+ global_subscribers = EmailFeedSetting.objects.filter(
+ feed_type = 'q_all',
+ frequency = 'i'
+ ).values_list(
+ 'subscriber',
+ flat=True
+ )
+ #todo: apply tag filters here
+ subscriber_set.update(global_subscribers)
+
+ #4) question asked by me
+ question_author = origin_post.author
+ if EmailFeedSetting.objects.filter(
+ subscriber = question_author,
+ frequency = 'i',
+ feed_type = 'q_ask'
+ ):
+ subscriber_set.add(question_author)
+
+ #4) questions answered by me -make sure is that people
+ #are authors of the answers to this question
+ #todo: replace this with a query set method
+ answer_authors = set()
+ for answer in origin_post.answers.all():
+ authors = answer.get_author_list()
+ answer_authors.update(authors)
+
+ if answer_authors:
+ answer_authors = EmailFeedSetting.objects.filter(
+ subscriber__in = answer_authors,
+ frequency = 'i',
+ feed_type = 'q_ans',
+ ).values_list(
+ 'subscriber',
+ flat=True
+ )
+ subscriber_set.update(answer_authors)
+ subscriber_set -= set(exclude_list)
+
+ return list(subscriber_set)
+
+ def passes_tag_filter_for_user(user):
+
+ post_tags = self.get_origin_post().tags.all()
+
+ if user.tag_filter_setting == 'ignored':
+ ignored_tags = user.tag_selections.filter(reason = 'bad')
+ if set(post_tags) & set(ignored_tags):
+ return False
+ else:
+ return True
+ else:
+ interesting_tags = user.tag_selections.filter(reason = 'good')
+ if set(post_tags) & set(interesting_tags):
+ return True
+ else:
+ return False
+
def get_latest_revision(self):
return self.revisions.all().order_by('-revised_at')[0]
@@ -82,7 +192,11 @@ class Content(models.Model):
return self.get_latest_revision().revision
def get_last_author(self):
- return self.last_edited_by
+ #todo: fix this issue
+ if self.last_edited_by:
+ return self.last_edited_by
+ else:
+ return self.author
def get_time_of_last_edit(self):
if self.last_edited_at:
@@ -90,7 +204,13 @@ class Content(models.Model):
else:
return self.added_at
- def get_author_list(self, include_comments = False, recursive = False, exclude_list = None):
+ def get_author_list(
+ self,
+ include_comments = False,
+ recursive = False,
+ exclude_list = None):
+
+ #todo: there may be a better way to do these queries
authors = set()
authors.update([r.author for r in self.revisions.all()])
if include_comments:
diff --git a/forum/models/meta.py b/forum/models/meta.py
index 5f36d76e..ee0d92ae 100644
--- a/forum/models/meta.py
+++ b/forum/models/meta.py
@@ -2,6 +2,7 @@ import datetime
from django.db import models
from forum import const
from forum.models import base
+from forum.models.user import EmailFeedSetting
class VoteManager(models.Manager):
def get_up_vote_count_from_user(self, user):
@@ -104,13 +105,17 @@ class Comment(base.MetaContent, base.UserContent):
def save(self,**kwargs):
base.save_post(self)
- def get_updated_activity_type(self, created = False):
+ def get_updated_activity_data(self, created = False):
if self.content_object.__class__.__name__ == 'Question':
- return const.TYPE_ACTIVITY_COMMENT_QUESTION
+ return const.TYPE_ACTIVITY_COMMENT_QUESTION, self
elif self.content_object.__class__.__name__ == 'Answer':
- return const.TYPE_ACTIVITY_COMMENT_ANSWER
+ return const.TYPE_ACTIVITY_COMMENT_ANSWER, self
- def get_potentially_interested_users(self):
+ def get_response_receivers(self, exclude_list = None):
+ """get list of users who authored comments on a post
+ and the post itself
+ """
+ assert(exclude_list is not None)
users = set()
users.update(
#get authors of parent object and all associated comments
@@ -118,10 +123,73 @@ class Comment(base.MetaContent, base.UserContent):
include_comments = True,
)
)
-
- users -= set([self.user])#remove activity user
+ users -= set(exclude_list)
return list(users)
+ def get_instant_notification_subscribers(
+ self,
+ potential_subscribers = None,
+ mentioned_users = None,
+ exclude_list = None
+ ):
+ """get list of users who want instant notifications
+ about this post
+
+ argument potential_subscribers is required as it saves on db hits
+ """
+
+ subscriber_set = set()
+
+ if potential_subscribers:
+ potential_subscribers = set(potential_subscribers)
+ else:
+ potential_subscribers = set()
+
+ if mentioned_users:
+ potential_subscribers.update(mentioned_users)
+
+ if potential_subscribers:
+ comment_subscribers = EmailFeedSetting.objects.filter(
+ subscriber__in = potential_subscribers,
+ feed_type = 'm_and_c',
+ frequency = 'i'
+ ).values_list(
+ 'subscriber',
+ flat=True
+ )
+ subscriber_set.update(comment_subscribers)
+
+ origin_post = self.get_origin_post()
+ selective_subscribers = origin_post.followed_by.all()
+ if selective_subscribers:
+ selective_subscribers = EmailFeedSetting.objects.filter(
+ subscriber__in = selective_subscribers,
+ feed_type = 'q_sel',
+ frequency = 'i'
+ ).values_list(
+ 'subscriber',
+ flat=True
+ )
+ for subscriber in selective_subscribers:
+ if origin_post.passes_tag_filter_for_user(subscriber):
+ subscriber_set.add(subscriber)
+
+ subscriber_set.update(selective_subscribers)
+
+ global_subscribers = EmailFeedSetting.objects.filter(
+ feed_type = 'q_all',
+ frequency = 'i'
+ ).values_list(
+ 'subscriber',
+ flat=True
+ )
+
+ subscriber_set.update(global_subscribers)
+ if exclude_list:
+ subscriber_set -= set(exclude_list)
+
+ return list(subscriber_set)
+
def get_time_of_last_edit(self):
return self.added_at
diff --git a/forum/models/question.py b/forum/models/question.py
index 0d63426d..88f01e4f 100644
--- a/forum/models/question.py
+++ b/forum/models/question.py
@@ -134,18 +134,18 @@ class QuestionManager(models.Manager):
uid_str = str(request_user.id)
#mark questions tagged with interesting tags
qs = qs.extra(
- select = SortedDict([
- (
- 'interesting_score',
- 'SELECT COUNT(1) FROM forum_markedtag, question_tags '
- + 'WHERE forum_markedtag.user_id = %s '
- + 'AND forum_markedtag.tag_id = question_tags.tag_id '
- + 'AND forum_markedtag.reason = \'good\' '
- + 'AND question_tags.question_id = question.id'
- ),
- ]),
- select_params = (uid_str,),
- )
+ select = SortedDict([
+ (
+ 'interesting_score',
+ 'SELECT COUNT(1) FROM forum_markedtag, question_tags '
+ + 'WHERE forum_markedtag.user_id = %s '
+ + 'AND forum_markedtag.tag_id = question_tags.tag_id '
+ + 'AND forum_markedtag.reason = \'good\' '
+ + 'AND question_tags.question_id = question.id'
+ ),
+ ]),
+ select_params = (uid_str,),
+ )
if request_user.hide_ignored_questions:
#exclude ignored tags if the user wants to
ignored_tags = Tag.objects.filter(user_selections__reason='bad',
@@ -180,6 +180,9 @@ class QuestionManager(models.Manager):
qs = qs.distinct()
return qs, meta_data
+ #todo: this function is similar to get_response_receivers
+ #profile this function against the other one
+ #todo: maybe this must be a query set method, not manager method
def get_question_and_answer_contributors(self, question_list):
answer_list = []
question_list = list(question_list)#important for MySQL, b/c it does not support
@@ -324,6 +327,35 @@ class Question(content.Content, DeletableContent):
except Exception:
logging.debug('problem pinging google did you register you sitemap with google?')
+ def get_updated_activity_data(self, created = False):
+ if created:
+ return const.TYPE_ACTIVITY_ASK_QUESTION, self
+ else:
+ latest_revision = self.get_latest_revision()
+ return const.TYPE_ACTIVITY_UPDATE_QUESTION, latest_revision
+
+ def get_response_receivers(self, exclude_list = None):
+ """returns list of users who might be interested
+ in the question update based on their participation
+ in the question activity
+
+ exclude_list is mandatory - it normally should have the
+ author of the update so the he/she is not notified about the update
+ """
+ assert(exclude_list != None)
+ receiving_users = set()
+ receiving_users.update(
+ self.get_author_list(
+ include_comments = True
+ )
+ )
+ #do not include answer commenters here
+ for a in self.answers.all():
+ receiving_users.update(a.get_author_list())
+
+ receiving_users -= set(exclude_list)
+ return receiving_users
+
def retag(self, retagged_by=None, retagged_at=None, tagnames=None):
if None in (retagged_by, retagged_at, tagnames):
raise Exception('arguments retagged_at, retagged_by and tagnames are required')
diff --git a/forum/models/signals.py b/forum/models/signals.py
index f711260b..b4ed0d1b 100644
--- a/forum/models/signals.py
+++ b/forum/models/signals.py
@@ -17,6 +17,7 @@ user_logged_in = django.dispatch.Signal(providing_args=['session'])
post_updated = django.dispatch.Signal(
providing_args=[
'post',
+ 'updated_by',
'newly_mentioned_users'
]
)
diff --git a/forum/models/user.py b/forum/models/user.py
index 083aca79..a00271a7 100644
--- a/forum/models/user.py
+++ b/forum/models/user.py
@@ -150,57 +150,6 @@ class Activity(models.Model):
assert(len(user_qs) == 1)
return user_qs[0]
-
-class EmailFeedSettingManager(models.Manager):
- def exists_match_to_post_and_subscriber(
- self,
- post = None,
- subscriber = None,
- newly_mentioned_users = [],
- **kwargs
- ):
- """returns list of feeds matching the post
- and subscriber
- newly_mentioned_user parameter is there to save
- on a database hit looking for mentions of subscriber
- in the current post
- """
- feeds = self.filter(subscriber = subscriber, **kwargs)
-
- for feed in feeds:
-
- if feed.feed_type == 'm_and_c':
- if post.__class__.__name__ == 'Comment':#isinstance(post, Comment):
- return True
- else:
- if subscriber in newly_mentioned_users:
- return True
- else:
- if feed.feed_type == 'q_all':
- #'everything' category is tag filtered
- if post.passes_tag_filter_for_user(subscriber):
- return True
- else:
-
- origin_post = post.get_origin_post()
-
- if feed.feed_type == 'q_ask':
- if origin_post.author == subscriber:
- return True
-
- elif feed.feed_type == 'q_ans':
- #make sure that subscriber answered origin post
- answers = origin_post.answers.exclude(deleted=True)
- if subscriber in answers.get_author_list():
- return True
-
- elif feed.feed_type == 'q_sel':
- #make sure that subscriber has selected this post
- #individually
- if subscriber in origin_post.followed_by.all():
- return True
- return False
-
class EmailFeedSetting(models.Model):
DELTA_TABLE = {
'i':datetime.timedelta(-1),#instant emails are processed separately
@@ -233,8 +182,6 @@ class EmailFeedSetting(models.Model):
added_at = models.DateTimeField(auto_now_add=True)
reported_at = models.DateTimeField(null=True)
- objects = EmailFeedSettingManager()
-
#functions for rich comparison
#PRECEDENCE = ('i','d','w','n')#the greater ones are first
#def __eq__(self, other):
diff --git a/forum/skins/default/templates/instant_notification.html b/forum/skins/default/templates/instant_notification.html
index b860ab02..db2f5a05 100644
--- a/forum/skins/default/templates/instant_notification.html
+++ b/forum/skins/default/templates/instant_notification.html
@@ -25,11 +25,13 @@ for question "{{origin_post_title}}"</p>
<p>{{update_author_name}} asked a question
<a href="{{post_url}}">{{origin_post_title}}</a></p>
{% endblocktrans %}
+ {% endif %}
{%if update_type == 'answer_update' %}
{% blocktrans %}
<p>{{update_author_name}} updated an answer to the question
<a href="{{post_url}}">{{origin_post_title}}</a></p>
{% endblocktrans %}
+ {% endif %}
{% if update_type == 'question_update' %}
{% blocktrans %}
<p>{{update_author_name}} updated a question
diff --git a/forum/skins/default/templates/user.html b/forum/skins/default/templates/user.html
index 833c2058..dee52cd7 100644
--- a/forum/skins/default/templates/user.html
+++ b/forum/skins/default/templates/user.html
@@ -3,6 +3,7 @@
{% load extra_tags %}
{% load extra_filters %}
{% load humanize %}
+{% load i18n %}
{% block title %}{% spaceless %}{{ page_title }}{% endspaceless %}{% endblock %}
{% block forestyle%}
<style type="text/css">
@@ -26,7 +27,13 @@
{% endblock %}
{% block content %}
<div id="mainbar-full">
- {% include "user_info.html" %}
+ <div id="subheader" class="headUser">
+ {% spaceless %}
+ <a href="{% url user_profile view_user.id view_user.username|slugify %}">
+ {% blocktrans with view_user.username as username %}{{username}}'s profile{% endblocktrans %}
+ </a>
+ {% endspaceless %}
+ </div>
{% include "user_tabs.html" %}
{% block usercontent %}
{% endblock %}
diff --git a/forum/skins/default/templates/user_info.html b/forum/skins/default/templates/user_info.html
index fda7133c..c99dd2c1 100644
--- a/forum/skins/default/templates/user_info.html
+++ b/forum/skins/default/templates/user_info.html
@@ -4,9 +4,6 @@
{% load humanize %}
{% load smart_if %}
{% load i18n %}
-<div id="subheader" class="headUser">
- <a href="{% url user_profile view_user.id view_user.username|slugify %}">{{view_user.username}}</a>
-</div>
<table class="user-info-table">
<tr>
<td width="180" style="vertical-align:middle;text-align:center;">
diff --git a/forum/skins/default/templates/user_stats.html b/forum/skins/default/templates/user_stats.html
index f5dcf4f5..0e0f4d36 100644
--- a/forum/skins/default/templates/user_stats.html
+++ b/forum/skins/default/templates/user_stats.html
@@ -5,7 +5,7 @@
{% load extra_filters %}
{% load humanize %}
{% block usercontent %}
-
+ {% include "user_info.html" %}
<a name="questions"></a>
{% spaceless %}
<h2>{% blocktrans count questions|length as counter %}<span class="count">{{counter}}</span> Question{% plural %}<span class="count">{{counter}}</span> Questions{% endblocktrans %}</h2>
diff --git a/forum/utils/markup.py b/forum/utils/markup.py
index 914eb508..95d2125c 100644
--- a/forum/utils/markup.py
+++ b/forum/utils/markup.py
@@ -55,10 +55,11 @@ def mentionize_text(text, anticipated_authors):
output = ''
mentioned_authors = list()
while '@' in text:
- #the purpose of this loop is to convert any occurance of '@mention ' syntax
- #to user account links leading space is required unless @ is the first
- #character in whole text, also, either a punctuation or a ' ' char is required
- #after the name
+ #the purpose of this loop is to convert any occurance of
+ #'@mention ' syntax
+ #to user account links leading space is required unless @ is the first
+ #character in whole text, also, either a punctuation or
+ #a ' ' char is required after the name
pos = text.index('@')
#save stuff before @mention to the output
@@ -88,7 +89,8 @@ def mentionize_text(text, anticipated_authors):
output += '@'
else:
- #if there isn't, i.e. text goes like something@mention, do not look up people
+ #if there isn't, i.e. text goes like something@mention,
+ #do not look up people
output += '@'
text = text[pos+1:]
else:
diff --git a/forum/views/users.py b/forum/views/users.py
index cb530af1..0d2c8c3f 100644
--- a/forum/views/users.py
+++ b/forum/views/users.py
@@ -5,7 +5,8 @@ from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
-from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect, Http404
+from django.http import HttpResponse, HttpResponseForbidden
+from django.http import HttpResponseRedirect, Http404
from django.utils.translation import ugettext as _
from django.utils.html import strip_tags
from django.utils import simplejson
@@ -22,8 +23,13 @@ import logging
question_type = ContentType.objects.get_for_model(models.Question)
answer_type = ContentType.objects.get_for_model(models.Answer)
comment_type = ContentType.objects.get_for_model(models.Comment)
-question_revision_type = ContentType.objects.get_for_model(models.QuestionRevision)
-answer_revision_type = ContentType.objects.get_for_model(models.AnswerRevision)
+question_revision_type = ContentType.objects.get_for_model(
+ models.QuestionRevision
+ )
+
+answer_revision_type = ContentType.objects.get_for_model(
+ models.AnswerRevision
+ )
repute_type = ContentType.objects.get_for_model(models.Repute)
question_type_id = question_type.id
answer_type_id = answer_type.id
@@ -53,7 +59,9 @@ def users(request):
order_by_parameter = '-reputation'
objects_list = Paginator(
- models.User.objects.all().order_by(order_by_parameter),
+ models.User.objects.all().order_by(
+ order_by_parameter
+ ),
const.USERS_PAGE_SIZE
)
base_url = reverse('users') + '?sort=%s&' % sortby
@@ -61,9 +69,11 @@ def users(request):
sortby = "reputation"
objects_list = Paginator(
models.User.objects.extra(
- where=['username like %s'],
- params=['%' + suser + '%']
- ).order_by('-reputation'),
+ where=['username like %s'],
+ params=['%' + suser + '%']
+ ).order_by(
+ '-reputation'
+ ),
const.USERS_PAGE_SIZE
)
base_url = reverse('users') + '?name=%s&sort=%s&' % (suser, sortby)
@@ -73,24 +83,27 @@ def users(request):
except (EmptyPage, InvalidPage):
users_page = objects_list.page(objects_list.num_pages)
- return render_to_response('users.html', {
- 'active_tab': 'users',
- 'users' : users_page,
- 'suser' : suser,
- 'keywords' : suser,
- 'tab_id' : sortby,
- 'context' : {
- 'is_paginated' : is_paginated,
- 'pages': objects_list.num_pages,
- 'page': page,
- 'has_previous': users_page.has_previous(),
- 'has_next': users_page.has_next(),
- 'previous': users_page.previous_page_number(),
- 'next': users_page.next_page_number(),
- 'base_url' : base_url
- }
-
- }, context_instance=RequestContext(request))
+ return render_to_response(
+ 'users.html',
+ {
+ 'active_tab': 'users',
+ 'users' : users_page,
+ 'suser' : suser,
+ 'keywords' : suser,
+ 'tab_id' : sortby,
+ 'context' : {
+ 'is_paginated' : is_paginated,
+ 'pages': objects_list.num_pages,
+ 'page': page,
+ 'has_previous': users_page.has_previous(),
+ 'has_next': users_page.has_next(),
+ 'previous': users_page.previous_page_number(),
+ 'next': users_page.next_page_number(),
+ 'base_url' : base_url
+ }
+ },
+ context_instance=RequestContext(request)
+ )
@login_required
def moderate_user(request, id):
@@ -107,7 +120,10 @@ def moderate_user(request, id):
if form.is_valid():
form.save()
logging.debug('data saved')
- response = HttpResponse(simplejson.dumps(''), mimetype="application/json")
+ response = HttpResponse(
+ simplejson.dumps(''),
+ mimetype="application/json"
+ )
else:
response = HttpResponseForbidden(mimetype="application/json")
return response
@@ -140,23 +156,33 @@ def edit_user(request, id):
user.website = sanitize_html(form.cleaned_data['website'])
user.location = sanitize_html(form.cleaned_data['city'])
user.date_of_birth = sanitize_html(form.cleaned_data['birthday'])
+
if len(user.date_of_birth) == 0:
user.date_of_birth = '1900-01-01'
+
user.about = sanitize_html(form.cleaned_data['about'])
user.save()
# send user updated singal if full fields have been updated
- if user.email and user.real_name and user.website and user.location and \
- user.date_of_birth and user.about:
- signals.user_updated.send(sender=user.__class__, instance=user, updated_by=user)
+ if user.email and user.real_name and user.website \
+ and user.location and user.date_of_birth and user.about:
+ signals.user_updated.send(
+ sender=user.__class__,
+ instance=user,
+ updated_by=user
+ )
return HttpResponseRedirect(user.get_profile_url())
else:
form = forms.EditUserForm(user)
- return render_to_response('user_edit.html', {
- 'active_tab': 'users',
- 'form' : form,
- 'gravatar_faq_url' : reverse('faq') + '#gravatar',
- }, context_instance=RequestContext(request))
+ return render_to_response(
+ 'user_edit.html',
+ {
+ 'active_tab': 'users',
+ 'form' : form,
+ 'gravatar_faq_url' : reverse('faq') + '#gravatar',
+ },
+ context_instance=RequestContext(request)
+ )
def user_stats(request, user_id, user_view):
user = get_object_or_404(models.User, id=user_id)
@@ -230,24 +256,26 @@ def user_stats(request, user_id, user_view):
votes_today = models.Vote.objects.get_votes_count_today_from_user(user)
votes_total = forum_settings.MAX_VOTES_PER_USER_PER_DAY
- question_id_set = set(map(lambda v: v['id'], list(questions))) \
- | set(map(lambda v: v['id'], list(answered_questions)))
-
+ question_id_set = set()
+ #todo: there may be a better way to do these queries
+ question_id_set.update([q['id'] for q in questions])
+ question_id_set.update([q['id'] for q in answered_questions])
user_tags = models.Tag.objects.filter(questions__id__in = question_id_set)
try:
from django.db.models import Count
#todo - rewrite template to do the table joins within standard ORM
#awards = models.Award.objects.filter(user=user).order_by('-awarded_at')
awards = models.Award.objects.extra(
- select={'id': 'badge.id',
- 'name':'badge.name',
- 'description': 'badge.description',
- 'type': 'badge.type'},
- tables=['award', 'badge'],
- order_by=['-awarded_at'],
- where=['user_id=%s AND badge_id=badge.id'],
- params=[user.id]
- ).values('id', 'name', 'description', 'type')
+ select={'id': 'badge.id',
+ 'name':'badge.name',
+ 'description': 'badge.description',
+ 'type': 'badge.type'},
+ tables=['award', 'badge'],
+ order_by=['-awarded_at'],
+ where=['user_id=%s AND badge_id=badge.id'],
+ params=[user.id]
+ ).values('id', 'name', 'description', 'type')
+
total_awards = awards.count()
awards = awards.annotate(count = Count('badge__id'))
user_tags = user_tags.annotate(user_tag_usage_count=Count('name'))
@@ -255,16 +283,17 @@ def user_stats(request, user_id, user_view):
except ImportError:
#todo: remove all old django stuff, e.g. with '.group_by = ' pattern
awards = models.Award.objects.extra(
- select={'id': 'badge.id',
- 'count': 'count(badge_id)',
- 'name':'badge.name',
- 'description': 'badge.description',
- 'type': 'badge.type'},
- tables=['award', 'badge'],
- order_by=['-awarded_at'],
- where=['user_id=%s AND badge_id=badge.id'],
- params=[user.id]
- ).values('id', 'count', 'name', 'description', 'type')
+ select={'id': 'badge.id',
+ 'count': 'count(badge_id)',
+ 'name':'badge.name',
+ 'description': 'badge.description',
+ 'type': 'badge.type'},
+ tables=['award', 'badge'],
+ order_by=['-awarded_at'],
+ where=['user_id=%s AND badge_id=badge.id'],
+ params=[user.id]
+ ).values('id', 'count', 'name', 'description', 'type')
+
total_awards = awards.count()
awards.query.group_by = ['badge_id']
@@ -279,24 +308,28 @@ def user_stats(request, user_id, user_view):
else:
moderate_user_form = None
- return render_to_response(user_view.template_file,{
- 'active_tab':'users',
- 'moderate_user_form': moderate_user_form,
- "tab_name" : user_view.id,
- "tab_description" : user_view.tab_description,
- "page_title" : user_view.page_title,
- "view_user" : user,
- "questions" : questions,
- "answered_questions" : answered_questions,
- "up_votes" : up_votes,
- "down_votes" : down_votes,
- "total_votes": up_votes + down_votes,
- "votes_today_left": votes_total-votes_today,
- "votes_total_per_day": votes_total,
- "user_tags" : user_tags[:50],
- "awards": awards,
- "total_awards" : total_awards,
- }, context_instance=RequestContext(request))
+ return render_to_response(
+ user_view.template_file,
+ {
+ 'active_tab':'users',
+ 'moderate_user_form': moderate_user_form,
+ "tab_name" : user_view.id,
+ "tab_description" : user_view.tab_description,
+ "page_title" : user_view.page_title,
+ "view_user" : user,
+ "questions" : questions,
+ "answered_questions" : answered_questions,
+ "up_votes" : up_votes,
+ "down_votes" : down_votes,
+ "total_votes": up_votes + down_votes,
+ "votes_today_left": votes_total-votes_today,
+ "votes_total_per_day": votes_total,
+ "user_tags" : user_tags[:50],
+ "awards": awards,
+ "total_awards" : total_awards,
+ },
+ context_instance=RequestContext(request)
+ )
def user_recent(request, user_id, user_view):
user = get_object_or_404(models.User, id=user_id)
@@ -313,7 +346,10 @@ def user_recent(request, user_id, user_view):
self.title = title
self.summary = summary
slug_title = slugify(title)
- self.title_link = reverse('question', kwargs={'id':question_id}) + u'%s' % slug_title
+ self.title_link = reverse(
+ 'question',
+ kwargs={'id':question_id}
+ ) + u'%s' % slug_title
if int(answer_id) > 0:
self.title_link += '#%s' % answer_id
@@ -345,9 +381,20 @@ def user_recent(request, user_id, user_view):
'activity_type'
)
if len(questions) > 0:
- questions = [(Event(q['active_at'], q['activity_type'], q['title'], '', '0', \
- q['question_id'])) for q in questions]
- activities.extend(questions)
+
+ question_activities = []
+ for q in questions:
+ q_event = Event(
+ q['active_at'],
+ q['activity_type'],
+ q['title'],
+ '',
+ '0',
+ q['question_id']
+ )
+ question_activities.append(q_event)
+
+ activities.extend(question_activities)
# answers
answers = models.Activity.objects.extra(