From c93baac1c311dfd883cf02635f48e5fb0b26949e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 2 Oct 2013 15:01:09 +0200 Subject: [PATCH] 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 --- greenmine/base/permissions.py | 2 +- greenmine/base/serializers.py | 14 + greenmine/base/users/serializers.py | 16 +- greenmine/dashboard.py | 3 +- greenmine/{scrum => projects}/__init__.py | 0 greenmine/projects/admin.py | 36 + greenmine/projects/api.py | 32 + greenmine/{scrum => projects}/choices.py | 63 +- .../{ => projects}/documents/__init__.py | 0 greenmine/{ => projects}/documents/admin.py | 0 greenmine/{ => projects}/documents/api.py | 5 +- greenmine/{ => projects}/documents/models.py | 0 .../{ => projects}/documents/permissions.py | 6 +- .../documents/search_indexes.py | 1 - .../{ => projects}/documents/serializers.py | 0 .../search/indexes/document_text.txt | 0 greenmine/projects/issues/__init__.py | 3 + greenmine/projects/issues/admin.py | 38 + greenmine/projects/issues/api.py | 91 ++ greenmine/projects/issues/models.py | 177 ++++ greenmine/projects/issues/permissions.py | 48 ++ greenmine/projects/issues/search_indexes.py | 18 + greenmine/projects/issues/serializers.py | 79 ++ .../create_issue_notification-body-html.jinja | 0 .../create_issue_notification-body-text.jinja | 0 .../create_issue_notification-subject.jinja | 0 ...destroy_issue_notification-body-html.jinja | 0 ...destroy_issue_notification-body-text.jinja | 0 .../destroy_issue_notification-subject.jinja | 0 .../update_issue_notification-body-html.jinja | 0 .../update_issue_notification-body-text.jinja | 0 .../update_issue_notification-subject.jinja | 0 ...date_user_story_notification-subject.jinja | 0 .../templates/search/indexes/issue_text.txt | 0 .../management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/sample_data.py | 4 +- greenmine/projects/milestones/__init__.py | 3 + greenmine/projects/milestones/admin.py | 24 + greenmine/projects/milestones/api.py | 29 + greenmine/projects/milestones/models.py | 126 +++ greenmine/projects/milestones/permissions.py | 13 + greenmine/projects/milestones/serializers.py | 16 + ...ate_milestone_notification-body-html.jinja | 0 ...ate_milestone_notification-body-text.jinja | 0 ...reate_milestone_notification-subject.jinja | 0 ...roy_milestone_notification-body-html.jinja | 0 ...roy_milestone_notification-body-text.jinja | 0 ...stroy_milestone_notification-subject.jinja | 0 ...ate_milestone_notification-body-html.jinja | 0 ...ate_milestone_notification-body-text.jinja | 0 ...pdate_milestone_notification-subject.jinja | 0 greenmine/projects/models.py | 219 +++++ greenmine/projects/permissions.py | 21 + .../{ => projects}/questions/__init__.py | 0 greenmine/{ => projects}/questions/admin.py | 12 +- greenmine/{ => projects}/questions/api.py | 2 +- greenmine/{ => projects}/questions/choices.py | 0 greenmine/{ => projects}/questions/models.py | 29 +- .../{ => projects}/questions/permissions.py | 6 +- .../questions/search_indexes.py | 0 .../{ => projects}/questions/serializers.py | 20 +- .../search/indexes/question_text.txt | 0 greenmine/projects/serializers.py | 32 + greenmine/{scrum => projects}/sigdispatch.py | 2 +- greenmine/projects/tasks/__init__.py | 3 + greenmine/projects/tasks/admin.py | 25 + greenmine/projects/tasks/api.py | 68 ++ greenmine/projects/tasks/models.py | 147 ++++ greenmine/projects/tasks/permissions.py | 21 + greenmine/projects/tasks/search_indexes.py | 18 + greenmine/projects/tasks/serializers.py | 63 ++ .../create_task_notification-body-html.jinja | 0 .../create_task_notification-body-text.jinja | 0 .../create_task_notification-subject.jinja | 0 .../destroy_task_notification-body-html.jinja | 0 .../destroy_task_notification-body-text.jinja | 0 .../destroy_task_notification-subject.jinja | 0 .../update_task_notification-body-html.jinja | 0 .../update_task_notification-body-text.jinja | 0 .../update_task_notification-subject.jinja | 0 .../templates/search/indexes/task_text.txt | 0 ...reate_project_notification-body-html.jinja | 0 ...reate_project_notification-body-text.jinja | 0 .../create_project_notification-subject.jinja | 0 ...stroy_project_notification-body-html.jinja | 0 ...stroy_project_notification-body-text.jinja | 0 ...destroy_project_notification-subject.jinja | 0 ...pdate_project_notification-body-html.jinja | 0 ...pdate_project_notification-body-text.jinja | 0 .../update_project_notification-subject.jinja | 0 greenmine/projects/userstories/__init__.py | 3 + greenmine/projects/userstories/admin.py | 46 + greenmine/projects/userstories/api.py | 52 ++ greenmine/projects/userstories/models.py | 161 ++++ greenmine/projects/userstories/permissions.py | 30 + .../projects/userstories/search_indexes.py | 18 + greenmine/projects/userstories/serializers.py | 94 ++ ...te_userstory_notification-body-html.jinja} | 0 ...te_userstory_notification-body-text.jinja} | 0 ...eate_userstory_notification-subject.jinja} | 0 ...oy_userstory_notification-body-html.jinja} | 0 ...oy_userstory_notification-body-text.jinja} | 0 ...troy_userstory_notification-subject.jinja} | 0 ...te_userstory_notification-body-html.jinja} | 0 ...te_userstory_notification-body-text.jinja} | 0 ...pdate_userstory_notification-subject.jinja | 1 + .../search/indexes/userstory_text.txt | 0 greenmine/{ => projects}/wiki/__init__.py | 0 greenmine/{ => projects}/wiki/admin.py | 0 greenmine/{ => projects}/wiki/api.py | 4 +- greenmine/{ => projects}/wiki/models.py | 2 +- greenmine/{ => projects}/wiki/permissions.py | 14 +- .../{ => projects}/wiki/search_indexes.py | 4 +- greenmine/{ => projects}/wiki/serializers.py | 8 +- .../search/indexes/wikipage_text.txt | 0 greenmine/routers.py | 76 ++ greenmine/scrum/admin.py | 122 --- greenmine/scrum/api.py | 317 ------- greenmine/scrum/models.py | 814 ------------------ greenmine/scrum/permissions.py | 120 --- greenmine/scrum/search_indexes.py | 46 - greenmine/scrum/serializers.py | 275 ------ greenmine/settings/common.py | 11 +- greenmine/urls.py | 63 +- 125 files changed, 1950 insertions(+), 1866 deletions(-) create mode 100644 greenmine/base/serializers.py rename greenmine/{scrum => projects}/__init__.py (100%) create mode 100644 greenmine/projects/admin.py create mode 100644 greenmine/projects/api.py rename greenmine/{scrum => projects}/choices.py (95%) rename greenmine/{ => projects}/documents/__init__.py (100%) rename greenmine/{ => projects}/documents/admin.py (100%) rename greenmine/{ => projects}/documents/api.py (70%) rename greenmine/{ => projects}/documents/models.py (100%) rename greenmine/{ => projects}/documents/permissions.py (55%) rename greenmine/{ => projects}/documents/search_indexes.py (99%) rename greenmine/{ => projects}/documents/serializers.py (100%) rename greenmine/{ => projects}/documents/templates/search/indexes/document_text.txt (100%) create mode 100644 greenmine/projects/issues/__init__.py create mode 100644 greenmine/projects/issues/admin.py create mode 100644 greenmine/projects/issues/api.py create mode 100644 greenmine/projects/issues/models.py create mode 100644 greenmine/projects/issues/permissions.py create mode 100644 greenmine/projects/issues/search_indexes.py create mode 100644 greenmine/projects/issues/serializers.py rename greenmine/{scrum => projects/issues}/templates/emails/create_issue_notification-body-html.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/emails/create_issue_notification-body-text.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/emails/create_issue_notification-subject.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/emails/destroy_issue_notification-body-html.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/emails/destroy_issue_notification-body-text.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/emails/destroy_issue_notification-subject.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/emails/update_issue_notification-body-html.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/emails/update_issue_notification-body-text.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/emails/update_issue_notification-subject.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/emails/update_user_story_notification-subject.jinja (100%) rename greenmine/{scrum => projects/issues}/templates/search/indexes/issue_text.txt (100%) rename greenmine/{scrum => projects}/management/__init__.py (100%) rename greenmine/{scrum => projects}/management/commands/__init__.py (100%) rename greenmine/{scrum => projects}/management/commands/sample_data.py (98%) create mode 100644 greenmine/projects/milestones/__init__.py create mode 100644 greenmine/projects/milestones/admin.py create mode 100644 greenmine/projects/milestones/api.py create mode 100644 greenmine/projects/milestones/models.py create mode 100644 greenmine/projects/milestones/permissions.py create mode 100644 greenmine/projects/milestones/serializers.py rename greenmine/{scrum => projects/milestones}/templates/emails/create_milestone_notification-body-html.jinja (100%) rename greenmine/{scrum => projects/milestones}/templates/emails/create_milestone_notification-body-text.jinja (100%) rename greenmine/{scrum => projects/milestones}/templates/emails/create_milestone_notification-subject.jinja (100%) rename greenmine/{scrum => projects/milestones}/templates/emails/destroy_milestone_notification-body-html.jinja (100%) rename greenmine/{scrum => projects/milestones}/templates/emails/destroy_milestone_notification-body-text.jinja (100%) rename greenmine/{scrum => projects/milestones}/templates/emails/destroy_milestone_notification-subject.jinja (100%) rename greenmine/{scrum => projects/milestones}/templates/emails/update_milestone_notification-body-html.jinja (100%) rename greenmine/{scrum => projects/milestones}/templates/emails/update_milestone_notification-body-text.jinja (100%) rename greenmine/{scrum => projects/milestones}/templates/emails/update_milestone_notification-subject.jinja (100%) create mode 100644 greenmine/projects/models.py create mode 100644 greenmine/projects/permissions.py rename greenmine/{ => projects}/questions/__init__.py (100%) rename greenmine/{ => projects}/questions/admin.py (100%) rename greenmine/{ => projects}/questions/api.py (92%) rename greenmine/{ => projects}/questions/choices.py (100%) rename greenmine/{ => projects}/questions/models.py (80%) rename greenmine/{ => projects}/questions/permissions.py (58%) rename greenmine/{ => projects}/questions/search_indexes.py (100%) rename greenmine/{ => projects}/questions/serializers.py (68%) rename greenmine/{ => projects}/questions/templates/search/indexes/question_text.txt (100%) create mode 100644 greenmine/projects/serializers.py rename greenmine/{scrum => projects}/sigdispatch.py (98%) create mode 100644 greenmine/projects/tasks/__init__.py create mode 100644 greenmine/projects/tasks/admin.py create mode 100644 greenmine/projects/tasks/api.py create mode 100644 greenmine/projects/tasks/models.py create mode 100644 greenmine/projects/tasks/permissions.py create mode 100644 greenmine/projects/tasks/search_indexes.py create mode 100644 greenmine/projects/tasks/serializers.py rename greenmine/{scrum => projects/tasks}/templates/emails/create_task_notification-body-html.jinja (100%) rename greenmine/{scrum => projects/tasks}/templates/emails/create_task_notification-body-text.jinja (100%) rename greenmine/{scrum => projects/tasks}/templates/emails/create_task_notification-subject.jinja (100%) rename greenmine/{scrum => projects/tasks}/templates/emails/destroy_task_notification-body-html.jinja (100%) rename greenmine/{scrum => projects/tasks}/templates/emails/destroy_task_notification-body-text.jinja (100%) rename greenmine/{scrum => projects/tasks}/templates/emails/destroy_task_notification-subject.jinja (100%) rename greenmine/{scrum => projects/tasks}/templates/emails/update_task_notification-body-html.jinja (100%) rename greenmine/{scrum => projects/tasks}/templates/emails/update_task_notification-body-text.jinja (100%) rename greenmine/{scrum => projects/tasks}/templates/emails/update_task_notification-subject.jinja (100%) rename greenmine/{scrum => projects/tasks}/templates/search/indexes/task_text.txt (100%) rename greenmine/{scrum => projects}/templates/emails/create_project_notification-body-html.jinja (100%) rename greenmine/{scrum => projects}/templates/emails/create_project_notification-body-text.jinja (100%) rename greenmine/{scrum => projects}/templates/emails/create_project_notification-subject.jinja (100%) rename greenmine/{scrum => projects}/templates/emails/destroy_project_notification-body-html.jinja (100%) rename greenmine/{scrum => projects}/templates/emails/destroy_project_notification-body-text.jinja (100%) rename greenmine/{scrum => projects}/templates/emails/destroy_project_notification-subject.jinja (100%) rename greenmine/{scrum => projects}/templates/emails/update_project_notification-body-html.jinja (100%) rename greenmine/{scrum => projects}/templates/emails/update_project_notification-body-text.jinja (100%) rename greenmine/{scrum => projects}/templates/emails/update_project_notification-subject.jinja (100%) create mode 100644 greenmine/projects/userstories/__init__.py create mode 100644 greenmine/projects/userstories/admin.py create mode 100644 greenmine/projects/userstories/api.py create mode 100644 greenmine/projects/userstories/models.py create mode 100644 greenmine/projects/userstories/permissions.py create mode 100644 greenmine/projects/userstories/search_indexes.py create mode 100644 greenmine/projects/userstories/serializers.py rename greenmine/{scrum/templates/emails/create_user_story_notification-body-html.jinja => projects/userstories/templates/emails/create_userstory_notification-body-html.jinja} (100%) rename greenmine/{scrum/templates/emails/create_user_story_notification-body-text.jinja => projects/userstories/templates/emails/create_userstory_notification-body-text.jinja} (100%) rename greenmine/{scrum/templates/emails/create_user_story_notification-subject.jinja => projects/userstories/templates/emails/create_userstory_notification-subject.jinja} (100%) rename greenmine/{scrum/templates/emails/destroy_user_story_notification-body-html.jinja => projects/userstories/templates/emails/destroy_userstory_notification-body-html.jinja} (100%) rename greenmine/{scrum/templates/emails/destroy_user_story_notification-body-text.jinja => projects/userstories/templates/emails/destroy_userstory_notification-body-text.jinja} (100%) rename greenmine/{scrum/templates/emails/destroy_user_story_notification-subject.jinja => projects/userstories/templates/emails/destroy_userstory_notification-subject.jinja} (100%) rename greenmine/{scrum/templates/emails/update_user_story_notification-body-html.jinja => projects/userstories/templates/emails/update_userstory_notification-body-html.jinja} (100%) rename greenmine/{scrum/templates/emails/update_user_story_notification-body-text.jinja => projects/userstories/templates/emails/update_userstory_notification-body-text.jinja} (100%) create mode 100644 greenmine/projects/userstories/templates/emails/update_userstory_notification-subject.jinja rename greenmine/{scrum => projects/userstories}/templates/search/indexes/userstory_text.txt (100%) rename greenmine/{ => projects}/wiki/__init__.py (100%) rename greenmine/{ => projects}/wiki/admin.py (100%) rename greenmine/{ => projects}/wiki/api.py (92%) rename greenmine/{ => projects}/wiki/models.py (97%) rename greenmine/{ => projects}/wiki/permissions.py (55%) rename greenmine/{ => projects}/wiki/search_indexes.py (86%) rename greenmine/{ => projects}/wiki/serializers.py (57%) rename greenmine/{ => projects}/wiki/templates/search/indexes/wikipage_text.txt (100%) create mode 100644 greenmine/routers.py delete mode 100644 greenmine/scrum/admin.py delete mode 100644 greenmine/scrum/api.py delete mode 100644 greenmine/scrum/models.py delete mode 100644 greenmine/scrum/permissions.py delete mode 100644 greenmine/scrum/search_indexes.py delete mode 100644 greenmine/scrum/serializers.py diff --git a/greenmine/base/permissions.py b/greenmine/base/permissions.py index a383cfee..c981dec1 100644 --- a/greenmine/base/permissions.py +++ b/greenmine/base/permissions.py @@ -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 diff --git a/greenmine/base/serializers.py b/greenmine/base/serializers.py new file mode 100644 index 00000000..975c208f --- /dev/null +++ b/greenmine/base/serializers.py @@ -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 diff --git a/greenmine/base/users/serializers.py b/greenmine/base/users/serializers.py index 2655d808..026ab9dc 100644 --- a/greenmine/base/users/serializers.py +++ b/greenmine/base/users/serializers.py @@ -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 diff --git a/greenmine/dashboard.py b/greenmine/dashboard.py index c8762d80..0682cdd1 100644 --- a/greenmine/dashboard.py +++ b/greenmine/dashboard.py @@ -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.*', ), ), diff --git a/greenmine/scrum/__init__.py b/greenmine/projects/__init__.py similarity index 100% rename from greenmine/scrum/__init__.py rename to greenmine/projects/__init__.py diff --git a/greenmine/projects/admin.py b/greenmine/projects/admin.py new file mode 100644 index 00000000..d55db956 --- /dev/null +++ b/greenmine/projects/admin.py @@ -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) diff --git a/greenmine/projects/api.py b/greenmine/projects/api.py new file mode 100644 index 00000000..cfd044e7 --- /dev/null +++ b/greenmine/projects/api.py @@ -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 diff --git a/greenmine/scrum/choices.py b/greenmine/projects/choices.py similarity index 95% rename from greenmine/scrum/choices.py rename to greenmine/projects/choices.py index 2be4b426..06952965 100644 --- a/greenmine/scrum/choices.py +++ b/greenmine/projects/choices.py @@ -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 diff --git a/greenmine/documents/__init__.py b/greenmine/projects/documents/__init__.py similarity index 100% rename from greenmine/documents/__init__.py rename to greenmine/projects/documents/__init__.py diff --git a/greenmine/documents/admin.py b/greenmine/projects/documents/admin.py similarity index 100% rename from greenmine/documents/admin.py rename to greenmine/projects/documents/admin.py diff --git a/greenmine/documents/api.py b/greenmine/projects/documents/api.py similarity index 70% rename from greenmine/documents/api.py rename to greenmine/projects/documents/api.py index bf99fd5d..be5b797d 100644 --- a/greenmine/documents/api.py +++ b/greenmine/projects/documents/api.py @@ -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,) diff --git a/greenmine/documents/models.py b/greenmine/projects/documents/models.py similarity index 100% rename from greenmine/documents/models.py rename to greenmine/projects/documents/models.py diff --git a/greenmine/documents/permissions.py b/greenmine/projects/documents/permissions.py similarity index 55% rename from greenmine/documents/permissions.py rename to greenmine/projects/documents/permissions.py index 7e2ef3a2..8ac50d45 100644 --- a/greenmine/documents/permissions.py +++ b/greenmine/projects/documents/permissions.py @@ -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 = [] diff --git a/greenmine/documents/search_indexes.py b/greenmine/projects/documents/search_indexes.py similarity index 99% rename from greenmine/documents/search_indexes.py rename to greenmine/projects/documents/search_indexes.py index 843483c1..153d31c6 100644 --- a/greenmine/documents/search_indexes.py +++ b/greenmine/projects/documents/search_indexes.py @@ -1,7 +1,6 @@ # -* coding: utf-8 -*- from haystack import indexes - from . import models diff --git a/greenmine/documents/serializers.py b/greenmine/projects/documents/serializers.py similarity index 100% rename from greenmine/documents/serializers.py rename to greenmine/projects/documents/serializers.py diff --git a/greenmine/documents/templates/search/indexes/document_text.txt b/greenmine/projects/documents/templates/search/indexes/document_text.txt similarity index 100% rename from greenmine/documents/templates/search/indexes/document_text.txt rename to greenmine/projects/documents/templates/search/indexes/document_text.txt diff --git a/greenmine/projects/issues/__init__.py b/greenmine/projects/issues/__init__.py new file mode 100644 index 00000000..faaaf799 --- /dev/null +++ b/greenmine/projects/issues/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + + diff --git a/greenmine/projects/issues/admin.py b/greenmine/projects/issues/admin.py new file mode 100644 index 00000000..1050895c --- /dev/null +++ b/greenmine/projects/issues/admin.py @@ -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) diff --git a/greenmine/projects/issues/api.py b/greenmine/projects/issues/api.py new file mode 100644 index 00000000..4ba6ad98 --- /dev/null +++ b/greenmine/projects/issues/api.py @@ -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) diff --git a/greenmine/projects/issues/models.py b/greenmine/projects/issues/models.py new file mode 100644 index 00000000..b53bd778 --- /dev/null +++ b/greenmine/projects/issues/models.py @@ -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__) diff --git a/greenmine/projects/issues/permissions.py b/greenmine/projects/issues/permissions.py new file mode 100644 index 00000000..6b334c87 --- /dev/null +++ b/greenmine/projects/issues/permissions.py @@ -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"] diff --git a/greenmine/projects/issues/search_indexes.py b/greenmine/projects/issues/search_indexes.py new file mode 100644 index 00000000..dc2a3e41 --- /dev/null +++ b/greenmine/projects/issues/search_indexes.py @@ -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() diff --git a/greenmine/projects/issues/serializers.py b/greenmine/projects/issues/serializers.py new file mode 100644 index 00000000..ef67e0c0 --- /dev/null +++ b/greenmine/projects/issues/serializers.py @@ -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 diff --git a/greenmine/scrum/templates/emails/create_issue_notification-body-html.jinja b/greenmine/projects/issues/templates/emails/create_issue_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_issue_notification-body-html.jinja rename to greenmine/projects/issues/templates/emails/create_issue_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/create_issue_notification-body-text.jinja b/greenmine/projects/issues/templates/emails/create_issue_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_issue_notification-body-text.jinja rename to greenmine/projects/issues/templates/emails/create_issue_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/create_issue_notification-subject.jinja b/greenmine/projects/issues/templates/emails/create_issue_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_issue_notification-subject.jinja rename to greenmine/projects/issues/templates/emails/create_issue_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/destroy_issue_notification-body-html.jinja b/greenmine/projects/issues/templates/emails/destroy_issue_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_issue_notification-body-html.jinja rename to greenmine/projects/issues/templates/emails/destroy_issue_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/destroy_issue_notification-body-text.jinja b/greenmine/projects/issues/templates/emails/destroy_issue_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_issue_notification-body-text.jinja rename to greenmine/projects/issues/templates/emails/destroy_issue_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/destroy_issue_notification-subject.jinja b/greenmine/projects/issues/templates/emails/destroy_issue_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_issue_notification-subject.jinja rename to greenmine/projects/issues/templates/emails/destroy_issue_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/update_issue_notification-body-html.jinja b/greenmine/projects/issues/templates/emails/update_issue_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_issue_notification-body-html.jinja rename to greenmine/projects/issues/templates/emails/update_issue_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/update_issue_notification-body-text.jinja b/greenmine/projects/issues/templates/emails/update_issue_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_issue_notification-body-text.jinja rename to greenmine/projects/issues/templates/emails/update_issue_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/update_issue_notification-subject.jinja b/greenmine/projects/issues/templates/emails/update_issue_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_issue_notification-subject.jinja rename to greenmine/projects/issues/templates/emails/update_issue_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/update_user_story_notification-subject.jinja b/greenmine/projects/issues/templates/emails/update_user_story_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_user_story_notification-subject.jinja rename to greenmine/projects/issues/templates/emails/update_user_story_notification-subject.jinja diff --git a/greenmine/scrum/templates/search/indexes/issue_text.txt b/greenmine/projects/issues/templates/search/indexes/issue_text.txt similarity index 100% rename from greenmine/scrum/templates/search/indexes/issue_text.txt rename to greenmine/projects/issues/templates/search/indexes/issue_text.txt diff --git a/greenmine/scrum/management/__init__.py b/greenmine/projects/management/__init__.py similarity index 100% rename from greenmine/scrum/management/__init__.py rename to greenmine/projects/management/__init__.py diff --git a/greenmine/scrum/management/commands/__init__.py b/greenmine/projects/management/commands/__init__.py similarity index 100% rename from greenmine/scrum/management/commands/__init__.py rename to greenmine/projects/management/commands/__init__.py diff --git a/greenmine/scrum/management/commands/sample_data.py b/greenmine/projects/management/commands/sample_data.py similarity index 98% rename from greenmine/scrum/management/commands/sample_data.py rename to greenmine/projects/management/commands/sample_data.py index 26aeafb6..3e5da410 100644 --- a/greenmine/scrum/management/commands/sample_data.py +++ b/greenmine/projects/management/commands/sample_data.py @@ -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() diff --git a/greenmine/projects/milestones/__init__.py b/greenmine/projects/milestones/__init__.py new file mode 100644 index 00000000..faaaf799 --- /dev/null +++ b/greenmine/projects/milestones/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + + diff --git a/greenmine/projects/milestones/admin.py b/greenmine/projects/milestones/admin.py new file mode 100644 index 00000000..539c5cd0 --- /dev/null +++ b/greenmine/projects/milestones/admin.py @@ -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) + + diff --git a/greenmine/projects/milestones/api.py b/greenmine/projects/milestones/api.py new file mode 100644 index 00000000..a6ba83dc --- /dev/null +++ b/greenmine/projects/milestones/api.py @@ -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 + + diff --git a/greenmine/projects/milestones/models.py b/greenmine/projects/milestones/models.py new file mode 100644 index 00000000..b53b1aed --- /dev/null +++ b/greenmine/projects/milestones/models.py @@ -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"".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) diff --git a/greenmine/projects/milestones/permissions.py b/greenmine/projects/milestones/permissions.py new file mode 100644 index 00000000..fa4110dd --- /dev/null +++ b/greenmine/projects/milestones/permissions.py @@ -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"] + diff --git a/greenmine/projects/milestones/serializers.py b/greenmine/projects/milestones/serializers.py new file mode 100644 index 00000000..ab184f5e --- /dev/null +++ b/greenmine/projects/milestones/serializers.py @@ -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 diff --git a/greenmine/scrum/templates/emails/create_milestone_notification-body-html.jinja b/greenmine/projects/milestones/templates/emails/create_milestone_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_milestone_notification-body-html.jinja rename to greenmine/projects/milestones/templates/emails/create_milestone_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/create_milestone_notification-body-text.jinja b/greenmine/projects/milestones/templates/emails/create_milestone_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_milestone_notification-body-text.jinja rename to greenmine/projects/milestones/templates/emails/create_milestone_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/create_milestone_notification-subject.jinja b/greenmine/projects/milestones/templates/emails/create_milestone_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_milestone_notification-subject.jinja rename to greenmine/projects/milestones/templates/emails/create_milestone_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/destroy_milestone_notification-body-html.jinja b/greenmine/projects/milestones/templates/emails/destroy_milestone_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_milestone_notification-body-html.jinja rename to greenmine/projects/milestones/templates/emails/destroy_milestone_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/destroy_milestone_notification-body-text.jinja b/greenmine/projects/milestones/templates/emails/destroy_milestone_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_milestone_notification-body-text.jinja rename to greenmine/projects/milestones/templates/emails/destroy_milestone_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/destroy_milestone_notification-subject.jinja b/greenmine/projects/milestones/templates/emails/destroy_milestone_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_milestone_notification-subject.jinja rename to greenmine/projects/milestones/templates/emails/destroy_milestone_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/update_milestone_notification-body-html.jinja b/greenmine/projects/milestones/templates/emails/update_milestone_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_milestone_notification-body-html.jinja rename to greenmine/projects/milestones/templates/emails/update_milestone_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/update_milestone_notification-body-text.jinja b/greenmine/projects/milestones/templates/emails/update_milestone_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_milestone_notification-body-text.jinja rename to greenmine/projects/milestones/templates/emails/update_milestone_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/update_milestone_notification-subject.jinja b/greenmine/projects/milestones/templates/emails/update_milestone_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_milestone_notification-subject.jinja rename to greenmine/projects/milestones/templates/emails/update_milestone_notification-subject.jinja diff --git a/greenmine/projects/models.py b/greenmine/projects/models.py new file mode 100644 index 00000000..c30ad3c8 --- /dev/null +++ b/greenmine/projects/models.py @@ -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"".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) diff --git a/greenmine/projects/permissions.py b/greenmine/projects/permissions.py new file mode 100644 index 00000000..3f13ee0e --- /dev/null +++ b/greenmine/projects/permissions.py @@ -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"] diff --git a/greenmine/questions/__init__.py b/greenmine/projects/questions/__init__.py similarity index 100% rename from greenmine/questions/__init__.py rename to greenmine/projects/questions/__init__.py diff --git a/greenmine/questions/admin.py b/greenmine/projects/questions/admin.py similarity index 100% rename from greenmine/questions/admin.py rename to greenmine/projects/questions/admin.py index 6e875f72..c18a5f52 100644 --- a/greenmine/questions/admin.py +++ b/greenmine/projects/questions/admin.py @@ -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) diff --git a/greenmine/questions/api.py b/greenmine/projects/questions/api.py similarity index 92% rename from greenmine/questions/api.py rename to greenmine/projects/questions/api.py index 3ba530a2..63ba0465 100644 --- a/greenmine/questions/api.py +++ b/greenmine/projects/questions/api.py @@ -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(): diff --git a/greenmine/questions/choices.py b/greenmine/projects/questions/choices.py similarity index 100% rename from greenmine/questions/choices.py rename to greenmine/projects/questions/choices.py diff --git a/greenmine/questions/models.py b/greenmine/projects/questions/models.py similarity index 80% rename from greenmine/questions/models.py rename to greenmine/projects/questions/models.py index f56d24fe..52e55759 100644 --- a/greenmine/questions/models.py +++ b/greenmine/projects/questions/models.py @@ -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) diff --git a/greenmine/questions/permissions.py b/greenmine/projects/questions/permissions.py similarity index 58% rename from greenmine/questions/permissions.py rename to greenmine/projects/questions/permissions.py index 0cb030e3..f0eab228 100644 --- a/greenmine/questions/permissions.py +++ b/greenmine/projects/questions/permissions.py @@ -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 = [] diff --git a/greenmine/questions/search_indexes.py b/greenmine/projects/questions/search_indexes.py similarity index 100% rename from greenmine/questions/search_indexes.py rename to greenmine/projects/questions/search_indexes.py diff --git a/greenmine/questions/serializers.py b/greenmine/projects/questions/serializers.py similarity index 68% rename from greenmine/questions/serializers.py rename to greenmine/projects/questions/serializers.py index 4e623945..eaf58e59 100644 --- a/greenmine/questions/serializers.py +++ b/greenmine/projects/questions/serializers.py @@ -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 diff --git a/greenmine/questions/templates/search/indexes/question_text.txt b/greenmine/projects/questions/templates/search/indexes/question_text.txt similarity index 100% rename from greenmine/questions/templates/search/indexes/question_text.txt rename to greenmine/projects/questions/templates/search/indexes/question_text.txt diff --git a/greenmine/projects/serializers.py b/greenmine/projects/serializers.py new file mode 100644 index 00000000..4beb9421 --- /dev/null +++ b/greenmine/projects/serializers.py @@ -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 diff --git a/greenmine/scrum/sigdispatch.py b/greenmine/projects/sigdispatch.py similarity index 98% rename from greenmine/scrum/sigdispatch.py rename to greenmine/projects/sigdispatch.py index 11443b99..84d1813f 100644 --- a/greenmine/scrum/sigdispatch.py +++ b/greenmine/projects/sigdispatch.py @@ -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, diff --git a/greenmine/projects/tasks/__init__.py b/greenmine/projects/tasks/__init__.py new file mode 100644 index 00000000..faaaf799 --- /dev/null +++ b/greenmine/projects/tasks/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + + diff --git a/greenmine/projects/tasks/admin.py b/greenmine/projects/tasks/admin.py new file mode 100644 index 00000000..6172ed0b --- /dev/null +++ b/greenmine/projects/tasks/admin.py @@ -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) + + diff --git a/greenmine/projects/tasks/api.py b/greenmine/projects/tasks/api.py new file mode 100644 index 00000000..08006fad --- /dev/null +++ b/greenmine/projects/tasks/api.py @@ -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) diff --git a/greenmine/projects/tasks/models.py b/greenmine/projects/tasks/models.py new file mode 100644 index 00000000..ed8d1106 --- /dev/null +++ b/greenmine/projects/tasks/models.py @@ -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() diff --git a/greenmine/projects/tasks/permissions.py b/greenmine/projects/tasks/permissions.py new file mode 100644 index 00000000..bbee043e --- /dev/null +++ b/greenmine/projects/tasks/permissions.py @@ -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"] diff --git a/greenmine/projects/tasks/search_indexes.py b/greenmine/projects/tasks/search_indexes.py new file mode 100644 index 00000000..d29df8b0 --- /dev/null +++ b/greenmine/projects/tasks/search_indexes.py @@ -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() diff --git a/greenmine/projects/tasks/serializers.py b/greenmine/projects/tasks/serializers.py new file mode 100644 index 00000000..ab9f02b2 --- /dev/null +++ b/greenmine/projects/tasks/serializers.py @@ -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 diff --git a/greenmine/scrum/templates/emails/create_task_notification-body-html.jinja b/greenmine/projects/tasks/templates/emails/create_task_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_task_notification-body-html.jinja rename to greenmine/projects/tasks/templates/emails/create_task_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/create_task_notification-body-text.jinja b/greenmine/projects/tasks/templates/emails/create_task_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_task_notification-body-text.jinja rename to greenmine/projects/tasks/templates/emails/create_task_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/create_task_notification-subject.jinja b/greenmine/projects/tasks/templates/emails/create_task_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_task_notification-subject.jinja rename to greenmine/projects/tasks/templates/emails/create_task_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/destroy_task_notification-body-html.jinja b/greenmine/projects/tasks/templates/emails/destroy_task_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_task_notification-body-html.jinja rename to greenmine/projects/tasks/templates/emails/destroy_task_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/destroy_task_notification-body-text.jinja b/greenmine/projects/tasks/templates/emails/destroy_task_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_task_notification-body-text.jinja rename to greenmine/projects/tasks/templates/emails/destroy_task_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/destroy_task_notification-subject.jinja b/greenmine/projects/tasks/templates/emails/destroy_task_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_task_notification-subject.jinja rename to greenmine/projects/tasks/templates/emails/destroy_task_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/update_task_notification-body-html.jinja b/greenmine/projects/tasks/templates/emails/update_task_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_task_notification-body-html.jinja rename to greenmine/projects/tasks/templates/emails/update_task_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/update_task_notification-body-text.jinja b/greenmine/projects/tasks/templates/emails/update_task_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_task_notification-body-text.jinja rename to greenmine/projects/tasks/templates/emails/update_task_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/update_task_notification-subject.jinja b/greenmine/projects/tasks/templates/emails/update_task_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_task_notification-subject.jinja rename to greenmine/projects/tasks/templates/emails/update_task_notification-subject.jinja diff --git a/greenmine/scrum/templates/search/indexes/task_text.txt b/greenmine/projects/tasks/templates/search/indexes/task_text.txt similarity index 100% rename from greenmine/scrum/templates/search/indexes/task_text.txt rename to greenmine/projects/tasks/templates/search/indexes/task_text.txt diff --git a/greenmine/scrum/templates/emails/create_project_notification-body-html.jinja b/greenmine/projects/templates/emails/create_project_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_project_notification-body-html.jinja rename to greenmine/projects/templates/emails/create_project_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/create_project_notification-body-text.jinja b/greenmine/projects/templates/emails/create_project_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_project_notification-body-text.jinja rename to greenmine/projects/templates/emails/create_project_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/create_project_notification-subject.jinja b/greenmine/projects/templates/emails/create_project_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_project_notification-subject.jinja rename to greenmine/projects/templates/emails/create_project_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/destroy_project_notification-body-html.jinja b/greenmine/projects/templates/emails/destroy_project_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_project_notification-body-html.jinja rename to greenmine/projects/templates/emails/destroy_project_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/destroy_project_notification-body-text.jinja b/greenmine/projects/templates/emails/destroy_project_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_project_notification-body-text.jinja rename to greenmine/projects/templates/emails/destroy_project_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/destroy_project_notification-subject.jinja b/greenmine/projects/templates/emails/destroy_project_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_project_notification-subject.jinja rename to greenmine/projects/templates/emails/destroy_project_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/update_project_notification-body-html.jinja b/greenmine/projects/templates/emails/update_project_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_project_notification-body-html.jinja rename to greenmine/projects/templates/emails/update_project_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/update_project_notification-body-text.jinja b/greenmine/projects/templates/emails/update_project_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_project_notification-body-text.jinja rename to greenmine/projects/templates/emails/update_project_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/update_project_notification-subject.jinja b/greenmine/projects/templates/emails/update_project_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_project_notification-subject.jinja rename to greenmine/projects/templates/emails/update_project_notification-subject.jinja diff --git a/greenmine/projects/userstories/__init__.py b/greenmine/projects/userstories/__init__.py new file mode 100644 index 00000000..faaaf799 --- /dev/null +++ b/greenmine/projects/userstories/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + + diff --git a/greenmine/projects/userstories/admin.py b/greenmine/projects/userstories/admin.py new file mode 100644 index 00000000..d3215cef --- /dev/null +++ b/greenmine/projects/userstories/admin.py @@ -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) diff --git a/greenmine/projects/userstories/api.py b/greenmine/projects/userstories/api.py new file mode 100644 index 00000000..2d71b969 --- /dev/null +++ b/greenmine/projects/userstories/api.py @@ -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) diff --git a/greenmine/projects/userstories/models.py b/greenmine/projects/userstories/models.py new file mode 100644 index 00000000..b48c260f --- /dev/null +++ b/greenmine/projects/userstories/models.py @@ -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"" % (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__) diff --git a/greenmine/projects/userstories/permissions.py b/greenmine/projects/userstories/permissions.py new file mode 100644 index 00000000..8349bc25 --- /dev/null +++ b/greenmine/projects/userstories/permissions.py @@ -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"] diff --git a/greenmine/projects/userstories/search_indexes.py b/greenmine/projects/userstories/search_indexes.py new file mode 100644 index 00000000..a38484d1 --- /dev/null +++ b/greenmine/projects/userstories/search_indexes.py @@ -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() diff --git a/greenmine/projects/userstories/serializers.py b/greenmine/projects/userstories/serializers.py new file mode 100644 index 00000000..553bff22 --- /dev/null +++ b/greenmine/projects/userstories/serializers.py @@ -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 diff --git a/greenmine/scrum/templates/emails/create_user_story_notification-body-html.jinja b/greenmine/projects/userstories/templates/emails/create_userstory_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_user_story_notification-body-html.jinja rename to greenmine/projects/userstories/templates/emails/create_userstory_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/create_user_story_notification-body-text.jinja b/greenmine/projects/userstories/templates/emails/create_userstory_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_user_story_notification-body-text.jinja rename to greenmine/projects/userstories/templates/emails/create_userstory_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/create_user_story_notification-subject.jinja b/greenmine/projects/userstories/templates/emails/create_userstory_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/create_user_story_notification-subject.jinja rename to greenmine/projects/userstories/templates/emails/create_userstory_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/destroy_user_story_notification-body-html.jinja b/greenmine/projects/userstories/templates/emails/destroy_userstory_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_user_story_notification-body-html.jinja rename to greenmine/projects/userstories/templates/emails/destroy_userstory_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/destroy_user_story_notification-body-text.jinja b/greenmine/projects/userstories/templates/emails/destroy_userstory_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_user_story_notification-body-text.jinja rename to greenmine/projects/userstories/templates/emails/destroy_userstory_notification-body-text.jinja diff --git a/greenmine/scrum/templates/emails/destroy_user_story_notification-subject.jinja b/greenmine/projects/userstories/templates/emails/destroy_userstory_notification-subject.jinja similarity index 100% rename from greenmine/scrum/templates/emails/destroy_user_story_notification-subject.jinja rename to greenmine/projects/userstories/templates/emails/destroy_userstory_notification-subject.jinja diff --git a/greenmine/scrum/templates/emails/update_user_story_notification-body-html.jinja b/greenmine/projects/userstories/templates/emails/update_userstory_notification-body-html.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_user_story_notification-body-html.jinja rename to greenmine/projects/userstories/templates/emails/update_userstory_notification-body-html.jinja diff --git a/greenmine/scrum/templates/emails/update_user_story_notification-body-text.jinja b/greenmine/projects/userstories/templates/emails/update_userstory_notification-body-text.jinja similarity index 100% rename from greenmine/scrum/templates/emails/update_user_story_notification-body-text.jinja rename to greenmine/projects/userstories/templates/emails/update_userstory_notification-body-text.jinja diff --git a/greenmine/projects/userstories/templates/emails/update_userstory_notification-subject.jinja b/greenmine/projects/userstories/templates/emails/update_userstory_notification-subject.jinja new file mode 100644 index 00000000..5e7ae2aa --- /dev/null +++ b/greenmine/projects/userstories/templates/emails/update_userstory_notification-subject.jinja @@ -0,0 +1 @@ +Updated the user story "{{ object }}" diff --git a/greenmine/scrum/templates/search/indexes/userstory_text.txt b/greenmine/projects/userstories/templates/search/indexes/userstory_text.txt similarity index 100% rename from greenmine/scrum/templates/search/indexes/userstory_text.txt rename to greenmine/projects/userstories/templates/search/indexes/userstory_text.txt diff --git a/greenmine/wiki/__init__.py b/greenmine/projects/wiki/__init__.py similarity index 100% rename from greenmine/wiki/__init__.py rename to greenmine/projects/wiki/__init__.py diff --git a/greenmine/wiki/admin.py b/greenmine/projects/wiki/admin.py similarity index 100% rename from greenmine/wiki/admin.py rename to greenmine/projects/wiki/admin.py diff --git a/greenmine/wiki/api.py b/greenmine/projects/wiki/api.py similarity index 92% rename from greenmine/wiki/api.py rename to greenmine/projects/wiki/api.py index 2a76fcf0..a10adbac 100644 --- a/greenmine/wiki/api.py +++ b/greenmine/projects/wiki/api.py @@ -30,7 +30,7 @@ class WikiPageList(generics.ListCreateAPIView): class WikiPageDetail(generics.RetrieveUpdateDestroyAPIView): model = models.WikiPage serializer_class = serializers.WikiPageSerializer - permission_classes = (IsAuthenticated, permissions.WikiPageDetailPermission,) + permission_classes = (IsAuthenticated, permissions.WikiPagePermission,) def get_object(self, queryset=None): if queryset is None: @@ -58,4 +58,4 @@ class WikiPageDetail(generics.RetrieveUpdateDestroyAPIView): #class WikiPageAttachmentDetail(generics.RetrieveUpdateDestroyAPIView): # model = WikiPageAttachment # serializer_class = WikiPageAttachmentSerializer -# permission_classes = (WikiPageAttachmentDetailPermission,) +# permission_classes = (WikiPageAttachmentPermission,) diff --git a/greenmine/wiki/models.py b/greenmine/projects/wiki/models.py similarity index 97% rename from greenmine/wiki/models.py rename to greenmine/projects/wiki/models.py index c70c803f..2c190b61 100644 --- a/greenmine/wiki/models.py +++ b/greenmine/projects/wiki/models.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ class WikiPage(models.Model): - project = models.ForeignKey('scrum.Project', null=False, blank=False, + project = models.ForeignKey('projects.Project', null=False, blank=False, related_name='wiki_pages', verbose_name=_('project')) slug = models.SlugField(max_length=500, db_index=True, null=False, blank=False, diff --git a/greenmine/wiki/permissions.py b/greenmine/projects/wiki/permissions.py similarity index 55% rename from greenmine/wiki/permissions.py rename to greenmine/projects/wiki/permissions.py index 73352807..d8847c84 100644 --- a/greenmine/wiki/permissions.py +++ b/greenmine/projects/wiki/permissions.py @@ -1,21 +1,21 @@ # -*- coding: utf-8 -*- -from greenmine.base.permissions import BaseDetailPermission +from greenmine.base.permissions import BasePermission -class WikiPageDetailPermission(BaseDetailPermission): +class WikiPagePermission(BasePermission): get_permission = "can_view_wikipage" put_permission = "change_wikipage" patch_permission = "change_wikipage" delete_permission = "can_delete_wikipage" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_project = ['project'] + safe_methods = ["HEAD", "OPTIONS"] + path_to_project = ["project"] -class WikiPageAttachmentDetailPermission(BaseDetailPermission): +class WikiPageAttachmentPermission(BasePermission): get_permission = "can_view_wikipageattachment" put_permission = "change_wikipageattachment" patch_permission = "change_wikipageattachment" delete_permission = "can_delete_wikipageattachment" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_project = ['project'] + safe_methods = ["HEAD", "OPTIONS"] + path_to_project = ["project"] diff --git a/greenmine/wiki/search_indexes.py b/greenmine/projects/wiki/search_indexes.py similarity index 86% rename from greenmine/wiki/search_indexes.py rename to greenmine/projects/wiki/search_indexes.py index fdc6cd2c..4ad016a1 100644 --- a/greenmine/wiki/search_indexes.py +++ b/greenmine/projects/wiki/search_indexes.py @@ -1,14 +1,14 @@ # -* coding: utf-8 -*- from haystack import indexes -from .models import WikiPage +from . import models class WikiPageIndex(indexes.SearchIndex, indexes.Indexable): text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/wikipage_text.txt') def get_model(self): - return WikiPage + return models.WikiPage def index_queryset(self, using=None): return self.get_model().objects.all() diff --git a/greenmine/wiki/serializers.py b/greenmine/projects/wiki/serializers.py similarity index 57% rename from greenmine/wiki/serializers.py rename to greenmine/projects/wiki/serializers.py index 6879c89a..73a30196 100644 --- a/greenmine/wiki/serializers.py +++ b/greenmine/projects/wiki/serializers.py @@ -2,16 +2,14 @@ from rest_framework import serializers -from greenmine.wiki.models import WikiPage, WikiPageAttachment +from . import models class WikiPageSerializer(serializers.ModelSerializer): class Meta: - model = WikiPage - fields = () + model = models.WikiPage class WikiPageAttachmentSerializer(serializers.ModelSerializer): class Meta: - model = WikiPageAttachment - fields = () + model = models.WikiPageAttachment diff --git a/greenmine/wiki/templates/search/indexes/wikipage_text.txt b/greenmine/projects/wiki/templates/search/indexes/wikipage_text.txt similarity index 100% rename from greenmine/wiki/templates/search/indexes/wikipage_text.txt rename to greenmine/projects/wiki/templates/search/indexes/wikipage_text.txt diff --git a/greenmine/routers.py b/greenmine/routers.py new file mode 100644 index 00000000..19bb6197 --- /dev/null +++ b/greenmine/routers.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +from greenmine.base import routers +from greenmine.base.api import ApiRoot +from greenmine.base.users.api import ( + LoginViewSet, + LogoutViewSet, + RolesViewSet, + UsersViewSet +) +from greenmine.base.searches.api import SearchViewSet +from greenmine.projects.api import ProjectViewSet +from greenmine.projects.milestones.api import MilestoneViewSet +from greenmine.projects.userstories.api import ( + PointsViewSet, + UserStoryStatusViewSet, + UserStoryViewSet +) +from greenmine.projects.tasks.api import ( + TaskStatusViewSet, + TaskViewSet, + TasksAttachmentViewSet +) +from greenmine.projects.issues.api import ( + PriorityViewSet, + SeverityViewSet, + IssueStatusViewSet, + IssueTypeViewSet, + IssueViewSet, + IssuesAttachmentViewSet +) + + +router = routers.DefaultRouter(trailing_slash=False) + +# greenmine.base.users +router.register(r"users", UsersViewSet, base_name="users") +router.register(r"roles", RolesViewSet, base_name="roles") +router.register(r"auth/login", LoginViewSet, base_name="auth-login") +router.register(r"auth/logout", LogoutViewSet, base_name="auth-logout") + +# greenmine.base.searches +router.register(r"search", SearchViewSet, base_name="search") + +# greenmine.projects +router.register(r"projects", ProjectViewSet, base_name="projects") + +# greenmine.projects.milestones +router.register(r"milestones", MilestoneViewSet, base_name="milestones") + +# greenmine.projects.userstories +router.register(r"points", PointsViewSet, base_name="points") +router.register(r"userstory-statuses", UserStoryStatusViewSet, base_name="userstory-statuses") +router.register(r"userstories", UserStoryViewSet, base_name="userstories") + +# greenmine.projects.tasks +router.register(r"task-statuses", TaskStatusViewSet, base_name="task-statuses") +router.register(r"task-attachments", TasksAttachmentViewSet, base_name="task-attachments") +router.register(r"tasks", TaskViewSet, base_name="tasks") + +# greenmine.projects.issues +router.register(r"severities", SeverityViewSet, base_name="severities") +router.register(r"priorities", PriorityViewSet, base_name="priorities") +router.register(r"issue-statuses", IssueStatusViewSet, base_name="issue-statuses") +router.register(r"issue-types", IssueTypeViewSet, base_name="issue-types") +router.register(r"issue-attachments", IssuesAttachmentViewSet, base_name="issue-attachments") +router.register(r"issues", IssueViewSet, base_name="issues") + +#greenmine.projects.questions +# TODO +#greenmine.projects.wiki +# TODO +#greenmine.projects.documents +# TODO + + diff --git a/greenmine/scrum/admin.py b/greenmine/scrum/admin.py deleted file mode 100644 index 35044338..00000000 --- a/greenmine/scrum/admin.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.contrib import admin -from greenmine.scrum import models -import reversion - - -class MembershipInline(admin.TabularInline): - model = models.Membership - fields = ('user', 'project', 'role') - extra = 0 - - -class MilestoneInline(admin.TabularInline): - model = models.Milestone - fields = ('name', 'owner', 'estimated_start', 'estimated_finish', 'closed', 'disponibility', 'order') - sortable_field_name = 'order' - extra = 0 - - -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 ProjectAdmin(reversion.VersionAdmin): - list_display = ["name", "owner"] - inlines = [MembershipInline, MilestoneInline, UserStoryInline] - -admin.site.register(models.Project, ProjectAdmin) - - -class MilestoneAdmin(reversion.VersionAdmin): - list_display = ["name", "project", "owner", "closed", "estimated_start", "estimated_finish"] - -admin.site.register(models.Milestone, MilestoneAdmin) - - -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) - - -class AttachmentAdmin(reversion.VersionAdmin): - list_display = ["id", "owner"] - -admin.site.register(models.Attachment, AttachmentAdmin) - - -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 - -class MembershipAdmin(admin.ModelAdmin): - list_display = ['project', 'role', 'user'] - list_filter = ['project', 'role'] - - -class IssueAdmin(reversion.VersionAdmin): - list_display = ["subject", "type"] - - -class SeverityAdmin(admin.ModelAdmin): - list_display = ["name", "order", "project"] - - -class PriorityAdmin(admin.ModelAdmin): - list_display = ["name", "order", "project"] - - -class PointsAdmin(admin.ModelAdmin): - list_display = ["name", "order", "project"] - - -class IssueTypeAdmin(admin.ModelAdmin): - list_display = ["name", "order", "project"] - - -class IssueStatusAdmin(admin.ModelAdmin): - list_display = ["name", "order", "is_closed", "project"] - - -class TaskStatusAdmin(admin.ModelAdmin): - list_display = ["name", "order", "is_closed", "project"] - - -class UserStoryStatusAdmin(admin.ModelAdmin): - list_display = ["name", "order", "is_closed", "project"] - - -admin.site.register(models.Task, TaskAdmin) -admin.site.register(models.Issue, IssueAdmin) - -admin.site.register(models.Severity, SeverityAdmin) -admin.site.register(models.IssueStatus, IssueStatusAdmin) -admin.site.register(models.TaskStatus, TaskStatusAdmin) -admin.site.register(models.UserStoryStatus, UserStoryStatusAdmin) -admin.site.register(models.Priority, PriorityAdmin) -admin.site.register(models.IssueType, IssueTypeAdmin) -admin.site.register(models.Points, PointsAdmin) -admin.site.register(models.Membership, MembershipAdmin) - diff --git a/greenmine/scrum/api.py b/greenmine/scrum/api.py deleted file mode 100644 index c5b6702e..00000000 --- a/greenmine/scrum/api.py +++ /dev/null @@ -1,317 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.db.models import Q - -from rest_framework import mixins, viewsets -from rest_framework.permissions import IsAuthenticated - -from greenmine.base.users.models import * -from greenmine.base.notifications.api import NotificationSenderMixin - -from greenmine.scrum.serializers import * -from greenmine.scrum.models import * -from greenmine.scrum.permissions import * - -# Generic viewset subclasses for this module - -class ModelCrudViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, - mixins.RetrieveModelMixin, mixins.UpdateModelMixin, - mixins.DestroyModelMixin, viewsets.GenericViewSet): - pass - - -class ModelListViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, - viewsets.GenericViewSet): - pass - - - -# ViewSets definition - -class ProjectViewSet(NotificationSenderMixin, ModelCrudViewSet): - queryset = Project.objects.all() - serializer_class = ProjectSerializer - permission_classes = (IsAuthenticated, ProjectDetailPermission,) - - create_notification_template = "create_project_notification" - update_notification_template = "update_project_notification" - destroy_notification_template = "destroy_project_notification" - - 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 - - - -class MilestoneViewSet(NotificationSenderMixin, ModelCrudViewSet): - queryset = Milestone.objects.all() - serializer_class = MilestoneSerializer - permission_classes = (IsAuthenticated, MilestoneDetailPermission,) - create_notification_template = "create_milestone_notification" - update_notification_template = "update_milestone_notification" - destroy_notification_template = "destroy_milestone_notification" - - filter_fields = ('project',) - - def get_queryset(self): - qs = super(MilestoneViewSet, self).get_queryset() - return qs.filter(project__members=self.request.user).distinct() - - def pre_save(self, obj): - super(MilestoneViewSet, self).pre_save(obj) - obj.owner = self.request.user - - -class UserStoryViewSet(NotificationSenderMixin, ModelCrudViewSet): - queryset = UserStory.objects.all() - serializer_class = UserStorySerializer - permission_classes = (IsAuthenticated, UserStoryDetailPermission,) - - create_notification_template = "create_user_story_notification" - update_notification_template = "update_user_story_notification" - destroy_notification_template = "destroy_user_story_notification" - - filter_fields = ['project', 'milestone', 'milestone__isnull'] - - def get_queryset(self): - qs = super(UserStoryViewSet, self).get_queryset() - return qs.filter(project__members=self.request.user).distinct() - - 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) - - - - -class IssuesAttachmentViewSet(ModelCrudViewSet): - model = Attachment - serializer_class = AttachmentSerializer - permission_classes = (IsAuthenticated, AttachmentDetailPermission,) - - filter_fields = ["project", "object_id"] - - def get_queryset(self): - ct = ContentType.objects.get_for_model(Issue) - qs = super(IssuesAttachmentViewSet, self).get_queryset() - - qs = qs.filter(project__members=self.request.user) - 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 TasksAttachmentViewSet(ModelCrudViewSet): - model = Attachment - serializer_class = AttachmentSerializer - permission_classes = (IsAuthenticated, AttachmentDetailPermission,) - filter_fields = ["project", "object_id"] - - def get_queryset(self): - ct = ContentType.objects.get_for_model(Task) - qs = super(TasksAttachmentViewSet, self).get_queryset() - - qs = qs.filter(project__members=self.request.user) - 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): - queryset = Task.objects.all() - - serializer_class = TaskSerializer - permission_classes = (IsAuthenticated, TaskDetailPermission,) - - create_notification_template = "create_task_notification" - update_notification_template = "update_task_notification" - destroy_notification_template = "destroy_task_notification" - filter_fields = ['user_story', 'milestone', 'project'] - - def get_queryset(self): - qs = super(TaskViewSet, self).get_queryset() - qs = qs.filter(project__members=self.request.user) - return qs.distinct() - - 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) - - - -class IssueViewSet(NotificationSenderMixin, ModelCrudViewSet): - queryset = Issue.objects.all() - serializer_class = IssueSerializer - permission_classes = (IsAuthenticated, IssueDetailPermission,) - - create_notification_template = "create_issue_notification" - update_notification_template = "update_issue_notification" - destroy_notification_template = "destroy_issue_notification" - - filter_fields = ('project',) - - 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) - - def get_queryset(self): - qs = super(IssueViewSet, self).get_queryset() - qs = qs.filter(project__members=self.request.user) - return qs.distinct() - - -class SeverityViewSet(ModelListViewSet): - queryset = Severity.objects.all() - serializer_class = SeveritySerializer - permission_classes = (IsAuthenticated,) - filter_fields = ('project',) - - def get_queryset(self): - qs = super(SeverityViewSet, self).get_queryset() - qs = qs.filter(project__members=self.request.user) - return qs.distinct() - - -#class SeverityDetail(generics.RetrieveUpdateDestroyAPIView): -# model = Severity -# serializer_class = SeveritySerializer -# permission_classes = (IsAuthenticated, SeverityDetailPermission,) - -class IssueStatusViewSet(ModelListViewSet): - queryset = IssueStatus.objects.all() - - serializer_class = IssueStatusSerializer - filter_fields = ('project',) - permission_classes = (IsAuthenticated,) - - def get_queryset(self): - qs = super(IssueStatusViewSet, self).get_queryset() - qs = qs.filter(project__members=self.request.user) - return qs.distinct() - - -class TaskStatusViewSet(ModelListViewSet): - model = TaskStatus - serializer_class = TaskStatusSerializer - permission_classes = (IsAuthenticated,) - filter_fields = ('project',) - - def get_queryset(self): - qs = super(TaskStatusViewSet, self).get_queryset() - qs = qs.filter(project__members=self.request.user) - return qs.distinct() - - -#class TaskStatusDetail(generics.RetrieveUpdateDestroyAPIView): -# model = TaskStatus -# serializer_class = TaskStatusSerializer -# permission_classes = (IsAuthenticated, TaskStatusDetailPermission,) - - -class UserStoryStatusViewSet(ModelListViewSet): - model = UserStoryStatus - serializer_class = UserStoryStatusSerializer - permission_classes = (IsAuthenticated,) - filter_fields = ('project',) - - def get_queryset(self): - qs = super(UserStoryStatusViewSet, self).get_queryset() - qs = qs.filter(project__members=self.request.user) - return qs.distinct() - - -#class UserStoryStatusDetail(generics.RetrieveUpdateDestroyAPIView): -# model = UserStoryStatus -# serializer_class = UserStoryStatusSerializer -# permission_classes = (IsAuthenticated, UserStoryStatusDetailPermission,) - - -class PriorityViewSet(ModelListViewSet): - model = Priority - serializer_class = PrioritySerializer - permission_classes = (IsAuthenticated,) - filter_fields = ('project',) - - def get_queryset(self): - qs = super(PriorityViewSet, self).get_queryset() - qs = qs.filter(project__members=self.request.user) - return qs.distinct() - - -#class PriorityDetail(generics.RetrieveUpdateDestroyAPIView): -# model = Priority -# serializer_class = PrioritySerializer -# permission_classes = (IsAuthenticated, PriorityDetailPermission,) - - -class IssueTypeViewSet(ModelListViewSet): - model = IssueType - serializer_class = IssueTypeSerializer - permission_classes = (IsAuthenticated,) - filter_fields = ('project',) - - def get_queryset(self): - qs = super(IssueTypeViewSet, self).get_queryset() - qs = qs.filter(project__members=self.request.user) - return qs.distinct() - -#class IssueTypeDetail(generics.RetrieveUpdateDestroyAPIView): -# model = IssueType -# serializer_class = IssueTypeSerializer -# permission_classes = (IsAuthenticated, IssueTypeDetailPermission,) - - -class PointsViewSet(ModelListViewSet): - model = Points - serializer_class = PointsSerializer - permission_classes = (IsAuthenticated,) - filter_fields = ('project',) - - def get_queryset(self): - qs = super(PointsViewSet, self).get_queryset() - qs = qs.filter(project__members=self.request.user) - return qs.distinct() - - -#class PointsDetail(generics.RetrieveUpdateDestroyAPIView): -# model = Points -# serializer_class = PointsSerializer -# permission_classes = (IsAuthenticated, PointsDetailPermission,) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py deleted file mode 100644 index d308a338..00000000 --- a/greenmine/scrum/models.py +++ /dev/null @@ -1,814 +0,0 @@ -# -*- 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.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 django.db.models.loading import get_model - -from picklefield.fields import PickledObjectField - -from greenmine.base.utils.slug import ( - slugify_uniquely, - ref_uniquely -) -from greenmine.base.utils import iter_points -from greenmine.base.notifications.models import WatchedMixin -from greenmine.scrum.choices import ( - ISSUESTATUSES, - TASKSTATUSES, - USSTATUSES, - POINTS_CHOICES, - SEVERITY_CHOICES, - ISSUETYPES, - TASK_CHANGE_CHOICES, - PRIORITY_CHOICES -) - -import reversion - - -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('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('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 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('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 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('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 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('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 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('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 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('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 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')) - sprints = models.IntegerField(default=1, null=True, blank=True, - verbose_name=_('number of sprints')) - 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''.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 - RolePoints.objects.filter(user_story__in=self.user_stories.all())\ - .exclude(role__id__in=role_ids)\ - .delete() - - -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('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''.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 - #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): - #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): - #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): - #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), - } - -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('Milestone', null=True, blank=True, default=None, - related_name='user_stories', - verbose_name=_('milestone')) - project = models.ForeignKey('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', - verbose_name=_('points'), - through="RolePoints") - 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'' % (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), - } - - -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 Task(models.Model, WatchedMixin): - uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, - verbose_name=_('uuid')) - user_story = models.ForeignKey('UserStory', null=True, blank=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')) - milestone = models.ForeignKey('Milestone', null=True, blank=True, default=None, - related_name='tasks', - verbose_name=_('milestone')) - project = models.ForeignKey('Project', null=False, blank=False, - related_name='tasks', - 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='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), - } - - -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('Milestone', null=True, blank=True, default=None, - related_name='issues', - verbose_name=_('milestone')) - project = models.ForeignKey('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(Project) -reversion.register(Milestone) -reversion.register(UserStory) -reversion.register(Task) -reversion.register(Issue) - - -# Model related signals handlers - -@receiver(models.signals.post_save, sender=Project, dispatch_uid='project_post_save') -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, is_closed in ISSUESTATUSES: - IssueStatus.objects.create(name=name, order=order, - is_closed=is_closed, project=instance) - - for order, name, is_closed, color in TASKSTATUSES: - TaskStatus.objects.create(name=name, order=order, color=color, - is_closed=is_closed, project=instance) - - for order, name, is_closed in USSTATUSES: - UserStoryStatus.objects.create(name=name, order=order, - is_closed=is_closed, project=instance) - - for order, name in PRIORITY_CHOICES: - Priority.objects.create(project=instance, name=name, order=order) - - for order, name in SEVERITY_CHOICES: - Severity.objects.create(project=instance, name=name, order=order) - - for order, name, value in POINTS_CHOICES: - Points.objects.create(project=instance, name=name, order=order, value=value) - - for order, name in ISSUETYPES: - IssueType.objects.create(project=instance, name=name, order=order) - - -@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.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=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__) - - -@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__) - - -@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() diff --git a/greenmine/scrum/permissions.py b/greenmine/scrum/permissions.py deleted file mode 100644 index e95c4a6d..00000000 --- a/greenmine/scrum/permissions.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- - -from greenmine.base.permissions import BaseDetailPermission - - -class ProjectDetailPermission(BaseDetailPermission): - get_permission = "view_project" - put_permission = "change_project" - patch_permission = "change_project" - delete_permission = "delete_project" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_project = [] - - -class MilestoneDetailPermission(BaseDetailPermission): - get_permission = "view_milestone" - put_permission = "change_milestone" - patch_permission = "change_milestone" - delete_permission = "delete_milestone" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_project = ['project'] - - -class UserStoryDetailPermission(BaseDetailPermission): - get_permission = "view_userstory" - put_permission = "change_userstory" - patch_permission = "change_userstory" - delete_permission = "delete_userstory" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_project = ['project'] - - -class TaskDetailPermission(BaseDetailPermission): - 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 IssueDetailPermission(BaseDetailPermission): - get_permission = "view_issue" - put_permission = "change_issue" - patch_permission = "change_issue" - delete_permission = "delete_issue" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_project = ['project'] - - -class AttachmentDetailPermission(BaseDetailPermission): - get_permission = "view_attachment" - put_permission = "change_attachment" - patch_permission = "change_attachment" - delete_permission = "delete_attachment" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_project = ['project'] - - -class SeverityDetailPermission(BaseDetailPermission): - 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 IssueStatusDetailPermission(BaseDetailPermission): - 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 TaskStatusDetailPermission(BaseDetailPermission): - get_permission = "view_taskstatus" - put_permission = "change_taskstatus" - patch_permission = "change_taskstatus" - delete_permission = "delete_taskstatus" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_project = ['project'] - - -class UserStoryStatusDetailPermission(BaseDetailPermission): - 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 PriorityDetailPermission(BaseDetailPermission): - 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 IssueTypeDetailPermission(BaseDetailPermission): - 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 PointsDetailPermission(BaseDetailPermission): - get_permission = "view_points" - put_permission = "severity_points" - patch_permission = "severity_points" - delete_permission = "delete_points" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_project = ['project'] diff --git a/greenmine/scrum/search_indexes.py b/greenmine/scrum/search_indexes.py deleted file mode 100644 index 94f668d0..00000000 --- a/greenmine/scrum/search_indexes.py +++ /dev/null @@ -1,46 +0,0 @@ -# -* coding: utf-8 -*- - -from haystack import indexes -from greenmine.scrum.models import UserStory, Task, Issue - - -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 UserStory - - def index_queryset(self, using=None): - return self.get_model().objects.all() - - -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 Task - - def index_queryset(self, using=None): - return self.get_model().objects.all() - - -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 Issue - - def index_queryset(self, using=None): - return self.get_model().objects.all() diff --git a/greenmine/scrum/serializers.py b/greenmine/scrum/serializers.py deleted file mode 100644 index b4212839..00000000 --- a/greenmine/scrum/serializers.py +++ /dev/null @@ -1,275 +0,0 @@ -# -*- coding: utf-8 -*- - -from rest_framework import serializers - -from greenmine.base.users.models import * -from greenmine.scrum.models import * -from picklefield.fields import dbsafe_encode, dbsafe_decode - -import json, reversion - - -class PickleField(serializers.WritableField): - """ - Pickle objects serializer. - """ - def to_native(self, obj): - return obj - - def from_native(self, data): - return data - - -class PointsSerializer(serializers.ModelSerializer): - class Meta: - model = Points - fields = () - - -class ProjectSerializer(serializers.ModelSerializer): - tags = PickleField() - list_of_milestones = serializers.Field(source='list_of_milestones') - - class Meta: - model = Project - fields = () - - -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 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 = UserStory - fields = () - 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 = Points.objects.get(project=obj.project, order=points_order) - role_points.save() - - def get_comment(self, obj): - 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 - - -class MilestoneSerializer(serializers.ModelSerializer): - user_stories = UserStorySerializer(many=True, required=False) - - class Meta: - model = Milestone - fields = () - - -class AttachmentSerializer(serializers.ModelSerializer): - url = serializers.SerializerMethodField('get_url') - - def get_url(self, obj): - # FIXME: add sites or correct url. - return "http://localhost:8000{0}".format(obj.attached_file.url) - - class Meta: - model = Attachment - fields = ('id', 'project', 'owner', 'attached_file', - 'created_date', 'object_id', 'url') - read_only_fields = ('owner',) - - -class TaskSerializer(serializers.ModelSerializer): - tags = PickleField(blank=True, default=[]) - comment = serializers.SerializerMethodField('get_comment') - history = serializers.SerializerMethodField('get_history') - - class Meta: - model = Task - fields = () - - 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 - - -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 = Issue - fields = () - - 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 - - -class SeveritySerializer(serializers.ModelSerializer): - class Meta: - model = Severity - fields = () - - -class IssueStatusSerializer(serializers.ModelSerializer): - class Meta: - model = IssueStatus - fields = () - - -class TaskStatusSerializer(serializers.ModelSerializer): - class Meta: - model = TaskStatus - fields = () - - -class UserStoryStatusSerializer(serializers.ModelSerializer): - class Meta: - model = UserStoryStatus - fields = () - - -class PrioritySerializer(serializers.ModelSerializer): - class Meta: - model = Priority - fields = () - - -class IssueTypeSerializer(serializers.ModelSerializer): - class Meta: - model = IssueType - fields = () - - -class RoleSerializer(serializers.ModelSerializer): - class Meta: - model = Role - fields = () - diff --git a/greenmine/settings/common.py b/greenmine/settings/common.py index ef6c4add..44a911d5 100644 --- a/greenmine/settings/common.py +++ b/greenmine/settings/common.py @@ -195,9 +195,14 @@ INSTALLED_APPS = [ 'greenmine.base.users', 'greenmine.base.mail', 'greenmine.base.searches', - 'greenmine.scrum', - 'greenmine.wiki', - 'greenmine.questions', + 'greenmine.projects', + 'greenmine.projects.milestones', + 'greenmine.projects.userstories', + 'greenmine.projects.tasks', + 'greenmine.projects.issues', + 'greenmine.projects.questions', + #'greenmine.projects.documents', + 'greenmine.projects.wiki', 'south', 'haystack', diff --git a/greenmine/urls.py b/greenmine/urls.py index 4708d05b..65242f70 100644 --- a/greenmine/urls.py +++ b/greenmine/urls.py @@ -4,71 +4,14 @@ from django.conf.urls import patterns, include, url from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.contrib import admin +from .routers import router + + admin.autodiscover() -from greenmine.base import routers -from greenmine.base.api import ApiRoot -from greenmine.base.users.api import ( - LoginViewSet, - LogoutViewSet, - RolesViewSet, - UsersViewSet -) -from greenmine.base.searches.api import SearchViewSet -from greenmine.scrum.api import ( - MilestoneViewSet, - PriorityViewSet, - ProjectViewSet, - SeverityViewSet, - UserStoryStatusViewSet, - UserStoryViewSet, - TaskStatusViewSet, - TaskViewSet, - TasksAttachmentViewSet, - PointsViewSet, - IssueStatusViewSet, - IssueTypeViewSet, - IssueViewSet, - IssuesAttachmentViewSet -) - - -router = routers.DefaultRouter(trailing_slash=False) -# greenmine.base.users -router.register(r"users", UsersViewSet, base_name="users") -router.register(r"roles", RolesViewSet, base_name="roles") -router.register(r"auth/login", LoginViewSet, base_name="auth-login") -router.register(r"auth/logout", LogoutViewSet, base_name="auth-logout") -# greenmine.base.searches -router.register(r"search", SearchViewSet, base_name="search") -# greenmine.scrum -router.register(r"projects", ProjectViewSet, base_name="projects") -router.register(r"milestones", MilestoneViewSet, base_name="milestones") -router.register(r"userstories", UserStoryViewSet, base_name="userstories") -router.register(r"issue-attachments", IssuesAttachmentViewSet, base_name="issue-attachments") -router.register(r"task-attachments", TasksAttachmentViewSet, base_name="task-attachments") -router.register(r"tasks", TaskViewSet, base_name="tasks") -router.register(r"issues", IssueViewSet, base_name="issues") -router.register(r"severities", SeverityViewSet, base_name="severities") -router.register(r"issue-statuses", IssueStatusViewSet, base_name="issue-statuses") -router.register(r"task-statuses", TaskStatusViewSet, base_name="task-statuses") -router.register(r"userstory-statuses", UserStoryStatusViewSet, base_name="userstory-statuses") -router.register(r"priorities", PriorityViewSet, base_name="priorities") -router.register(r"issue-types", IssueTypeViewSet, base_name="issue-types") -router.register(r"points", PointsViewSet, base_name="points") -#greenmine.issues -#greenmine.wiki -#greenmine.documents - urlpatterns = patterns('', url(r'^api/v1$', ApiRoot.as_view()), url(r'^api/v1/', include(router.urls)), - # TODO: Refactor to use ViewSet - #url(r'^api/v1/', include('greenmine.wiki.urls')), - # TODO: Finish the documents and questions app - #url(r'^api/v1/', include('greenmine.questions.urls')), - #url(r'^api/v1/', include('greenmine.documents.urls')), - url(r'^admin/', include(admin.site.urls)), url(r'^grappelli/', include('grappelli.urls')), )