From 48b5dcb2a3e64dc15a70ea20641be43405ca50cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 15 Jul 2013 11:00:13 +0200 Subject: [PATCH 01/19] Add notifications module (not finished) --- greenmine/base/models.py | 3 +- greenmine/base/notifications/__init__.py | 0 .../base/notifications/email/__init__.py | 17 ++ greenmine/base/notifications/models.py | 145 ++++++++++++++++++ greenmine/scrum/models.py | 81 +++++++++- greenmine/settings/common.py | 2 + 6 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 greenmine/base/notifications/__init__.py create mode 100644 greenmine/base/notifications/email/__init__.py create mode 100644 greenmine/base/notifications/models.py diff --git a/greenmine/base/models.py b/greenmine/base/models.py index 7c47899b..ec274432 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 @@ -45,7 +46,7 @@ def attach_unique_reference(sender, instance, **kwargs): project.save() -class User(AbstractUser): +class User(AbstractUser, WatcherMixin): color = models.CharField(max_length=9, null=False, blank=False, verbose_name=_('color')) description = models.TextField(null=False, blank=True, 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 142f9301..d744470e 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -11,6 +11,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, @@ -167,7 +168,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, @@ -222,8 +223,25 @@ class Project(models.Model): super(Project, self).save(*args, **kwargs) + def _get_watchers_by_role(self): + return { + 'owner': self.owner, + } -class Milestone(models.Model): + def _get_attributes_to_notify(self): + return { + 'name': self.name, + 'slug': self.slug, + 'description': self.description, + 'modified_date': self.modified_date, + 'owner': self.owner.get_full_name(), + 'members': ', '.join([member.get_full_name() for member in self.members.all()]), + 'public': self.public, + 'tags': self.tags, + } + + +class Milestone(models.Model, WatchedMixin): uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, verbose_name=_('uuid')) name = models.CharField(max_length=200, db_index=True, null=False, blank=False, @@ -272,8 +290,22 @@ class Milestone(models.Model): super(Milestone, self).save(*args, **kwargs) + def _get_watchers_by_role(self): + return { + 'owner': self.owner, + 'project_owner': (self.project, self.project.owner), + } -class UserStory(models.Model): + def _get_attributes_to_notify(self): + return { + 'name': self.name, + 'slug': self.slug, + 'owner': self.owner.get_full_name(), + 'modified_date': self.modified_date, + } + + +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, @@ -338,6 +370,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, @@ -370,7 +425,7 @@ class Attachment(models.Model): ) -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=False, blank=False, @@ -443,8 +498,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, @@ -516,6 +579,14 @@ class Issue(models.Model): super(Issue, 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), + } + # Model related signals handlers diff --git a/greenmine/settings/common.py b/greenmine/settings/common.py index f6d0a414..768700f5 100644 --- a/greenmine/settings/common.py +++ b/greenmine/settings/common.py @@ -204,6 +204,8 @@ INSTALLED_APPS = [ 'greenmine.base', 'greenmine.base.mail', + 'greenmine.base.notifications', + 'greenmine.base.notifications.email', 'greenmine.scrum', 'greenmine.wiki', 'greenmine.documents', From a7104009d2f4fcfe0d9aa99c443b149b0cb69738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 10:45:37 +0200 Subject: [PATCH 02/19] Smallfix: Removed a commented code line --- greenmine/base/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/greenmine/base/models.py b/greenmine/base/models.py index 397d6f07..c764863b 100644 --- a/greenmine/base/models.py +++ b/greenmine/base/models.py @@ -20,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() From b0ed850da2573cd8a99fc9f628f2da8a3bc97c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 10:46:21 +0200 Subject: [PATCH 03/19] Smallfix: Send print message to sys.stderr --- greenmine/base/monkey.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/greenmine/base/monkey.py b/greenmine/base/monkey.py index b947d05f..1af68054 100644 --- a/greenmine/base/monkey.py +++ b/greenmine/base/monkey.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import print_function +import sys from rest_framework import views from rest_framework import status, exceptions @@ -27,5 +29,5 @@ def patch_api_view(): view.cls_instance = cls(**initkwargs) return view - print "Patching APIView" + print("Patching APIView", file=sys.stderr) views.APIView = APIView From e1d85f7843f8da7eb441ded50225dd29566586af Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 Jul 2013 11:35:06 +0200 Subject: [PATCH 04/19] Add regenerate.sh script. --- regenerate.sh | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 regenerate.sh diff --git a/regenerate.sh b/regenerate.sh new file mode 100755 index 00000000..7abd52eb --- /dev/null +++ b/regenerate.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +dropdb greenmine +createdb greenmine + +python manage.py syncdb --migrate --noinput +python manage.py loaddata initial_user +python manage.py sample_data From 3ac7398dc5e7d4b52a7fc2af7194836d7c8cd350 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 15 Jul 2013 19:44:45 +0200 Subject: [PATCH 05/19] Update requirements. --- greenmine/settings/common.py | 9 +++------ requirements.txt | 32 ++++++++++++++++++++------------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/greenmine/settings/common.py b/greenmine/settings/common.py index 2f00fa8e..4812d3a2 100644 --- a/greenmine/settings/common.py +++ b/greenmine/settings/common.py @@ -63,12 +63,7 @@ CACHES = { } PASSWORD_HASHERS = [ - 'django.contrib.auth.hashers.BCryptPasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', - 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', - 'django.contrib.auth.hashers.SHA1PasswordHasher', - 'django.contrib.auth.hashers.MD5PasswordHasher', - 'django.contrib.auth.hashers.CryptPasswordHasher', ] SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') @@ -76,6 +71,8 @@ SEND_BROKEN_LINK_EMAILS = True IGNORABLE_404_ENDS = ('.php', '.cgi') IGNORABLE_404_STARTS = ('/phpmyadmin/',) +ATOMIC_REQUESTS = True + TIME_ZONE = 'Europe/Madrid' LANGUAGE_CODE = 'en' USE_I18N = True @@ -170,7 +167,7 @@ MIDDLEWARE_CLASSES = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', #'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.transaction.TransactionMiddleware', + #'django.middleware.transaction.TransactionMiddleware', 'reversion.middleware.RevisionMiddleware', ] diff --git a/requirements.txt b/requirements.txt index b77bcf7f..ab96f44b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,30 @@ -Django==1.5.1 +git+https://github.com/django/django.git@stable/1.6.x +Fabric==1.6.0 +Flask==0.9 +Jinja2==2.6 South==0.7.6 +Werkzeug==0.8.3 Whoosh==2.4.1 -amqp==1.0.10 +amqp==1.0.12 anyjson==0.3.3 -billiard==2.7.3.23 -celery==3.0.17 -django-celery==3.0.11 +billiard==2.7.3.31 +celery==3.0.21 +django-celery==3.0.17 +django-filter==0.6 django-grappelli==2.4.4 -django-reversion==1.7 +django-guardian==1.1.0.beta git+git://github.com/toastdriven/django-haystack.git django-picklefield==0.3.0 -kombu==2.5.8 +django-reversion==1.7 +django-sampledatahelper==0.0.1 +django-tastypie==0.9.14 +djangorestframework==2.2.5 +gunicorn==17.5 +kombu==2.5.12 mimeparse==0.1.3 -py-bcrypt==0.3 +paramiko==1.10.1 +psycopg2==2.5.1 +pycrypto==2.6 python-dateutil==2.1 pytz==2013b six==1.3.0 -djangorestframework==2.2.5 -django-filter==0.6 -psycopg2==2.4.6 -django-sampledatahelper==0.0.1 From 0a58d85f8bf11970ee9d898569e3825a697dfa50 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 16 Jul 2013 09:56:46 +0200 Subject: [PATCH 06/19] Updating initial user password --- greenmine/base/fixtures/initial_user.json | 40 +++++++++++------------ 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/greenmine/base/fixtures/initial_user.json b/greenmine/base/fixtures/initial_user.json index 7b1e0cac..9de7e84c 100644 --- a/greenmine/base/fixtures/initial_user.json +++ b/greenmine/base/fixtures/initial_user.json @@ -1,26 +1,26 @@ [ { - "pk": 1, - "model": "base.user", + "pk": 1, + "model": "base.user", "fields": { - "username": "admin", - "first_name": "", - "last_name": "", - "description": "", - "default_language": "", - "color": "", - "photo": "", - "is_active": true, - "colorize_tags": false, - "default_timezone": "", - "is_superuser": true, - "token": "", - "is_staff": true, - "last_login": "2013-04-04T07:36:09.880Z", - "groups": [], - "user_permissions": [], - "password": "bcrypt$$2a$12$3DrLUj1bqp2wq7.suH6DXOUxBRyNIedWT7kr5Av7oOmA/KVkVIQGG", - "email": "niwi@niwi.be", + "username": "admin", + "first_name": "", + "last_name": "", + "description": "", + "default_language": "", + "color": "", + "photo": "", + "is_active": true, + "colorize_tags": false, + "default_timezone": "", + "is_superuser": true, + "token": "", + "is_staff": true, + "last_login": "2013-04-04T07:36:09.880Z", + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$10000$oRIbCKOL1U3w$/gaYMnOlc/GnN4mn3UUXvXpk2Hx0vvht6Uqhu46aikI=" + "email": "niwi@niwi.be", "date_joined": "2013-04-01T13:48:21.711Z" } } From ab319206bc3484925ad41eaa66c2e664872f4461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 10:14:33 +0200 Subject: [PATCH 07/19] Smallfix: Fixed the fixture, added a , --- greenmine/base/fixtures/initial_user.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/greenmine/base/fixtures/initial_user.json b/greenmine/base/fixtures/initial_user.json index 9de7e84c..b3390b6f 100644 --- a/greenmine/base/fixtures/initial_user.json +++ b/greenmine/base/fixtures/initial_user.json @@ -19,7 +19,7 @@ "last_login": "2013-04-04T07:36:09.880Z", "groups": [], "user_permissions": [], - "password": "pbkdf2_sha256$10000$oRIbCKOL1U3w$/gaYMnOlc/GnN4mn3UUXvXpk2Hx0vvht6Uqhu46aikI=" + "password": "pbkdf2_sha256$10000$oRIbCKOL1U3w$/gaYMnOlc/GnN4mn3UUXvXpk2Hx0vvht6Uqhu46aikI=", "email": "niwi@niwi.be", "date_joined": "2013-04-01T13:48:21.711Z" } From fd7c55f080bcf2588ee47963f884346d322d3cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 10:28:16 +0200 Subject: [PATCH 08/19] Smallfix: Set user token not unique --- greenmine/base/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/greenmine/base/models.py b/greenmine/base/models.py index c764863b..c4d0cfcf 100644 --- a/greenmine/base/models.py +++ b/greenmine/base/models.py @@ -42,7 +42,7 @@ class User(AbstractUser, WatcherMixin): verbose_name=_('default language')) default_timezone = models.CharField(max_length=20, null=False, blank=True, default='', verbose_name=_('default timezone')) - token = models.CharField(max_length=200, unique=True, null=False, blank=True, default='', + token = models.CharField(max_length=200, null=False, blank=True, default='', verbose_name=_('token')) colorize_tags = models.BooleanField(null=False, blank=True, default=False, verbose_name=_('colorize tags')) From 2c8349f355b6f67a4a6db96e3c08be36f90b6ecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 15 Jul 2013 11:00:13 +0200 Subject: [PATCH 09/19] Add notifications module (not finished) --- greenmine/base/models.py | 1 - greenmine/scrum/models.py | 36 +++++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/greenmine/base/models.py b/greenmine/base/models.py index c4d0cfcf..91818a4b 100644 --- a/greenmine/base/models.py +++ b/greenmine/base/models.py @@ -30,7 +30,6 @@ def attach_uuid(sender, instance, **kwargs): instance.uuid = unicode(uuid.uuid1()) - class User(AbstractUser, WatcherMixin): color = models.CharField(max_length=9, null=False, blank=False, default="#669933", verbose_name=_('color')) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 9b4a60d1..39512a3c 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -233,9 +233,7 @@ class Project(models.Model, WatchedMixin): super(Project, self).save(*args, **kwargs) def _get_watchers_by_role(self): - return { - 'owner': self.owner, - } + return {'owner': self.owner} def _get_attributes_to_notify(self): return { @@ -263,28 +261,22 @@ class Project(models.Model, WatchedMixin): class Milestone(models.Model, WatchedMixin): - uuid = models.CharField( - max_length=40, unique=True, null=False, blank=True, - verbose_name=_('uuid')) - + uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, + verbose_name=_('uuid')) name = models.CharField( max_length=200, db_index=True, null=False, blank=False, verbose_name=_('name')) - slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, verbose_name=_('slug')) - owner = models.ForeignKey( 'base.User', null=True, blank=True, related_name='owned_milestones', verbose_name=_('owner')) - project = models.ForeignKey( 'Project', null=False, blank=False, related_name='milestones', verbose_name=_('project')) - estimated_start = models.DateField(null=True, blank=True, default=None, verbose_name=_('estimated start')) estimated_finish = models.DateField(null=True, blank=True, default=None, @@ -376,6 +368,20 @@ class Milestone(models.Model, WatchedMixin): points = [ us.points.value for us in user_stories ] return sum(points) + def _get_watchers_by_role(self): + return { + 'owner': self.owner, + 'project_owner': (self.project, self.project.owner), + } + + def _get_attributes_to_notify(self): + return { + 'name': self.name, + 'slug': self.slug, + 'owner': self.owner.get_full_name(), + 'modified_date': self.modified_date, + } + class UserStory(WatchedMixin, models.Model): uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, @@ -640,6 +646,10 @@ class Issue(models.Model, WatchedMixin): super(Issue, self).save(*args, **kwargs) + @property + def is_closed(self): + return self.status.is_closed + def _get_watchers_by_role(self): return { 'owner': self.owner, @@ -648,10 +658,6 @@ class Issue(models.Model, WatchedMixin): 'project_owner': (self.project, self.project.owner), } - @property - def is_closed(self): - return self.status.is_closed - # Model related signals handlers From 29e01ab226f5451e22ba3291e81bbaff13ce1867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 10:53:18 +0200 Subject: [PATCH 10/19] Smallfix: Send more print message to sys.stderr --- greenmine/settings/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/greenmine/settings/__init__.py b/greenmine/settings/__init__.py index da0350a5..dbbe083a 100644 --- a/greenmine/settings/__init__.py +++ b/greenmine/settings/__init__.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- - -from __future__ import absolute_import -import os +from __future__ import ( + absolute_import, + print_function +) +import os, sys try: - print "Trying import local.py settings..." + print("Trying import local.py settings...", file=sys.stderr) from .local import * except ImportError: - print "Trying import development.py settings..." + print("Trying import development.py settings...", file=sys.stderr) from .development import * From 63316366ff00539768839f383fd72f5d51912508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 12:08:50 +0200 Subject: [PATCH 11/19] Smallfix: Fixed a 'trospido' rebase --- greenmine/scrum/models.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 39512a3c..0405de89 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -313,20 +313,6 @@ class Milestone(models.Model, WatchedMixin): super(Milestone, self).save(*args, **kwargs) - def _get_watchers_by_role(self): - return { - 'owner': self.owner, - 'project_owner': (self.project, self.project.owner), - } - - def _get_attributes_to_notify(self): - return { - 'name': self.name, - 'slug': self.slug, - 'owner': self.owner.get_full_name(), - 'modified_date': self.modified_date, - } - @property def closed_points(self): points = [ us.points.value for us in self.user_stories.all() if us.is_closed ] From 066ed5124dbc46794f048d3077dd862d4b548154 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 12:09:02 +0200 Subject: [PATCH 12/19] Smallfix: Fixed minor syntax error --- greenmine/base/notifications/models.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/greenmine/base/notifications/models.py b/greenmine/base/notifications/models.py index 5b58a842..06bd633a 100644 --- a/greenmine/base/notifications/models.py +++ b/greenmine/base/notifications/models.py @@ -5,7 +5,7 @@ from django.dispatch import Signal from django.utils.translation import ugettext_lazy as _ -watched_changed = Signal(providing_args = ['changed_attributes']) +watched_changed = Signal(providing_args=['changed_attributes']) class WatcherMixin(object): @@ -56,7 +56,6 @@ class WatcherMixin(object): class WatchedMixin(object): - class Meta: abstract = True @@ -70,32 +69,28 @@ class WatchedMixin(object): 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) + self.cancel_change() + 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): + 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() + 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): + 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') From 6e71ebde5e954d3f8f4fbc9ab3f3486881945a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 15 Jul 2013 11:00:13 +0200 Subject: [PATCH 13/19] Add notifications module (not finished) --- greenmine/base/models.py | 7 +- greenmine/base/notifications/__init__.py | 0 .../base/notifications/email/__init__.py | 17 ++ greenmine/base/notifications/models.py | 145 ++++++++++++++++++ greenmine/scrum/models.py | 81 ++++++++-- greenmine/settings/common.py | 2 + 6 files changed, 232 insertions(+), 20 deletions(-) create mode 100644 greenmine/base/notifications/__init__.py create mode 100644 greenmine/base/notifications/email/__init__.py create mode 100644 greenmine/base/notifications/models.py 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', From fcdcf2b997c4adebd852ce399492a76868e8b0ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 10:46:21 +0200 Subject: [PATCH 14/19] Smallfix: Send print message to sys.stderr --- greenmine/base/monkey.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/greenmine/base/monkey.py b/greenmine/base/monkey.py index b947d05f..1af68054 100644 --- a/greenmine/base/monkey.py +++ b/greenmine/base/monkey.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from __future__ import print_function +import sys from rest_framework import views from rest_framework import status, exceptions @@ -27,5 +29,5 @@ def patch_api_view(): view.cls_instance = cls(**initkwargs) return view - print "Patching APIView" + print("Patching APIView", file=sys.stderr) views.APIView = APIView From fbdb889f552c910c99fa22c833d3dcffde96aee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 15 Jul 2013 11:00:13 +0200 Subject: [PATCH 15/19] Add notifications module (not finished) --- greenmine/scrum/models.py | 59 ++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 5fe41987..0740fce8 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -268,47 +268,34 @@ class Project(models.Model, WatchedMixin): .delete() def _get_watchers_by_role(self): - return { - 'owner': self.owner, - } + 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( - max_length=200, db_index=True, null=False, blank=False, - verbose_name=_('name')) - + verbose_name=_('uuid')) + name = models.CharField(max_length=200, db_index=True, null=False, blank=False, + verbose_name=_('name')) slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, - verbose_name=_('slug')) - - owner = models.ForeignKey( - 'base.User', - null=True, blank=True, - related_name='owned_milestones', verbose_name=_('owner')) - - project = models.ForeignKey( - 'Project', - null=False, blank=False, - related_name='milestones', - verbose_name=_('project')) - + verbose_name=_('slug')) + owner = models.ForeignKey('base.User', null=True, blank=True, related_name='owned_milestones', + verbose_name=_('owner')) + project = models.ForeignKey('Project', null=False, blank=False, related_name='milestones', + verbose_name=_('project')) estimated_start = models.DateField(null=True, blank=True, default=None, - verbose_name=_('estimated start')) + verbose_name=_('estimated start')) estimated_finish = models.DateField(null=True, blank=True, default=None, - verbose_name=_('estimated finish')) + verbose_name=_('estimated finish')) created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, - verbose_name=_('created date')) + verbose_name=_('created date')) modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, - verbose_name=_('modified date')) + verbose_name=_('modified date')) closed = models.BooleanField(default=False, null=False, blank=True, - verbose_name=_('is closed')) + verbose_name=_('is closed')) disponibility = models.FloatField(default=0.0, null=True, blank=True, - verbose_name=_('disponibility')) + verbose_name=_('disponibility')) order = models.PositiveSmallIntegerField(default=1, null=False, blank=False, - verbose_name=_('order')) + verbose_name=_('order')) class Meta: verbose_name = u'milestone' @@ -398,6 +385,20 @@ class RolePoints(models.Model): class Meta: unique_together = ('user_story', 'role') + def _get_watchers_by_role(self): + return { + 'owner': self.owner, + 'project_owner': (self.project, self.project.owner), + } + + def _get_attributes_to_notify(self): + return { + 'name': self.name, + 'slug': self.slug, + 'owner': self.owner.get_full_name(), + 'modified_date': self.modified_date, + } + class UserStory(WatchedMixin, models.Model): uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, From cba07745953e4b5c2c66c1698841b5f081e5da9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 10:53:18 +0200 Subject: [PATCH 16/19] Smallfix: Send more print message to sys.stderr --- greenmine/settings/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/greenmine/settings/__init__.py b/greenmine/settings/__init__.py index da0350a5..dbbe083a 100644 --- a/greenmine/settings/__init__.py +++ b/greenmine/settings/__init__.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- - -from __future__ import absolute_import -import os +from __future__ import ( + absolute_import, + print_function +) +import os, sys try: - print "Trying import local.py settings..." + print("Trying import local.py settings...", file=sys.stderr) from .local import * except ImportError: - print "Trying import development.py settings..." + print("Trying import development.py settings...", file=sys.stderr) from .development import * From 109a1d03b7caaf368029baa9696c0507fcf645f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 12:09:02 +0200 Subject: [PATCH 17/19] Smallfix: Fixed minor syntax error --- greenmine/base/notifications/models.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/greenmine/base/notifications/models.py b/greenmine/base/notifications/models.py index 5b58a842..06bd633a 100644 --- a/greenmine/base/notifications/models.py +++ b/greenmine/base/notifications/models.py @@ -5,7 +5,7 @@ from django.dispatch import Signal from django.utils.translation import ugettext_lazy as _ -watched_changed = Signal(providing_args = ['changed_attributes']) +watched_changed = Signal(providing_args=['changed_attributes']) class WatcherMixin(object): @@ -56,7 +56,6 @@ class WatcherMixin(object): class WatchedMixin(object): - class Meta: abstract = True @@ -70,32 +69,28 @@ class WatchedMixin(object): 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) + self.cancel_change() + 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): + 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() + 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): + 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') From a4925e8be5bba8bd71a0b537078e14e45b10fe20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 16 Jul 2013 23:05:19 +0200 Subject: [PATCH 18/19] Made a refactor of the notifications app --- README.rst | 1 + greenmine/base/notifications/api.py | 37 +++++ .../base/notifications/email/__init__.py | 17 --- greenmine/base/notifications/models.py | 140 ++++++++--------- greenmine/scrum/api.py | 24 +-- greenmine/scrum/models.py | 53 ++++--- greenmine/scrum/sigdispatch.py | 143 +++++++++--------- .../create_issue_notification-body-html.jinja | 0 .../create_issue_notification-body-text.jinja | 0 .../create_issue_notification-subject.jinja | 0 ...ate_milestone_notification-body-html.jinja | 0 ...ate_milestone_notification-body-text.jinja | 0 ...reate_milestone_notification-subject.jinja | 0 ...reate_project_notification-body-html.jinja | 0 ...reate_project_notification-body-text.jinja | 0 .../create_project_notification-subject.jinja | 0 .../create_task_notification-body-html.jinja | 0 .../create_task_notification-body-text.jinja | 0 .../create_task_notification-subject.jinja | 0 ...te_user_story_notification-body-html.jinja | 0 ...te_user_story_notification-body-text.jinja | 0 ...eate_user_story_notification-subject.jinja | 0 regenerate.sh | 12 +- requirements.txt | 3 +- 24 files changed, 228 insertions(+), 202 deletions(-) create mode 100644 greenmine/base/notifications/api.py delete mode 100644 greenmine/base/notifications/email/__init__.py create mode 100644 greenmine/scrum/templates/emails/create_issue_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/create_issue_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/create_issue_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/create_milestone_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/create_milestone_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/create_milestone_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/create_project_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/create_project_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/create_project_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/create_task_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/create_task_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/create_task_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/create_user_story_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/create_user_story_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/create_user_story_notification-subject.jinja diff --git a/README.rst b/README.rst index f1e8cc5b..3e216d79 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,7 @@ Setup development environment. python manage.py syncdb --migrate --noinput python manage.py loaddata initial_user python manage.py sample_data + python manage.py createinitialrevisions Auth: admin/123123 diff --git a/greenmine/base/notifications/api.py b/greenmine/base/notifications/api.py new file mode 100644 index 00000000..77e97dcf --- /dev/null +++ b/greenmine/base/notifications/api.py @@ -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) diff --git a/greenmine/base/notifications/email/__init__.py b/greenmine/base/notifications/email/__init__.py deleted file mode 100644 index 2d0c9efd..00000000 --- a/greenmine/base/notifications/email/__init__.py +++ /dev/null @@ -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 - diff --git a/greenmine/base/notifications/models.py b/greenmine/base/notifications/models.py index 06bd633a..5e933fe9 100644 --- a/greenmine/base/notifications/models.py +++ b/greenmine/base/notifications/models.py @@ -1,99 +1,94 @@ # -*- coding: utf-8 -*- 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 _ - -watched_changed = Signal(providing_args=['changed_attributes']) +import reversion 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')), + ("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_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')) + 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', + "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', + "only_assigned", + "only_watching", + "all_owned_projects", ]) def allow_notify_suscribed(self): return (self.notify_level in [ - 'only_watching', - 'all_owned_projects', + "only_watching", + "all_owned_projects", ]) def allow_notify_project(self, project): - return self.notify_level == 'all_owned_projects' \ - and project.owner.pk == self.pk + 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 + 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() + @property + def last_version(self): + version_list = reversion.get_for_object(self) + return version_list and version_list[0] or None - def cancel_change(self): - del self._changer - del self._saved_attributes + def get_changed_fields_dict(self, data_dict): + field_dict = {} + 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): - changed_attributes = self._get_changed_attributes() - self.cancel_change() - watched_changed.send(sender=self, changed_attributes=changed_attributes) - - def get_watchers_to_notify(self): + def get_watchers_to_notify(self, changer): watchers_to_notify = set() 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() - and owner.allow_notify_by_me(self._changer)): + and owner.allow_notify_by_me(changer)): 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() - and assigned_to.allow_notify_by_me(self._changer)): + and assigned_to.allow_notify_by_me(changer)): 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: for suscribed_watcher in suscribed_watchers: 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) - #(project, project_owner) = watchers_by_role.get('project_owner') + #(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): @@ -101,40 +96,39 @@ class WatchedMixin(object): 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_changed_field_verbose_name(self, field_name): + try: + return self._meta.get_field(field_name).verbose_name + except FieldDoesNotExist: + return field_name + + def _get_changed_field_old_value(self, field_name, data_value): + 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): - ''' + """ 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), + "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') - + """ + raise NotImplementedError("You must subclass WatchedMixin and provide _get_watchers_by_role method") diff --git a/greenmine/scrum/api.py b/greenmine/scrum/api.py index 9643d27e..53103ed3 100644 --- a/greenmine/scrum/api.py +++ b/greenmine/scrum/api.py @@ -6,6 +6,8 @@ import django_filters from rest_framework import generics from rest_framework.permissions import IsAuthenticated +from greenmine.base.notifications.api import NotificationSenderMixin + from greenmine.scrum.serializers import * from greenmine.scrum.models import * from greenmine.scrum.permissions import * @@ -47,7 +49,7 @@ class SimpleFilterMixin(object): return queryset -class ProjectList(generics.ListCreateAPIView): +class ProjectList(NotificationSenderMixin, generics.ListCreateAPIView): model = Project serializer_class = ProjectSerializer permission_classes = (IsAuthenticated,) @@ -61,13 +63,13 @@ class ProjectList(generics.ListCreateAPIView): obj.owner = self.request.user -class ProjectDetail(generics.RetrieveUpdateDestroyAPIView): +class ProjectDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView): model = Project serializer_class = ProjectSerializer permission_classes = (IsAuthenticated, ProjectDetailPermission,) -class MilestoneList(generics.ListCreateAPIView): +class MilestoneList(NotificationSenderMixin, generics.ListCreateAPIView): model = Milestone serializer_class = MilestoneSerializer filter_fields = ('project',) @@ -80,13 +82,13 @@ class MilestoneList(generics.ListCreateAPIView): obj.owner = self.request.user -class MilestoneDetail(generics.RetrieveUpdateDestroyAPIView): +class MilestoneDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView): model = Milestone serializer_class = MilestoneSerializer permission_classes = (IsAuthenticated, MilestoneDetailPermission,) -class UserStoryList(generics.ListCreateAPIView): +class UserStoryList(NotificationSenderMixin, generics.ListCreateAPIView): model = UserStory serializer_class = UserStorySerializer filter_class = UserStoryFilter @@ -99,7 +101,7 @@ class UserStoryList(generics.ListCreateAPIView): obj.owner = self.request.user -class UserStoryDetail(generics.RetrieveUpdateDestroyAPIView): +class UserStoryDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView): model = UserStory serializer_class = UserStorySerializer permission_classes = (IsAuthenticated, UserStoryDetailPermission,) @@ -157,7 +159,7 @@ class TasksAttachmentDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = (IsAuthenticated, AttachmentDetailPermission,) -class TaskList(generics.ListCreateAPIView): +class TaskList(NotificationSenderMixin, generics.ListCreateAPIView): model = Task serializer_class = TaskSerializer filter_fields = ('user_story', 'milestone', 'project') @@ -171,7 +173,7 @@ class TaskList(generics.ListCreateAPIView): obj.milestone = obj.user_story.milestone -class TaskDetail(generics.RetrieveUpdateDestroyAPIView): +class TaskDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView): model = Task serializer_class = TaskSerializer permission_classes = (IsAuthenticated, TaskDetailPermission,) @@ -181,9 +183,10 @@ class TaskDetail(generics.RetrieveUpdateDestroyAPIView): if "comment" in self.request.DATA: # Update the comment in the last version 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 serializer_class = IssueSerializer filter_fields = ('project',) @@ -196,7 +199,7 @@ class IssueList(generics.ListCreateAPIView): return self.model.objects.filter(project__members=self.request.user) -class IssueDetail(generics.RetrieveUpdateDestroyAPIView): +class IssueDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView): model = Issue serializer_class = IssueSerializer permission_classes = (IsAuthenticated, IssueDetailPermission,) @@ -206,6 +209,7 @@ class IssueDetail(generics.RetrieveUpdateDestroyAPIView): if "comment" in self.request.DATA: # Update the comment in the last version reversion.set_comment(self.request.DATA['comment']) + super(IssueDetail, self).post_save(obj, created) class SeverityList(generics.ListCreateAPIView): diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index c26a6c98..587f698d 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -10,13 +10,24 @@ from django.db.models.loading import get_model 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.notifications.models import WatchedMixin -from greenmine.scrum.choices import (ISSUESTATUSES, TASKSTATUSES, USSTATUSES, - POINTS_CHOICES, SEVERITY_CHOICES, - ISSUETYPES, TASK_CHANGE_CHOICES, - PRIORITY_CHOICES) +from greenmine.scrum.choices import ( + ISSUESTATUSES, + TASKSTATUSES, + USSTATUSES, + POINTS_CHOICES, + SEVERITY_CHOICES, + ISSUETYPES, + TASK_CHANGE_CHOICES, + PRIORITY_CHOICES +) + +import reversion class Severity(models.Model): @@ -232,7 +243,7 @@ class Project(models.Model, WatchedMixin): def _get_watchers_by_role(self): return {'owner': self.owner} - def _get_attributes_to_notify(self): + def eget_attrinutes_to_notify(self): return { 'name': self.name, 'slug': self.slug, @@ -385,7 +396,6 @@ class Milestone(models.Model, WatchedMixin): 'project_owner': (self.project, self.project.owner), } - class RolePoints(models.Model): user_story = models.ForeignKey('UserStory', null=False, blank=False, related_name='role_points', @@ -474,22 +484,6 @@ class UserStory(WatchedMixin, models.Model): '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, @@ -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 @receiver(models.signals.post_save, sender=Project, dispatch_uid='project_post_save') @@ -764,7 +767,3 @@ def tasks_close_handler(sender, instance, **kwargs): else: instance.user_story.finish_date = None instance.user_story.save() - -# Email alerts signals handlers -# TODO: temporary commented (Pending refactor) -# from . import sigdispatch diff --git a/greenmine/scrum/sigdispatch.py b/greenmine/scrum/sigdispatch.py index ec46bbef..ffd60945 100644 --- a/greenmine/scrum/sigdispatch.py +++ b/greenmine/scrum/sigdispatch.py @@ -32,74 +32,75 @@ def mail_recovery_password(sender, user, **kwargs): subject = ugettext("Greenmine: password recovery.") send_mail.delay(subject, template, [user.email]) - -@receiver(signals.mail_milestone_created) -def mail_milestone_created(sender, milestone, user, **kwargs): - participants = milestone.project.all_participants() - - emails_list = [] - subject = ugettext("Greenmine: sprint created") - for person in participants: - template = render_to_string("email/milestone.created.html", { - "person": person, - "current_host": settings.HOST, - "milestone": milestone, - "user": user, - }) - - emails_list.append([subject, template, [person.email]]) - - send_bulk_mail.delay(emails_list) - - -@receiver(signals.mail_userstory_created) -def mail_userstory_created(sender, us, user, **kwargs): - participants = us.milestone.project.all_participants() - - emails_list = [] - subject = ugettext("Greenmine: user story created") - - for person in participants: - template = render_to_string("email/userstory.created.html", { - "person": person, - "current_host": settings.HOST, - "us": us, - "user": user, - }) - - emails_list.append([subject, template, [person.email]]) - - send_bulk_mail.delay(emails_list) - - -@receiver(signals.mail_task_created) -def mail_task_created(sender, task, user, **kwargs): - participants = task.us.milestone.project.all_participants() - - emails_list = [] - subject = ugettext("Greenmine: task created") - - for person in participants: - template = render_to_string("email/task.created.html", { - "person": person, - "current_host": settings.HOST, - "task": task, - "user": user, - }) - - emails_list.append([subject, template, [person.email]]) - - send_bulk_mail.delay(emails_list) - - -@receiver(signals.mail_task_assigned) -def mail_task_assigned(sender, task, user, **kwargs): - template = render_to_string("email/task.assigned.html", { - "person": task.assigned_to, - "task": task, - "user": user, - "current_host": settings.HOST, - }) - - subject = ugettext("Greenmine: task assigned") - send_mail.delay(subject, template, [task.assigned_to.email]) +## TODO: Remove me when base.notifications is finished +## +#@receiver(signals.mail_milestone_created) +#def mail_milestone_created(sender, milestone, user, **kwargs): +# participants = milestone.project.all_participants() +# +# emails_list = [] +# subject = ugettext("Greenmine: sprint created") +# for person in participants: +# template = render_to_string("email/milestone.created.html", { +# "person": person, +# "current_host": settings.HOST, +# "milestone": milestone, +# "user": user, +# }) +# +# emails_list.append([subject, template, [person.email]]) +# +# send_bulk_mail.delay(emails_list) +# +# +#@receiver(signals.mail_userstory_created) +#def mail_userstory_created(sender, us, user, **kwargs): +# participants = us.milestone.project.all_participants() +# +# emails_list = [] +# subject = ugettext("Greenmine: user story created") +# +# for person in participants: +# template = render_to_string("email/userstory.created.html", { +# "person": person, +# "current_host": settings.HOST, +# "us": us, +# "user": user, +# }) +# +# emails_list.append([subject, template, [person.email]]) +# +# send_bulk_mail.delay(emails_list) +# +# +#@receiver(signals.mail_task_created) +#def mail_task_created(sender, task, user, **kwargs): +# participants = task.us.milestone.project.all_participants() +# +# emails_list = [] +# subject = ugettext("Greenmine: task created") +# +# for person in participants: +# template = render_to_string("email/task.created.html", { +# "person": person, +# "current_host": settings.HOST, +# "task": task, +# "user": user, +# }) +# +# emails_list.append([subject, template, [person.email]]) +# +# send_bulk_mail.delay(emails_list) +# +# +#@receiver(signals.mail_task_assigned) +#def mail_task_assigned(sender, task, user, **kwargs): +# template = render_to_string("email/task.assigned.html", { +# "person": task.assigned_to, +# "task": task, +# "user": user, +# "current_host": settings.HOST, +# }) +# +# subject = ugettext("Greenmine: task assigned") +# send_mail.delay(subject, template, [task.assigned_to.email]) diff --git a/greenmine/scrum/templates/emails/create_issue_notification-body-html.jinja b/greenmine/scrum/templates/emails/create_issue_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_issue_notification-body-text.jinja b/greenmine/scrum/templates/emails/create_issue_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_issue_notification-subject.jinja b/greenmine/scrum/templates/emails/create_issue_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_milestone_notification-body-html.jinja b/greenmine/scrum/templates/emails/create_milestone_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_milestone_notification-body-text.jinja b/greenmine/scrum/templates/emails/create_milestone_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_milestone_notification-subject.jinja b/greenmine/scrum/templates/emails/create_milestone_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_project_notification-body-html.jinja b/greenmine/scrum/templates/emails/create_project_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_project_notification-body-text.jinja b/greenmine/scrum/templates/emails/create_project_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_project_notification-subject.jinja b/greenmine/scrum/templates/emails/create_project_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_task_notification-body-html.jinja b/greenmine/scrum/templates/emails/create_task_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_task_notification-body-text.jinja b/greenmine/scrum/templates/emails/create_task_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_task_notification-subject.jinja b/greenmine/scrum/templates/emails/create_task_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_user_story_notification-body-html.jinja b/greenmine/scrum/templates/emails/create_user_story_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_user_story_notification-body-text.jinja b/greenmine/scrum/templates/emails/create_user_story_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/create_user_story_notification-subject.jinja b/greenmine/scrum/templates/emails/create_user_story_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/regenerate.sh b/regenerate.sh index 7abd52eb..3c69ca24 100755 --- a/regenerate.sh +++ b/regenerate.sh @@ -1,8 +1,14 @@ #!/bin/bash +# For sqlite +rm -f database.sqlite + +# For postgresql dropdb greenmine createdb greenmine -python manage.py syncdb --migrate --noinput -python manage.py loaddata initial_user -python manage.py sample_data +python manage.py syncdb --migrate --noinput --traceback +python manage.py loaddata initial_user --traceback +python manage.py sample_data --traceback +python manage.py createinitialrevisions --traceback + diff --git a/requirements.txt b/requirements.txt index ab96f44b..add67eec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,7 +17,6 @@ git+git://github.com/toastdriven/django-haystack.git django-picklefield==0.3.0 django-reversion==1.7 django-sampledatahelper==0.0.1 -django-tastypie==0.9.14 djangorestframework==2.2.5 gunicorn==17.5 kombu==2.5.12 @@ -28,3 +27,5 @@ pycrypto==2.6 python-dateutil==2.1 pytz==2013b six==1.3.0 +djmail>=0.1 + From 641ecfbc22ad1218ec5c4d22782590edd7194890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 17 Jul 2013 11:08:06 +0200 Subject: [PATCH 19/19] Added all notification emails --- greenmine/base/notifications/api.py | 10 +++++-- greenmine/scrum/api.py | 30 +++++++++++++++++++ ...destroy_issue_notification-body-html.jinja | 0 ...destroy_issue_notification-body-text.jinja | 0 .../destroy_issue_notification-subject.jinja | 0 ...roy_milestone_notification-body-html.jinja | 0 ...roy_milestone_notification-body-text.jinja | 0 ...stroy_milestone_notification-subject.jinja | 0 ...stroy_project_notification-body-html.jinja | 0 ...stroy_project_notification-body-text.jinja | 0 ...destroy_project_notification-subject.jinja | 0 .../destroy_task_notification-body-html.jinja | 0 .../destroy_task_notification-body-text.jinja | 0 .../destroy_task_notification-subject.jinja | 0 ...oy_user_story_notification-body-html.jinja | 0 ...oy_user_story_notification-body-text.jinja | 0 ...troy_user_story_notification-subject.jinja | 0 .../update_issue_notification-body-html.jinja | 0 .../update_issue_notification-body-text.jinja | 0 .../update_issue_notification-subject.jinja | 0 ...ate_milestone_notification-body-html.jinja | 0 ...ate_milestone_notification-body-text.jinja | 0 ...pdate_milestone_notification-subject.jinja | 0 ...pdate_project_notification-body-html.jinja | 0 ...pdate_project_notification-body-text.jinja | 0 .../update_project_notification-subject.jinja | 0 .../update_task_notification-body-html.jinja | 0 .../update_task_notification-body-text.jinja | 0 .../update_task_notification-subject.jinja | 0 ...te_user_story_notification-body-html.jinja | 0 ...te_user_story_notification-body-text.jinja | 0 ...date_user_story_notification-subject.jinja | 0 32 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 greenmine/scrum/templates/emails/destroy_issue_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_issue_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_issue_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_milestone_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_milestone_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_milestone_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_project_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_project_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_project_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_task_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_task_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_task_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_user_story_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_user_story_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/destroy_user_story_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/update_issue_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/update_issue_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/update_issue_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/update_milestone_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/update_milestone_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/update_milestone_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/update_project_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/update_project_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/update_project_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/update_task_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/update_task_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/update_task_notification-subject.jinja create mode 100644 greenmine/scrum/templates/emails/update_user_story_notification-body-html.jinja create mode 100644 greenmine/scrum/templates/emails/update_user_story_notification-body-text.jinja create mode 100644 greenmine/scrum/templates/emails/update_user_story_notification-subject.jinja diff --git a/greenmine/base/notifications/api.py b/greenmine/base/notifications/api.py index 77e97dcf..7509cadc 100644 --- a/greenmine/base/notifications/api.py +++ b/greenmine/base/notifications/api.py @@ -23,9 +23,11 @@ class NotificationSenderMixin(object): } if created: - self._send_notification_email(self.create_notification_template, users=users, context=context) + #self._send_notification_email(self.create_notification_template, users=users, context=context) + print "TODO: Send the notification email of object creation" else: - self._send_notification_email(self.update_notification_template, users=users, context=context) + #self._send_notification_email(self.update_notification_template, users=users, context=context) + print "TODO: Send the notification email of object modification" def destroy(self, request, *args, **kwargs): users = obj.get_watchers_to_notify(self.request.user) @@ -33,5 +35,7 @@ class NotificationSenderMixin(object): 'changer': self.request.user, 'object': obj } - self._send_notification_email(self.destroy_notification_template, users=users, context=context) + #self._send_notification_email(self.destroy_notification_template, users=users, context=context) + print "TODO: Send the notification email of object deletion" + return super(NotificationSenderMixin, self).destroy(request, *args, **kwargs) diff --git a/greenmine/scrum/api.py b/greenmine/scrum/api.py index 53103ed3..f3ddac25 100644 --- a/greenmine/scrum/api.py +++ b/greenmine/scrum/api.py @@ -53,6 +53,9 @@ class ProjectList(NotificationSenderMixin, generics.ListCreateAPIView): model = Project serializer_class = ProjectSerializer permission_classes = (IsAuthenticated,) + create_notification_template = "create_project_notification" + update_notification_template = "update_project_notification" + destroy_notification_template = "destroy_project_notification" def get_queryset(self): return self.model.objects.filter( @@ -67,6 +70,9 @@ class ProjectDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIVi model = Project serializer_class = ProjectSerializer permission_classes = (IsAuthenticated, ProjectDetailPermission,) + create_notification_template = "create_project_notification" + update_notification_template = "update_project_notification" + destroy_notification_template = "destroy_project_notification" class MilestoneList(NotificationSenderMixin, generics.ListCreateAPIView): @@ -74,6 +80,9 @@ class MilestoneList(NotificationSenderMixin, generics.ListCreateAPIView): serializer_class = MilestoneSerializer filter_fields = ('project',) permission_classes = (IsAuthenticated,) + create_notification_template = "create_milestone_notification" + update_notification_template = "update_milestone_notification" + destroy_notification_template = "destroy_milestone_notification" def get_queryset(self): return self.model.objects.filter(project__members=self.request.user) @@ -86,6 +95,9 @@ class MilestoneDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPI model = Milestone serializer_class = MilestoneSerializer permission_classes = (IsAuthenticated, MilestoneDetailPermission,) + create_notification_template = "create_milestone_notification" + update_notification_template = "update_milestone_notification" + destroy_notification_template = "destroy_milestone_notification" class UserStoryList(NotificationSenderMixin, generics.ListCreateAPIView): @@ -93,6 +105,9 @@ class UserStoryList(NotificationSenderMixin, generics.ListCreateAPIView): serializer_class = UserStorySerializer filter_class = UserStoryFilter permission_classes = (IsAuthenticated,) + create_notification_template = "create_user_story_notification" + update_notification_template = "update_user_story_notification" + destroy_notification_template = "destroy_user_story_notification" def get_queryset(self): return self.model.objects.filter(project__members=self.request.user) @@ -105,6 +120,9 @@ class UserStoryDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPI model = UserStory serializer_class = UserStorySerializer permission_classes = (IsAuthenticated, UserStoryDetailPermission,) + create_notification_template = "create_user_story_notification" + update_notification_template = "update_user_story_notification" + destroy_notification_template = "destroy_user_story_notification" class AttachmentFilter(django_filters.FilterSet): @@ -164,6 +182,9 @@ class TaskList(NotificationSenderMixin, generics.ListCreateAPIView): serializer_class = TaskSerializer filter_fields = ('user_story', 'milestone', 'project') permission_classes = (IsAuthenticated,) + create_notification_template = "create_task_notification" + update_notification_template = "update_task_notification" + destroy_notification_template = "destroy_task_notification" def get_queryset(self): return self.model.objects.filter(project__members=self.request.user) @@ -177,6 +198,9 @@ class TaskDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView) model = Task serializer_class = TaskSerializer permission_classes = (IsAuthenticated, TaskDetailPermission,) + create_notification_template = "create_task_notification" + update_notification_template = "update_task_notification" + destroy_notification_template = "destroy_task_notification" def post_save(self, obj, created=False): with reversion.create_revision(): @@ -191,6 +215,9 @@ class IssueList(NotificationSenderMixin, generics.ListCreateAPIView): serializer_class = IssueSerializer filter_fields = ('project',) permission_classes = (IsAuthenticated,) + create_notification_template = "create_issue_notification" + update_notification_template = "update_issue_notification" + destroy_notification_template = "destroy_issue_notification" def pre_save(self, obj): obj.owner = self.request.user @@ -203,6 +230,9 @@ class IssueDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView model = Issue serializer_class = IssueSerializer permission_classes = (IsAuthenticated, IssueDetailPermission,) + create_notification_template = "create_issue_notification" + update_notification_template = "update_issue_notification" + destroy_notification_template = "destroy_issue_notification" def post_save(self, obj, created=False): with reversion.create_revision(): diff --git a/greenmine/scrum/templates/emails/destroy_issue_notification-body-html.jinja b/greenmine/scrum/templates/emails/destroy_issue_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_issue_notification-body-text.jinja b/greenmine/scrum/templates/emails/destroy_issue_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_issue_notification-subject.jinja b/greenmine/scrum/templates/emails/destroy_issue_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_milestone_notification-body-html.jinja b/greenmine/scrum/templates/emails/destroy_milestone_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_milestone_notification-body-text.jinja b/greenmine/scrum/templates/emails/destroy_milestone_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_milestone_notification-subject.jinja b/greenmine/scrum/templates/emails/destroy_milestone_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_project_notification-body-html.jinja b/greenmine/scrum/templates/emails/destroy_project_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_project_notification-body-text.jinja b/greenmine/scrum/templates/emails/destroy_project_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_project_notification-subject.jinja b/greenmine/scrum/templates/emails/destroy_project_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_task_notification-body-html.jinja b/greenmine/scrum/templates/emails/destroy_task_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_task_notification-body-text.jinja b/greenmine/scrum/templates/emails/destroy_task_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_task_notification-subject.jinja b/greenmine/scrum/templates/emails/destroy_task_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_user_story_notification-body-html.jinja b/greenmine/scrum/templates/emails/destroy_user_story_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_user_story_notification-body-text.jinja b/greenmine/scrum/templates/emails/destroy_user_story_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/destroy_user_story_notification-subject.jinja b/greenmine/scrum/templates/emails/destroy_user_story_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_issue_notification-body-html.jinja b/greenmine/scrum/templates/emails/update_issue_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_issue_notification-body-text.jinja b/greenmine/scrum/templates/emails/update_issue_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_issue_notification-subject.jinja b/greenmine/scrum/templates/emails/update_issue_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_milestone_notification-body-html.jinja b/greenmine/scrum/templates/emails/update_milestone_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_milestone_notification-body-text.jinja b/greenmine/scrum/templates/emails/update_milestone_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_milestone_notification-subject.jinja b/greenmine/scrum/templates/emails/update_milestone_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_project_notification-body-html.jinja b/greenmine/scrum/templates/emails/update_project_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_project_notification-body-text.jinja b/greenmine/scrum/templates/emails/update_project_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_project_notification-subject.jinja b/greenmine/scrum/templates/emails/update_project_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_task_notification-body-html.jinja b/greenmine/scrum/templates/emails/update_task_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_task_notification-body-text.jinja b/greenmine/scrum/templates/emails/update_task_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_task_notification-subject.jinja b/greenmine/scrum/templates/emails/update_task_notification-subject.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_user_story_notification-body-html.jinja b/greenmine/scrum/templates/emails/update_user_story_notification-body-html.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_user_story_notification-body-text.jinja b/greenmine/scrum/templates/emails/update_user_story_notification-body-text.jinja new file mode 100644 index 00000000..e69de29b diff --git a/greenmine/scrum/templates/emails/update_user_story_notification-subject.jinja b/greenmine/scrum/templates/emails/update_user_story_notification-subject.jinja new file mode 100644 index 00000000..e69de29b