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
|
||||
|
||||
|
||||
class BaseDetailPermission(permissions.BasePermission):
|
||||
class BasePermission(permissions.BasePermission):
|
||||
get_permission = None
|
||||
put_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):
|
||||
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.username = username
|
||||
self.first_name = first_name
|
||||
|
@ -46,10 +47,13 @@ class LoginSerializer(serializers.Serializer):
|
|||
instance.last_login = attrs.get('last_login', instance.last_login)
|
||||
instance.color = attrs.get('color', instance.color)
|
||||
instance.description = attrs.get('description', instance.description)
|
||||
instance.default_language = attrs.get('default_language', instance.default_language)
|
||||
instance.default_timezone = attrs.get('default_timezone', instance.default_timezone)
|
||||
instance.default_language = attrs.get('default_language',
|
||||
instance.default_language)
|
||||
instance.default_timezone = attrs.get('default_timezone',
|
||||
instance.default_timezone)
|
||||
instance.colorize_tags = attrs.get('colorize_tags', instance.colorize_tags)
|
||||
return instance
|
||||
|
||||
return UserLogged(**attrs)
|
||||
|
||||
|
||||
|
@ -58,14 +62,14 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('id', 'username', 'first_name', 'last_name', 'email', 'color', 'description',
|
||||
'default_language', 'default_timezone', 'is_active', 'photo', 'projects')
|
||||
fields = ('id', 'username', 'first_name', 'last_name', 'email', 'color',
|
||||
'description', 'default_language', 'default_timezone', 'is_active',
|
||||
'photo', 'projects')
|
||||
|
||||
def get_projects(self, obj):
|
||||
return [x.id for x in obj.projects.all()]
|
||||
|
||||
|
||||
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Role
|
||||
|
|
|
@ -13,6 +13,8 @@ from grappelli.dashboard import modules, Dashboard
|
|||
from grappelli.dashboard.utils import get_admin_site_name
|
||||
|
||||
|
||||
# TODO: Fix me
|
||||
|
||||
class CustomIndexDashboard(Dashboard):
|
||||
"""
|
||||
Custom index dashboard for www.
|
||||
|
@ -46,7 +48,6 @@ class CustomIndexDashboard(Dashboard):
|
|||
models=(
|
||||
'greenmine.documents.*',
|
||||
'greenmine.questions.*',
|
||||
'greenmine.taggit.*',
|
||||
'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 _
|
||||
|
||||
|
||||
PRIORITY_CHOICES = (
|
||||
(1, _(u'Low')),
|
||||
(3, _(u'Normal')),
|
||||
(5, _(u'High')),
|
||||
US_STATUSES = (
|
||||
(1, _(u"Open"), False),
|
||||
(2, _(u"Closed"), True),
|
||||
)
|
||||
|
||||
SEVERITY_CHOICES = (
|
||||
(1, _(u'Wishlist')),
|
||||
(2, _(u'Minor')),
|
||||
(3, _(u'Normal')),
|
||||
(4, _(u'Important')),
|
||||
(5, _(u'Critical')),
|
||||
)
|
||||
|
||||
TASKSTATUSES = (
|
||||
TASK_STATUSES = (
|
||||
(1, _(u"New"), False, "#999999"),
|
||||
(2, _(u"In progress"), False, "#ff9900"),
|
||||
(3, _(u"Ready for test"), True, "#ffcc00"),
|
||||
|
@ -25,25 +16,6 @@ TASKSTATUSES = (
|
|||
(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 = (
|
||||
(1, u'?', None),
|
||||
(2, u'0', 0),
|
||||
|
@ -59,6 +31,33 @@ POINTS_CHOICES = (
|
|||
(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
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from greenmine.base import filters
|
||||
from greenmine.base.api import ModelCrudViewSet,
|
||||
|
||||
from . import serializers
|
||||
from . import models
|
||||
from . import permissions
|
||||
|
||||
|
||||
class DocumentsViewSet(viewsets.ModelViewSet):
|
||||
class DocumentsViewSet(ModelCrudViewSet):
|
||||
model = models.Document
|
||||
serializer_class = serializers.DocumentSerializer
|
||||
permission_classes = (permissions.DocumentPermission,)
|
|
@ -1,11 +1,11 @@
|
|||
# -*- 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"
|
||||
put_permission = "can_change_document"
|
||||
delete_permission = "can_delete_document"
|
||||
safe_methods = ['HEAD', 'OPTIONS']
|
||||
safe_methods = ["HEAD", "OPTIONS"]
|
||||
path_to_document = []
|
|
@ -1,7 +1,6 @@
|
|||
# -* coding: utf-8 -*-
|
||||
|
||||
from haystack import indexes
|
||||
|
||||
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():
|
||||
task = self.create_task(project, milestone, us, start_date, now())
|
||||
else:
|
||||
# No task on not initiated sprints
|
||||
# No task on not initiated milestones
|
||||
pass
|
||||
|
||||
start_date = end_date
|
||||
|
@ -191,7 +191,7 @@ class Command(BaseCommand):
|
|||
owner=random.choice(self.users),
|
||||
public=True,
|
||||
total_story_points=self.sd.int(100, 150),
|
||||
sprints=self.sd.int(5,10)
|
||||
milestones=self.sd.int(5,10)
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
class QuestionAdmin(reversion.VersionAdmin):
|
||||
list_display = ["subject", "project", "owner"]
|
||||
|
||||
admin.site.register(models.Question, QuestionAdmin)
|
||||
|
||||
|
||||
class QuestionStatusAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "order", "is_closed", "project"]
|
||||
|
||||
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):
|
||||
model = models.Question
|
||||
serializer_class = serializers.QuestionSerializer
|
||||
permission_classes = (IsAuthenticated, permissions.QuestionDetailPermission,)
|
||||
permission_classes = (IsAuthenticated, permissions.QuestionPermission,)
|
||||
|
||||
def post_save(self, obj, created=False):
|
||||
with reversion.create_revision():
|
|
@ -6,11 +6,11 @@ from django.conf import settings
|
|||
from django.utils import timezone
|
||||
from django.dispatch import receiver
|
||||
|
||||
from greenmine.base.utils.slug import slugify_uniquely, ref_uniquely
|
||||
from greenmine.scrum.models import Project
|
||||
from greenmine.base.utils.slug import ref_uniquely
|
||||
|
||||
from picklefield.fields import PickledObjectField
|
||||
from greenmine.questions.choices import QUESTION_STATUS
|
||||
|
||||
from . import choices
|
||||
|
||||
|
||||
class QuestionStatus(models.Model):
|
||||
|
@ -20,7 +20,7 @@ class QuestionStatus(models.Model):
|
|||
verbose_name=_('order'))
|
||||
is_closed = models.BooleanField(default=False, null=False, blank=True,
|
||||
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',
|
||||
verbose_name=_('project'))
|
||||
|
||||
|
@ -53,10 +53,10 @@ class Question(models.Model):
|
|||
attached_file = models.FileField(max_length=500, null=True, blank=True,
|
||||
upload_to='messages',
|
||||
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',
|
||||
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',
|
||||
verbose_name=_('milestone'))
|
||||
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__)
|
||||
|
||||
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 -*-
|
||||
|
||||
from greenmine.base.permissions import BaseDetailPermission
|
||||
from greenmine.base.permissions import BasePermission
|
||||
|
||||
|
||||
class QuestionDetailPermission(BaseDetailPermission):
|
||||
class QuestionPermission(BasePermission):
|
||||
get_permission = "can_view_question"
|
||||
put_permission = "change_question"
|
||||
patch_permission = "change_question"
|
||||
delete_permission = "delete_question"
|
||||
safe_methods = ['HEAD', 'OPTIONS']
|
||||
safe_methods = ["HEAD", "OPTIONS"]
|
||||
path_to_project = []
|
||||
|
|
@ -4,43 +4,43 @@ from rest_framework import serializers
|
|||
|
||||
import reversion
|
||||
|
||||
from greenmine.scrum.serializers import PickleField
|
||||
from greenmine.base.serializers import PickleField
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class QuestionSerializer(serializers.ModelSerializer):
|
||||
tags = PickleField()
|
||||
comment = serializers.SerializerMethodField('get_comment')
|
||||
history = serializers.SerializerMethodField('get_history')
|
||||
comment = serializers.SerializerMethodField("get_comment")
|
||||
history = serializers.SerializerMethodField("get_history")
|
||||
|
||||
class Meta:
|
||||
model = models.Question
|
||||
fields = ()
|
||||
|
||||
def get_comment(self, obj):
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def get_questions_diff(self, old_question_version, new_question_version):
|
||||
old_obj = old_question_version.field_dict
|
||||
new_obj = new_question_version.field_dict
|
||||
|
||||
diff_dict = {
|
||||
'modified_date': new_obj['modified_date'],
|
||||
'by': old_question_version.revision.user,
|
||||
'comment': old_question_version.revision.comment,
|
||||
"modified_date": new_obj["modified_date"],
|
||||
"by": old_question_version.revision.user,
|
||||
"comment": old_question_version.revision.comment,
|
||||
}
|
||||
|
||||
for key in old_obj.keys():
|
||||
if key == 'modified_date':
|
||||
if key == "modified_date":
|
||||
continue
|
||||
|
||||
if old_obj[key] == new_obj[key]:
|
||||
continue
|
||||
|
||||
diff_dict[key] = {
|
||||
'old': old_obj[key],
|
||||
'new': new_obj[key],
|
||||
"old": old_obj[key],
|
||||
"new": new_obj[key],
|
||||
}
|
||||
|
||||
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()
|
||||
#
|
||||
# emails_list = []
|
||||
# subject = ugettext("Greenmine: sprint created")
|
||||
# subject = ugettext("Greenmine: milestone created")
|
||||
# for person in participants:
|
||||
# template = render_to_string("email/milestone.created.html", {
|
||||
# "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