Merge branch 'notifications'
Conflicts: greenmine/scrum/api.py greenmine/scrum/models.pyremotes/origin/enhancement/email-actions
commit
50566807c0
|
@ -11,6 +11,7 @@ Setup development environment.
|
||||||
python manage.py syncdb --migrate --noinput
|
python manage.py syncdb --migrate --noinput
|
||||||
python manage.py loaddata initial_user
|
python manage.py loaddata initial_user
|
||||||
python manage.py sample_data
|
python manage.py sample_data
|
||||||
|
python manage.py createinitialrevisions
|
||||||
|
|
||||||
|
|
||||||
Auth: admin/123123
|
Auth: admin/123123
|
||||||
|
|
|
@ -11,6 +11,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
from django.contrib.auth.models import UserManager, AbstractUser, Group
|
from django.contrib.auth.models import UserManager, AbstractUser, Group
|
||||||
|
|
||||||
from greenmine.scrum.models import Project, UserStory, Task
|
from greenmine.scrum.models import Project, UserStory, Task
|
||||||
|
from greenmine.base.notifications.models import WatcherMixin
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@ -19,7 +20,6 @@ import uuid
|
||||||
@receiver(signals.pre_save)
|
@receiver(signals.pre_save)
|
||||||
def attach_uuid(sender, instance, **kwargs):
|
def attach_uuid(sender, instance, **kwargs):
|
||||||
fields = sender._meta.init_name_map()
|
fields = sender._meta.init_name_map()
|
||||||
#fields = sender._meta.get_all_field_names()
|
|
||||||
|
|
||||||
if 'modified_date' in fields:
|
if 'modified_date' in fields:
|
||||||
instance.modified_date = now()
|
instance.modified_date = now()
|
||||||
|
@ -30,8 +30,7 @@ def attach_uuid(sender, instance, **kwargs):
|
||||||
instance.uuid = unicode(uuid.uuid1())
|
instance.uuid = unicode(uuid.uuid1())
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractUser, WatcherMixin):
|
||||||
class User(AbstractUser):
|
|
||||||
color = models.CharField(max_length=9, null=False, blank=False, default="#669933",
|
color = models.CharField(max_length=9, null=False, blank=False, default="#669933",
|
||||||
verbose_name=_('color'))
|
verbose_name=_('color'))
|
||||||
description = models.TextField(null=False, blank=True,
|
description = models.TextField(null=False, blank=True,
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import print_function
|
||||||
|
import sys
|
||||||
|
|
||||||
from rest_framework import views
|
from rest_framework import views
|
||||||
from rest_framework import status, exceptions
|
from rest_framework import status, exceptions
|
||||||
|
@ -27,5 +29,5 @@ def patch_api_view():
|
||||||
view.cls_instance = cls(**initkwargs)
|
view.cls_instance = cls(**initkwargs)
|
||||||
return view
|
return view
|
||||||
|
|
||||||
print "Patching APIView"
|
print("Patching APIView", file=sys.stderr)
|
||||||
views.APIView = APIView
|
views.APIView = APIView
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
# -*- 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)
|
||||||
|
print "TODO: Send the notification email of object creation"
|
||||||
|
else:
|
||||||
|
#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)
|
||||||
|
context = {
|
||||||
|
'changer': self.request.user,
|
||||||
|
'object': obj
|
||||||
|
}
|
||||||
|
#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)
|
|
@ -0,0 +1,134 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.fields import FieldDoesNotExist
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
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")),
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_version(self):
|
||||||
|
version_list = reversion.get_for_object(self)
|
||||||
|
return version_list and version_list[0] or None
|
||||||
|
|
||||||
|
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 get_watchers_to_notify(self, changer):
|
||||||
|
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(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(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(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_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),
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("You must subclass WatchedMixin and provide _get_watchers_by_role method")
|
|
@ -7,10 +7,13 @@ from rest_framework import generics
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
from greenmine.base.models import *
|
from greenmine.base.models import *
|
||||||
|
from greenmine.base.notifications.api import NotificationSenderMixin
|
||||||
|
|
||||||
from greenmine.scrum.serializers import *
|
from greenmine.scrum.serializers import *
|
||||||
from greenmine.scrum.models import *
|
from greenmine.scrum.models import *
|
||||||
from greenmine.scrum.permissions import *
|
from greenmine.scrum.permissions import *
|
||||||
|
|
||||||
|
|
||||||
class UserStoryFilter(django_filters.FilterSet):
|
class UserStoryFilter(django_filters.FilterSet):
|
||||||
no_milestone = django_filters.NumberFilter(name="milestone", lookup_type='isnull')
|
no_milestone = django_filters.NumberFilter(name="milestone", lookup_type='isnull')
|
||||||
|
|
||||||
|
@ -47,10 +50,13 @@ class SimpleFilterMixin(object):
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class ProjectList(generics.ListCreateAPIView):
|
class ProjectList(NotificationSenderMixin, generics.ListCreateAPIView):
|
||||||
model = Project
|
model = Project
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
|
create_notification_template = "create_project_notification"
|
||||||
|
update_notification_template = "update_project_notification"
|
||||||
|
destroy_notification_template = "destroy_project_notification"
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.model.objects.filter(
|
return self.model.objects.filter(
|
||||||
|
@ -61,17 +67,23 @@ class ProjectList(generics.ListCreateAPIView):
|
||||||
obj.owner = self.request.user
|
obj.owner = self.request.user
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetail(generics.RetrieveUpdateDestroyAPIView):
|
class ProjectDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Project
|
model = Project
|
||||||
serializer_class = ProjectSerializer
|
serializer_class = ProjectSerializer
|
||||||
permission_classes = (IsAuthenticated, ProjectDetailPermission,)
|
permission_classes = (IsAuthenticated, ProjectDetailPermission,)
|
||||||
|
create_notification_template = "create_project_notification"
|
||||||
|
update_notification_template = "update_project_notification"
|
||||||
|
destroy_notification_template = "destroy_project_notification"
|
||||||
|
|
||||||
|
|
||||||
class MilestoneList(generics.ListCreateAPIView):
|
class MilestoneList(NotificationSenderMixin, generics.ListCreateAPIView):
|
||||||
model = Milestone
|
model = Milestone
|
||||||
serializer_class = MilestoneSerializer
|
serializer_class = MilestoneSerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
permission_classes = (IsAuthenticated,)
|
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):
|
def get_queryset(self):
|
||||||
return self.model.objects.filter(project__members=self.request.user)
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
@ -80,17 +92,23 @@ class MilestoneList(generics.ListCreateAPIView):
|
||||||
obj.owner = self.request.user
|
obj.owner = self.request.user
|
||||||
|
|
||||||
|
|
||||||
class MilestoneDetail(generics.RetrieveUpdateDestroyAPIView):
|
class MilestoneDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Milestone
|
model = Milestone
|
||||||
serializer_class = MilestoneSerializer
|
serializer_class = MilestoneSerializer
|
||||||
permission_classes = (IsAuthenticated, MilestoneDetailPermission,)
|
permission_classes = (IsAuthenticated, MilestoneDetailPermission,)
|
||||||
|
create_notification_template = "create_milestone_notification"
|
||||||
|
update_notification_template = "update_milestone_notification"
|
||||||
|
destroy_notification_template = "destroy_milestone_notification"
|
||||||
|
|
||||||
|
|
||||||
class UserStoryList(generics.ListCreateAPIView):
|
class UserStoryList(NotificationSenderMixin, generics.ListCreateAPIView):
|
||||||
model = UserStory
|
model = UserStory
|
||||||
serializer_class = UserStorySerializer
|
serializer_class = UserStorySerializer
|
||||||
filter_class = UserStoryFilter
|
filter_class = UserStoryFilter
|
||||||
permission_classes = (IsAuthenticated,)
|
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):
|
def get_queryset(self):
|
||||||
return self.model.objects.filter(project__members=self.request.user)
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
@ -99,10 +117,13 @@ class UserStoryList(generics.ListCreateAPIView):
|
||||||
obj.owner = self.request.user
|
obj.owner = self.request.user
|
||||||
|
|
||||||
|
|
||||||
class UserStoryDetail(generics.RetrieveUpdateDestroyAPIView):
|
class UserStoryDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = UserStory
|
model = UserStory
|
||||||
serializer_class = UserStorySerializer
|
serializer_class = UserStorySerializer
|
||||||
permission_classes = (IsAuthenticated, UserStoryDetailPermission,)
|
permission_classes = (IsAuthenticated, UserStoryDetailPermission,)
|
||||||
|
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):
|
class AttachmentFilter(django_filters.FilterSet):
|
||||||
|
@ -157,11 +178,14 @@ class TasksAttachmentDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
permission_classes = (IsAuthenticated, AttachmentDetailPermission,)
|
permission_classes = (IsAuthenticated, AttachmentDetailPermission,)
|
||||||
|
|
||||||
|
|
||||||
class TaskList(generics.ListCreateAPIView):
|
class TaskList(NotificationSenderMixin, generics.ListCreateAPIView):
|
||||||
model = Task
|
model = Task
|
||||||
serializer_class = TaskSerializer
|
serializer_class = TaskSerializer
|
||||||
filter_fields = ('user_story', 'milestone', 'project')
|
filter_fields = ('user_story', 'milestone', 'project')
|
||||||
permission_classes = (IsAuthenticated,)
|
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):
|
def get_queryset(self):
|
||||||
return self.model.objects.filter(project__members=self.request.user)
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
@ -171,23 +195,30 @@ class TaskList(generics.ListCreateAPIView):
|
||||||
obj.milestone = obj.user_story.milestone
|
obj.milestone = obj.user_story.milestone
|
||||||
|
|
||||||
|
|
||||||
class TaskDetail(generics.RetrieveUpdateDestroyAPIView):
|
class TaskDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Task
|
model = Task
|
||||||
serializer_class = TaskSerializer
|
serializer_class = TaskSerializer
|
||||||
permission_classes = (IsAuthenticated, TaskDetailPermission,)
|
permission_classes = (IsAuthenticated, TaskDetailPermission,)
|
||||||
|
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):
|
def post_save(self, obj, created=False):
|
||||||
with reversion.create_revision():
|
with reversion.create_revision():
|
||||||
if "comment" in self.request.DATA:
|
if "comment" in self.request.DATA:
|
||||||
# Update the comment in the last version
|
# Update the comment in the last version
|
||||||
reversion.set_comment(self.request.DATA['comment'])
|
reversion.set_comment(self.request.DATA['comment'])
|
||||||
|
super(TaskDetail, self).post_save(obj, created)
|
||||||
|
|
||||||
|
|
||||||
class IssueList(generics.ListCreateAPIView):
|
class IssueList(NotificationSenderMixin, generics.ListCreateAPIView):
|
||||||
model = Issue
|
model = Issue
|
||||||
serializer_class = IssueSerializer
|
serializer_class = IssueSerializer
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
permission_classes = (IsAuthenticated,)
|
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):
|
def pre_save(self, obj):
|
||||||
obj.owner = self.request.user
|
obj.owner = self.request.user
|
||||||
|
@ -196,16 +227,20 @@ class IssueList(generics.ListCreateAPIView):
|
||||||
return self.model.objects.filter(project__members=self.request.user)
|
return self.model.objects.filter(project__members=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class IssueDetail(generics.RetrieveUpdateDestroyAPIView):
|
class IssueDetail(NotificationSenderMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = Issue
|
model = Issue
|
||||||
serializer_class = IssueSerializer
|
serializer_class = IssueSerializer
|
||||||
permission_classes = (IsAuthenticated, IssueDetailPermission,)
|
permission_classes = (IsAuthenticated, IssueDetailPermission,)
|
||||||
|
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):
|
def post_save(self, obj, created=False):
|
||||||
with reversion.create_revision():
|
with reversion.create_revision():
|
||||||
if "comment" in self.request.DATA:
|
if "comment" in self.request.DATA:
|
||||||
# Update the comment in the last version
|
# Update the comment in the last version
|
||||||
reversion.set_comment(self.request.DATA['comment'])
|
reversion.set_comment(self.request.DATA['comment'])
|
||||||
|
super(IssueDetail, self).post_save(obj, created)
|
||||||
|
|
||||||
|
|
||||||
class SeverityList(generics.ListCreateAPIView):
|
class SeverityList(generics.ListCreateAPIView):
|
||||||
|
|
|
@ -10,12 +10,24 @@ from django.db.models.loading import get_model
|
||||||
|
|
||||||
from picklefield.fields import PickledObjectField
|
from picklefield.fields import PickledObjectField
|
||||||
|
|
||||||
from greenmine.base.utils.slug import slugify_uniquely, ref_uniquely
|
from greenmine.base.utils.slug import (
|
||||||
|
slugify_uniquely,
|
||||||
|
ref_uniquely
|
||||||
|
)
|
||||||
from greenmine.base.utils import iter_points
|
from greenmine.base.utils import iter_points
|
||||||
from greenmine.scrum.choices import (ISSUESTATUSES, TASKSTATUSES, USSTATUSES,
|
from greenmine.base.notifications.models import WatchedMixin
|
||||||
POINTS_CHOICES, SEVERITY_CHOICES,
|
from greenmine.scrum.choices import (
|
||||||
ISSUETYPES, TASK_CHANGE_CHOICES,
|
ISSUESTATUSES,
|
||||||
PRIORITY_CHOICES)
|
TASKSTATUSES,
|
||||||
|
USSTATUSES,
|
||||||
|
POINTS_CHOICES,
|
||||||
|
SEVERITY_CHOICES,
|
||||||
|
ISSUETYPES,
|
||||||
|
TASK_CHANGE_CHOICES,
|
||||||
|
PRIORITY_CHOICES
|
||||||
|
)
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
class Severity(models.Model):
|
class Severity(models.Model):
|
||||||
|
@ -173,7 +185,7 @@ class Membership(models.Model):
|
||||||
unique_together = ('user', 'project')
|
unique_together = ('user', 'project')
|
||||||
|
|
||||||
|
|
||||||
class Project(models.Model):
|
class Project(models.Model, WatchedMixin):
|
||||||
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
||||||
verbose_name=_('uuid'))
|
verbose_name=_('uuid'))
|
||||||
name = models.CharField(max_length=250, unique=True, null=False, blank=False,
|
name = models.CharField(max_length=250, unique=True, null=False, blank=False,
|
||||||
|
@ -228,17 +240,30 @@ class Project(models.Model):
|
||||||
|
|
||||||
super(Project, self).save(*args, **kwargs)
|
super(Project, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def _get_watchers_by_role(self):
|
||||||
|
return {'owner': self.owner}
|
||||||
|
|
||||||
|
def eget_attrinutes_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,
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_of_milestones(self):
|
def list_of_milestones(self):
|
||||||
return [
|
return [{
|
||||||
{
|
|
||||||
'name': milestone.name,
|
'name': milestone.name,
|
||||||
'finish_date': milestone.estimated_finish,
|
'finish_date': milestone.estimated_finish,
|
||||||
'closed_points': milestone.closed_points,
|
'closed_points': milestone.closed_points,
|
||||||
'client_increment_points': milestone.client_increment_points,
|
'client_increment_points': milestone.client_increment_points,
|
||||||
'team_increment_points': milestone.team_increment_points
|
'team_increment_points': milestone.team_increment_points
|
||||||
} for milestone in self.milestones.all().order_by('estimated_start')
|
} for milestone in self.milestones.all().order_by('estimated_start')]
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_roles(self):
|
def list_roles(self):
|
||||||
|
@ -268,29 +293,21 @@ class Project(models.Model):
|
||||||
.exclude(role__id__in=role_ids)\
|
.exclude(role__id__in=role_ids)\
|
||||||
.delete()
|
.delete()
|
||||||
|
|
||||||
class Milestone(models.Model):
|
def _get_watchers_by_role(self):
|
||||||
uuid = models.CharField(
|
return {'owner': self.owner}
|
||||||
max_length=40, unique=True, null=False, blank=True,
|
|
||||||
|
|
||||||
|
class Milestone(models.Model, WatchedMixin):
|
||||||
|
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
||||||
verbose_name=_('uuid'))
|
verbose_name=_('uuid'))
|
||||||
|
name = models.CharField(max_length=200, db_index=True, null=False, blank=False,
|
||||||
name = models.CharField(
|
|
||||||
max_length=200, db_index=True, null=False, blank=False,
|
|
||||||
verbose_name=_('name'))
|
verbose_name=_('name'))
|
||||||
|
|
||||||
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
|
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
|
||||||
verbose_name=_('slug'))
|
verbose_name=_('slug'))
|
||||||
|
owner = models.ForeignKey('base.User', null=True, blank=True, related_name='owned_milestones',
|
||||||
owner = models.ForeignKey(
|
verbose_name=_('owner'))
|
||||||
'base.User',
|
project = models.ForeignKey('Project', null=False, blank=False, related_name='milestones',
|
||||||
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=_('project'))
|
||||||
|
|
||||||
estimated_start = models.DateField(null=True, blank=True, default=None,
|
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,
|
estimated_finish = models.DateField(null=True, blank=True, default=None,
|
||||||
|
@ -373,6 +390,11 @@ class Milestone(models.Model):
|
||||||
#return sum(points)
|
#return sum(points)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
def _get_watchers_by_role(self):
|
||||||
|
return {
|
||||||
|
'owner': self.owner,
|
||||||
|
'project_owner': (self.project, self.project.owner),
|
||||||
|
}
|
||||||
|
|
||||||
class RolePoints(models.Model):
|
class RolePoints(models.Model):
|
||||||
user_story = models.ForeignKey('UserStory', null=False, blank=False,
|
user_story = models.ForeignKey('UserStory', null=False, blank=False,
|
||||||
|
@ -389,7 +411,7 @@ class RolePoints(models.Model):
|
||||||
unique_together = ('user_story', 'role')
|
unique_together = ('user_story', 'role')
|
||||||
|
|
||||||
|
|
||||||
class UserStory(models.Model):
|
class UserStory(WatchedMixin, models.Model):
|
||||||
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
||||||
verbose_name=_('uuid'))
|
verbose_name=_('uuid'))
|
||||||
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
|
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
|
||||||
|
@ -458,6 +480,13 @@ class UserStory(models.Model):
|
||||||
def get_role_points(self):
|
def get_role_points(self):
|
||||||
return self.role_points
|
return self.role_points
|
||||||
|
|
||||||
|
def _get_watchers_by_role(self):
|
||||||
|
return {
|
||||||
|
'owner': self.owner,
|
||||||
|
'suscribed_watchers': self.watchers.all(),
|
||||||
|
'project_owner': (self.project, self.project.owner),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Attachment(models.Model):
|
class Attachment(models.Model):
|
||||||
owner = models.ForeignKey('base.User', null=False, blank=False,
|
owner = models.ForeignKey('base.User', null=False, blank=False,
|
||||||
|
@ -489,7 +518,7 @@ class Attachment(models.Model):
|
||||||
self.content_type, self.object_id, self.id)
|
self.content_type, self.object_id, self.id)
|
||||||
|
|
||||||
|
|
||||||
class Task(models.Model):
|
class Task(models.Model, WatchedMixin):
|
||||||
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
||||||
verbose_name=_('uuid'))
|
verbose_name=_('uuid'))
|
||||||
user_story = models.ForeignKey('UserStory', null=True, blank=False,
|
user_story = models.ForeignKey('UserStory', null=True, blank=False,
|
||||||
|
@ -555,8 +584,16 @@ class Task(models.Model):
|
||||||
|
|
||||||
super(Task, self).save(*args, **kwargs)
|
super(Task, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def _get_watchers_by_role(self):
|
||||||
|
return {
|
||||||
|
'owner': self.owner,
|
||||||
|
'assigned_to': self.assigned_to,
|
||||||
|
'suscribed_watchers': self.watchers.all(),
|
||||||
|
'project_owner': (self.project, self.project.owner),
|
||||||
|
}
|
||||||
|
|
||||||
class Issue(models.Model):
|
|
||||||
|
class Issue(models.Model, WatchedMixin):
|
||||||
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
||||||
verbose_name=_('uuid'))
|
verbose_name=_('uuid'))
|
||||||
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
|
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
|
||||||
|
@ -629,6 +666,23 @@ class Issue(models.Model):
|
||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
return self.status.is_closed
|
return self.status.is_closed
|
||||||
|
|
||||||
|
def _get_watchers_by_role(self):
|
||||||
|
return {
|
||||||
|
'owner': self.owner,
|
||||||
|
'assigned_to': self.assigned_to,
|
||||||
|
'suscribed_watchers': self.watchers.all(),
|
||||||
|
'project_owner': (self.project, self.project.owner),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Reversion registration (usufull for base.notification and for meke a historical)
|
||||||
|
|
||||||
|
reversion.register(Project)
|
||||||
|
reversion.register(Milestone)
|
||||||
|
reversion.register(UserStory)
|
||||||
|
reversion.register(Task)
|
||||||
|
reversion.register(Issue)
|
||||||
|
|
||||||
|
|
||||||
# Model related signals handlers
|
# Model related signals handlers
|
||||||
|
|
||||||
|
@ -716,7 +770,3 @@ def tasks_close_handler(sender, instance, **kwargs):
|
||||||
else:
|
else:
|
||||||
instance.user_story.finish_date = None
|
instance.user_story.finish_date = None
|
||||||
instance.user_story.save()
|
instance.user_story.save()
|
||||||
|
|
||||||
# Email alerts signals handlers
|
|
||||||
# TODO: temporary commented (Pending refactor)
|
|
||||||
# from . import sigdispatch
|
|
||||||
|
|
|
@ -32,74 +32,75 @@ def mail_recovery_password(sender, user, **kwargs):
|
||||||
subject = ugettext("Greenmine: password recovery.")
|
subject = ugettext("Greenmine: password recovery.")
|
||||||
send_mail.delay(subject, template, [user.email])
|
send_mail.delay(subject, template, [user.email])
|
||||||
|
|
||||||
|
## TODO: Remove me when base.notifications is finished
|
||||||
@receiver(signals.mail_milestone_created)
|
##
|
||||||
def mail_milestone_created(sender, milestone, user, **kwargs):
|
#@receiver(signals.mail_milestone_created)
|
||||||
participants = milestone.project.all_participants()
|
#def mail_milestone_created(sender, milestone, user, **kwargs):
|
||||||
|
# participants = milestone.project.all_participants()
|
||||||
emails_list = []
|
#
|
||||||
subject = ugettext("Greenmine: sprint created")
|
# emails_list = []
|
||||||
for person in participants:
|
# subject = ugettext("Greenmine: sprint created")
|
||||||
template = render_to_string("email/milestone.created.html", {
|
# for person in participants:
|
||||||
"person": person,
|
# template = render_to_string("email/milestone.created.html", {
|
||||||
"current_host": settings.HOST,
|
# "person": person,
|
||||||
"milestone": milestone,
|
# "current_host": settings.HOST,
|
||||||
"user": user,
|
# "milestone": milestone,
|
||||||
})
|
# "user": user,
|
||||||
|
# })
|
||||||
emails_list.append([subject, template, [person.email]])
|
#
|
||||||
|
# emails_list.append([subject, template, [person.email]])
|
||||||
send_bulk_mail.delay(emails_list)
|
#
|
||||||
|
# send_bulk_mail.delay(emails_list)
|
||||||
|
#
|
||||||
@receiver(signals.mail_userstory_created)
|
#
|
||||||
def mail_userstory_created(sender, us, user, **kwargs):
|
#@receiver(signals.mail_userstory_created)
|
||||||
participants = us.milestone.project.all_participants()
|
#def mail_userstory_created(sender, us, user, **kwargs):
|
||||||
|
# participants = us.milestone.project.all_participants()
|
||||||
emails_list = []
|
#
|
||||||
subject = ugettext("Greenmine: user story created")
|
# emails_list = []
|
||||||
|
# subject = ugettext("Greenmine: user story created")
|
||||||
for person in participants:
|
#
|
||||||
template = render_to_string("email/userstory.created.html", {
|
# for person in participants:
|
||||||
"person": person,
|
# template = render_to_string("email/userstory.created.html", {
|
||||||
"current_host": settings.HOST,
|
# "person": person,
|
||||||
"us": us,
|
# "current_host": settings.HOST,
|
||||||
"user": user,
|
# "us": us,
|
||||||
})
|
# "user": user,
|
||||||
|
# })
|
||||||
emails_list.append([subject, template, [person.email]])
|
#
|
||||||
|
# emails_list.append([subject, template, [person.email]])
|
||||||
send_bulk_mail.delay(emails_list)
|
#
|
||||||
|
# send_bulk_mail.delay(emails_list)
|
||||||
|
#
|
||||||
@receiver(signals.mail_task_created)
|
#
|
||||||
def mail_task_created(sender, task, user, **kwargs):
|
#@receiver(signals.mail_task_created)
|
||||||
participants = task.us.milestone.project.all_participants()
|
#def mail_task_created(sender, task, user, **kwargs):
|
||||||
|
# participants = task.us.milestone.project.all_participants()
|
||||||
emails_list = []
|
#
|
||||||
subject = ugettext("Greenmine: task created")
|
# emails_list = []
|
||||||
|
# subject = ugettext("Greenmine: task created")
|
||||||
for person in participants:
|
#
|
||||||
template = render_to_string("email/task.created.html", {
|
# for person in participants:
|
||||||
"person": person,
|
# template = render_to_string("email/task.created.html", {
|
||||||
"current_host": settings.HOST,
|
# "person": person,
|
||||||
"task": task,
|
# "current_host": settings.HOST,
|
||||||
"user": user,
|
# "task": task,
|
||||||
})
|
# "user": user,
|
||||||
|
# })
|
||||||
emails_list.append([subject, template, [person.email]])
|
#
|
||||||
|
# emails_list.append([subject, template, [person.email]])
|
||||||
send_bulk_mail.delay(emails_list)
|
#
|
||||||
|
# send_bulk_mail.delay(emails_list)
|
||||||
|
#
|
||||||
@receiver(signals.mail_task_assigned)
|
#
|
||||||
def mail_task_assigned(sender, task, user, **kwargs):
|
#@receiver(signals.mail_task_assigned)
|
||||||
template = render_to_string("email/task.assigned.html", {
|
#def mail_task_assigned(sender, task, user, **kwargs):
|
||||||
"person": task.assigned_to,
|
# template = render_to_string("email/task.assigned.html", {
|
||||||
"task": task,
|
# "person": task.assigned_to,
|
||||||
"user": user,
|
# "task": task,
|
||||||
"current_host": settings.HOST,
|
# "user": user,
|
||||||
})
|
# "current_host": settings.HOST,
|
||||||
|
# })
|
||||||
subject = ugettext("Greenmine: task assigned")
|
#
|
||||||
send_mail.delay(subject, template, [task.assigned_to.email])
|
# subject = ugettext("Greenmine: task assigned")
|
||||||
|
# send_mail.delay(subject, template, [task.assigned_to.email])
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import (
|
||||||
from __future__ import absolute_import
|
absolute_import,
|
||||||
import os
|
print_function
|
||||||
|
)
|
||||||
|
import os, sys
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print "Trying import local.py settings..."
|
print("Trying import local.py settings...", file=sys.stderr)
|
||||||
from .local import *
|
from .local import *
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print "Trying import development.py settings..."
|
print("Trying import development.py settings...", file=sys.stderr)
|
||||||
from .development import *
|
from .development import *
|
||||||
|
|
|
@ -201,6 +201,8 @@ INSTALLED_APPS = [
|
||||||
|
|
||||||
'greenmine.base',
|
'greenmine.base',
|
||||||
'greenmine.base.mail',
|
'greenmine.base.mail',
|
||||||
|
'greenmine.base.notifications',
|
||||||
|
'greenmine.base.notifications.email',
|
||||||
'greenmine.scrum',
|
'greenmine.scrum',
|
||||||
'greenmine.wiki',
|
'greenmine.wiki',
|
||||||
'greenmine.documents',
|
'greenmine.documents',
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# For sqlite
|
||||||
|
rm -f database.sqlite
|
||||||
|
|
||||||
|
# For postgresql
|
||||||
dropdb greenmine
|
dropdb greenmine
|
||||||
createdb greenmine
|
createdb greenmine
|
||||||
|
|
||||||
python manage.py syncdb --migrate --noinput
|
python manage.py syncdb --migrate --noinput --traceback
|
||||||
python manage.py loaddata initial_user
|
python manage.py loaddata initial_user --traceback
|
||||||
python manage.py sample_data
|
python manage.py sample_data --traceback
|
||||||
|
python manage.py createinitialrevisions --traceback
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ git+git://github.com/toastdriven/django-haystack.git
|
||||||
django-picklefield==0.3.0
|
django-picklefield==0.3.0
|
||||||
django-reversion==1.7
|
django-reversion==1.7
|
||||||
django-sampledatahelper==0.0.1
|
django-sampledatahelper==0.0.1
|
||||||
django-tastypie==0.9.14
|
|
||||||
djangorestframework==2.2.5
|
djangorestframework==2.2.5
|
||||||
gunicorn==17.5
|
gunicorn==17.5
|
||||||
kombu==2.5.12
|
kombu==2.5.12
|
||||||
|
@ -28,3 +27,5 @@ pycrypto==2.6
|
||||||
python-dateutil==2.1
|
python-dateutil==2.1
|
||||||
pytz==2013b
|
pytz==2013b
|
||||||
six==1.3.0
|
six==1.3.0
|
||||||
|
djmail>=0.1
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue