diff --git a/greenmine/base/models.py b/greenmine/base/models.py index cfc7e288..e55209a0 100644 --- a/greenmine/base/models.py +++ b/greenmine/base/models.py @@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import UserManager, AbstractUser, Group from greenmine.scrum.models import Project, UserStory, Task +from greenmine.base.notifications.models import WatcherMixin import uuid @@ -19,7 +20,6 @@ import uuid @receiver(signals.pre_save) def attach_uuid(sender, instance, **kwargs): fields = sender._meta.init_name_map() - #fields = sender._meta.get_all_field_names() if 'modified_date' in fields: instance.modified_date = now() @@ -30,9 +30,8 @@ def attach_uuid(sender, instance, **kwargs): instance.uuid = unicode(uuid.uuid1()) - -class User(AbstractUser): - color = models.CharField(max_length=9, null=False, blank=False, default="#669933", +class User(AbstractUser, WatcherMixin): + color = models.CharField(max_length=9, null=False, blank=False, verbose_name=_('color')) description = models.TextField(null=False, blank=True, verbose_name=_('description')) diff --git a/greenmine/base/notifications/__init__.py b/greenmine/base/notifications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/base/notifications/email/__init__.py b/greenmine/base/notifications/email/__init__.py new file mode 100644 index 00000000..2d0c9efd --- /dev/null +++ b/greenmine/base/notifications/email/__init__.py @@ -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 + diff --git a/greenmine/base/notifications/models.py b/greenmine/base/notifications/models.py new file mode 100644 index 00000000..5b58a842 --- /dev/null +++ b/greenmine/base/notifications/models.py @@ -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') + diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index ef7bd810..5fe41987 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -12,6 +12,7 @@ from picklefield.fields import PickledObjectField from greenmine.base.utils.slug import slugify_uniquely, ref_uniquely from greenmine.base.utils import iter_points +from greenmine.base.notifications.models import WatchedMixin from greenmine.scrum.choices import (ISSUESTATUSES, TASKSTATUSES, USSTATUSES, POINTS_CHOICES, SEVERITY_CHOICES, ISSUETYPES, TASK_CHANGE_CHOICES, @@ -173,7 +174,7 @@ class Membership(models.Model): 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, verbose_name=_('uuid')) name = models.CharField(max_length=250, unique=True, null=False, blank=False, @@ -230,15 +231,13 @@ class Project(models.Model): @property def list_of_milestones(self): - return [ - { - 'name': milestone.name, - 'finish_date': milestone.estimated_finish, - 'closed_points': milestone.closed_points, - 'client_increment_points': milestone.client_increment_points, - 'team_increment_points': milestone.team_increment_points - } for milestone in self.milestones.all().order_by('estimated_start') - ] + return [{ + 'name': milestone.name, + 'finish_date': milestone.estimated_finish, + 'closed_points': milestone.closed_points, + 'client_increment_points': milestone.client_increment_points, + 'team_increment_points': milestone.team_increment_points + } for milestone in self.milestones.all().order_by('estimated_start')] @property def list_roles(self): @@ -268,9 +267,14 @@ class Project(models.Model): .exclude(role__id__in=role_ids)\ .delete() -class Milestone(models.Model): - uuid = models.CharField( - max_length=40, unique=True, null=False, blank=True, + def _get_watchers_by_role(self): + return { + 'owner': self.owner, + } + + +class Milestone(models.Model, WatchedMixin): + uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, verbose_name=_('uuid')) name = models.CharField( @@ -373,6 +377,12 @@ class Milestone(models.Model): #return sum(points) return 0 + def _get_watchers_by_role(self): + return { + 'owner': self.owner, + 'project_owner': (self.project, self.project.owner), + } + class RolePoints(models.Model): user_story = models.ForeignKey('UserStory', null=False, blank=False, @@ -389,7 +399,7 @@ class RolePoints(models.Model): 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, verbose_name=_('uuid')) ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, @@ -455,6 +465,29 @@ class UserStory(models.Model): def is_closed(self): 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): 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) -class Task(models.Model): +class Task(models.Model, WatchedMixin): uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, verbose_name=_('uuid')) user_story = models.ForeignKey('UserStory', null=True, blank=False, @@ -552,8 +585,16 @@ class Task(models.Model): 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, verbose_name=_('uuid')) ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, @@ -626,6 +667,14 @@ class Issue(models.Model): def is_closed(self): 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 diff --git a/greenmine/settings/common.py b/greenmine/settings/common.py index 49d621e3..4812d3a2 100644 --- a/greenmine/settings/common.py +++ b/greenmine/settings/common.py @@ -201,6 +201,8 @@ INSTALLED_APPS = [ 'greenmine.base', 'greenmine.base.mail', + 'greenmine.base.notifications', + 'greenmine.base.notifications.email', 'greenmine.scrum', 'greenmine.wiki', 'greenmine.documents',