From 056b2a1ffbdbe04dc42cd2719923398e0bf222ce Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sat, 5 Jun 2010 17:35:56 -0400 Subject: mentions work in all kinds of posts, unified activity logging for post updates --- forum/const/__init__.py | 7 +- forum/management/commands/send_email_alerts.py | 12 +- forum/middleware/pagesize.py | 2 + forum/models/__init__.py | 196 +++++++------------- forum/models/answer.py | 34 ++++ forum/models/base.py | 12 +- forum/models/content.py | 124 ++++++++++++- forum/models/meta.py | 80 +++++++- forum/models/question.py | 56 ++++-- forum/models/signals.py | 1 + forum/models/user.py | 53 ------ .../default/templates/instant_notification.html | 2 + forum/skins/default/templates/user.html | 9 +- forum/skins/default/templates/user_info.html | 3 - forum/skins/default/templates/user_stats.html | 2 +- forum/utils/markup.py | 12 +- forum/views/users.py | 203 +++++++++++++-------- 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 objects + removed_mentions - list of mention 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}}"

{{update_author_name}} asked a question {{origin_post_title}}

{% endblocktrans %} + {% endif %} {%if update_type == 'answer_update' %} {% blocktrans %}

{{update_author_name}} updated an answer to the question {{origin_post_title}}

{% endblocktrans %} + {% endif %} {% if update_type == 'question_update' %} {% blocktrans %}

{{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%}