Add notifications module (not finished)

remotes/origin/enhancement/email-actions
Andrés Moya 2013-07-15 11:00:13 +02:00 committed by David Barragán Merino
parent 28a59bbc88
commit 6e71ebde5e
6 changed files with 232 additions and 20 deletions

View File

@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import UserManager, AbstractUser, Group from django.contrib.auth.models import UserManager, AbstractUser, Group
from greenmine.scrum.models import Project, UserStory, Task from greenmine.scrum.models import Project, UserStory, Task
from greenmine.base.notifications.models import WatcherMixin
import uuid import uuid
@ -19,7 +20,6 @@ import uuid
@receiver(signals.pre_save) @receiver(signals.pre_save)
def attach_uuid(sender, instance, **kwargs): def attach_uuid(sender, instance, **kwargs):
fields = sender._meta.init_name_map() fields = sender._meta.init_name_map()
#fields = sender._meta.get_all_field_names()
if 'modified_date' in fields: if 'modified_date' in fields:
instance.modified_date = now() instance.modified_date = now()
@ -30,9 +30,8 @@ def attach_uuid(sender, instance, **kwargs):
instance.uuid = unicode(uuid.uuid1()) instance.uuid = unicode(uuid.uuid1())
class User(AbstractUser, WatcherMixin):
class User(AbstractUser): color = models.CharField(max_length=9, null=False, blank=False,
color = models.CharField(max_length=9, null=False, blank=False, default="#669933",
verbose_name=_('color')) verbose_name=_('color'))
description = models.TextField(null=False, blank=True, description = models.TextField(null=False, blank=True,
verbose_name=_('description')) verbose_name=_('description'))

View File

View File

@ -0,0 +1,17 @@
# -*- 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

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
from django.db import models
from django.dispatch import Signal
from django.utils.translation import ugettext_lazy as _
watched_changed = Signal(providing_args = ['changed_attributes'])
class WatcherMixin(object):
NOTIFY_LEVEL_CHOICES = (
('all_owned_projects', _(u'All events on my projects')),
('only_watching', _(u'Only events for objects i watch')),
('only_assigned', _(u'Only events for objects assigned to me')),
('only_owner', _(u'Only events for objects owned by me')),
('no_events', _(u'No events')),
)
notify_level = models.CharField(max_length=32, null=False, blank=False, default='only_watching',
choices=NOTIFY_LEVEL_CHOICES, verbose_name=_(u'notify level'))
notify_changes_by_me = models.BooleanField(null=False, blank=True,
verbose_name=_(u'notify changes made by me'))
class Meta:
abstract = True
def allow_notify_owned(self):
return (self.notify_level in [
'only_owner',
'only_assigned',
'only_watching',
'all_owned_projects',
])
def allow_notify_assigned_to(self):
return (self.notify_level in [
'only_assigned',
'only_watching',
'all_owned_projects',
])
def allow_notify_suscribed(self):
return (self.notify_level in [
'only_watching',
'all_owned_projects',
])
def allow_notify_project(self, project):
return self.notify_level == 'all_owned_projects' \
and project.owner.pk == self.pk
def allow_notify_by_me(self, changer):
return (changer.pk != self.pk) \
or self.notify_changes_by_me
class WatchedMixin(object):
class Meta:
abstract = True
def start_change(self, changer):
self._changer = changer
self._saved_attributes = self._get_attributes_to_notify()
def cancel_change(self):
del self._changer
del self._saved_attributes
def complete_change(self):
changed_attributes = self._get_changed_attributes()
del self._changer
del self._saved_attributes
watched_changed.send(sender = self, changed_attributes = changed_attributes)
def get_watchers_to_notify(self):
watchers_to_notify = set()
watchers_by_role = self._get_watchers_by_role()
owner = watchers_by_role.get('owner')
if owner \
and owner.allow_notify_owned() \
and owner.allow_notify_by_me(self._changer):
watchers_to_notify.add(owner)
assigned_to = watchers_by_role.get('assigned_to')
if (assigned_to
and assigned_to.allow_notify_assigned_to()
and assigned_to.allow_notify_by_me(self._changer)):
watchers_to_notify.add(assigned_to)
suscribed_watchers = watchers_by_role.get('suscribed_watchers')
if suscribed_watchers:
for suscribed_watcher in suscribed_watchers:
if suscribed_watcher \
and suscribed_watcher.allow_notify_suscribed() \
and suscribed_watcher.allow_notify_by_me(self._changer):
watchers_to_notify.add(suscribed_watcher)
#(project, project_owner) = watchers_by_role.get('project_owner')
#if project_owner \
# and project_owner.allow_notify_project(project) \
# and project_owner.allow_notify_by_me(self._changer):
# watchers_to_notify.add(project_owner)
return watchers_to_notify
def _get_changed_attributes(self):
changed_attributes = {}
current_attributes = self._get_attributes_to_notify()
for name, saved_value in self._saved_attributes.items():
current_value = current_attributes.get(name)
if saved_value != current_value:
changed_attributes[name] = (saved_value, current_value)
return changed_attributes
def _get_watchers_by_role(self):
'''
Return the actual instances of watchers of this object, classified by role.
For example:
return {
'owner': self.owner,
'assigned_to': self.assigned_to,
'suscribed_watchers': self.watchers.all(),
'project_owner': (self.project, self.project.owner),
}
'''
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

