Refactor of scrum app (but not finished yet).
TODO: - Migrate Wiki app to new django-restframework version. - Migrate Questions app to new django-restframework version. - Added tests s Please enter the commit message for your changes. Lines startingremotes/origin/enhancement/email-actions
parent
d8516a20c7
commit
c93baac1c3
|
@ -18,7 +18,7 @@ def has_project_perm(user, project, perm):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class BaseDetailPermission(permissions.BasePermission):
|
class BasePermission(permissions.BasePermission):
|
||||||
get_permission = None
|
get_permission = None
|
||||||
put_permission = None
|
put_permission = None
|
||||||
patch_permission = None
|
patch_permission = None
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class PickleField(serializers.WritableField):
|
||||||
|
"""
|
||||||
|
Pickle objects serializer.
|
||||||
|
"""
|
||||||
|
def to_native(self, obj):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def from_native(self, data):
|
||||||
|
return data
|
|
@ -5,7 +5,8 @@ from .models import User, Role
|
||||||
|
|
||||||
|
|
||||||
class UserLogged(object):
|
class UserLogged(object):
|
||||||
def __init__(self, token, username, first_name, last_name, email, last_login, color, description, default_language, default_timezone, colorize_tags):
|
def __init__(self, token, username, first_name, last_name, email, last_login, color,
|
||||||
|
description, default_language, default_timezone, colorize_tags):
|
||||||
self.token = token
|
self.token = token
|
||||||
self.username = username
|
self.username = username
|
||||||
self.first_name = first_name
|
self.first_name = first_name
|
||||||
|
@ -46,10 +47,13 @@ class LoginSerializer(serializers.Serializer):
|
||||||
instance.last_login = attrs.get('last_login', instance.last_login)
|
instance.last_login = attrs.get('last_login', instance.last_login)
|
||||||
instance.color = attrs.get('color', instance.color)
|
instance.color = attrs.get('color', instance.color)
|
||||||
instance.description = attrs.get('description', instance.description)
|
instance.description = attrs.get('description', instance.description)
|
||||||
instance.default_language = attrs.get('default_language', instance.default_language)
|
instance.default_language = attrs.get('default_language',
|
||||||
instance.default_timezone = attrs.get('default_timezone', instance.default_timezone)
|
instance.default_language)
|
||||||
|
instance.default_timezone = attrs.get('default_timezone',
|
||||||
|
instance.default_timezone)
|
||||||
instance.colorize_tags = attrs.get('colorize_tags', instance.colorize_tags)
|
instance.colorize_tags = attrs.get('colorize_tags', instance.colorize_tags)
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
return UserLogged(**attrs)
|
return UserLogged(**attrs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,14 +62,14 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = ('id', 'username', 'first_name', 'last_name', 'email', 'color', 'description',
|
fields = ('id', 'username', 'first_name', 'last_name', 'email', 'color',
|
||||||
'default_language', 'default_timezone', 'is_active', 'photo', 'projects')
|
'description', 'default_language', 'default_timezone', 'is_active',
|
||||||
|
'photo', 'projects')
|
||||||
|
|
||||||
def get_projects(self, obj):
|
def get_projects(self, obj):
|
||||||
return [x.id for x in obj.projects.all()]
|
return [x.id for x in obj.projects.all()]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class RoleSerializer(serializers.ModelSerializer):
|
class RoleSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Role
|
model = Role
|
||||||
|
|
|
@ -13,6 +13,8 @@ from grappelli.dashboard import modules, Dashboard
|
||||||
from grappelli.dashboard.utils import get_admin_site_name
|
from grappelli.dashboard.utils import get_admin_site_name
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: Fix me
|
||||||
|
|
||||||
class CustomIndexDashboard(Dashboard):
|
class CustomIndexDashboard(Dashboard):
|
||||||
"""
|
"""
|
||||||
Custom index dashboard for www.
|
Custom index dashboard for www.
|
||||||
|
@ -46,7 +48,6 @@ class CustomIndexDashboard(Dashboard):
|
||||||
models=(
|
models=(
|
||||||
'greenmine.documents.*',
|
'greenmine.documents.*',
|
||||||
'greenmine.questions.*',
|
'greenmine.questions.*',
|
||||||
'greenmine.taggit.*',
|
|
||||||
'greenmine.wiki.*',
|
'greenmine.wiki.*',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from greenmine.projects.milestones.admin import MilestoneInline
|
||||||
|
from greenmine.projects.userstories.admin import UserStoryInline
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentAdmin(reversion.VersionAdmin):
|
||||||
|
list_display = ["id", "owner"]
|
||||||
|
|
||||||
|
admin.site.register(models.Attachment, AttachmentAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['project', 'role', 'user']
|
||||||
|
list_filter = ['project', 'role']
|
||||||
|
|
||||||
|
admin.site.register(models.Membership, MembershipAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipInline(admin.TabularInline):
|
||||||
|
model = models.Membership
|
||||||
|
fields = ('user', 'project', 'role')
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectAdmin(reversion.VersionAdmin):
|
||||||
|
list_display = ["name", "owner"]
|
||||||
|
inlines = [MembershipInline, MilestoneInline, UserStoryInline]
|
||||||
|
|
||||||
|
admin.site.register(models.Project, ProjectAdmin)
|
|
@ -0,0 +1,32 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from greenmine.base import filters
|
||||||
|
from greenmine.base.api import ModelCrudViewSet,
|
||||||
|
from greenmine.base.notifications.api import NotificationSenderMixin
|
||||||
|
|
||||||
|
from . import serializers
|
||||||
|
from . import models
|
||||||
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectViewSet(NotificationSenderMixin, ModelCrudViewSet):
|
||||||
|
model = models.Project
|
||||||
|
serializer_class = serializers.ProjectSerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.ProjectPermission)
|
||||||
|
create_notification_template = "create_project_notification"
|
||||||
|
update_notification_template = "update_project_notification"
|
||||||
|
destroy_notification_template = "destroy_project_notification"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = super(ProjectViewSet, self).get_queryset()
|
||||||
|
qs = qs.filter(Q(owner=self.request.user) |
|
||||||
|
Q(members=self.request.user))
|
||||||
|
return qs.distinct()
|
||||||
|
|
||||||
|
def pre_save(self, obj):
|
||||||
|
super(ProjectViewSet, self).pre_save(obj)
|
||||||
|
obj.owner = self.request.user
|
|
@ -3,21 +3,12 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
PRIORITY_CHOICES = (
|
US_STATUSES = (
|
||||||
(1, _(u'Low')),
|
(1, _(u"Open"), False),
|
||||||
(3, _(u'Normal')),
|
(2, _(u"Closed"), True),
|
||||||
(5, _(u'High')),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SEVERITY_CHOICES = (
|
TASK_STATUSES = (
|
||||||
(1, _(u'Wishlist')),
|
|
||||||
(2, _(u'Minor')),
|
|
||||||
(3, _(u'Normal')),
|
|
||||||
(4, _(u'Important')),
|
|
||||||
(5, _(u'Critical')),
|
|
||||||
)
|
|
||||||
|
|
||||||
TASKSTATUSES = (
|
|
||||||
(1, _(u"New"), False, "#999999"),
|
(1, _(u"New"), False, "#999999"),
|
||||||
(2, _(u"In progress"), False, "#ff9900"),
|
(2, _(u"In progress"), False, "#ff9900"),
|
||||||
(3, _(u"Ready for test"), True, "#ffcc00"),
|
(3, _(u"Ready for test"), True, "#ffcc00"),
|
||||||
|
@ -25,25 +16,6 @@ TASKSTATUSES = (
|
||||||
(5, _(u"Needs Info"), False, "#999999"),
|
(5, _(u"Needs Info"), False, "#999999"),
|
||||||
)
|
)
|
||||||
|
|
||||||
ISSUESTATUSES = (
|
|
||||||
(1, _(u"New"), False),
|
|
||||||
(2, _(u"In progress"), False),
|
|
||||||
(3, _(u"Ready for test"), True),
|
|
||||||
(4, _(u"Closed"), True),
|
|
||||||
(5, _(u"Needs Info"), False),
|
|
||||||
(6, _(u"Rejected"), True),
|
|
||||||
(7, _(u"Postponed"), False),
|
|
||||||
)
|
|
||||||
|
|
||||||
USSTATUSES = (
|
|
||||||
(1, _(u"Open"), False),
|
|
||||||
(2, _(u"Closed"), True),
|
|
||||||
)
|
|
||||||
|
|
||||||
ISSUETYPES = (
|
|
||||||
(1, _(u'Bug')),
|
|
||||||
)
|
|
||||||
|
|
||||||
POINTS_CHOICES = (
|
POINTS_CHOICES = (
|
||||||
(1, u'?', None),
|
(1, u'?', None),
|
||||||
(2, u'0', 0),
|
(2, u'0', 0),
|
||||||
|
@ -59,6 +31,33 @@ POINTS_CHOICES = (
|
||||||
(12, u'40', 40),
|
(12, u'40', 40),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PRIORITY_CHOICES = (
|
||||||
|
(1, _(u'Low')),
|
||||||
|
(3, _(u'Normal')),
|
||||||
|
(5, _(u'High')),
|
||||||
|
)
|
||||||
|
|
||||||
|
SEVERITY_CHOICES = (
|
||||||
|
(1, _(u'Wishlist')),
|
||||||
|
(2, _(u'Minor')),
|
||||||
|
(3, _(u'Normal')),
|
||||||
|
(4, _(u'Important')),
|
||||||
|
(5, _(u'Critical')),
|
||||||
|
)
|
||||||
|
|
||||||
|
ISSUE_STATUSES = (
|
||||||
|
(1, _(u"New"), False),
|
||||||
|
(2, _(u"In progress"), False),
|
||||||
|
(3, _(u"Ready for test"), True),
|
||||||
|
(4, _(u"Closed"), True),
|
||||||
|
(5, _(u"Needs Info"), False),
|
||||||
|
(6, _(u"Rejected"), True),
|
||||||
|
(7, _(u"Postponed"), False),
|
||||||
|
)
|
||||||
|
|
||||||
|
ISSUE_TYPES = (
|
||||||
|
(1, _(u'Bug')),
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: pending to refactor
|
# TODO: pending to refactor
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from rest_framework import viewsets
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
from greenmine.base import filters
|
from greenmine.base import filters
|
||||||
|
from greenmine.base.api import ModelCrudViewSet,
|
||||||
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
class DocumentsViewSet(viewsets.ModelViewSet):
|
class DocumentsViewSet(ModelCrudViewSet):
|
||||||
model = models.Document
|
model = models.Document
|
||||||
serializer_class = serializers.DocumentSerializer
|
serializer_class = serializers.DocumentSerializer
|
||||||
permission_classes = (permissions.DocumentPermission,)
|
permission_classes = (permissions.DocumentPermission,)
|
|
@ -1,11 +1,11 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from greenmine.base.permissions import BaseDetailPermission
|
from greenmine.base.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
class DocumentPermission(BaseDetailPermission):
|
class DocumentPermission(BasePermission):
|
||||||
get_permission = "can_view_document"
|
get_permission = "can_view_document"
|
||||||
put_permission = "can_change_document"
|
put_permission = "can_change_document"
|
||||||
delete_permission = "can_delete_document"
|
delete_permission = "can_delete_document"
|
||||||
safe_methods = ['HEAD', 'OPTIONS']
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
path_to_document = []
|
path_to_document = []
|
|
@ -1,7 +1,6 @@
|
||||||
# -* coding: utf-8 -*-
|
# -* coding: utf-8 -*-
|
||||||
|
|
||||||
from haystack import indexes
|
from haystack import indexes
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SeverityAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "order", "project"]
|
||||||
|
|
||||||
|
admin.site.register(models.Severity, SeverityAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class PriorityAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "order", "project"]
|
||||||
|
|
||||||
|
admin.site.register(models.Priority, PriorityAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueTypeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "order", "project"]
|
||||||
|
|
||||||
|
admin.site.register(models.IssueType, IssueTypeAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueStatusAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "order", "is_closed", "project"]
|
||||||
|
|
||||||
|
admin.site.register(models.IssueStatus, IssueStatusAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueAdmin(reversion.VersionAdmin):
|
||||||
|
list_display = ["subject", "type"]
|
||||||
|
|
||||||
|
admin.site.register(models.Issue, IssueAdmin)
|
|
@ -0,0 +1,91 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from greenmine.base import filters
|
||||||
|
from greenmine.base.api import (
|
||||||
|
ModelCrudViewSet,
|
||||||
|
ModelListViewSet
|
||||||
|
)
|
||||||
|
from greenmine.base.notifications.api import NotificationSenderMixin
|
||||||
|
from greenmine.projects.permissions import AttachmentPermission
|
||||||
|
from greenmine.projects.serializers import AttachmentSerializer
|
||||||
|
|
||||||
|
from . import serializers
|
||||||
|
from . import models
|
||||||
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class SeverityViewSet(ModelListViewSet):
|
||||||
|
model = models.Severity
|
||||||
|
serializer_class = serializers.SeveritySerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.SeverityiPermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ("project",)
|
||||||
|
|
||||||
|
|
||||||
|
class PriorityViewSet(ModelListViewSet):
|
||||||
|
model = models.Priority
|
||||||
|
serializer_class = serializer.PrioritySerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.PriorityPermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ("project",)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueTypeViewSet(ModelListViewSet):
|
||||||
|
model = models.IssueType
|
||||||
|
serializer_class = serializer.IssueTypeSerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.IssueTypePermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ("project",)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueStatusViewSet(ModelListViewSet):
|
||||||
|
model = models.IssueStatus
|
||||||
|
serializer_class = serializers.IssueStatusSerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.IssueStatusPermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ("project",)
|
||||||
|
|
||||||
|
|
||||||
|
class IssuesAttachmentViewSet(ModelCrudViewSet):
|
||||||
|
model = Attachment
|
||||||
|
serializer_class = AttachmentSerializer
|
||||||
|
permission_classes = (IsAuthenticated, AttachmentPermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ["project", "object_id"]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
ct = ContentType.objects.get_for_model(models.Issue)
|
||||||
|
qs = super(IssuesAttachmentViewSet, self).get_queryset()
|
||||||
|
qs = qs.filter(content_type=ct)
|
||||||
|
return qs.distinct()
|
||||||
|
|
||||||
|
def pre_save(self, obj):
|
||||||
|
super(IssuesAttachmentViewSet, self).pre_save(obj)
|
||||||
|
obj.content_type = ContentType.objects.get_for_model(Issue)
|
||||||
|
obj.owner = self.request.user
|
||||||
|
|
||||||
|
|
||||||
|
class IssueViewSet(NotificationSenderMixin, ModelCrudViewSet):
|
||||||
|
model = models.Issue
|
||||||
|
serializer_class = serializers.IssueSerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.IssuePermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ("project",)
|
||||||
|
create_notification_template = "create_issue_notification"
|
||||||
|
update_notification_template = "update_issue_notification"
|
||||||
|
destroy_notification_template = "destroy_issue_notification"
|
||||||
|
|
||||||
|
def pre_save(self, obj):
|
||||||
|
super(IssueViewSet, self).pre_save(obj)
|
||||||
|
obj.owner = self.request.user
|
||||||
|
|
||||||
|
def post_save(self, obj, created=False):
|
||||||
|
with reversion.create_revision():
|
||||||
|
if "comment" in self.request.DATA:
|
||||||
|
# Update the comment in the last version
|
||||||
|
reversion.set_comment(self.request.DATA["comment"])
|
||||||
|
super(IssueViewSet, self).post_save(obj, created)
|
|
@ -0,0 +1,177 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from picklefield.fields import PickledObjectField
|
||||||
|
|
||||||
|
from greenmine.base.utils.slug import ref_uniquely
|
||||||
|
from greenmine.base.notifications.models import WatchedMixin
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class Priority(models.Model):
|
||||||
|
name = models.CharField(max_length=255, null=False, blank=False, verbose_name=_("name"))
|
||||||
|
order = models.IntegerField(default=10, null=False, blank=False, verbose_name=_("order"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="priorities", verbose_name=_("project"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"priority"
|
||||||
|
verbose_name_plural = u"priorities"
|
||||||
|
ordering = ["project", "name"]
|
||||||
|
unique_together = ("project", "name")
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"project {0} - {1}".format(self.project_id, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class Severity(models.Model):
|
||||||
|
name = models.CharField(max_length=255, null=False, blank=False, verbose_name=_("name"))
|
||||||
|
order = models.IntegerField(default=10, null=False, blank=False, verbose_name=_("order"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="severities", verbose_name=_("project"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"severity"
|
||||||
|
verbose_name_plural = u"severities"
|
||||||
|
ordering = ["project", "name"]
|
||||||
|
unique_together = ("project", "name")
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"project {0} - {1}".format(self.project_id, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueStatus(models.Model):
|
||||||
|
name = models.CharField(max_length=255, null=False, blank=False, verbose_name=_("name"))
|
||||||
|
order = models.IntegerField(default=10, null=False, blank=False, verbose_name=_("order"))
|
||||||
|
is_closed = models.BooleanField(default=False, null=False, blank=True,
|
||||||
|
verbose_name=_("is closed"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="issue_statuses", verbose_name=_("project"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"issue status"
|
||||||
|
verbose_name_plural = u"issue statuses"
|
||||||
|
ordering = ["project", "name"]
|
||||||
|
unique_together = ("project", "name")
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"project {0} - {1}".format(self.project_id, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueType(models.Model):
|
||||||
|
name = models.CharField(max_length=255, null=False, blank=False, verbose_name=_("name"))
|
||||||
|
order = models.IntegerField(default=10, null=False, blank=False, verbose_name=_("order"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="issue_types", verbose_name=_("project"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"issue type"
|
||||||
|
verbose_name_plural = u"issue types"
|
||||||
|
ordering = ["project", "name"]
|
||||||
|
unique_together = ("project", "name")
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"project {0} - {1}".format(self.project_id, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
verbose_name=_("ref"))
|
||||||
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None,
|
||||||
|
related_name="owned_issues", verbose_name=_("owner"))
|
||||||
|
status = models.ForeignKey("IssueStatus", null=False, blank=False, related_name="issues",
|
||||||
|
verbose_name=_("status"))
|
||||||
|
severity = models.ForeignKey("Severity", null=False, blank=False, related_name="issues",
|
||||||
|
verbose_name=_("severity"))
|
||||||
|
priority = models.ForeignKey("Priority", null=False, blank=False, related_name="issues",
|
||||||
|
verbose_name=_("priority"))
|
||||||
|
type = models.ForeignKey("IssueType", null=False, blank=False, related_name="issues",
|
||||||
|
verbose_name=_("type"))
|
||||||
|
milestone = models.ForeignKey("milestones.Milestone", null=True, blank=True, default=None,
|
||||||
|
related_name="issues", verbose_name=_("milestone"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="issues", verbose_name=_("project"))
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
|
||||||
|
verbose_name=_("created date"))
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
|
||||||
|
verbose_name=_("modified date"))
|
||||||
|
finished_date = models.DateTimeField(null=True, blank=True,
|
||||||
|
verbose_name=_("finished date"))
|
||||||
|
subject = models.CharField(max_length=500, null=False, blank=False,
|
||||||
|
verbose_name=_("subject"))
|
||||||
|
description = models.TextField(null=False, blank=True, verbose_name=_("description"))
|
||||||
|
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||||
|
default=None, related_name="issues_assigned_to_me",
|
||||||
|
verbose_name=_("assigned to"))
|
||||||
|
watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
|
||||||
|
related_name="watched_issues",
|
||||||
|
verbose_name=_("watchers"))
|
||||||
|
tags = PickledObjectField(null=False, blank=True, verbose_name=_("tags"))
|
||||||
|
|
||||||
|
notifiable_fields = [
|
||||||
|
"owner",
|
||||||
|
"status",
|
||||||
|
"severity",
|
||||||
|
"priority",
|
||||||
|
"type",
|
||||||
|
"milestone",
|
||||||
|
"finished_date",
|
||||||
|
"subject",
|
||||||
|
"description",
|
||||||
|
"assigned_to",
|
||||||
|
"tags",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"issue"
|
||||||
|
verbose_name_plural = u"issues"
|
||||||
|
ordering = ["project", "created_date"]
|
||||||
|
unique_together = ("ref", "project")
|
||||||
|
permissions = (
|
||||||
|
("comment_issue", "Can comment issues"),
|
||||||
|
("change_owned_issue", "Can modify owned issues"),
|
||||||
|
("change_assigned_issue", "Can modify assigned issues"),
|
||||||
|
("assign_issue_to_other", "Can assign issues to others"),
|
||||||
|
("assign_issue_to_myself", "Can assign issues to myself"),
|
||||||
|
("change_issue_state", "Can change the issue state"),
|
||||||
|
("view_issue", "Can view the issue"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"({1}) {0}".format(self.ref, self.subject)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.id:
|
||||||
|
self.modified_date = timezone.now()
|
||||||
|
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,
|
||||||
|
"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(Issue)
|
||||||
|
|
||||||
|
|
||||||
|
# Model related signals handlers
|
||||||
|
@receiver(models.signals.pre_save, sender=Issue, dispatch_uid="issue_ref_handler")
|
||||||
|
def issue_ref_handler(sender, instance, **kwargs):
|
||||||
|
if not instance.id and instance.project:
|
||||||
|
instance.ref = ref_uniquely(instance.project, "last_issue_ref", instance.__class__)
|
|
@ -0,0 +1,48 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from greenmine.base.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
|
class SeverityPermission(BasePermission):
|
||||||
|
get_permission = "view_severity"
|
||||||
|
put_permission = "change_severity"
|
||||||
|
patch_permission = "change_severity"
|
||||||
|
delete_permission = "delete_severity"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
||||||
|
|
||||||
|
|
||||||
|
class PriorityPermission(BasePermission):
|
||||||
|
get_permission = "view_priority"
|
||||||
|
put_permission = "change_priority"
|
||||||
|
patch_permission = "change_priority"
|
||||||
|
delete_permission = "delete_priority"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
||||||
|
|
||||||
|
|
||||||
|
class IssueStatusPermission(BasePermission):
|
||||||
|
get_permission = "view_issuestatus"
|
||||||
|
put_permission = "change_issuestatus"
|
||||||
|
patch_permission = "change_issuestatus"
|
||||||
|
delete_permission = "delete_issuestatus"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
||||||
|
|
||||||
|
|
||||||
|
class IssueTypePermission(BasePermission):
|
||||||
|
get_permission = "view_issuetype"
|
||||||
|
put_permission = "severity_issuetype"
|
||||||
|
patch_permission = "severity_issuetype"
|
||||||
|
delete_permission = "delete_issuetype"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
||||||
|
|
||||||
|
|
||||||
|
class IssuePermission(BasePermission):
|
||||||
|
get_permission = "view_issue"
|
||||||
|
put_permission = "change_issue"
|
||||||
|
patch_permission = "change_issue"
|
||||||
|
delete_permission = "delete_issue"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -* coding: utf-8 -*-
|
||||||
|
|
||||||
|
from haystack import indexes
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class IssueIndex(indexes.SearchIndex, indexes.Indexable):
|
||||||
|
text = indexes.CharField(document=True, use_template=True,
|
||||||
|
template_name='search/indexes/issue_text.txt')
|
||||||
|
title = indexes.CharField(model_attr='subject')
|
||||||
|
project_id = indexes.IntegerField(model_attr="project_id")
|
||||||
|
description = indexes.CharField(model_attr="description")
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return models.Issue
|
||||||
|
|
||||||
|
def index_queryset(self, using=None):
|
||||||
|
return self.get_model().objects.all()
|
|
@ -0,0 +1,79 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from greenmine.base.serializers import PickleField
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class SeveritySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.Severity
|
||||||
|
|
||||||
|
|
||||||
|
class PrioritySerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.Priority
|
||||||
|
|
||||||
|
|
||||||
|
class IssueStatusSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.IssueStatus
|
||||||
|
|
||||||
|
|
||||||
|
class IssueTypeSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.IssueType
|
||||||
|
|
||||||
|
|
||||||
|
class IssueSerializer(serializers.ModelSerializer):
|
||||||
|
tags = PickleField()
|
||||||
|
comment = serializers.SerializerMethodField("get_comment")
|
||||||
|
history = serializers.SerializerMethodField("get_history")
|
||||||
|
is_closed = serializers.Field(source="is_closed")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Issue
|
||||||
|
|
||||||
|
def get_comment(self, obj):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_issues_diff(self, old_issue_version, new_issue_version):
|
||||||
|
old_obj = old_issue_version.field_dict
|
||||||
|
new_obj = new_issue_version.field_dict
|
||||||
|
|
||||||
|
diff_dict = {
|
||||||
|
"modified_date": new_obj["modified_date"],
|
||||||
|
"by": old_issue_version.revision.user,
|
||||||
|
"comment": old_issue_version.revision.comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in old_obj.keys():
|
||||||
|
if key == "modified_date":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if old_obj[key] == new_obj[key]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
diff_dict[key] = {
|
||||||
|
"old": old_obj[key],
|
||||||
|
"new": new_obj[key],
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff_dict
|
||||||
|
|
||||||
|
def get_history(self, obj):
|
||||||
|
diff_list = []
|
||||||
|
current = None
|
||||||
|
|
||||||
|
for version in reversed(list(reversion.get_for_object(obj))):
|
||||||
|
if current:
|
||||||
|
issues_diff = self.get_issues_diff(current, version)
|
||||||
|
diff_list.append(issues_diff)
|
||||||
|
|
||||||
|
current = version
|
||||||
|
|
||||||
|
return diff_list
|
|
@ -67,7 +67,7 @@ class Command(BaseCommand):
|
||||||
elif start_date <= now() and end_date >= now():
|
elif start_date <= now() and end_date >= now():
|
||||||
task = self.create_task(project, milestone, us, start_date, now())
|
task = self.create_task(project, milestone, us, start_date, now())
|
||||||
else:
|
else:
|
||||||
# No task on not initiated sprints
|
# No task on not initiated milestones
|
||||||
pass
|
pass
|
||||||
|
|
||||||
start_date = end_date
|
start_date = end_date
|
||||||
|
@ -191,7 +191,7 @@ class Command(BaseCommand):
|
||||||
owner=random.choice(self.users),
|
owner=random.choice(self.users),
|
||||||
public=True,
|
public=True,
|
||||||
total_story_points=self.sd.int(100, 150),
|
total_story_points=self.sd.int(100, 150),
|
||||||
sprints=self.sd.int(5,10)
|
milestones=self.sd.int(5,10)
|
||||||
)
|
)
|
||||||
|
|
||||||
project.save()
|
project.save()
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneInline(admin.TabularInline):
|
||||||
|
model = models.Milestone
|
||||||
|
fields = ('name', 'owner', 'estimated_start', 'estimated_finish', 'closed',
|
||||||
|
'disponibility', 'order')
|
||||||
|
sortable_field_name = 'order'
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneAdmin(reversion.VersionAdmin):
|
||||||
|
list_display = ["name", "project", "owner", "closed", "estimated_start",
|
||||||
|
"estimated_finish"]
|
||||||
|
|
||||||
|
admin.site.register(models.Milestone, MilestoneAdmin)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from greenmine.base import filters
|
||||||
|
from greenmine.base.api import ModelCrudViewSet,
|
||||||
|
|
||||||
|
from greenmine.base.notifications.api import NotificationSenderMixin
|
||||||
|
|
||||||
|
from . import serializers
|
||||||
|
from . import models
|
||||||
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneViewSet(NotificationSenderMixin, ModelCrudViewSet):
|
||||||
|
model= models.Milestone
|
||||||
|
serializer_class = serializers.MilestoneSerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.MilestonePermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ("project",)
|
||||||
|
create_notification_template = "create_milestone_notification"
|
||||||
|
update_notification_template = "update_milestone_notification"
|
||||||
|
destroy_notification_template = "destroy_milestone_notification"
|
||||||
|
|
||||||
|
def pre_save(self, obj):
|
||||||
|
super(MilestoneViewSet, self).pre_save(obj)
|
||||||
|
obj.owner = self.request.user
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from greenmine.base.utils.slug import slugify_uniquely
|
||||||
|
from greenmine.base.notifications.models import WatchedMixin
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
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"))
|
||||||
|
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
|
||||||
|
verbose_name=_("slug"))
|
||||||
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
|
||||||
|
related_name="owned_milestones", verbose_name=_("owner"))
|
||||||
|
project = models.ForeignKey("projects.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,
|
||||||
|
verbose_name=_("estimated finish"))
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
|
||||||
|
verbose_name=_("created date"))
|
||||||
|
modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
|
||||||
|
verbose_name=_("modified date"))
|
||||||
|
closed = models.BooleanField(default=False, null=False, blank=True,
|
||||||
|
verbose_name=_("is closed"))
|
||||||
|
disponibility = models.FloatField(default=0.0, null=True, blank=True,
|
||||||
|
verbose_name=_("disponibility"))
|
||||||
|
order = models.PositiveSmallIntegerField(default=1, null=False, blank=False,
|
||||||
|
verbose_name=_("order"))
|
||||||
|
|
||||||
|
notifiable_fields = [
|
||||||
|
"name",
|
||||||
|
"owner",
|
||||||
|
"estimated_start",
|
||||||
|
"estimated_finish",
|
||||||
|
"closed",
|
||||||
|
"disponibility",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"milestone"
|
||||||
|
verbose_name_plural = u"milestones"
|
||||||
|
ordering = ["project", "-created_date"]
|
||||||
|
unique_together = ("name", "project")
|
||||||
|
permissions = (
|
||||||
|
("view_milestone", "Can view milestones"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return u"<Milestone {0}>".format(self.id)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify_uniquely(self.name, self.__class__)
|
||||||
|
|
||||||
|
super(Milestone, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def closed_points(self):
|
||||||
|
# TODO: refactor or remove
|
||||||
|
#points = [ us.points.value for us in self.user_stories.all() if us.is_closed ]
|
||||||
|
#return sum(points)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client_increment_points(self):
|
||||||
|
# TODO: refactor or remove
|
||||||
|
#user_stories = UserStory.objects.filter(
|
||||||
|
# created_date__gte=self.estimated_start,
|
||||||
|
# created_date__lt=self.estimated_finish,
|
||||||
|
# project_id = self.project_id,
|
||||||
|
# client_requirement=True,
|
||||||
|
# team_requirement=False
|
||||||
|
#)
|
||||||
|
#points = [ us.points.value for us in user_stories ]
|
||||||
|
#return sum(points) + (self.shared_increment_points / 2)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def team_increment_points(self):
|
||||||
|
# TODO: refactor or remove
|
||||||
|
#user_stories = UserStory.objects.filter(
|
||||||
|
# created_date__gte=self.estimated_start,
|
||||||
|
# created_date__lt=self.estimated_finish,
|
||||||
|
# project_id = self.project_id,
|
||||||
|
# client_requirement=False,
|
||||||
|
# team_requirement=True
|
||||||
|
#)
|
||||||
|
#points = [ us.points.value for us in user_stories ]
|
||||||
|
#return sum(points) + (self.shared_increment_points / 2)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shared_increment_points(self):
|
||||||
|
# TODO: refactor or remove
|
||||||
|
#user_stories = UserStory.objects.filter(
|
||||||
|
# created_date__gte=self.estimated_start,
|
||||||
|
# created_date__lt=self.estimated_finish,
|
||||||
|
# project_id = self.project_id,
|
||||||
|
# client_requirement=True,
|
||||||
|
# team_requirement=True
|
||||||
|
#)
|
||||||
|
#points = [ us.points.value for us in user_stories ]
|
||||||
|
#return sum(points)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def _get_watchers_by_role(self):
|
||||||
|
return {
|
||||||
|
"owner": self.owner,
|
||||||
|
"project_owner": (self.project, self.project.owner),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Reversion registration (usufull for base.notification and for meke a historical)
|
||||||
|
reversion.register(Milestone)
|
|
@ -0,0 +1,13 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from greenmine.base.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
|
class MilestonePermission(BasePermission):
|
||||||
|
get_permission = "view_milestone"
|
||||||
|
put_permission = "change_milestone"
|
||||||
|
patch_permission = "change_milestone"
|
||||||
|
delete_permission = "delete_milestone"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from greenmine.projects.userstories.serializers import user_stories
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
import json, reversion
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneSerializer(serializers.ModelSerializer):
|
||||||
|
user_stories = UserStorySerializer(many=True, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Milestone
|
|
@ -0,0 +1,219 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.loading import get_model
|
||||||
|
from django.conf import settings
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.contenttypes import generic
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from picklefield.fields import PickledObjectField
|
||||||
|
|
||||||
|
from greenmine.base.utils.slug import slugify_uniquely
|
||||||
|
from greenmine.base.notifications.models import WatchedMixin
|
||||||
|
from . import choices
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class Attachment(models.Model):
|
||||||
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
|
||||||
|
related_name="change_attachments", verbose_name=_("owner"))
|
||||||
|
project = models.ForeignKey("Project", null=False, blank=False,
|
||||||
|
related_name="attachments", verbose_name=_("project"))
|
||||||
|
content_type = models.ForeignKey(ContentType, null=False, blank=False,
|
||||||
|
verbose_name=_("content type"))
|
||||||
|
object_id = models.PositiveIntegerField(null=False, blank=False,
|
||||||
|
verbose_name=_("object id"))
|
||||||
|
content_object = generic.GenericForeignKey("content_type", "object_id")
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
|
||||||
|
verbose_name=_("created date"))
|
||||||
|
modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
|
||||||
|
verbose_name=_("modified date"))
|
||||||
|
attached_file = models.FileField(max_length=500, null=True, blank=True,
|
||||||
|
upload_to="files/msg", verbose_name=_("attached file"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"attachment"
|
||||||
|
verbose_name_plural = u"attachments"
|
||||||
|
ordering = ["project", "created_date"]
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"content_type {0} - object_id {1} - attachment {2}".format(
|
||||||
|
self.content_type, self.object_id, self.id)
|
||||||
|
|
||||||
|
|
||||||
|
class Membership(models.Model):
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
|
||||||
|
related_name="memberships")
|
||||||
|
project = models.ForeignKey("Project", null=False, blank=False,
|
||||||
|
related_name="memberships")
|
||||||
|
role = models.ForeignKey("users.Role", null=False, blank=False,
|
||||||
|
related_name="memberships")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("user", "project")
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
verbose_name=_("name"))
|
||||||
|
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
|
||||||
|
verbose_name=_("slug"))
|
||||||
|
description = models.TextField(null=False, blank=False,
|
||||||
|
verbose_name=_("description"))
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
|
||||||
|
verbose_name=_("created date"))
|
||||||
|
modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
|
||||||
|
verbose_name=_("modified date"))
|
||||||
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, blank=False,
|
||||||
|
related_name="owned_projects", verbose_name=_("owner"))
|
||||||
|
members = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="projects",
|
||||||
|
through="Membership", verbose_name=_("members"))
|
||||||
|
public = models.BooleanField(default=True, null=False, blank=True,
|
||||||
|
verbose_name=_("public"))
|
||||||
|
last_us_ref = models.BigIntegerField(null=True, blank=False, default=1,
|
||||||
|
verbose_name=_("last us ref"))
|
||||||
|
last_task_ref = models.BigIntegerField(null=True, blank=False, default=1,
|
||||||
|
verbose_name=_("last task ref"))
|
||||||
|
last_issue_ref = models.BigIntegerField(null=True, blank=False, default=1,
|
||||||
|
verbose_name=_("last issue ref"))
|
||||||
|
total_milestones = models.IntegerField(default=1, null=True, blank=True,
|
||||||
|
verbose_name=_("total of milestones"))
|
||||||
|
total_story_points = models.FloatField(default=None, null=True, blank=False,
|
||||||
|
verbose_name=_("total story points"))
|
||||||
|
tags = PickledObjectField(null=False, blank=True,
|
||||||
|
verbose_name=_("tags"))
|
||||||
|
|
||||||
|
notifiable_fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"owner",
|
||||||
|
"members",
|
||||||
|
"public",
|
||||||
|
"tags",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"project"
|
||||||
|
verbose_name_plural = u"projects"
|
||||||
|
ordering = ["name"]
|
||||||
|
permissions = (
|
||||||
|
("list_projects", "Can list projects"),
|
||||||
|
("view_project", "Can view project"),
|
||||||
|
("manage_users", "Can manage users"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return u"<Project {0}>".format(self.id)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.slug:
|
||||||
|
self.slug = slugify_uniquely(self.name, self.__class__)
|
||||||
|
|
||||||
|
super(Project, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
def _get_watchers_by_role(self):
|
||||||
|
return {"owner": self.owner}
|
||||||
|
|
||||||
|
@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")]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_roles(self):
|
||||||
|
role_model = get_model("users", "Role")
|
||||||
|
return role_model.objects.filter(id__in=list(self.memberships.values_list(
|
||||||
|
"role", flat=True)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def list_users(self):
|
||||||
|
user_model = get_user_model()
|
||||||
|
return user_model.objects.filter(id__in=list(self.memberships.values_list(
|
||||||
|
"user", flat=True)))
|
||||||
|
|
||||||
|
def update_role_points(self):
|
||||||
|
roles = self.list_roles
|
||||||
|
role_ids = roles.values_list("id", flat=True)
|
||||||
|
null_points = self.points.get(value=None)
|
||||||
|
for us in self.user_stories.all():
|
||||||
|
for role in roles:
|
||||||
|
try:
|
||||||
|
sp = us.role_points.get(role=role, user_story=us)
|
||||||
|
except RolePoints.DoesNotExist:
|
||||||
|
sp = RolePoints.objects.create(role=role,
|
||||||
|
user_story=us,
|
||||||
|
points=null_points)
|
||||||
|
|
||||||
|
#Remove unnecesary Role points
|
||||||
|
rp_query = RolePoints.objects.filter(user_story__in=self.user_stories.all())
|
||||||
|
rp_query = rp_query.exclude(role__id__in=role_ids)
|
||||||
|
rp_query.delete()
|
||||||
|
|
||||||
|
|
||||||
|
# Reversion registration (usufull for base.notification and for meke a historical)
|
||||||
|
reversion.register(Project)
|
||||||
|
|
||||||
|
# Signals dispatches
|
||||||
|
@receiver(models.signals.post_save, sender=Membership,
|
||||||
|
dispatch_uid='membership_post_save')
|
||||||
|
def membership_post_save(sender, instance, created, **kwargs):
|
||||||
|
instance.project.update_role_points()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_delete, sender=Membership,
|
||||||
|
dispatch_uid='membership_pre_delete')
|
||||||
|
def membership_post_delete(sender, instance, using, **kwargs):
|
||||||
|
instance.project.update_role_points()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.post_save, sender=Project, dispatch_uid='project_post_save')
|
||||||
|
def project_post_save(sender, instance, created, **kwargs):
|
||||||
|
"""
|
||||||
|
Create all project model depences on project is
|
||||||
|
created.
|
||||||
|
"""
|
||||||
|
if not created:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Populate new project dependen default data
|
||||||
|
for order, name in choices.PRIORITY_CHOICES:
|
||||||
|
Priority.objects.create(project=instance, name=name, order=order)
|
||||||
|
|
||||||
|
for order, name in choices.SEVERITY_CHOICES:
|
||||||
|
Severity.objects.create(project=instance, name=name, order=order)
|
||||||
|
|
||||||
|
for order, name, value in choices.POINTS_CHOICES:
|
||||||
|
Points.objects.create(project=instance, name=name, order=order, value=value)
|
||||||
|
|
||||||
|
for order, name, is_closed in choices.USSTATUSES:
|
||||||
|
UserStoryStatus.objects.create(name=name, order=order,
|
||||||
|
is_closed=is_closed, project=instance)
|
||||||
|
|
||||||
|
for order, name, is_closed, color in choices.TASKSTATUSES:
|
||||||
|
TaskStatus.objects.create(name=name, order=order, color=color,
|
||||||
|
is_closed=is_closed, project=instance)
|
||||||
|
|
||||||
|
for order, name, is_closed in choices.ISSUESTATUSES:
|
||||||
|
IssueStatus.objects.create(name=name, order=order,
|
||||||
|
is_closed=is_closed, project=instance)
|
||||||
|
|
||||||
|
for order, name in choices.ISSUETYPES:
|
||||||
|
IssueType.objects.create(project=instance, name=name, order=order)
|
||||||
|
|
||||||
|
for order, name, is_closed in choices.QUESTION_STATUS:
|
||||||
|
QuestionStatus.objects.create(name=name, order=order,
|
||||||
|
is_closed=is_closed, project=instance)
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from greenmine.base.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectPermission(BasePermission):
|
||||||
|
get_permission = "view_project"
|
||||||
|
put_permission = "change_project"
|
||||||
|
patch_permission = "change_project"
|
||||||
|
delete_permission = "delete_project"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = []
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentPermission(BasePermission):
|
||||||
|
get_permission = "view_attachment"
|
||||||
|
put_permission = "change_attachment"
|
||||||
|
patch_permission = "change_attachment"
|
||||||
|
delete_permission = "delete_attachment"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
|
@ -7,13 +7,13 @@ from . import models
|
||||||
import reversion
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
class QuestionAdmin(reversion.VersionAdmin):
|
|
||||||
list_display = ["subject", "project", "owner"]
|
|
||||||
|
|
||||||
admin.site.register(models.Question, QuestionAdmin)
|
|
||||||
|
|
||||||
|
|
||||||
class QuestionStatusAdmin(admin.ModelAdmin):
|
class QuestionStatusAdmin(admin.ModelAdmin):
|
||||||
list_display = ["name", "order", "is_closed", "project"]
|
list_display = ["name", "order", "is_closed", "project"]
|
||||||
|
|
||||||
admin.site.register(models.QuestionStatus, QuestionStatusAdmin)
|
admin.site.register(models.QuestionStatus, QuestionStatusAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionAdmin(reversion.VersionAdmin):
|
||||||
|
list_display = ["subject", "project", "owner"]
|
||||||
|
|
||||||
|
admin.site.register(models.Question, QuestionAdmin)
|
|
@ -27,7 +27,7 @@ class QuestionList(generics.ListCreateAPIView):
|
||||||
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
|
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||||
model = models.Question
|
model = models.Question
|
||||||
serializer_class = serializers.QuestionSerializer
|
serializer_class = serializers.QuestionSerializer
|
||||||
permission_classes = (IsAuthenticated, permissions.QuestionDetailPermission,)
|
permission_classes = (IsAuthenticated, permissions.QuestionPermission,)
|
||||||
|
|
||||||
def post_save(self, obj, created=False):
|
def post_save(self, obj, created=False):
|
||||||
with reversion.create_revision():
|
with reversion.create_revision():
|
|
@ -6,11 +6,11 @@ from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from greenmine.base.utils.slug import slugify_uniquely, ref_uniquely
|
from greenmine.base.utils.slug import ref_uniquely
|
||||||
from greenmine.scrum.models import Project
|
|
||||||
|
|
||||||
from picklefield.fields import PickledObjectField
|
from picklefield.fields import PickledObjectField
|
||||||
from greenmine.questions.choices import QUESTION_STATUS
|
|
||||||
|
from . import choices
|
||||||
|
|
||||||
|
|
||||||
class QuestionStatus(models.Model):
|
class QuestionStatus(models.Model):
|
||||||
|
@ -20,7 +20,7 @@ class QuestionStatus(models.Model):
|
||||||
verbose_name=_('order'))
|
verbose_name=_('order'))
|
||||||
is_closed = models.BooleanField(default=False, null=False, blank=True,
|
is_closed = models.BooleanField(default=False, null=False, blank=True,
|
||||||
verbose_name=_('is closed'))
|
verbose_name=_('is closed'))
|
||||||
project = models.ForeignKey(Project, null=False, blank=False,
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
related_name='question_status',
|
related_name='question_status',
|
||||||
verbose_name=_('project'))
|
verbose_name=_('project'))
|
||||||
|
|
||||||
|
@ -53,10 +53,10 @@ class Question(models.Model):
|
||||||
attached_file = models.FileField(max_length=500, null=True, blank=True,
|
attached_file = models.FileField(max_length=500, null=True, blank=True,
|
||||||
upload_to='messages',
|
upload_to='messages',
|
||||||
verbose_name=_('attached_file'))
|
verbose_name=_('attached_file'))
|
||||||
project = models.ForeignKey('scrum.Project', null=False, blank=False,
|
project = models.ForeignKey('projects.Project', null=False, blank=False,
|
||||||
related_name='questions',
|
related_name='questions',
|
||||||
verbose_name=_('project'))
|
verbose_name=_('project'))
|
||||||
milestone = models.ForeignKey('scrum.Milestone', null=True, blank=True, default=None,
|
milestone = models.ForeignKey('milestones.Milestone', null=True, blank=True, default=None,
|
||||||
related_name='questions',
|
related_name='questions',
|
||||||
verbose_name=_('milestone'))
|
verbose_name=_('milestone'))
|
||||||
finished_date = models.DateTimeField(null=True, blank=True,
|
finished_date = models.DateTimeField(null=True, blank=True,
|
||||||
|
@ -100,20 +100,3 @@ class Question(models.Model):
|
||||||
self.ref = ref_uniquely(self.project, 'last_issue_ref', self.__class__)
|
self.ref = ref_uniquely(self.project, 'last_issue_ref', self.__class__)
|
||||||
|
|
||||||
super(Question, self).save(*args, **kwargs)
|
super(Question, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
# Model related signals handlers
|
|
||||||
@receiver(models.signals.post_save, sender=Project, dispatch_uid='project_post_save_add_question_states')
|
|
||||||
def project_post_save_add_question_states(sender, instance, created, **kwargs):
|
|
||||||
"""
|
|
||||||
Create all project model depences on project is
|
|
||||||
created.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not created:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Populate new project dependen default data
|
|
||||||
for order, name, is_closed in QUESTION_STATUS:
|
|
||||||
QuestionStatus.objects.create(name=name, order=order,
|
|
||||||
is_closed=is_closed, project=instance)
|
|
|
@ -1,13 +1,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from greenmine.base.permissions import BaseDetailPermission
|
from greenmine.base.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
class QuestionDetailPermission(BaseDetailPermission):
|
class QuestionPermission(BasePermission):
|
||||||
get_permission = "can_view_question"
|
get_permission = "can_view_question"
|
||||||
put_permission = "change_question"
|
put_permission = "change_question"
|
||||||
patch_permission = "change_question"
|
patch_permission = "change_question"
|
||||||
delete_permission = "delete_question"
|
delete_permission = "delete_question"
|
||||||
safe_methods = ['HEAD', 'OPTIONS']
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
path_to_project = []
|
path_to_project = []
|
||||||
|
|
|
@ -4,43 +4,43 @@ from rest_framework import serializers
|
||||||
|
|
||||||
import reversion
|
import reversion
|
||||||
|
|
||||||
from greenmine.scrum.serializers import PickleField
|
from greenmine.base.serializers import PickleField
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class QuestionSerializer(serializers.ModelSerializer):
|
class QuestionSerializer(serializers.ModelSerializer):
|
||||||
tags = PickleField()
|
tags = PickleField()
|
||||||
comment = serializers.SerializerMethodField('get_comment')
|
comment = serializers.SerializerMethodField("get_comment")
|
||||||
history = serializers.SerializerMethodField('get_history')
|
history = serializers.SerializerMethodField("get_history")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Question
|
model = models.Question
|
||||||
fields = ()
|
fields = ()
|
||||||
|
|
||||||
def get_comment(self, obj):
|
def get_comment(self, obj):
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def get_questions_diff(self, old_question_version, new_question_version):
|
def get_questions_diff(self, old_question_version, new_question_version):
|
||||||
old_obj = old_question_version.field_dict
|
old_obj = old_question_version.field_dict
|
||||||
new_obj = new_question_version.field_dict
|
new_obj = new_question_version.field_dict
|
||||||
|
|
||||||
diff_dict = {
|
diff_dict = {
|
||||||
'modified_date': new_obj['modified_date'],
|
"modified_date": new_obj["modified_date"],
|
||||||
'by': old_question_version.revision.user,
|
"by": old_question_version.revision.user,
|
||||||
'comment': old_question_version.revision.comment,
|
"comment": old_question_version.revision.comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
for key in old_obj.keys():
|
for key in old_obj.keys():
|
||||||
if key == 'modified_date':
|
if key == "modified_date":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if old_obj[key] == new_obj[key]:
|
if old_obj[key] == new_obj[key]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
diff_dict[key] = {
|
diff_dict[key] = {
|
||||||
'old': old_obj[key],
|
"old": old_obj[key],
|
||||||
'new': new_obj[key],
|
"new": new_obj[key],
|
||||||
}
|
}
|
||||||
|
|
||||||
return diff_dict
|
return diff_dict
|
|
@ -0,0 +1,32 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from greenmine.base.serializers import PickleField
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentSerializer(serializers.ModelSerializer):
|
||||||
|
url = serializers.SerializerMethodField("get_url")
|
||||||
|
|
||||||
|
def get_url(self, obj):
|
||||||
|
# FIXME: add sites or correct url.
|
||||||
|
if obj.attached_file:
|
||||||
|
return "http://localhost:8000{0}".format(obj.attached_file.url)
|
||||||
|
return None
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Attachment
|
||||||
|
fields = ("id", "project", "owner", "attached_file",
|
||||||
|
"created_date", "object_id", "url")
|
||||||
|
read_only_fields = ("owner",)
|
||||||
|
fields = ()
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectSerializer(serializers.ModelSerializer):
|
||||||
|
tags = PickleField()
|
||||||
|
list_of_milestones = serializers.Field(source="list_of_milestones")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Project
|
|
@ -39,7 +39,7 @@ def mail_recovery_password(sender, user, **kwargs):
|
||||||
# participants = milestone.project.all_participants()
|
# participants = milestone.project.all_participants()
|
||||||
#
|
#
|
||||||
# emails_list = []
|
# emails_list = []
|
||||||
# subject = ugettext("Greenmine: sprint created")
|
# subject = ugettext("Greenmine: milestone created")
|
||||||
# for person in participants:
|
# for person in participants:
|
||||||
# template = render_to_string("email/milestone.created.html", {
|
# template = render_to_string("email/milestone.created.html", {
|
||||||
# "person": person,
|
# "person": person,
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatusAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "order", "is_closed", "project"]
|
||||||
|
|
||||||
|
admin.site.register(models.TaskStatus, TaskStatusAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class TaskAdmin(reversion.VersionAdmin):
|
||||||
|
list_display = ["subject", "ref", "user_story", "milestone", "project", "user_story_id"]
|
||||||
|
list_filter = ["user_story", "milestone", "project"]
|
||||||
|
|
||||||
|
def user_story_id(self, instance):
|
||||||
|
return instance.user_story.id
|
||||||
|
|
||||||
|
admin.site.register(models.Task, TaskAdmin)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from greenmine.base import filters
|
||||||
|
from greenmine.base.api import (
|
||||||
|
ModelCrudViewSet,
|
||||||
|
ModelListViewSet
|
||||||
|
)
|
||||||
|
from greenmine.base.notifications.api import NotificationSenderMixin
|
||||||
|
from greenmine.projects.permissions import AttachmentPermission
|
||||||
|
from greenmine.projects.serializers import AttachmentSerializer
|
||||||
|
|
||||||
|
from . import serializers
|
||||||
|
from . import models
|
||||||
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatusViewSet(ModelListViewSet):
|
||||||
|
model = models.TaskStatus
|
||||||
|
serializer_class = serializers.TaskStatusSerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.TaskStatusPermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ("project",)
|
||||||
|
|
||||||
|
|
||||||
|
class TasksAttachmentViewSet(ModelCrudViewSet):
|
||||||
|
model = Attachment
|
||||||
|
serializer_class = AttachmentSerializer
|
||||||
|
permission_classes = (IsAuthenticated, AttachmentPermission,)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ["project", "object_id"]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
ct = ContentType.objects.get_for_model(models.Task)
|
||||||
|
qs = super(TasksAttachmentViewSet, self).get_queryset()
|
||||||
|
qs = qs.filter(content_type=ct)
|
||||||
|
return qs.distinct()
|
||||||
|
|
||||||
|
def pre_save(self, obj):
|
||||||
|
super(TasksAttachmentViewSet, self).pre_save(obj)
|
||||||
|
obj.content_type = ContentType.objects.get_for_model(Task)
|
||||||
|
obj.owner = self.request.user
|
||||||
|
|
||||||
|
|
||||||
|
class TaskViewSet(NotificationSenderMixin, ModelCrudViewSet):
|
||||||
|
model = models.Task
|
||||||
|
serializer_class = serializers.TaskSerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.TaskPermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ["user_story", "milestone", "project"]
|
||||||
|
create_notification_template = "create_task_notification"
|
||||||
|
update_notification_template = "update_task_notification"
|
||||||
|
destroy_notification_template = "destroy_task_notification"
|
||||||
|
|
||||||
|
def pre_save(self, obj):
|
||||||
|
super(TaskViewSet, self).pre_save(obj)
|
||||||
|
obj.owner = self.request.user
|
||||||
|
obj.milestone = obj.user_story.milestone
|
||||||
|
|
||||||
|
def post_save(self, obj, created=False):
|
||||||
|
with reversion.create_revision():
|
||||||
|
if "comment" in self.request.DATA:
|
||||||
|
# Update the comment in the last version
|
||||||
|
reversion.set_comment(self.request.DATA["comment"])
|
||||||
|
super(TaskViewSet, self).post_save(obj, created)
|
|
@ -0,0 +1,147 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from picklefield.fields import PickledObjectField
|
||||||
|
|
||||||
|
from greenmine.base.utils.slug import ref_uniquely
|
||||||
|
from greenmine.base.notifications.models import WatchedMixin
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatus(models.Model):
|
||||||
|
name = models.CharField(max_length=255, null=False, blank=False, verbose_name=_("name"))
|
||||||
|
order = models.IntegerField(default=10, null=False, blank=False, verbose_name=_("order"))
|
||||||
|
is_closed = models.BooleanField(default=False, null=False, blank=True,
|
||||||
|
verbose_name=_("is closed"))
|
||||||
|
color = models.CharField(max_length=20, null=False, blank=False, default="#999999",
|
||||||
|
verbose_name=_("color"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="task_statuses", verbose_name=_("project"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"task status"
|
||||||
|
verbose_name_plural = u"task statuses"
|
||||||
|
ordering = ["project", "name"]
|
||||||
|
unique_together = ("project", "name")
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"project {0} - {1}".format(self.project_id, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class Task(models.Model, WatchedMixin):
|
||||||
|
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
|
||||||
|
verbose_name=_("uuid"))
|
||||||
|
user_story = models.ForeignKey("userstories.UserStory", null=True, blank=True,
|
||||||
|
related_name="tasks", verbose_name=_("user story"))
|
||||||
|
ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None,
|
||||||
|
verbose_name=_("ref"))
|
||||||
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, default=None,
|
||||||
|
related_name="owned_tasks", verbose_name=_("owner"))
|
||||||
|
status = models.ForeignKey("TaskStatus", null=False, blank=False,
|
||||||
|
related_name="tasks", verbose_name=_("status"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="tasks", verbose_name=_("project"))
|
||||||
|
milestone = models.ForeignKey("milestones.Milestone", null=True, blank=True, default=None,
|
||||||
|
related_name="tasks", verbose_name=_("milestone"))
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
|
||||||
|
verbose_name=_("created date"))
|
||||||
|
modified_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
|
||||||
|
verbose_name=_("modified date"))
|
||||||
|
finished_date = models.DateTimeField(null=True, blank=True,
|
||||||
|
verbose_name=_("finished date"))
|
||||||
|
subject = models.CharField(max_length=500, null=False, blank=False,
|
||||||
|
verbose_name=_("subject"))
|
||||||
|
description = models.TextField(null=False, blank=True, verbose_name=_("description"))
|
||||||
|
assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True,
|
||||||
|
default=None, related_name="user_storys_assigned_to_me",
|
||||||
|
verbose_name=_("assigned to"))
|
||||||
|
watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
|
||||||
|
related_name="watched_tasks", verbose_name=_("watchers"))
|
||||||
|
tags = PickledObjectField(null=False, blank=True, verbose_name=_("tags"))
|
||||||
|
is_iocaine = models.BooleanField(default=False, null=False, blank=True,
|
||||||
|
verbose_name=_("is iocaine"))
|
||||||
|
|
||||||
|
notifiable_fields = [
|
||||||
|
"owner",
|
||||||
|
"status",
|
||||||
|
"finished_date",
|
||||||
|
"subject",
|
||||||
|
"description",
|
||||||
|
"assigned_to",
|
||||||
|
"tags",
|
||||||
|
"is_iocaine",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"task"
|
||||||
|
verbose_name_plural = u"tasks"
|
||||||
|
ordering = ["project", "created_date"]
|
||||||
|
unique_together = ("ref", "project")
|
||||||
|
permissions = (
|
||||||
|
("comment_task", "Can comment tasks"),
|
||||||
|
("change_owned_task", "Can modify owned tasks"),
|
||||||
|
("change_assigned_task", "Can modify assigned tasks"),
|
||||||
|
("assign_task_to_other", "Can assign tasks to others"),
|
||||||
|
("assign_task_to_myself", "Can assign tasks to myself"),
|
||||||
|
("change_task_state", "Can change the task state"),
|
||||||
|
("view_task", "Can view the task"),
|
||||||
|
("add_task_to_us", "Can add tasks to a user story"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"({1}) {0}".format(self.ref, self.subject)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.id:
|
||||||
|
self.modified_date = timezone.now()
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Reversion registration (usufull for base.notification and for meke a historical)
|
||||||
|
reversion.register(Task)
|
||||||
|
|
||||||
|
|
||||||
|
# Model related signals handlers
|
||||||
|
@receiver(models.signals.pre_save, sender=Task, dispatch_uid="task_ref_handler")
|
||||||
|
def task_ref_handler(sender, instance, **kwargs):
|
||||||
|
if not instance.id and instance.project:
|
||||||
|
instance.ref = ref_uniquely(instance.project, "last_task_ref", instance.__class__)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(models.signals.pre_save, sender=Task, dispatch_uid="tasks_close_handler")
|
||||||
|
def tasks_close_handler(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Automatically assignes a seguent reference code to a
|
||||||
|
user story if that is not created.
|
||||||
|
"""
|
||||||
|
if instance.id:
|
||||||
|
if (sender.objects.get(id=instance.id).status.is_closed == False and
|
||||||
|
instance.status.is_closed == True):
|
||||||
|
instance.finished_date = timezone.now()
|
||||||
|
if (all([task.status.is_closed for task in
|
||||||
|
instance.user_story.tasks.exclude(id=instance.id)])):
|
||||||
|
instance.user_story.finish_date = timezone.now()
|
||||||
|
instance.user_story.save()
|
||||||
|
elif (sender.objects.get(id=instance.id).status.is_closed == True and
|
||||||
|
instance.status.is_closed == False):
|
||||||
|
instance.finished_date = None
|
||||||
|
instance.user_story.finish_date = None
|
||||||
|
instance.user_story.save()
|
||||||
|
else:
|
||||||
|
instance.user_story.finish_date = None
|
||||||
|
instance.user_story.save()
|
|
@ -0,0 +1,21 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from greenmine.base.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
|
class TaskPermission(BasePermission):
|
||||||
|
get_permission = "view_task"
|
||||||
|
put_permission = "change_task"
|
||||||
|
patch_permission = "change_task"
|
||||||
|
delete_permission = "delete_task"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatusPermission(BasePermission):
|
||||||
|
get_permission = "view_taskstatus"
|
||||||
|
put_permission = "change_taskstatus"
|
||||||
|
patch_permission = "change_taskstatus"
|
||||||
|
delete_permission = "delete_taskstatus"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -* coding: utf-8 -*-
|
||||||
|
|
||||||
|
from haystack import indexes
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class TaskIndex(indexes.SearchIndex, indexes.Indexable):
|
||||||
|
text = indexes.CharField(document=True, use_template=True,
|
||||||
|
template_name='search/indexes/task_text.txt')
|
||||||
|
title = indexes.CharField(model_attr='subject')
|
||||||
|
project_id = indexes.IntegerField(model_attr="project_id")
|
||||||
|
description = indexes.CharField(model_attr="description")
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return models.Task
|
||||||
|
|
||||||
|
def index_queryset(self, using=None):
|
||||||
|
return self.get_model().objects.all()
|
|
@ -0,0 +1,63 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from greenmine.base.serializers import PickleField
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class TaskStatusSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.TaskStatus
|
||||||
|
|
||||||
|
|
||||||
|
class TaskSerializer(serializers.ModelSerializer):
|
||||||
|
tags = PickleField(blank=True, default=[])
|
||||||
|
comment = serializers.SerializerMethodField("get_comment")
|
||||||
|
history = serializers.SerializerMethodField("get_history")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Task
|
||||||
|
|
||||||
|
def get_comment(self, obj):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_task_diff(self, old_task_version, new_task_version):
|
||||||
|
old_obj = old_task_version.field_dict
|
||||||
|
new_obj = new_task_version.field_dict
|
||||||
|
|
||||||
|
diff_dict = {
|
||||||
|
"modified_date": new_obj["modified_date"],
|
||||||
|
"by": new_task_version.revision.user,
|
||||||
|
"comment": new_task_version.revision.comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in old_obj.keys():
|
||||||
|
if key == "modified_date":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if old_obj[key] == new_obj[key]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
diff_dict[key] = {
|
||||||
|
"old": old_obj[key],
|
||||||
|
"new": new_obj[key],
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff_dict
|
||||||
|
|
||||||
|
def get_history(self, obj):
|
||||||
|
diff_list = []
|
||||||
|
current = None
|
||||||
|
|
||||||
|
for version in reversed(list(reversion.get_for_object(obj))):
|
||||||
|
if current:
|
||||||
|
task_diff = self.get_task_diff(current, version)
|
||||||
|
diff_list.append(task_diff)
|
||||||
|
|
||||||
|
current = version
|
||||||
|
|
||||||
|
return diff_list
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
i
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryInline(admin.TabularInline):
|
||||||
|
model = models.UserStory
|
||||||
|
fields = ('subject', 'order')
|
||||||
|
sortable_field_name = 'order'
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
def get_inline_instances(self, request, obj=None):
|
||||||
|
if obj:
|
||||||
|
return obj.user_stories.filter(mileston__isnone=True)
|
||||||
|
else:
|
||||||
|
return models.UserStory.objects.none()
|
||||||
|
|
||||||
|
|
||||||
|
class PointsAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "order", "project"]
|
||||||
|
|
||||||
|
admin.site.register(models.Points, PointsAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryStatusAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ["name", "order", "is_closed", "project"]
|
||||||
|
|
||||||
|
admin.site.register(models.UserStoryStatus, UserStoryStatusAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class RolePointsInline(admin.TabularInline):
|
||||||
|
model = models.RolePoints
|
||||||
|
sortable_field_name = 'role'
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryAdmin(reversion.VersionAdmin):
|
||||||
|
list_display = ["id", "ref", "milestone", "project", "owner", 'status', 'is_closed']
|
||||||
|
list_filter = ["milestone", "project"]
|
||||||
|
inlines = [RolePointsInline]
|
||||||
|
|
||||||
|
admin.site.register(models.UserStory, UserStoryAdmin)
|
|
@ -0,0 +1,52 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
|
||||||
|
from greenmine.base import filters
|
||||||
|
from greenmine.base.api import (
|
||||||
|
ModelCrudViewSet,
|
||||||
|
ModelListViewSet
|
||||||
|
)
|
||||||
|
from greenmine.base.notifications.api import NotificationSenderMixin
|
||||||
|
|
||||||
|
from . import serializers
|
||||||
|
from . import models
|
||||||
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
|
class PointsViewSet(ModelListViewSet):
|
||||||
|
model = models.Points
|
||||||
|
serializer_class = serializer.PointsSerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.PointsPermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryStatusViewSet(ModelListViewSet):
|
||||||
|
model = models.UserStoryStatus
|
||||||
|
serializer_class = serializers.UserStoryStatusSerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.UserStoryStatusPermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ('project',)
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryViewSet(NotificationSenderMixin, ModelCrudViewSet):
|
||||||
|
model = models.UserStory
|
||||||
|
serializer_class = serializersUserStorySerializer
|
||||||
|
permission_classes = (IsAuthenticated, permissions.UserStoryPermission)
|
||||||
|
filter_backends = (filters.IsProjectMemberFilterBackend,)
|
||||||
|
filter_fields = ['project', 'milestone', 'milestone__isnull']
|
||||||
|
create_notification_template = "create_userstory_notification"
|
||||||
|
update_notification_template = "update_userstory_notification"
|
||||||
|
destroy_notification_template = "destroy_userstory_notification"
|
||||||
|
|
||||||
|
def pre_save(self, obj):
|
||||||
|
super(UserStoryViewSet, self).pre_save(obj)
|
||||||
|
obj.owner = self.request.user
|
||||||
|
|
||||||
|
def post_save(self, obj, created=False):
|
||||||
|
with reversion.create_revision():
|
||||||
|
if "comment" in self.request.DATA:
|
||||||
|
# Update the comment in the last version
|
||||||
|
reversion.set_comment(self.request.DATA['comment'])
|
||||||
|
super(UserStoryViewSet, self).post_save(obj, created)
|
|
@ -0,0 +1,161 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.conf import settings
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from picklefield.fields import PickledObjectField
|
||||||
|
|
||||||
|
from greenmine.base.utils.slug import ref_uniquely
|
||||||
|
from greenmine.base.notifications.models import WatchedMixin
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryStatus(models.Model):
|
||||||
|
name = models.CharField(max_length=255, null=False, blank=False,
|
||||||
|
verbose_name=_("name"))
|
||||||
|
order = models.IntegerField(default=10, null=False, blank=False,
|
||||||
|
verbose_name=_("order"))
|
||||||
|
is_closed = models.BooleanField(default=False, null=False, blank=True,
|
||||||
|
verbose_name=_("is closed"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="us_statuses", verbose_name=_("project"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"user story status"
|
||||||
|
verbose_name_plural = u"user story statuses"
|
||||||
|
ordering = ["project", "name"]
|
||||||
|
unique_together = ("project", "name")
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"project {0} - {1}".format(self.project_id, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class Points(models.Model):
|
||||||
|
name = models.CharField(max_length=255, null=False, blank=False,
|
||||||
|
verbose_name=_("name"))
|
||||||
|
order = models.IntegerField(default=10, null=False, blank=False,
|
||||||
|
verbose_name=_("order"))
|
||||||
|
value = models.FloatField(default=None, null=True, blank=True,
|
||||||
|
verbose_name=_("value"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="points", verbose_name=_("project"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"point"
|
||||||
|
verbose_name_plural = u"points"
|
||||||
|
ordering = ["project", "name"]
|
||||||
|
unique_together = ("project", "name")
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"project {0} - {1}".format(self.project_id, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class RolePoints(models.Model):
|
||||||
|
user_story = models.ForeignKey("UserStory", null=False, blank=False,
|
||||||
|
related_name="role_points",
|
||||||
|
verbose_name=_("user story"))
|
||||||
|
role = models.ForeignKey("users.Role", null=False, blank=False,
|
||||||
|
related_name="role_points",
|
||||||
|
verbose_name=_("role"))
|
||||||
|
points = models.ForeignKey("Points", null=False, blank=False,
|
||||||
|
related_name="role_points",
|
||||||
|
verbose_name=_("points"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ("user_story", "role")
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
verbose_name=_("ref"))
|
||||||
|
milestone = models.ForeignKey("milestones.Milestone", null=True, blank=True, default=None,
|
||||||
|
related_name="user_stories", verbose_name=_("milestone"))
|
||||||
|
project = models.ForeignKey("projects.Project", null=False, blank=False,
|
||||||
|
related_name="user_stories", verbose_name=_("project"))
|
||||||
|
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True,
|
||||||
|
related_name="owned_user_stories", verbose_name=_("owner"))
|
||||||
|
status = models.ForeignKey("UserStoryStatus", null=False, blank=False,
|
||||||
|
related_name="user_stories", verbose_name=_("status"))
|
||||||
|
points = models.ManyToManyField("Points", null=False, blank=False,
|
||||||
|
related_name="userstories", through="RolePoints",
|
||||||
|
verbose_name=_("points"))
|
||||||
|
order = models.PositiveSmallIntegerField(null=False, blank=False, default=100,
|
||||||
|
verbose_name=_("order"))
|
||||||
|
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
|
||||||
|
verbose_name=_("created date"))
|
||||||
|
modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
|
||||||
|
verbose_name=_("modified date"))
|
||||||
|
finish_date = models.DateTimeField(null=True, blank=True,
|
||||||
|
verbose_name=_("finish date"))
|
||||||
|
subject = models.CharField(max_length=500, null=False, blank=False,
|
||||||
|
verbose_name=_("subject"))
|
||||||
|
description = models.TextField(null=False, blank=True, verbose_name=_("description"))
|
||||||
|
watchers = models.ManyToManyField(settings.AUTH_USER_MODEL, null=True, blank=True,
|
||||||
|
related_name="watched_us", verbose_name=_("watchers"))
|
||||||
|
client_requirement = models.BooleanField(default=False, null=False, blank=True,
|
||||||
|
verbose_name=_("is client requirement"))
|
||||||
|
team_requirement = models.BooleanField(default=False, null=False, blank=True,
|
||||||
|
verbose_name=_("is team requirement"))
|
||||||
|
tags = PickledObjectField(null=False, blank=True,
|
||||||
|
verbose_name=_("tags"))
|
||||||
|
|
||||||
|
notifiable_fields = [
|
||||||
|
"milestone",
|
||||||
|
"owner",
|
||||||
|
"status",
|
||||||
|
"points",
|
||||||
|
"finish_date",
|
||||||
|
"subject",
|
||||||
|
"description",
|
||||||
|
"client_requirement",
|
||||||
|
"team_requirement",
|
||||||
|
"tags",
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = u"user story"
|
||||||
|
verbose_name_plural = u"user stories"
|
||||||
|
ordering = ["project", "order"]
|
||||||
|
unique_together = ("ref", "project")
|
||||||
|
permissions = (
|
||||||
|
("comment_userstory", "Can comment user stories"),
|
||||||
|
("view_userstory", "Can view user stories"),
|
||||||
|
("change_owned_userstory", "Can modify owned user stories"),
|
||||||
|
("add_userstory_to_milestones", "Can add user stories to milestones"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return u"({1}) {0}".format(self.ref, self.subject)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return u"<UserStory %s>" % (self.id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_closed(self):
|
||||||
|
return self.status.is_closed
|
||||||
|
|
||||||
|
def get_role_points(self):
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Reversion registration (usufull for base.notification and for meke a historical)
|
||||||
|
reversion.register(UserStory)
|
||||||
|
|
||||||
|
|
||||||
|
# Model related signals handlers
|
||||||
|
@receiver(models.signals.pre_save, sender=UserStory, dispatch_uid="user_story_ref_handler")
|
||||||
|
def us_ref_handler(sender, instance, **kwargs):
|
||||||
|
if not instance.id and instance.project:
|
||||||
|
instance.ref = ref_uniquely(instance.project, "last_us_ref", instance.__class__)
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from greenmine.base.permissions import BasePermission
|
||||||
|
|
||||||
|
|
||||||
|
class PointsDetailPermission(BasePermission):
|
||||||
|
get_permission = "view_points"
|
||||||
|
put_permission = "severity_points"
|
||||||
|
patch_permission = "severity_points"
|
||||||
|
delete_permission = "delete_points"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryStatusDetailPermission(BasePermission):
|
||||||
|
get_permission = "view_userstorystatus"
|
||||||
|
put_permission = "change_userstorystatus"
|
||||||
|
patch_permission = "change_userstorystatus"
|
||||||
|
delete_permission = "delete_userstorystatus"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryPermission(BasePermission):
|
||||||
|
get_permission = "view_userstory"
|
||||||
|
put_permission = "change_userstory"
|
||||||
|
patch_permission = "change_userstory"
|
||||||
|
delete_permission = "delete_userstory"
|
||||||
|
safe_methods = ["HEAD", "OPTIONS"]
|
||||||
|
path_to_project = ["project"]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# -* coding: utf-8 -*-
|
||||||
|
|
||||||
|
from haystack import indexes
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryIndex(indexes.SearchIndex, indexes.Indexable):
|
||||||
|
text = indexes.CharField(document=True, use_template=True,
|
||||||
|
template_name='search/indexes/userstory_text.txt')
|
||||||
|
title = indexes.CharField(model_attr='subject')
|
||||||
|
project_id = indexes.IntegerField(model_attr="project_id")
|
||||||
|
description = indexes.CharField(model_attr="description")
|
||||||
|
|
||||||
|
def get_model(self):
|
||||||
|
return models.UserStory
|
||||||
|
|
||||||
|
def index_queryset(self, using=None):
|
||||||
|
return self.get_model().objects.all()
|
|
@ -0,0 +1,94 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from greenmine.base.serializers import PickleField
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
import json, reversion
|
||||||
|
|
||||||
|
|
||||||
|
class PointsSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.Points
|
||||||
|
|
||||||
|
|
||||||
|
class RolePointsField(serializers.WritableField):
|
||||||
|
def to_native(self, obj):
|
||||||
|
return {str(o.role.id): o.points.order for o in obj.all()}
|
||||||
|
|
||||||
|
def from_native(self, obj):
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
return obj
|
||||||
|
return json.loads(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class UserStoryStatusSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = models.UserStoryStatus
|
||||||
|
|
||||||
|
|
||||||
|
class UserStorySerializer(serializers.ModelSerializer):
|
||||||
|
tags = PickleField(blank=True, default=[])
|
||||||
|
is_closed = serializers.Field(source="is_closed")
|
||||||
|
points = RolePointsField(source="role_points")
|
||||||
|
comment = serializers.SerializerMethodField("get_comment")
|
||||||
|
history = serializers.SerializerMethodField("get_history")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.UserStory
|
||||||
|
depth = 0
|
||||||
|
|
||||||
|
def save_object(self, obj, **kwargs):
|
||||||
|
role_points = obj._related_data.pop("role_points", None)
|
||||||
|
super(UserStorySerializer, self).save_object(obj, **kwargs)
|
||||||
|
obj.project.update_role_points()
|
||||||
|
|
||||||
|
if role_points:
|
||||||
|
for role_id, points_order in role_points.items():
|
||||||
|
role_points = obj.role_points.get(role__id=role_id)
|
||||||
|
role_points.points = models.Points.objects.get(project=obj.project,
|
||||||
|
order=points_order)
|
||||||
|
role_points.save()
|
||||||
|
|
||||||
|
def get_comment(self, obj):
|
||||||
|
# TODO
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_user_stories_diff(self, old_us_version, new_us_version):
|
||||||
|
old_obj = old_us_version.field_dict
|
||||||
|
new_obj = new_us_version.field_dict
|
||||||
|
|
||||||
|
diff_dict = {
|
||||||
|
"modified_date": new_obj["modified_date"],
|
||||||
|
"by": new_us_version.revision.user,
|
||||||
|
"comment": new_us_version.revision.comment,
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in old_obj.keys():
|
||||||
|
if key == "modified_date":
|
||||||
|
continue
|
||||||
|
|
||||||
|
if old_obj[key] == new_obj[key]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
diff_dict[key] = {
|
||||||
|
"old": old_obj[key],
|
||||||
|
"new": new_obj[key],
|
||||||
|
}
|
||||||
|
|
||||||
|
return diff_dict
|
||||||
|
|
||||||
|
def get_history(self, obj):
|
||||||
|
diff_list = []
|
||||||
|
current = None
|
||||||
|
|
||||||
|
for version in reversed(list(reversion.get_for_object(obj))):
|
||||||
|
if current:
|
||||||
|
us_diff = self.get_user_stories_diff(current, version)
|
||||||
|
diff_list.append(us_diff)
|
||||||
|
|
||||||
|
current = version
|
||||||
|
|
||||||
|
return diff_list
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue