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 starting
remotes/origin/enhancement/email-actions
David Barragán Merino 2013-10-02 15:01:09 +02:00
parent d8516a20c7
commit c93baac1c3
125 changed files with 1950 additions and 1866 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.*',
), ),
), ),

View File

@ -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)

32
greenmine/projects/api.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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,)

View File

@ -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 = []

View File

@ -1,7 +1,6 @@
# -* coding: utf-8 -*- # -* coding: utf-8 -*-
from haystack import indexes from haystack import indexes
from . import models from . import models

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

View File

@ -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)

View File

@ -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)

View File

@ -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__)

View File

@ -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"]

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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"]

View File

@ -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

View File

@ -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)

View File

@ -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"]

View File

@ -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)

View File

@ -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():

View File

@ -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)

View File

@ -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 = []

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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"]

View File

@ -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()

View File

@ -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

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-

View File

@ -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)

View File

@ -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)

View File

@ -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__)

View File

@ -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"]

View File

@ -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()

View File

@ -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