Made a refactor of the notifications app

remotes/origin/enhancement/email-actions
David Barragán Merino 2013-07-16 23:05:19 +02:00
parent a4777190a1
commit a4925e8be5
24 changed files with 228 additions and 202 deletions

View File

@ -11,6 +11,7 @@ Setup development environment.
python manage.py syncdb --migrate --noinput python manage.py syncdb --migrate --noinput
python manage.py loaddata initial_user python manage.py loaddata initial_user
python manage.py sample_data python manage.py sample_data
python manage.py createinitialrevisions
Auth: admin/123123 Auth: admin/123123

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-i
from djmail import template_mail
class NotificationSenderMixin(object):
create_notification_template = None
update_notification_template = None
destroy_notification_template = None
def _send_notification_email(template_method, users=None, context=None):
mails = template_mail.MagicMailBuilder()
for user in users:
email = getattr(mails, template_method)(user, context)
email.send()
def post_save(self, obj, created=False):
users = obj.get_watchers_to_notify(self.request.user)
context = {
'changer': self.request.user,
'changed_fields_dict': obj.get_changed_fields_dict(self.request.DATA),
'object': obj
}
if created:
self._send_notification_email(self.create_notification_template, users=users, context=context)
else:
self._send_notification_email(self.update_notification_template, users=users, context=context)
def destroy(self, request, *args, **kwargs):
users = obj.get_watchers_to_notify(self.request.user)
context = {
'changer': self.request.user,
'object': obj
}
self._send_notification_email(self.destroy_notification_template, users=users, context=context)
return super(NotificationSenderMixin, self).destroy(request, *args, **kwargs)

View File

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
from django.dispatch import receiver
from django.utils.translation import ugettext_lazy as _
from greenmine.base.notifications.models import watched_changed
@receiver(watched_changed)
def send_mail_when_watched_changed(sender, **kwargs):
changed_attributes = kwargs.get('changed_attributes')
watchers_to_notify = sender.get_watchers_to_notify()
print 'Cambiado', sender
print 'Atributos', changed_attributes
print 'Notificar a', watchers_to_notify

View File