@ -12,6 +12,7 @@ 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.scrum.choices import (ISSUESTATUSES, TASKSTATUSES, USSTATUSES, from greenmine.scrum.choices import (ISSUESTATUSES, TASKSTATUSES, USSTATUSES,
POINTS_CHOICES, SEVERITY_CHOICES, POINTS_CHOICES, SEVERITY_CHOICES,
ISSUETYPES, TASK_CHANGE_CHOICES, ISSUETYPES, TASK_CHANGE_CHOICES,
@ -173,7 +174,7 @@ class Membership(models.Model):
unique_together = ('user', 'project') unique_together = ('user', 'project')
class Project(models.Model): class Project(models.Model, WatchedMixin):
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
verbose_name=_('uuid')) verbose_name=_('uuid'))
name = models.CharField(max_length=250, unique=True, null=False, blank=False, name = models.CharField(max_length=250, unique=True, null=False, blank=False,
@ -230,15 +231,13 @@ class Project(models.Model):
@property @property
def list_of_milestones(self): def list_of_milestones(self):
return [ return [{
{ 'name': milestone.name,
'name': milestone.name, 'finish_date': milestone.estimated_finish,
'finish_date': milestone.estimated_finish, 'closed_points': milestone.closed_points,
'closed_points': milestone.closed_points, 'client_increment_points': milestone.client_increment_points,
'client_increment_points': milestone.client_increment_points, 'team_increment_points': milestone.team_increment_points
'team_increment_points': milestone.team_increment_points } for milestone in self.milestones.all().order_by('estimated_start')]
} for milestone in self.milestones.all().order_by('estimated_start')
]
@property @property
def list_roles(self): def list_roles(self):
@ -268,9 +267,14 @@ class Project(models.Model):
.exclude(role__id__in=role_ids)\ .exclude(role__id__in=role_ids)\
.delete() .delete()
class Milestone(models.Model): def _get_watchers_by_role(self):
uuid = models.CharField( return {
max_length=40, unique=True, null=False, blank=True, 'owner': self.owner,
}
class Milestone(models.Model, WatchedMixin):
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
verbose_name=_('uuid')) verbose_name=_('uuid'))
name = models.CharField( name = models.CharField(
@ -373,6 +377,12 @@ class Milestone(models.Model):
#return sum(points) #return sum(points)
return 0 return 0
def _get_watchers_by_role(self):
return {
'owner': self.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,
@ -389,7 +399,7 @@ class RolePoints(models.Model):
unique_together = ('user_story', 'role') unique_together = ('user_story', 'role')
class UserStory(models.Model): class UserStory(WatchedMixin, models.Model):
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
verbose_name=_('uuid')) verbose_name=_('uuid'))
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
@ -455,6 +465,29 @@ class UserStory(models.Model):
def is_closed(self): def is_closed(self):
return self.status.is_closed return self.status.is_closed
def _get_watchers_by_role(self):
return {
'owner': self.owner,
'suscribed_watchers': self.watchers.all(),
'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,
@ -486,7 +519,7 @@ class Attachment(models.Model):
self.content_type, self.object_id, self.id) self.content_type, self.object_id, self.id)
class Task(models.Model): class Task(models.Model, WatchedMixin):
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
verbose_name=_('uuid')) verbose_name=_('uuid'))
user_story = models.ForeignKey('UserStory', null=True, blank=False, user_story = models.ForeignKey('UserStory', null=True, blank=False,
@ -552,8 +585,16 @@ class Task(models.Model):
super(Task, self).save(*args, **kwargs) super(Task, self).save(*args, **kwargs)
def _get_watchers_by_role(self):
return {
'owner': self.owner,
'assigned_to': self.assigned_to,
'suscribed_watchers': self.watchers.all(),
'project_owner': (self.project, self.project.owner),
}
class Issue(models.Model):
class Issue(models.Model, WatchedMixin):
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
verbose_name=_('uuid')) verbose_name=_('uuid'))
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
@ -626,6 +667,14 @@ class Issue(models.Model):
def is_closed(self): def is_closed(self):
return self.status.is_closed return self.status.is_closed
def _get_watchers_by_role(self):
return {
'owner': self.owner,
'assigned_to': self.assigned_to,
'suscribed_watchers': self.watchers.all(),
'project_owner': (self.project, self.project.owner),
}
# Model related signals handlers # Model related signals handlers

View File

@ -201,6 +201,8 @@ INSTALLED_APPS = [
'greenmine.base', 'greenmine.base',
'greenmine.base.mail', 'greenmine.base.mail',
'greenmine.base.notifications',
'greenmine.base.notifications.email',
'greenmine.scrum', 'greenmine.scrum',
'greenmine.wiki', 'greenmine.wiki',
'greenmine.documents', 'greenmine.documents',