From 1881f765d56d842b4a0d31e2432e577fa01a6447 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 23 May 2010 23:33:37 -0400 Subject: closer to making instant notifications work --- django_authopenid/views.py | 5 +- fbconnect/views.py | 4 +- forum/auth.py | 6 +- forum/const/__init__.py | 1 + forum/models/__init__.py | 118 +++++++++++++++++---- forum/models/question.py | 5 +- forum/models/signals.py | 17 +++ forum/models/user.py | 8 ++ .../default/templates/instant_notification.html | 38 +++---- forum/views/users.py | 3 +- forum_modules/authentication/auth.py | 4 +- 11 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 forum/models/signals.py diff --git a/django_authopenid/views.py b/django_authopenid/views.py index 688a41fc..cce4beb5 100644 --- a/django_authopenid/views.py +++ b/django_authopenid/views.py @@ -75,7 +75,7 @@ EXTERNAL_LOGIN_APP = settings.LOAD_EXTERNAL_LOGIN_APP() #todo: decouple from forum def login(request,user): from django.contrib.auth import login as _login - from forum.models import user_logged_in #custom signal + from forum.models import signals if settings.USE_EXTERNAL_LEGACY_LOGIN == True: EXTERNAL_LOGIN_APP.api.login(request,user) @@ -95,7 +95,8 @@ def login(request,user): request.session['search_state'] = search_state #5) send signal with old session key as argument logging.debug('logged in user %s with session key %s' % (user.username, session_key)) - user_logged_in.send(user=user,session_key=session_key,sender=None) + #todo: move to auth app + signals.user_logged_in.send(user=user,session_key=session_key,sender=None) #todo: uncouple this from forum def logout(request): diff --git a/fbconnect/views.py b/fbconnect/views.py index 1781f6bf..91ea757a 100644 --- a/fbconnect/views.py +++ b/fbconnect/views.py @@ -92,8 +92,8 @@ def login_and_forward(request, user, newquestion = False, newanswer = False): login(request, user) logging.debug('user logged in!') - from forum.models import user_logged_in - user_logged_in.send(user=user,session_key=old_session,sender=None) + from forum.models import signals#todo: move to authentication app + signals.user_logged_in.send(user=user,session_key=old_session,sender=None) logging.debug('user_logged_in signal sent') if (newquestion): diff --git a/forum/auth.py b/forum/auth.py index fb3a5a68..7664a02e 100644 --- a/forum/auth.py +++ b/forum/auth.py @@ -10,7 +10,7 @@ from django.db import transaction from models import Repute from models import Question from models import Answer -from models import mark_offensive, delete_post_or_answer +from models import signals from const import TYPE_REPUTATION import logging @@ -213,7 +213,7 @@ def onFlaggedItem(item, post, user, timestamp=None): #post.deleted_at = timestamp #post.deleted_by = Admin post.save() - mark_offensive.send( + signals.mark_offensive.send( sender=post.__class__, instance=post, mark_by=user @@ -482,7 +482,7 @@ def onDeleted(post, user, timestamp=None): elif isinstance(post, Answer): Question.objects.update_answer_count(post.question) logging.debug('updated answer count to %d' % post.question.answer_count) - delete_post_or_answer.send( + signals.delete_post_or_answer.send( sender=post.__class__, instance=post, delete_by=user diff --git a/forum/const/__init__.py b/forum/const/__init__.py index 3e4dedef..dba43161 100644 --- a/forum/const/__init__.py +++ b/forum/const/__init__.py @@ -142,6 +142,7 @@ RESPONSE_ACTIVITY_TYPES_FOR_EMAIL = ( TYPE_ACTIVITY_COMMENT_ANSWER, TYPE_ACTIVITY_UPDATE_ANSWER, TYPE_ACTIVITY_UPDATE_QUESTION, + TYPE_ACTIVITY_MENTION, ) TYPE_RESPONSE = { diff --git a/forum/models/__init__.py b/forum/models/__init__.py index 1c20e74b..ebcbe6e2 100644 --- a/forum/models/__init__.py +++ b/forum/models/__init__.py @@ -5,9 +5,13 @@ from meta import Vote, Comment, FlaggedItem from user import Activity, ValidationHash, EmailFeedSetting from user import AuthKeyUserAssociation from repute import Badge, Award, Repute +import signals from django.core.urlresolvers import reverse +from django.core.mail import EmailMessage from forum.search.indexer import create_fulltext_indexes from django.db.models.signals import post_syncdb +from django.template import loader, Context +from django.utils.translation import ugettext as _ from forum import const import logging import re @@ -16,15 +20,6 @@ from base import * import datetime from django.contrib.contenttypes.models import ContentType -#todo: move to a separate file? -# custom signals -tags_updated = django.dispatch.Signal(providing_args=["question"]) -edit_question_or_answer = django.dispatch.Signal(providing_args=["instance", "modified_by"]) -delete_post_or_answer = django.dispatch.Signal(providing_args=["instance", "deleted_by"]) -mark_offensive = django.dispatch.Signal(providing_args=["instance", "mark_by"]) -user_updated = django.dispatch.Signal(providing_args=["instance", "updated_by"]) -user_logged_in = django.dispatch.Signal(providing_args=["session"]) - #todo: must go after signals from forum import auth @@ -229,6 +224,14 @@ def flag_post(self, post, timestamp=None, cancel=False): ) auth.onFlaggedItem(flag, post, user, timestamp=timestamp) +def user_should_receive_instant_notification_about_post(user, post): + return EmailFeedSetting.objects.exists_match_to_post_and_subscriber( + subscriber = user, + post = post, + frequency = 'i', + ) + + User.add_to_class('upvote', upvote) User.add_to_class('downvote', downvote) User.add_to_class('accept_answer', accept_answer) @@ -238,6 +241,10 @@ 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 + ) def calculate_gravatar_hash(instance, **kwargs): """Calculates a User's gravatar hash from their email address.""" @@ -300,7 +307,10 @@ def record_comment_event(instance, created, **kwargs): elif isinstance(instance.content_object, Answer): activity_type = const.TYPE_ACTIVITY_COMMENT_ANSWER else: - logging.critical('recording comment for %s is not implemented' % type(instance.content_object)) + logging.critical( + 'recording comment for %s is not implemented'\ + % type(instance.content_object) + ) activity = Activity( user = instance.user, @@ -315,6 +325,8 @@ def record_comment_event(instance, created, **kwargs): exclude_list = [instance.user], ) activity.receiving_users.add(*receiving_users) + #todo: remove this upon migration to 1.2 + signals.fake_m2m_changed.send(sender = Activity, instance = activity, created = True) def record_revision_question_event(instance, created, **kwargs): @@ -337,6 +349,7 @@ def record_revision_question_event(instance, created, **kwargs): receiving_users = list(receiving_users) activity.receiving_users.add(*receiving_users) + def record_revision_answer_event(instance, created, **kwargs): if created and instance.revision <> 1: activity = Activity( @@ -358,6 +371,65 @@ def record_revision_answer_event(instance, created, **kwargs): activity.receiving_users.add(*receiving_users) + +def maybe_send_instant_notifications(instance, created, **kwargs): + """todo: this handler must change when we switch to django 1.2 + """ + activity_instance = instance + if not created: + return + activity_type = activity_instance.activity_type + if activity_type not in const.RESPONSE_ACTIVITY_TYPES_FOR_EMAIL: + return + + #todo: remove this after migrating to string type for const.TYPE_ACTIVITY... + update_type_map = { + const.TYPE_ACTIVITY_COMMENT_QUESTION: 'question_comment', + const.TYPE_ACTIVITY_COMMENT_ANSWER: 'answer_comment', + const.TYPE_ACTIVITY_UPDATE_ANSWER: 'answer_update', + const.TYPE_ACTIVITY_UPDATE_QUESTION: 'question_update', + const.TYPE_ACTIVITY_MENTION: 'mention', + } + + post = activity_instance.get_response_type_content_object() + template = loader.get_template('instant_notification.html') + for u in activity_instance.receiving_users.all(): + if u.should_receive_instant_notification_about_post(post): + + mentions = Activity.objects.get_mentions( + mentioned_whom = u, + mentioned_in = post, + reported = False + ) + if mentions: + #todo: find a more semantic way to do this + mentions.update(is_auditted = True) + has_mention = True + else: + has_mention = False + + #get details about update + #todo: is there a way to solve this import issue? + from forum.conf import settings as forum_settings + data = { + 'receiving_user': u, + 'update_author': activity_instance.user, + 'updated_post': post, + 'update_url': forum_settings.APP_URL + post.get_absolute_url(), + 'update_type': update_type_map[activity_type], + 'revision_number': post.get_latest_revision_number(), + 'related_origin_post': post.get_origin_post(), + 'has_mention': has_mention, + } + #send update + subject = _('email update message subject') + text = template.render(Context(data)) + msg = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [u.email]) + #print 'sending email to %s' % u.email + #print 'subject: %s' % subject + #print 'body: %s' % text + #msg.send() + def record_award_event(instance, created, **kwargs): """ After we awarded a badge to user, we need to record this activity and notify user. @@ -542,7 +614,7 @@ def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs) for aa in aa_list: aa.publish(user) -#signal for User modle save changes +#signal for User model save changes pre_save.connect(calculate_gravatar_hash, sender=User) post_save.connect(record_ask_event, sender=Question) post_save.connect(record_answer_event, sender=Answer) @@ -554,17 +626,23 @@ post_save.connect(notify_award_message, sender=Award) post_save.connect(record_answer_accepted, sender=Answer) post_save.connect(update_last_seen, sender=Activity) post_save.connect(record_vote, sender=Vote) -post_delete.connect(record_cancel_vote, sender=Vote) -delete_post_or_answer.connect(record_delete_question, sender=Question) -delete_post_or_answer.connect(record_delete_question, sender=Answer) -mark_offensive.connect(record_mark_offensive, sender=Question) -mark_offensive.connect(record_mark_offensive, sender=Answer) -tags_updated.connect(record_update_tags, sender=Question) post_save.connect(record_favorite_question, sender=FavoriteQuestion) -user_updated.connect(record_user_full_updated, sender=User) -user_logged_in.connect(post_stored_anonymous_content) +post_delete.connect(record_cancel_vote, sender=Vote) + +#change this to real m2m_changed with Django1.2 +signals.fake_m2m_changed.connect(maybe_send_instant_notifications, sender=Activity) +signals.delete_post_or_answer.connect(record_delete_question, sender=Question) +signals.delete_post_or_answer.connect(record_delete_question, sender=Answer) +signals.mark_offensive.connect(record_mark_offensive, sender=Question) +signals.mark_offensive.connect(record_mark_offensive, sender=Answer) +signals.tags_updated.connect(record_update_tags, sender=Question) +signals.user_updated.connect(record_user_full_updated, sender=User) +signals.user_logged_in.connect(post_stored_anonymous_content) #post_syncdb.connect(create_fulltext_indexes) +#todo: wtf??? what is x=x about? +signals = signals + Question = Question QuestionRevision = QuestionRevision QuestionView = QuestionView @@ -591,6 +669,8 @@ ValidationHash = ValidationHash AuthKeyUserAssociation = AuthKeyUserAssociation __all__ = [ + 'signals', + 'Question', 'QuestionRevision', 'QuestionView', diff --git a/forum/models/question.py b/forum/models/question.py index c2fb6203..0793e601 100644 --- a/forum/models/question.py +++ b/forum/models/question.py @@ -1,4 +1,5 @@ from base import * #todo maybe remove * +from forum.models import signals from tag import Tag #todo: make uniform import for consts from forum.const import CONST @@ -321,7 +322,7 @@ class Question(Content, DeletableContent): self.last_activity_by = retagged_by # Update the Question's tag associations - tags_updated = self.objects.update_tags( + signals.tags_updated = self.objects.update_tags( self, form.cleaned_data['tags'], request.user @@ -339,7 +340,7 @@ class Question(Content, DeletableContent): text = latest_revision.text ) # send tags updated singal - tags_updated.send(sender=question.__class__, question=self) + signals.tags_updated.send(sender=question.__class__, question=self) def get_origin_post(self): return self diff --git a/forum/models/signals.py b/forum/models/signals.py new file mode 100644 index 00000000..b32ffe4a --- /dev/null +++ b/forum/models/signals.py @@ -0,0 +1,17 @@ +import django.dispatch + +tags_updated = django.dispatch.Signal(providing_args=['question']) + +#todo: this one seems to be unused +edit_question_or_answer = django.dispatch.Signal( + providing_args=['instance', 'modified_by'] + ) +delete_post_or_answer = django.dispatch.Signal( + providing_args=['instance', 'deleted_by'] + ) +mark_offensive = django.dispatch.Signal(providing_args=['instance', 'mark_by']) +user_updated = django.dispatch.Signal(providing_args=['instance', 'updated_by']) +#todo: move this to authentication app +user_logged_in = django.dispatch.Signal(providing_args=['session']) +#todo: remove this upon migration to 1.2 +fake_m2m_changed = django.dispatch.Signal(providing_args=['instance','created']) diff --git a/forum/models/user.py b/forum/models/user.py index 2f4db6aa..ef709c70 100644 --- a/forum/models/user.py +++ b/forum/models/user.py @@ -1,5 +1,7 @@ from base import * +#todo: remove this with Django 1.2 from django.contrib.contenttypes.models import ContentType +from forum.models import signals from django.contrib.contenttypes import generic from django.contrib.auth.models import User from forum.models.question import Question, QuestionRevision @@ -74,6 +76,12 @@ class ActivityManager(models.Manager): else: mention_activity.receiving_users.add(mentioned_whom) + signals.fake_m2m_changed.send( + sender = Activity, + instance = mention_activity, + created = True + ) + return mention_activity diff --git a/forum/skins/default/templates/instant_notification.html b/forum/skins/default/templates/instant_notification.html index a8218c12..1bab99cf 100644 --- a/forum/skins/default/templates/instant_notification.html +++ b/forum/skins/default/templates/instant_notification.html @@ -16,31 +16,17 @@ has_mention - Boolean {% blocktrans with receiving_user.get_best_name as user_name %}Dear {{user_name}},{% endblocktrans %} {% if has_mention %} -{% if update_type == 'question_comment' or update_type == 'answer_comment' %} -{% blocktrans with post_author.get_profile_link as author_link and related_origin_post.get_absolute_url as origin_post_url and related_origin_post.title as origin_post_title %} -{{author_link}} has left you a comment -related to question {{origin_post_title}} -{% endblocktrans %} -{% endif %} + {% if update_type == 'question_comment' or update_type == 'answer_comment' %} + {% blocktrans with post_author.get_profile_link as author_link and related_origin_post.get_absolute_url as origin_post_url and related_origin_post.title as origin_post_title %} + {{author_link}} has left you a comment + related to question {{origin_post_title}} + {% endblocktrans %} + {% endif %} {% else %}{# updated post has no mention of user #} -{% if update_type == 'question_comment' or update_type == 'answer_comment' %} -{% blocktrans with post_author.get_profile_link as author_link and related_origin_post.get_absolute_url as origin_post_url and related_origin_post.title as origin_post_title %} -{{author_link}} has left a new comment -related to question {{origin_post_title}} -{% endblocktrans %} -{% if update -{% endif %} -{% if update_type == 'question_comment' or update_type == 'answer_comment' %} -{% if has_mention %} -{% blocktrans %} - -{% endblocktrans %} + {% if update_type == 'question_comment' or update_type == 'answer_comment' %} + {% blocktrans with post_author.get_profile_link as author_link and related_origin_post.get_absolute_url as origin_post_url and related_origin_post.title as origin_post_title %} + {{author_link}} has left a new comment + related to question {{origin_post_title}} + {% endblocktrans %} + {% endif %} {% endif %} -{% endwith %} -{% endif %} -{% if update_type == 'answer_update' %} -{% endif %} -{% if update_type == 'question_update' %} -{% endif %} - -{% include "email_footer.txt" %} diff --git a/forum/views/users.py b/forum/views/users.py index afd78ac5..dcc247bf 100644 --- a/forum/views/users.py +++ b/forum/views/users.py @@ -19,6 +19,7 @@ from forum import const from django.conf import settings from forum.conf import settings as forum_settings from forum import models +from forum.models import signals question_type = ContentType.objects.get_for_model(models.Question) answer_type = ContentType.objects.get_for_model(models.Answer) @@ -149,7 +150,7 @@ def edit_user(request, id): # 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: - models.user_updated.send(sender=user.__class__, instance=user, updated_by=user) + signals.user_updated.send(sender=user.__class__, instance=user, updated_by=user) return HttpResponseRedirect(user.get_profile_url()) else: form = forms.EditUserForm(user) diff --git a/forum_modules/authentication/auth.py b/forum_modules/authentication/auth.py index 96025dc1..b46e3df3 100644 --- a/forum_modules/authentication/auth.py +++ b/forum_modules/authentication/auth.py @@ -110,8 +110,8 @@ def login_and_forward(request, user, forward=None, message=None): user.backend = "django.contrib.auth.backends.ModelBackend" login(request, user) - from forum.models import user_logged_in - user_logged_in.send(user=user,session_key=old_session,sender=None) + from forum.models import signals#todo: move to auth app + signals.user_logged_in.send(user=user,session_key=old_session,sender=None) if not forward: signin_action = request.session.get('on_signin_action', None) -- cgit v1.2.3-1-g7c22