@ -1,99 +1,94 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.db import models from django.db import models
from django.dispatch import Signal from django.db.models.fields import FieldDoesNotExist
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import reversion
watched_changed = Signal(providing_args=['changed_attributes'])
class WatcherMixin(object): class WatcherMixin(object):
NOTIFY_LEVEL_CHOICES = ( NOTIFY_LEVEL_CHOICES = (
('all_owned_projects', _(u'All events on my projects')), ("all_owned_projects", _(u"All events on my projects")),
('only_watching', _(u'Only events for objects i watch')), ("only_watching", _(u"Only events for objects i watch")),
('only_assigned', _(u'Only events for objects assigned to me')), ("only_assigned", _(u"Only events for objects assigned to me")),
('only_owner', _(u'Only events for objects owned by me')), ("only_owner", _(u"Only events for objects owned by me")),
('no_events', _(u'No events')), ("no_events", _(u"No events")),
) )
notify_level = models.CharField(max_length=32, null=False, blank=False, default='only_watching', notify_level = models.CharField(max_length=32, null=False, blank=False, default="only_watching",
choices=NOTIFY_LEVEL_CHOICES, verbose_name=_(u'notify level')) choices=NOTIFY_LEVEL_CHOICES, verbose_name=_(u"notify level"))
notify_changes_by_me = models.BooleanField(null=False, blank=True, notify_changes_by_me = models.BooleanField(null=False, blank=True,
verbose_name=_(u'notify changes made by me')) verbose_name=_(u"notify changes made by me"))
class Meta: class Meta:
abstract = True abstract = True
def allow_notify_owned(self): def allow_notify_owned(self):
return (self.notify_level in [ return (self.notify_level in [
'only_owner', "only_owner",
'only_assigned', "only_assigned",
'only_watching', "only_watching",
'all_owned_projects', "all_owned_projects",
]) ])
def allow_notify_assigned_to(self): def allow_notify_assigned_to(self):
return (self.notify_level in [ return (self.notify_level in [
'only_assigned', "only_assigned",
'only_watching', "only_watching",
'all_owned_projects', "all_owned_projects",
]) ])
def allow_notify_suscribed(self): def allow_notify_suscribed(self):
return (self.notify_level in [ return (self.notify_level in [
'only_watching', "only_watching",
'all_owned_projects', "all_owned_projects",
]) ])
def allow_notify_project(self, project): def allow_notify_project(self, project):
return self.notify_level == 'all_owned_projects' \ return self.notify_level == "all_owned_projects" and project.owner.pk == self.pk
and project.owner.pk == self.pk
def allow_notify_by_me(self, changer): def allow_notify_by_me(self, changer):
return (changer.pk != self.pk) \ return (changer.pk != self.pk) or self.notify_changes_by_me
or self.notify_changes_by_me
class WatchedMixin(object): class WatchedMixin(object):
class Meta: class Meta:
abstract = True abstract = True
def start_change(self, changer): @property
self._changer = changer def last_version(self):
self._saved_attributes = self._get_attributes_to_notify() version_list = reversion.get_for_object(self)
return version_list and version_list[0] or None
def cancel_change(self): def get_changed_fields_dict(self, data_dict):
del self._changer field_dict = {}
del self._saved_attributes for field_name, data_value in data_dict.items():
field_dict.update(self._get_changed_field(field_name, data_value))
return field_dict
def complete_change(self): def get_watchers_to_notify(self, changer):
changed_attributes = self._get_changed_attributes()
self.cancel_change()
watched_changed.send(sender=self, changed_attributes=changed_attributes)
def get_watchers_to_notify(self):
watchers_to_notify = set() watchers_to_notify = set()
watchers_by_role = self._get_watchers_by_role() watchers_by_role = self._get_watchers_by_role()
owner = watchers_by_role.get('owner') owner = watchers_by_role.get("owner")
if (owner and owner.allow_notify_owned() if (owner and owner.allow_notify_owned()
and owner.allow_notify_by_me(self._changer)): and owner.allow_notify_by_me(changer)):
watchers_to_notify.add(owner) watchers_to_notify.add(owner)
assigned_to = watchers_by_role.get('assigned_to') assigned_to = watchers_by_role.get("assigned_to")
if (assigned_to and assigned_to.allow_notify_assigned_to() if (assigned_to and assigned_to.allow_notify_assigned_to()
and assigned_to.allow_notify_by_me(self._changer)): and assigned_to.allow_notify_by_me(changer)):
watchers_to_notify.add(assigned_to) watchers_to_notify.add(assigned_to)
suscribed_watchers = watchers_by_role.get('suscribed_watchers') suscribed_watchers = watchers_by_role.get("suscribed_watchers")
if suscribed_watchers: if suscribed_watchers:
for suscribed_watcher in suscribed_watchers: for suscribed_watcher in suscribed_watchers:
if (suscribed_watcher and suscribed_watcher.allow_notify_suscribed() if (suscribed_watcher and suscribed_watcher.allow_notify_suscribed()
and suscribed_watcher.allow_notify_by_me(self._changer)): and suscribed_watcher.allow_notify_by_me(changer)):
watchers_to_notify.add(suscribed_watcher) watchers_to_notify.add(suscribed_watcher)
#(project, project_owner) = watchers_by_role.get('project_owner') #(project, project_owner) = watchers_by_role.get("project_owner")
#if project_owner \ #if project_owner \
# and project_owner.allow_notify_project(project) \ # and project_owner.allow_notify_project(project) \
# and project_owner.allow_notify_by_me(self._changer): # and project_owner.allow_notify_by_me(self._changer):
@ -101,40 +96,39 @@ class WatchedMixin(object):
return watchers_to_notify return watchers_to_notify
def _get_changed_attributes(self): def _get_changed_field_verbose_name(self, field_name):
changed_attributes = {} try:
current_attributes = self._get_attributes_to_notify() return self._meta.get_field(field_name).verbose_name
for name, saved_value in self._saved_attributes.items(): except FieldDoesNotExist:
current_value = current_attributes.get(name) return field_name
if saved_value != current_value:
changed_attributes[name] = (saved_value, current_value) def _get_changed_field_old_value(self, field_name, data_value):
return changed_attributes return (self.last_version and self.last_version.field_dict.get(field_name, data_value) or None)
def _get_changed_field_new_value(self, field_name, data_value):
return getattr(self, field_name, data_value)
def _get_changed_field(self, field_name, data_value):
verbose_name = self._get_changed_field_verbose_name(field_name)
old_value = self._get_changed_field_old_value(field_name, data_value)
new_value = self._get_changed_field_new_value(field_name, data_value)
return {field_name: {
"verbose_name": verbose_name,
"old_value": old_value,
"new_value": new_value,
}}
def _get_watchers_by_role(self): def _get_watchers_by_role(self):
''' """
Return the actual instances of watchers of this object, classified by role. Return the actual instances of watchers of this object, classified by role.
For example: For example:
return { return {
'owner': self.owner, "owner": self.owner,
'assigned_to': self.assigned_to, "assigned_to": self.assigned_to,
'suscribed_watchers': self.watchers.all(), "suscribed_watchers": self.watchers.all(),
'project_owner': (self.project, self.project.owner), "project_owner": (self.project, self.project.owner),
} }
''' """
raise NotImplementedError('You must subclass WatchedMixin and provide _get_watchers_by_role method') raise NotImplementedError("You must subclass WatchedMixin and provide _get_watchers_by_role method")
def _get_attributes_to_notify(self):
'''
Return the names and values of the attributes of this object that will be checked for change in
change notifications. Example:
return {
'name': self.name,
'description': self.description,
'status': self.status.name,
...
}
'''
raise NotImplementedError('You must subclass WatchedMixin and provide _get_attributes_to_notify method')

View File

@ -6,6 +6,8 @@ import django_filters
from rest_framework import generics from rest_framework import generics
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from greenmine.base.notifications.api import NotificationSenderMixin
from greenmine.scrum.serializers import * from greenmine.scrum.serializers import *
from greenmine.scrum.models import * from greenmine.scrum.models import *
from greenmine.scrum.permissions import * from greenmine.scrum.permissions import *
@ -47,7 +49,7 @@ class SimpleFilterMixin(object):
return queryset return queryset
class ProjectList(generics.ListCreateAPIView): class ProjectList(NotificationSenderMixin, generics.ListCreateAPIView):
model = Project model = Project
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
@ -61,13 +63,13 @@ class ProjectList(generics.ListCreateAPIView):
obj.owner = self.request.user obj.owner = self.request.user
class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): class ProjectDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
model = Project model = Project
serializer_class = ProjectSerializer serializer_class = ProjectSerializer
permission_classes = (IsAuthenticated, ProjectDetailPermission,) permission_classes = (IsAuthenticated, ProjectDetailPermission,)
class MilestoneList(generics.ListCreateAPIView): class MilestoneList(NotificationSenderMixin, generics.ListCreateAPIView):
model = Milestone model = Milestone
serializer_class = MilestoneSerializer serializer_class = MilestoneSerializer
filter_fields = ('project',) filter_fields = ('project',)
@ -80,13 +82,13 @@ class MilestoneList(generics.ListCreateAPIView):
obj.owner = self.request.user obj.owner = self.request.user
class MilestoneDetail(generics.RetrieveUpdateDestroyAPIView): class MilestoneDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
model = Milestone model = Milestone
serializer_class = MilestoneSerializer serializer_class = MilestoneSerializer
permission_classes = (IsAuthenticated, MilestoneDetailPermission,) permission_classes = (IsAuthenticated, MilestoneDetailPermission,)
class UserStoryList(generics.ListCreateAPIView): class UserStoryList(NotificationSenderMixin, generics.ListCreateAPIView):
model = UserStory model = UserStory
serializer_class = UserStorySerializer serializer_class = UserStorySerializer
filter_class = UserStoryFilter filter_class = UserStoryFilter
@ -99,7 +101,7 @@ class UserStoryList(generics.ListCreateAPIView):
obj.owner = self.request.user obj.owner = self.request.user
class UserStoryDetail(generics.RetrieveUpdateDestroyAPIView): class UserStoryDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
model = UserStory model = UserStory
serializer_class = UserStorySerializer serializer_class = UserStorySerializer
permission_classes = (IsAuthenticated, UserStoryDetailPermission,) permission_classes = (IsAuthenticated, UserStoryDetailPermission,)
@ -157,7 +159,7 @@ class TasksAttachmentDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsAuthenticated, AttachmentDetailPermission,) permission_classes = (IsAuthenticated, AttachmentDetailPermission,)
class TaskList(generics.ListCreateAPIView): class TaskList(NotificationSenderMixin, generics.ListCreateAPIView):
model = Task model = Task
serializer_class = TaskSerializer serializer_class = TaskSerializer
filter_fields = ('user_story', 'milestone', 'project') filter_fields = ('user_story', 'milestone', 'project')
@ -171,7 +173,7 @@ class TaskList(generics.ListCreateAPIView):
obj.milestone = obj.user_story.milestone obj.milestone = obj.user_story.milestone
class TaskDetail(generics.RetrieveUpdateDestroyAPIView): class TaskDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
model = Task model = Task
serializer_class = TaskSerializer serializer_class = TaskSerializer
permission_classes = (IsAuthenticated, TaskDetailPermission,) permission_classes = (IsAuthenticated, TaskDetailPermission,)
@ -181,9 +183,10 @@ class TaskDetail(generics.RetrieveUpdateDestroyAPIView):
if "comment" in self.request.DATA: if "comment" in self.request.DATA:
# Update the comment in the last version # Update the comment in the last version
reversion.set_comment(self.request.DATA['comment']) reversion.set_comment(self.request.DATA['comment'])
super(TaskDetail, self).post_save(obj, created)
class IssueList(generics.ListCreateAPIView): class IssueList(NotificationSenderMixin, generics.ListCreateAPIView):
model = Issue model = Issue
serializer_class = IssueSerializer serializer_class = IssueSerializer
filter_fields = ('project',) filter_fields = ('project',)
@ -196,7 +199,7 @@ class IssueList(generics.ListCreateAPIView):
return self.model.objects.filter(project__members=self.request.user) return self.model.objects.filter(project__members=self.request.user)
class IssueDetail(generics.RetrieveUpdateDestroyAPIView): class IssueDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
model = Issue model = Issue
serializer_class = IssueSerializer serializer_class = IssueSerializer
permission_classes = (IsAuthenticated, IssueDetailPermission,) permission_classes = (IsAuthenticated, IssueDetailPermission,)
@ -206,6 +209,7 @@ class IssueDetail(generics.RetrieveUpdateDestroyAPIView):
if "comment" in self.request.DATA: if "comment" in self.request.DATA:
# Update the comment in the last version # Update the comment in the last version
reversion.set_comment(self.request.DATA['comment']) reversion.set_comment(self.request.DATA['comment'])
super(IssueDetail, self).post_save(obj, created)
class SeverityList(generics.ListCreateAPIView): class SeverityList(generics.ListCreateAPIView):

View File

@ -10,13 +10,24 @@ from django.db.models.loading import get_model
from picklefield.fields import PickledObjectField from picklefield.fields import PickledObjectField
from greenmine.base.utils.slug import slugify_uniquely, ref_uniquely from greenmine.base.utils.slug import (
slugify_uniquely,
ref_uniquely
)
from greenmine.base.utils import iter_points from greenmine.base.utils import iter_points
from greenmine.base.notifications.models import WatchedMixin from greenmine.base.notifications.models import WatchedMixin
from greenmine.scrum.choices import (ISSUESTATUSES, TASKSTATUSES, USSTATUSES, from greenmine.scrum.choices import (
POINTS_CHOICES, SEVERITY_CHOICES, ISSUESTATUSES,
ISSUETYPES, TASK_CHANGE_CHOICES, TASKSTATUSES,
PRIORITY_CHOICES) USSTATUSES,
POINTS_CHOICES,
SEVERITY_CHOICES,
ISSUETYPES,
TASK_CHANGE_CHOICES,
PRIORITY_CHOICES
)
import reversion
class Severity(models.Model): class Severity(models.Model):
@ -232,7 +243,7 @@ class Project(models.Model, WatchedMixin):
def _get_watchers_by_role(self): def _get_watchers_by_role(self):
return {'owner': self.owner} return {'owner': self.owner}
def _get_attributes_to_notify(self): def eget_attrinutes_to_notify(self):
return { return {
'name': self.name, 'name': self.name,
'slug': self.slug, 'slug': self.slug,
@ -385,7 +396,6 @@ class Milestone(models.Model, WatchedMixin):
'project_owner': (self.project, self.project.owner), 'project_owner': (self.project, self.project.owner),
} }
class RolePoints(models.Model): class RolePoints(models.Model):
user_story = models.ForeignKey('UserStory', null=False, blank=False, user_story = models.ForeignKey('UserStory', null=False, blank=False,
related_name='role_points', related_name='role_points',
@ -474,22 +484,6 @@ class UserStory(WatchedMixin, models.Model):
'project_owner': (self.project, self.project.owner), 'project_owner': (self.project, self.project.owner),
} }
def _get_attributes_to_notify(self):
return {
'milestone': self.milestone.name,
'owner': self.owner.get_full_name(),
'status': self.status.name,
'points': self.points.name,
'order': self.order,
'modified_date': self.modified_date,
'finish_date': self.finish_date,
'subject': self.subject,
'description': self.description,
'client_requirement': self.client_requirement,
'team_requirement': self.team_requirement,
'tags': self.tags,
}
class Attachment(models.Model): class Attachment(models.Model):
owner = models.ForeignKey('base.User', null=False, blank=False, owner = models.ForeignKey('base.User', null=False, blank=False,
@ -678,6 +672,15 @@ class Issue(models.Model, WatchedMixin):
} }
# Reversion registration (usufull for base.notification and for meke a historical)
reversion.register(Project)
reversion.register(Milestone)
reversion.register(UserStory)
reversion.register(Task)
reversion.register(Issue)
# Model related signals handlers # Model related signals handlers
@receiver(models.signals.post_save, sender=Project, dispatch_uid='project_post_save') @receiver(models.signals.post_save, sender=Project, dispatch_uid='project_post_save')
@ -764,7 +767,3 @@ def tasks_close_handler(sender, instance, **kwargs):
else: else:
instance.user_story.finish_date = None instance.user_story.finish_date = None
instance.user_story.save() instance.user_story.save()
# Email alerts signals handlers
# TODO: temporary commented (Pending refactor)
# from . import sigdispatch

View File

@ -32,74 +32,75 @@ def mail_recovery_password(sender, user, **kwargs):
subject = ugettext("Greenmine: password recovery.") subject = ugettext("Greenmine: password recovery.")
send_mail.delay(subject, template, [user.email]) send_mail.delay(subject, template, [user.email])
## TODO: Remove me when base.notifications is finished
@receiver(signals.mail_milestone_created) ##
def mail_milestone_created(sender, milestone, user, **kwargs): #@receiver(signals.mail_milestone_created)
participants = milestone.project.all_participants() #def mail_milestone_created(sender, milestone, user, **kwargs):
# participants = milestone.project.all_participants()
emails_list = [] #
subject = ugettext("Greenmine: sprint created") # emails_list = []
for person in participants: # subject = ugettext("Greenmine: sprint created")
template = render_to_string("email/milestone.created.html", { # for person in participants:
"person": person, # template = render_to_string("email/milestone.created.html", {
"current_host": settings.HOST, # "person": person,
"milestone": milestone, # "current_host": settings.HOST,
"user": user, # "milestone": milestone,
}) # "user": user,
# })
emails_list.append([subject, template, [person.email]]) #
# emails_list.append([subject, template, [person.email]])
send_bulk_mail.delay(emails_list) #
# send_bulk_mail.delay(emails_list)
#
@receiver(signals.mail_userstory_created) #
def mail_userstory_created(sender, us, user, **kwargs): #@receiver(signals.mail_userstory_created)
participants = us.milestone.project.all_participants() #def mail_userstory_created(sender, us, user, **kwargs):
# participants = us.milestone.project.all_participants()
emails_list = [] #
subject = ugettext("Greenmine: user story created") # emails_list = []
# subject = ugettext("Greenmine: user story created")
for person in participants: #
template = render_to_string("email/userstory.created.html", { # for person in participants:
"person": person, # template = render_to_string("email/userstory.created.html", {
"current_host": settings.HOST, # "person": person,
"us": us, # "current_host": settings.HOST,
"user": user, # "us": us,
}) # "user": user,
# })
emails_list.append([subject, template, [person.email]]) #
# emails_list.append([subject, template, [person.email]])
send_bulk_mail.delay(emails_list) #
# send_bulk_mail.delay(emails_list)
#
@receiver(signals.mail_task_created) #
def mail_task_created(sender, task, user, **kwargs): #@receiver(signals.mail_task_created)
participants = task.us.milestone.project.all_participants() #def mail_task_created(sender, task, user, **kwargs):
# participants = task.us.milestone.project.all_participants()
emails_list = [] #
subject = ugettext("Greenmine: task created") # emails_list = []
# subject = ugettext("Greenmine: task created")
for person in participants: #
template = render_to_string("email/task.created.html", { # for person in participants:
"person": person, # template = render_to_string("email/task.created.html", {
"current_host": settings.HOST, # "person": person,
"task": task, # "current_host": settings.HOST,
"user": user, # "task": task,
}) # "user": user,
# })
emails_list.append([subject, template, [person.email]]) #
# emails_list.append([subject, template, [person.email]])
send_bulk_mail.delay(emails_list) #
# send_bulk_mail.delay(emails_list)
#
@receiver(signals.mail_task_assigned) #
def mail_task_assigned(sender, task, user, **kwargs): #@receiver(signals.mail_task_assigned)
template = render_to_string("email/task.assigned.html", { #def mail_task_assigned(sender, task, user, **kwargs):
"person": task.assigned_to, # template = render_to_string("email/task.assigned.html", {
"task": task, # "person": task.assigned_to,
"user": user, # "task": task,
"current_host": settings.HOST, # "user": user,
}) # "current_host": settings.HOST,
# })
subject = ugettext("Greenmine: task assigned") #
send_mail.delay(subject, template, [task.assigned_to.email]) # subject = ugettext("Greenmine: task assigned")
# send_mail.delay(subject, template, [task.assigned_to.email])

View File

@ -1,8 +1,14 @@
#!/bin/bash #!/bin/bash
# For sqlite
rm -f database.sqlite
# For postgresql
dropdb greenmine dropdb greenmine
createdb greenmine createdb greenmine
python manage.py syncdb --migrate --noinput python manage.py syncdb --migrate --noinput --traceback
python manage.py loaddata initial_user python manage.py loaddata initial_user --traceback
python manage.py sample_data python manage.py sample_data --traceback
python manage.py createinitialrevisions --traceback

View File

@ -17,7 +17,6 @@ git+git://github.com/toastdriven/django-haystack.git
django-picklefield==0.3.0 django-picklefield==0.3.0
django-reversion==1.7 django-reversion==1.7
django-sampledatahelper==0.0.1 django-sampledatahelper==0.0.1
django-tastypie==0.9.14
djangorestframework==2.2.5 djangorestframework==2.2.5
gunicorn==17.5 gunicorn==17.5
kombu==2.5.12 kombu==2.5.12
@ -28,3 +27,5 @@ pycrypto==2.6
python-dateutil==2.1 python-dateutil==2.1
pytz==2013b pytz==2013b
six==1.3.0 six==1.3.0
djmail>=0.1