From cd7daa0ed9a90a1641754158ac725b360d38c862 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 22 Apr 2013 21:31:53 +0200 Subject: [PATCH 01/25] Fixes milestone creation. --- greenmine/base/api.py | 4 +--- greenmine/base/serializers.py | 7 +++++- greenmine/scrum/api.py | 1 + .../scrum/management/commands/sample_data.py | 2 -- greenmine/scrum/models.py | 22 ++++++++++++++----- greenmine/scrum/serializers.py | 2 +- 6 files changed, 25 insertions(+), 13 deletions(-) diff --git a/greenmine/base/api.py b/greenmine/base/api.py index 3f710506..4d890388 100644 --- a/greenmine/base/api.py +++ b/greenmine/base/api.py @@ -65,11 +65,9 @@ class RoleList(generics.ListCreateAPIView): class UserFilter(django_filters.FilterSet): - is_active = django_filters.BooleanFilter(name="is_active") - class Meta: model = User - fields = ['is_active',] + fields = ['is_active'] class UserList(generics.ListCreateAPIView): diff --git a/greenmine/base/serializers.py b/greenmine/base/serializers.py index fa071063..92de8a2c 100644 --- a/greenmine/base/serializers.py +++ b/greenmine/base/serializers.py @@ -54,11 +54,16 @@ class LoginSerializer(serializers.Serializer): class UserSerializer(serializers.ModelSerializer): + projects = serializers.SerializerMethodField('get_projects') + class Meta: model = User - fields = ('id', 'username', 'first_name', 'last_name', 'color', 'is_active',) + fields = ('id', 'username', 'first_name', 'last_name', 'color', 'is_active', '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/scrum/api.py b/greenmine/scrum/api.py index 6251f68c..c5e2b219 100644 --- a/greenmine/scrum/api.py +++ b/greenmine/scrum/api.py @@ -140,6 +140,7 @@ class TaskList(generics.ListCreateAPIView): def pre_save(self, obj): obj.owner = self.request.user + obj.milestone = obj.user_story.milestone class TaskDetail(generics.RetrieveUpdateDestroyAPIView): diff --git a/greenmine/scrum/management/commands/sample_data.py b/greenmine/scrum/management/commands/sample_data.py index 3c84087c..3e82877b 100644 --- a/greenmine/scrum/management/commands/sample_data.py +++ b/greenmine/scrum/management/commands/sample_data.py @@ -107,9 +107,7 @@ class Command(BaseCommand): owner=random.choice(users), milestone=milestone, user_story=us, - severity=Severity.objects.get(project=project, order=2), status=TaskStatus.objects.get(project=project, order=4), - priority=Priority.objects.get(project=project, order=3), ) # created unassociated uss. diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 142f9301..a41b9e4e 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -224,18 +224,28 @@ class Project(models.Model): class Milestone(models.Model): - uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, + uuid = models.CharField( + max_length=40, unique=True, null=False, blank=True, verbose_name=_('uuid')) - name = models.CharField(max_length=200, db_index=True, null=False, blank=False, + + name = models.CharField( + max_length=200, db_index=True, null=False, blank=False, verbose_name=_('name')) + slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, verbose_name=_('slug')) - owner = models.ForeignKey('base.User', null=True, blank=False, - related_name='owned_milestones', - verbose_name=_('owner')) - project = models.ForeignKey('Project', null=False, blank=False, + + owner = models.ForeignKey( + 'base.User', + 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, diff --git a/greenmine/scrum/serializers.py b/greenmine/scrum/serializers.py index ed497aae..964b1a15 100644 --- a/greenmine/scrum/serializers.py +++ b/greenmine/scrum/serializers.py @@ -63,7 +63,7 @@ class AttachmentSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer): - tags = PickleField() + tags = PickleField(blank=True, default=[]) class Meta: model = Task From 8fbb269ca12c3b0346bc5b3a985f99b81f444cb9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 23 Apr 2013 22:33:52 +0200 Subject: [PATCH 02/25] Remove again severity and priority fron task model. --- greenmine/scrum/models.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index a41b9e4e..431a52bc 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -391,12 +391,6 @@ class Task(models.Model): owner = models.ForeignKey('base.User', null=True, blank=True, default=None, related_name='owned_tasks', verbose_name=_('owner')) - severity = models.ForeignKey('Severity', null=False, blank=False, - related_name='tasks', - verbose_name=_('severity')) - priority = models.ForeignKey('Priority', null=False, blank=False, - related_name='tasks', - verbose_name=_('priority')) status = models.ForeignKey('TaskStatus', null=False, blank=False, related_name='tasks', verbose_name=_('status')) From 5da1a7e1c888c26e6918831893840cd43f4bb0c1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 Apr 2013 12:27:30 +0200 Subject: [PATCH 03/25] Change permission name for issue delete action. --- greenmine/scrum/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/greenmine/scrum/permissions.py b/greenmine/scrum/permissions.py index a9b70ca7..aa705584 100644 --- a/greenmine/scrum/permissions.py +++ b/greenmine/scrum/permissions.py @@ -41,7 +41,7 @@ class IssueDetailPermission(BaseDetailPermission): get_permission = "can_view_issue" put_permission = "change_issue" patch_permission = "change_issue" - delete_permission = "can_delete_issue" + delete_permission = "delete_issue" safe_methods = ['HEAD', 'OPTIONS'] path_to_project = ['project'] From 1986b6a2d28210b7fd6214235d781f57b4f2789e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 Apr 2013 15:34:37 +0200 Subject: [PATCH 04/25] Improved wiki api. --- greenmine/wiki/api.py | 20 ++++++++++++++++++++ greenmine/wiki/models.py | 6 ++++-- greenmine/wiki/urls.py | 4 ++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/greenmine/wiki/api.py b/greenmine/wiki/api.py index 36ea1a99..b37dd073 100644 --- a/greenmine/wiki/api.py +++ b/greenmine/wiki/api.py @@ -1,3 +1,9 @@ +# -*- coding: utf-8 -*- + +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.utils.translation import ugettext as _ +from django.http import Http404 + from rest_framework import generics from greenmine.wiki.serializers import WikiPageSerializer, WikiPageAttachmentSerializer @@ -21,6 +27,20 @@ class WikiPageDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = WikiPageSerializer permission_classes = (WikiPageDetailPermission,) + def get_object(self, queryset=None): + if queryset is None: + queryset = self.get_queryset() + + queryset = queryset.filter(project=self.kwargs["projectid"], + slug=self.kwargs["slug"]) + try: + # Get the single item from the filtered queryset + obj = queryset.get() + except ObjectDoesNotExist: + raise Http404(_("No %(verbose_name)s found matching the query") % + {'verbose_name': queryset.model._meta.verbose_name}) + return obj + class WikiPageAttachmentList(generics.ListCreateAPIView): model = WikiPageAttachment diff --git a/greenmine/wiki/models.py b/greenmine/wiki/models.py index f468440e..9ccd67c2 100644 --- a/greenmine/wiki/models.py +++ b/greenmine/wiki/models.py @@ -1,8 +1,8 @@ +# -*- coding: utf-8 -*- + from django.db import models from django.utils.translation import ugettext_lazy as _ -from greenmine.base.fields import DictField - class WikiPage(models.Model): project = models.ForeignKey('scrum.Project', null=False, blank=False, @@ -27,6 +27,8 @@ class WikiPage(models.Model): verbose_name = u'wiki page' verbose_name_plural = u'wiki pages' ordering = ['project', 'slug'] + unique_together = ('project', 'slug',) + permissions = ( ('can_view_wikipage', 'Can modify owned wiki pages'), ('can_change_owned_wikipage', 'Can modify owned wiki pages'), diff --git a/greenmine/wiki/urls.py b/greenmine/wiki/urls.py index be46664f..0128e065 100644 --- a/greenmine/wiki/urls.py +++ b/greenmine/wiki/urls.py @@ -4,8 +4,8 @@ from rest_framework.urlpatterns import format_suffix_patterns from greenmine.wiki import api urlpatterns = format_suffix_patterns(patterns('', - url(r'^wiki/pages/$', api.WikiPageList.as_view(), name='wiki-page-list'), - url(r'^wiki/pages/(?P[\w\-\d]+)/$', api.WikiPageDetail.as_view(), name='wiki-page-detail'), + url(r'^pages/$', api.WikiPageList.as_view(), name='wiki-page-list'), + url(r'^pages/(?P\d+)-(?P[\w\-\d]+)/$', api.WikiPageDetail.as_view(), name='wiki-page-detail'), #url(r'^wiki_page_attachments/$', api.WikiPageAttachmentList.as_view(), name='wiki-page-attachment-list'), #url(r'^wiki_page_attachments/(?P[0-9]+)/$', api.WikiPageAttachmentDetail.as_view(), name='wiki-page-attachment-detail'), )) From 22528bc9dcf2de32a32b81731e2d1e6b168d8016 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 25 Apr 2013 12:04:23 +0200 Subject: [PATCH 05/25] Adding search api --- greenmine/base/api.py | 19 +++++++++++++++++++ greenmine/base/serializers.py | 23 ++++++++++++++++++++++- greenmine/base/urls.py | 1 + greenmine/documents/search_indexes.py | 5 +++-- greenmine/questions/search_indexes.py | 5 +++-- greenmine/scrum/search_indexes.py | 6 ++++-- greenmine/settings/common.py | 3 +++ greenmine/wiki/search_indexes.py | 4 ++-- 8 files changed, 57 insertions(+), 9 deletions(-) diff --git a/greenmine/base/api.py b/greenmine/base/api.py index 4d890388..b2e0432e 100644 --- a/greenmine/base/api.py +++ b/greenmine/base/api.py @@ -13,9 +13,13 @@ from rest_framework.permissions import IsAuthenticated from rest_framework import status from rest_framework import generics +from haystack.query import SearchQuerySet + from greenmine.base.serializers import LoginSerializer, UserLogged, UserSerializer, RoleSerializer +from greenmine.base.serializers import SearchSerializer from greenmine.base.models import User, Role from greenmine.scrum import models +from django.conf import settings import django_filters @@ -44,6 +48,7 @@ class ApiRoot(APIView): 'wiki/pages': reverse('wiki-page-list', request=request, format=format), 'users': reverse('user-list', request=request, format=format), 'roles': reverse('user-roles', request=request, format=format), + 'search': reverse('search', request=request, format=format), }) @@ -127,3 +132,17 @@ class Logout(APIView): def post(self, request, format=None): logout(request) return Response() + + +class Search(APIView): + def get(self, request, format=None): + text = request.QUERY_PARAMS.get('text', None) + + if text: + #TODO: permission check + results = SearchQuerySet().filter(content=text)[:settings.MAX_SEARCH_RESULTS] + return_data = SearchSerializer(results) + return Response(return_data.data) + + return Response({"detail": "Parameter text can't be empty"}, status.HTTP_400_BAD_REQUEST) + diff --git a/greenmine/base/serializers.py b/greenmine/base/serializers.py index 92de8a2c..5c55fdc6 100644 --- a/greenmine/base/serializers.py +++ b/greenmine/base/serializers.py @@ -60,11 +60,32 @@ class UserSerializer(serializers.ModelSerializer): model = User fields = ('id', 'username', 'first_name', 'last_name', 'color', 'is_active', 'projects') - def get_projects(self, obj): return [x.id for x in obj.projects.all()] + class RoleSerializer(serializers.ModelSerializer): class Meta: model = Role fields = ('id', 'name', 'slug', 'permissions',) + + +class SearchSerializer(serializers.Serializer): + id = serializers.CharField(max_length=255) + model_name = serializers.CharField(max_length=255) + pk = serializers.IntegerField() + score = serializers.FloatField() + stored_fields = serializers.SerializerMethodField('get_stored_fields') + + def get_stored_fields(self, obj): + return obj.get_stored_fields() + + def restore_object(self, attrs, instance=None): + """ + Given a dictionary of deserialized field values, either update + an existing model instance, or create a new model instance. + """ + if instance is not None: + return instance + + return attrs diff --git a/greenmine/base/urls.py b/greenmine/base/urls.py index ae7cfcdc..6bc4ea5f 100644 --- a/greenmine/base/urls.py +++ b/greenmine/base/urls.py @@ -10,5 +10,6 @@ urlpatterns = format_suffix_patterns(patterns('', url(r'^auth/logout/$', api.Logout.as_view(), name='logout'), url(r'^users/$', api.UserList.as_view(), name="user-list"), url(r'^roles/$', api.RoleList.as_view(), name="user-roles"), + url(r'^search/$', api.Search.as_view(), name="search"), url(r'^$', api.ApiRoot.as_view(), name='api_root'), )) diff --git a/greenmine/documents/search_indexes.py b/greenmine/documents/search_indexes.py index acb57125..bc89050e 100644 --- a/greenmine/documents/search_indexes.py +++ b/greenmine/documents/search_indexes.py @@ -3,11 +3,12 @@ from haystack import indexes from .models import Document -class DocumentIndex(indexes.RealTimeSearchIndex, indexes.Indexable): +class DocumentIndex(indexes.SearchIndex, indexes.Indexable): text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/document_text.txt') + title = indexes.CharField(model_attr='title') def get_model(self): return Document - def index_queryset(self): + def index_queryset(self, using=None): return self.get_model().objects.all() diff --git a/greenmine/questions/search_indexes.py b/greenmine/questions/search_indexes.py index 6ba50536..ecb0bbee 100644 --- a/greenmine/questions/search_indexes.py +++ b/greenmine/questions/search_indexes.py @@ -3,11 +3,12 @@ from haystack import indexes from .models import Question -class QuestionIndex(indexes.RealTimeSearchIndex, indexes.Indexable): +class QuestionIndex(indexes.SearchIndex, indexes.Indexable): text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/question_text.txt') + title = indexes.CharField(model_attr='subject') def get_model(self): return Question - def index_queryset(self): + def index_queryset(self, using=None): return self.get_model().objects.all() diff --git a/greenmine/scrum/search_indexes.py b/greenmine/scrum/search_indexes.py index 25f9ecb9..064977a8 100644 --- a/greenmine/scrum/search_indexes.py +++ b/greenmine/scrum/search_indexes.py @@ -5,19 +5,21 @@ from greenmine.scrum.models import UserStory, Task 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') def get_model(self): return UserStory - def index_queryset(self): + 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') def get_model(self): return Task - def index_queryset(self): + def index_queryset(self, using=None): return self.get_model().objects.all() diff --git a/greenmine/settings/common.py b/greenmine/settings/common.py index f6d0a414..bcdd1b63 100644 --- a/greenmine/settings/common.py +++ b/greenmine/settings/common.py @@ -315,6 +315,9 @@ HAYSTACK_CONNECTIONS = { HAYSTACK_DEFAULT_OPERATOR = 'AND' +MAX_SEARCH_RESULTS = 100 + +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( diff --git a/greenmine/wiki/search_indexes.py b/greenmine/wiki/search_indexes.py index bf1063e2..0a14b8f6 100644 --- a/greenmine/wiki/search_indexes.py +++ b/greenmine/wiki/search_indexes.py @@ -3,11 +3,11 @@ from haystack import indexes from .models import WikiPage -class WikiPageIndex(indexes.RealTimeSearchIndex, indexes.Indexable): +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 - def index_queryset(self): + def index_queryset(self, using=None): return self.get_model().objects.all() From 5e502ffb421ee562f7061131960e5b16b9289cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 26 Apr 2013 10:10:41 +0200 Subject: [PATCH 06/25] Ignored greenmine/search --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7426f5e1..1c69fe6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .*.sw* *.log +greenmine/search greenmine/settings/local.py database.sqlite logs From af893793bb23e99a1e2d2c48e00b14994750ea04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 26 Apr 2013 10:22:09 +0200 Subject: [PATCH 07/25] Set a default color --- greenmine/base/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/greenmine/base/models.py b/greenmine/base/models.py index 7c47899b..5d2f54f8 100644 --- a/greenmine/base/models.py +++ b/greenmine/base/models.py @@ -46,7 +46,7 @@ def attach_unique_reference(sender, instance, **kwargs): class User(AbstractUser): - color = models.CharField(max_length=9, null=False, blank=False, + color = models.CharField(max_length=9, null=False, blank=False, default="#669933", verbose_name=_('color')) description = models.TextField(null=False, blank=True, verbose_name=_('description')) From 7df355d92eb87e475ea251a0c6ed2d44a2315e5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 26 Apr 2013 10:28:13 +0200 Subject: [PATCH 08/25] Added the UserDatail API --- greenmine/base/api.py | 6 ++++++ greenmine/base/serializers.py | 3 ++- greenmine/base/urls.py | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/greenmine/base/api.py b/greenmine/base/api.py index b2e0432e..0972a726 100644 --- a/greenmine/base/api.py +++ b/greenmine/base/api.py @@ -96,6 +96,12 @@ class UserList(generics.ListCreateAPIView): pass +class UserDetail(generics.RetrieveUpdateDestroyAPIView): + model = User + serializer_class = UserSerializer + permission_classes = (IsAuthenticated,) + + class Login(APIView): def post(self, request, format=None): username = request.DATA.get('username', None) diff --git a/greenmine/base/serializers.py b/greenmine/base/serializers.py index 5c55fdc6..53f482f1 100644 --- a/greenmine/base/serializers.py +++ b/greenmine/base/serializers.py @@ -58,7 +58,8 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ('id', 'username', 'first_name', 'last_name', 'color', 'is_active', '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()] diff --git a/greenmine/base/urls.py b/greenmine/base/urls.py index 6bc4ea5f..43489811 100644 --- a/greenmine/base/urls.py +++ b/greenmine/base/urls.py @@ -9,6 +9,7 @@ urlpatterns = format_suffix_patterns(patterns('', url(r'^auth/login/$', api.Login.as_view(), name='login'), url(r'^auth/logout/$', api.Logout.as_view(), name='logout'), url(r'^users/$', api.UserList.as_view(), name="user-list"), + url(r'^users/(?P[0-9]+)/$', api.UserDetail.as_view(), name="user-detail"), url(r'^roles/$', api.RoleList.as_view(), name="user-roles"), url(r'^search/$', api.Search.as_view(), name="search"), url(r'^$', api.ApiRoot.as_view(), name='api_root'), From bce92c42f0322bf38b6538c7d6ad6f7cdfc43622 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 26 Apr 2013 10:49:18 +0200 Subject: [PATCH 09/25] Adding questions support --- greenmine/base/api.py | 1 - greenmine/base/fixtures/initial_data.json | 712 +++++++++--------- greenmine/questions/admin.py | 12 +- greenmine/questions/api.py | 34 +- greenmine/questions/choices.py | 9 + greenmine/questions/models.py | 106 ++- greenmine/questions/permissions.py | 14 +- greenmine/questions/serializers.py | 53 +- greenmine/questions/urls.py | 2 - .../scrum/management/commands/sample_data.py | 17 + greenmine/scrum/serializers.py | 2 + 11 files changed, 554 insertions(+), 408 deletions(-) create mode 100644 greenmine/questions/choices.py diff --git a/greenmine/base/api.py b/greenmine/base/api.py index 0972a726..e831dcd8 100644 --- a/greenmine/base/api.py +++ b/greenmine/base/api.py @@ -44,7 +44,6 @@ class ApiRoot(APIView): 'priorities': reverse('priority-list', request=request, format=format), 'documents': reverse('document-list', request=request, format=format), 'questions': reverse('question-list', request=request, format=format), - 'question_responses': reverse('question-response-list', request=request, format=format), 'wiki/pages': reverse('wiki-page-list', request=request, format=format), 'users': reverse('user-list', request=request, format=format), 'roles': reverse('user-roles', request=request, format=format), diff --git a/greenmine/base/fixtures/initial_data.json b/greenmine/base/fixtures/initial_data.json index 9cfc6c01..7b82e8e2 100644 --- a/greenmine/base/fixtures/initial_data.json +++ b/greenmine/base/fixtures/initial_data.json @@ -1,561 +1,601 @@ [ { - "pk": 1, - "model": "base.role", + "pk": 1, + "model": "base.role", "fields": { "permissions": [ [ - "add_logentry", - "admin", + "add_logentry", + "admin", "logentry" - ], + ], [ - "change_logentry", - "admin", + "change_logentry", + "admin", "logentry" - ], + ], [ - "delete_logentry", - "admin", + "delete_logentry", + "admin", "logentry" - ], + ], [ - "add_group", - "auth", + "add_group", + "auth", "group" - ], + ], [ - "change_group", - "auth", + "change_group", + "auth", "group" - ], + ], [ - "delete_group", - "auth", + "delete_group", + "auth", "group" - ], + ], [ - "add_permission", - "auth", + "add_permission", + "auth", "permission" - ], + ], [ - "change_permission", - "auth", + "change_permission", + "auth", "permission" - ], + ], [ - "delete_permission", - "auth", + "delete_permission", + "auth", "permission" - ], + ], [ - "add_role", - "base", + "add_role", + "base", "role" - ], + ], [ - "change_role", - "base", + "change_role", + "base", "role" - ], + ], [ - "delete_role", - "base", + "delete_role", + "base", "role" - ], + ], [ - "add_user", - "base", + "add_user", + "base", "user" - ], + ], [ - "change_user", - "base", + "change_user", + "base", "user" - ], + ], [ - "delete_user", - "base", + "delete_user", + "base", "user" - ], + ], [ - "add_contenttype", - "contenttypes", + "add_contenttype", + "contenttypes", "contenttype" - ], + ], [ - "change_contenttype", - "contenttypes", + "change_contenttype", + "contenttypes", "contenttype" - ], + ], [ - "delete_contenttype", - "contenttypes", + "delete_contenttype", + "contenttypes", "contenttype" - ], + ], [ - "add_document", - "documents", + "add_document", + "documents", "document" - ], + ], [ - "can_change_owned_documents", - "documents", + "can_change_owned_documents", + "documents", "document" - ], + ], [ - "can_download_from_my_projects", - "documents", + "can_download_from_my_projects", + "documents", "document" - ], + ], [ - "can_download_from_other_projects", - "documents", + "can_download_from_other_projects", + "documents", "document" - ], + ], [ - "can_view_documents", - "documents", + "can_view_documents", + "documents", "document" - ], + ], [ - "change_document", - "documents", + "change_document", + "documents", "document" - ], + ], [ - "delete_document", - "documents", + "delete_document", + "documents", "document" - ], + ], [ - "add_question", - "questions", + "add_question", + "questions", "question" - ], + ], [ - "can_change_owned_question", - "questions", + "can_assign_question_to_myself", + "questions", "question" - ], + ], [ - "can_reply_question", - "questions", + "can_assign_question_to_other", + "questions", "question" - ], + ], [ - "change_question", - "questions", + "can_change_assigned_question", + "questions", "question" - ], + ], [ - "delete_question", - "questions", + "can_change_owned_question", + "questions", "question" - ], + ], [ - "add_questionresponse", - "questions", - "questionresponse" - ], + "can_change_question_state", + "questions", + "question" + ], [ - "change_questionresponse", - "questions", - "questionresponse" - ], + "can_reply_question", + "questions", + "question" + ], [ - "delete_questionresponse", - "questions", - "questionresponse" - ], + "can_view_question", + "questions", + "question" + ], [ - "add_issue", - "scrum", + "change_question", + "questions", + "question" + ], + [ + "delete_question", + "questions", + "question" + ], + [ + "add_questionstatus", + "questions", + "questionstatus" + ], + [ + "change_questionstatus", + "questions", + "questionstatus" + ], + [ + "delete_questionstatus", + "questions", + "questionstatus" + ], + [ + "add_attachment", + "scrum", + "attachment" + ], + [ + "change_attachment", + "scrum", + "attachment" + ], + [ + "delete_attachment", + "scrum", + "attachment" + ], + [ + "add_issue", + "scrum", "issue" - ], + ], [ - "can_assign_issue_to_myself", - "scrum", + "can_assign_issue_to_myself", + "scrum", "issue" - ], + ], [ - "can_assign_issue_to_other", - "scrum", + "can_assign_issue_to_other", + "scrum", "issue" - ], + ], [ - "can_change_assigned_issue", - "scrum", + "can_change_assigned_issue", + "scrum", "issue" - ], + ], [ - "can_change_issue_state", - "scrum", + "can_change_issue_state", + "scrum", "issue" - ], + ], [ - "can_change_owned_issue", - "scrum", + "can_change_owned_issue", + "scrum", "issue" - ], + ], [ - "can_comment_issue", - "scrum", + "can_comment_issue", + "scrum", "issue" - ], + ], [ - "can_view_issue", - "scrum", + "can_view_issue", + "scrum", "issue" - ], + ], [ - "change_issue", - "scrum", + "change_issue", + "scrum", "issue" - ], + ], [ - "delete_issue", - "scrum", + "delete_issue", + "scrum", "issue" - ], + ], [ - "add_issuestatus", - "scrum", + "add_issuestatus", + "scrum", "issuestatus" - ], + ], [ - "change_issuestatus", - "scrum", + "change_issuestatus", + "scrum", "issuestatus" - ], + ], [ - "delete_issuestatus", - "scrum", + "delete_issuestatus", + "scrum", "issuestatus" - ], + ], [ - "add_issuetype", - "scrum", + "add_issuetype", + "scrum", "issuetype" - ], + ], [ - "change_issuetype", - "scrum", + "change_issuetype", + "scrum", "issuetype" - ], + ], [ - "delete_issuetype", - "scrum", + "delete_issuetype", + "scrum", "issuetype" - ], + ], [ - "add_membership", - "scrum", + "add_membership", + "scrum", "membership" - ], + ], [ - "change_membership", - "scrum", + "change_membership", + "scrum", "membership" - ], + ], [ - "delete_membership", - "scrum", + "delete_membership", + "scrum", "membership" - ], + ], [ - "add_milestone", - "scrum", + "add_milestone", + "scrum", "milestone" - ], + ], [ - "can_view_milestone", - "scrum", + "can_view_milestone", + "scrum", "milestone" - ], + ], [ - "change_milestone", - "scrum", + "change_milestone", + "scrum", "milestone" - ], + ], [ - "delete_milestone", - "scrum", + "delete_milestone", + "scrum", "milestone" - ], + ], [ - "add_points", - "scrum", + "add_points", + "scrum", "points" - ], + ], [ - "change_points", - "scrum", + "change_points", + "scrum", "points" - ], + ], [ - "delete_points", - "scrum", + "delete_points", + "scrum", "points" - ], + ], [ - "add_priority", - "scrum", + "add_priority", + "scrum", "priority" - ], + ], [ - "change_priority", - "scrum", + "change_priority", + "scrum", "priority" - ], + ], [ - "delete_priority", - "scrum", + "delete_priority", + "scrum", "priority" - ], + ], [ - "add_project", - "scrum", + "add_project", + "scrum", "project" - ], + ], [ - "can_list_projects", - "scrum", + "can_list_projects", + "scrum", "project" - ], + ], [ - "can_manage_users", - "scrum", + "can_manage_users", + "scrum", "project" - ], + ], [ - "can_view_project", - "scrum", + "can_view_project", + "scrum", "project" - ], + ], [ - "change_project", - "scrum", + "change_project", + "scrum", "project" - ], + ], [ - "delete_project", - "scrum", + "delete_project", + "scrum", "project" - ], + ], [ - "add_severity", - "scrum", + "add_severity", + "scrum", "severity" - ], + ], [ - "change_severity", - "scrum", + "change_severity", + "scrum", "severity" - ], + ], [ - "delete_severity", - "scrum", + "delete_severity", + "scrum", "severity" - ], + ], [ - "add_task", - "scrum", + "add_task", + "scrum", "task" - ], + ], [ - "can_add_task_to_us", - "scrum", + "can_add_task_to_us", + "scrum", "task" - ], + ], [ - "can_assign_task_to_myself", - "scrum", + "can_assign_task_to_myself", + "scrum", "task" - ], + ], [ - "can_assign_task_to_other", - "scrum", + "can_assign_task_to_other", + "scrum", "task" - ], + ], [ - "can_change_assigned_task", - "scrum", + "can_change_assigned_task", + "scrum", "task" - ], + ], [ - "can_change_owned_task", - "scrum", + "can_change_owned_task", + "scrum", "task" - ], + ], [ - "can_change_task_state", - "scrum", + "can_change_task_state", + "scrum", "task" - ], + ], [ - "can_comment_task", - "scrum", + "can_comment_task", + "scrum", "task" - ], + ], [ - "can_view_task", - "scrum", + "can_view_task", + "scrum", "task" - ], + ], [ - "change_task", - "scrum", + "change_task", + "scrum", "task" - ], + ], [ - "delete_task", - "scrum", + "delete_task", + "scrum", "task" - ], + ], [ - "add_taskstatus", - "scrum", + "add_taskstatus", + "scrum", "taskstatus" - ], + ], [ - "change_taskstatus", - "scrum", + "change_taskstatus", + "scrum", "taskstatus" - ], + ], [ - "delete_taskstatus", - "scrum", + "delete_taskstatus", + "scrum", "taskstatus" - ], + ], [ - "add_userstory", - "scrum", + "add_userstory", + "scrum", "userstory" - ], + ], [ - "can_add_userstory_to_milestones", - "scrum", + "can_add_userstory_to_milestones", + "scrum", "userstory" - ], + ], [ - "can_change_owned_userstory", - "scrum", + "can_change_owned_userstory", + "scrum", "userstory" - ], + ], [ - "can_comment_userstory", - "scrum", + "can_comment_userstory", + "scrum", "userstory" - ], + ], [ - "can_delete_userstory", - "scrum", + "can_delete_userstory", + "scrum", "userstory" - ], + ], [ - "can_view_userstory", - "scrum", + "can_view_userstory", + "scrum", "userstory" - ], + ], [ - "change_userstory", - "scrum", + "change_userstory", + "scrum", "userstory" - ], + ], [ - "delete_userstory", - "scrum", + "delete_userstory", + "scrum", "userstory" - ], + ], [ - "add_userstorystatus", - "scrum", + "add_userstorystatus", + "scrum", "userstorystatus" - ], + ], [ - "change_userstorystatus", - "scrum", + "change_userstorystatus", + "scrum", "userstorystatus" - ], + ], [ - "delete_userstorystatus", - "scrum", + "delete_userstorystatus", + "scrum", "userstorystatus" - ], + ], [ - "add_session", - "sessions", + "add_session", + "sessions", "session" - ], + ], [ - "change_session", - "sessions", + "change_session", + "sessions", "session" - ], + ], [ - "delete_session", - "sessions", + "delete_session", + "sessions", "session" - ], + ], [ - "add_migrationhistory", - "south", + "add_migrationhistory", + "south", "migrationhistory" - ], + ], [ - "change_migrationhistory", - "south", + "change_migrationhistory", + "south", "migrationhistory" - ], + ], [ - "delete_migrationhistory", - "south", + "delete_migrationhistory", + "south", "migrationhistory" - ], + ], [ - "add_wikipage", - "wiki", + "add_wikipage", + "wiki", "wikipage" - ], + ], [ - "can_change_owned_wikipage", - "wiki", + "can_change_owned_wikipage", + "wiki", "wikipage" - ], + ], [ - "can_view_wikipage", - "wiki", + "can_view_wikipage", + "wiki", "wikipage" - ], + ], [ - "change_wikipage", - "wiki", + "change_wikipage", + "wiki", "wikipage" - ], + ], [ - "delete_wikipage", - "wiki", + "delete_wikipage", + "wiki", "wikipage" - ], + ], [ - "add_wikipageattachment", - "wiki", + "add_wikipageattachment", + "wiki", "wikipageattachment" - ], + ], [ - "change_wikipageattachment", - "wiki", + "change_wikipageattachment", + "wiki", "wikipageattachment" - ], + ], [ - "delete_wikipageattachment", - "wiki", + "delete_wikipageattachment", + "wiki", "wikipageattachment" ] - ], - "name": "Developer", + ], + "name": "Developer", "slug": "" } } diff --git a/greenmine/questions/admin.py b/greenmine/questions/admin.py index b93e733a..2d807148 100644 --- a/greenmine/questions/admin.py +++ b/greenmine/questions/admin.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- from django.contrib import admin -from greenmine.questions.models import Question, QuestionResponse +from greenmine.questions.models import Question, QuestionStatus + +import reversion -class QuestionAdmin(admin.ModelAdmin): +class QuestionAdmin(reversion.VersionAdmin): list_display = ["subject", "project", "owner"] admin.site.register(Question, QuestionAdmin) -class QuestionResponseAdmin(admin.ModelAdmin): - list_display = ["id", "question", "owner"] +class QuestionStatusAdmin(admin.ModelAdmin): + list_display = ["name", "order", "is_closed", "project"] -admin.site.register(QuestionResponse, QuestionResponseAdmin) +admin.site.register(QuestionStatus, QuestionStatusAdmin) diff --git a/greenmine/questions/api.py b/greenmine/questions/api.py index e74c6cf2..83c8d728 100644 --- a/greenmine/questions/api.py +++ b/greenmine/questions/api.py @@ -1,32 +1,34 @@ +# -*- coding: utf-8 -*- from rest_framework import generics +from rest_framework.permissions import IsAuthenticated -from greenmine.questions.serializers import QuestionSerializer, QuestionResponseSerializer -from greenmine.questions.models import Question, QuestionResponse -from greenmine.questions.permissions import QuestionDetailPermission, QuestionResponseDetailPermission +from greenmine.questions.serializers import QuestionSerializer +from greenmine.questions.models import Question +from greenmine.questions.permissions import QuestionDetailPermission +import reversion class QuestionList(generics.ListCreateAPIView): model = Question serializer_class = QuestionSerializer + filter_fields = ('project',) + permission_classes = (IsAuthenticated,) def get_queryset(self): return self.model.objects.filter(project__members=self.request.user) + def pre_save(self, obj): + obj.owner = self.request.user + + class QuestionDetail(generics.RetrieveUpdateDestroyAPIView): model = Question serializer_class = QuestionSerializer - permission_classes = (QuestionDetailPermission,) + permission_classes = (IsAuthenticated, QuestionDetailPermission,) - -class QuestionResponseList(generics.ListCreateAPIView): - model = QuestionResponse - serializer_class = QuestionResponseSerializer - - def get_queryset(self): - return self.model.objects.filter(question__project__members=self.request.user) - -class QuestionResponseDetail(generics.RetrieveUpdateDestroyAPIView): - model = QuestionResponse - serializer_class = QuestionResponseSerializer - permission_classes = (QuestionResponseDetailPermission,) + 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']) diff --git a/greenmine/questions/choices.py b/greenmine/questions/choices.py new file mode 100644 index 00000000..e52b1e78 --- /dev/null +++ b/greenmine/questions/choices.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from django.utils.translation import ugettext_lazy as _ + +QUESTION_STATUS = ( + (1, _(u"New"), False), + (2, _(u"Pending"), False), + (3, _(u"Answered"), True), +) diff --git a/greenmine/questions/models.py b/greenmine/questions/models.py index dd1d8c25..b2d0988e 100644 --- a/greenmine/questions/models.py +++ b/greenmine/questions/models.py @@ -1,15 +1,51 @@ +# -*- coding: utf-8 -*- + from django.db import models from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone +from django.dispatch import receiver -from greenmine.base.utils.slug import slugify_uniquely +from greenmine.base.utils.slug import slugify_uniquely, ref_uniquely from greenmine.base.fields import DictField +from greenmine.scrum.models import Project + +from picklefield.fields import PickledObjectField +from greenmine.questions.choices import QUESTION_STATUS + + +class QuestionStatus(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='question_status', + verbose_name=_('project')) + + class Meta: + verbose_name = u'question status' + verbose_name_plural = u'question status' + ordering = ['project', 'name'] + unique_together = ('project', 'name') + + def __unicode__(self): + return u'project {0} - {1}'.format(self.project_id, self.name) class Question(models.Model): - subject = models.CharField(max_length=150, null=False, blank=False, + + ref = models.BigIntegerField(db_index=True, null=True, blank=True, default=None, + verbose_name=_('ref')) + owner = models.ForeignKey('base.User', null=True, blank=True, default=None, + related_name='owned_questions', + verbose_name=_('owner')) + status = models.ForeignKey('QuestionStatus', null=False, blank=False, + related_name='questions', + verbose_name=_('status')) + subject = models.CharField(max_length=250, null=False, blank=False, verbose_name=_('subject')) - slug = models.SlugField(unique=True, max_length=250, null=False, blank=True, - verbose_name=_('slug')) content = models.TextField(null=False, blank=True, verbose_name=_('content')) closed = models.BooleanField(default=False, null=False, blank=True, @@ -18,66 +54,68 @@ class Question(models.Model): upload_to='messages', verbose_name=_('attached_file')) project = models.ForeignKey('scrum.Project', null=False, blank=False, - related_name='questions') + related_name='questions', + verbose_name=_('project')) milestone = models.ForeignKey('scrum.Milestone', null=True, blank=True, default=None, related_name='questions', verbose_name=_('milestone')) - assigned_to = models.ForeignKey('base.User', null=False, blank=False, + finished_date = models.DateTimeField(null=True, blank=True, + verbose_name=_('finished date')) + assigned_to = models.ForeignKey('base.User', null=True, blank=True, default=None, related_name='questions_assigned_to_me', verbose_name=_('assigned_to')) 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')) - owner = models.ForeignKey('base.User', null=False, blank=False, - related_name='owned_questions') watchers = models.ManyToManyField('base.User', null=True, blank=True, related_name='watched_questions', verbose_name=_('watchers')) - tags = DictField(null=False, blank=True, + tags = PickledObjectField(null=False, blank=True, verbose_name=_('tags')) class Meta: verbose_name = u'question' verbose_name_plural = u'questions' ordering = ['project', 'subject', 'id'] + #TODO: permissions permissions = ( ('can_reply_question', 'Can reply questions'), ('can_change_owned_question', 'Can modify owned questions'), + ('can_change_assigned_question', 'Can modify assigned questions'), + ('can_assign_question_to_other', 'Can assign questions to others'), + ('can_assign_question_to_myself', 'Can assign questions to myself'), + ('can_change_question_state', 'Can change the question state'), + ('can_view_question', 'Can view the question'), ) def __unicode__(self): return self.subject def save(self, *args, **kwargs): - if not self.slug: - self.slug = slugify_uniquely(self.subject, self.__class__) + if self.id: + self.modified_date = timezone.now() + + if not self.ref: + self.ref = ref_uniquely(self.project, 'last_issue_ref', self.__class__) + super(Question, self).save(*args, **kwargs) -class QuestionResponse(models.Model): - content = models.TextField(null=False, blank=False, - verbose_name=_('content')) - 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='messages', - verbose_name=_('attached file')) - question = models.ForeignKey('Question', null=False, blank=False, - related_name='responses', - verbose_name=_('question')) - owner = models.ForeignKey('base.User', null=False, blank=False, - related_name='question_responses') - tags = DictField(null=False, blank=True, - verbose_name=_('tags')) - class Meta: - verbose_name = u'question response' - verbose_name_plural = u'question responses' - ordering = ['question', 'created_date'] +# Model related signals handlers - def __unicode__(self): - return u'{0} - response {1}'.format(unicode(self.question), self.id) +@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/questions/permissions.py index fedf72fe..b276fd41 100644 --- a/greenmine/questions/permissions.py +++ b/greenmine/questions/permissions.py @@ -1,15 +1,11 @@ from greenmine.base.permissions import BaseDetailPermission + class QuestionDetailPermission(BaseDetailPermission): get_permission = "can_view_question" - put_permission = "can_change_question" - delete_permission = "can_delete_question" + put_permission = "change_question" + patch_permission = "change_question" + delete_permission = "delete_question" safe_methods = ['HEAD', 'OPTIONS'] - path_to_document = [] + path_to_project = [] -class QuestionResponseDetailPermission(BaseDetailPermission): - get_permission = "can_view_questionresponse" - put_permission = "can_change_questionresponse" - delete_permission = "can_delete_questionresponse" - safe_methods = ['HEAD', 'OPTIONS'] - path_to_document = [] diff --git a/greenmine/questions/serializers.py b/greenmine/questions/serializers.py index 83d59b41..412c1652 100644 --- a/greenmine/questions/serializers.py +++ b/greenmine/questions/serializers.py @@ -1,15 +1,58 @@ +# -*- coding: utf-8 -*- + from rest_framework import serializers -from greenmine.questions.models import Question, QuestionResponse +from greenmine.questions.models import Question +from greenmine.scrum.serializers import PickleField + +import reversion class QuestionSerializer(serializers.ModelSerializer): + tags = PickleField() + comment = serializers.SerializerMethodField('get_comment') + history = serializers.SerializerMethodField('get_history') + class Meta: model = Question fields = () + def get_comment(self, obj): + return '' -class QuestionResponseSerializer(serializers.ModelSerializer): - class Meta: - model = QuestionResponse - fields = () + 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, + } + + 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: + questions_diff = self.get_questions_diff(version, current) + diff_list.append(questions_diff) + + current = version + + return diff_list diff --git a/greenmine/questions/urls.py b/greenmine/questions/urls.py index 80085fa7..9eef951c 100644 --- a/greenmine/questions/urls.py +++ b/greenmine/questions/urls.py @@ -6,7 +6,5 @@ from greenmine.questions import api urlpatterns = format_suffix_patterns(patterns('', url(r'^questions/$', api.QuestionList.as_view(), name='question-list'), url(r'^questions/(?P[0-9]+)/$', api.QuestionDetail.as_view(), name='question-detail'), - url(r'^question_responses/$', api.QuestionResponseList.as_view(), name='question-response-list'), - url(r'^question_responses/(?P[0-9]+)/$', api.QuestionResponseDetail.as_view(), name='question-response-detail'), )) diff --git a/greenmine/scrum/management/commands/sample_data.py b/greenmine/scrum/management/commands/sample_data.py index 3e82877b..290499da 100644 --- a/greenmine/scrum/management/commands/sample_data.py +++ b/greenmine/scrum/management/commands/sample_data.py @@ -11,6 +11,7 @@ from django.contrib.webdesign import lorem_ipsum from greenmine.base.models import User, Role from greenmine.scrum.models import * +from greenmine.questions.models import * subjects = [ "Fixing templates for Django 1.2.", @@ -146,3 +147,19 @@ class Command(BaseCommand): bug.tags.append(tag) bug.save() + + # create questions. + for y in xrange(20): + question = Question.objects.create( + project=project, + subject=lorem_ipsum.words(random.randint(1, 5), common=False), + content=lorem_ipsum.words(random.randint(1, 15), common=False), + owner=project.owner, + status=QuestionStatus.objects.get(project=project, order=1), + tags=[], + ) + + for tag in lorem_ipsum.words(random.randint(1, 5), common=True).split(" "): + question.tags.append(tag) + + question.save() diff --git a/greenmine/scrum/serializers.py b/greenmine/scrum/serializers.py index 964b1a15..c25f3989 100644 --- a/greenmine/scrum/serializers.py +++ b/greenmine/scrum/serializers.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from rest_framework import serializers from greenmine.scrum.models import * From 8cb299c2180ae2f9b7fe930e14842bbce3fedb3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Fri, 26 Apr 2013 15:30:06 +0200 Subject: [PATCH 10/25] Mede some minor fixes --- greenmine/documents/admin.py | 5 +++-- greenmine/documents/api.py | 20 ++++++++++--------- greenmine/documents/models.py | 1 + greenmine/documents/permissions.py | 3 +++ greenmine/documents/search_indexes.py | 9 ++++++--- greenmine/documents/serializers.py | 7 +++++-- greenmine/documents/urls.py | 5 ++++- greenmine/questions/admin.py | 7 ++++--- greenmine/questions/api.py | 20 ++++++++++--------- greenmine/questions/models.py | 2 -- greenmine/questions/permissions.py | 2 ++ greenmine/questions/search_indexes.py | 9 ++++++--- greenmine/questions/serializers.py | 7 ++++--- greenmine/questions/urls.py | 5 ++++- greenmine/scrum/admin.py | 10 ++++++++++ greenmine/scrum/api.py | 2 ++ greenmine/scrum/choices.py | 1 + .../scrum/management/commands/sample_data.py | 17 ++++++++-------- greenmine/scrum/models.py | 3 +-- greenmine/scrum/permissions.py | 2 ++ greenmine/scrum/search_indexes.py | 7 +++++-- greenmine/scrum/serializers.py | 1 + greenmine/scrum/urls.py | 1 + greenmine/wiki/admin.py | 1 + greenmine/wiki/api.py | 4 ++-- greenmine/wiki/models.py | 4 ++-- greenmine/wiki/permissions.py | 4 ++++ greenmine/wiki/search_indexes.py | 1 + greenmine/wiki/serializers.py | 2 ++ greenmine/wiki/urls.py | 3 +++ 30 files changed, 111 insertions(+), 54 deletions(-) diff --git a/greenmine/documents/admin.py b/greenmine/documents/admin.py index 4754e991..d5ef6709 100644 --- a/greenmine/documents/admin.py +++ b/greenmine/documents/admin.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- + from django.contrib import admin -from greenmine.documents.models import Document +from . import models class DocumentAdmin(admin.ModelAdmin): list_display = ["title", "project", "owner"] -admin.site.register(Document, DocumentAdmin) +admin.site.register(models.Document, DocumentAdmin) diff --git a/greenmine/documents/api.py b/greenmine/documents/api.py index 0f05260d..b16cbe11 100644 --- a/greenmine/documents/api.py +++ b/greenmine/documents/api.py @@ -1,19 +1,21 @@ +# -*- coding: utf-8 -*- + from rest_framework import generics -from greenmine.documents.serializers import DocumentSerializer -from greenmine.documents.models import Document -from greenmine.documents.permissions import DocumentDetailPermission +from . import serializers +from . import models +from . import permissions class DocumentList(generics.ListCreateAPIView): - model = Document - serializer_class = DocumentSerializer + model = models.Document + serializer_class = serializers.DocumentSerializer def get_queryset(self): - return self.model.objects.filter(project__members=self.request.user) + return super(DocumentList, self).filter(project__members=self.request.user) class DocumentDetail(generics.RetrieveUpdateDestroyAPIView): - model = Document - serializer_class = DocumentSerializer - permission_classes = (DocumentDetailPermission,) + model = models.Document + serializer_class = serializers.DocumentSerializer + permission_classes = (permissions.DocumentDetailPermission,) diff --git a/greenmine/documents/models.py b/greenmine/documents/models.py index 4a557c7a..172f5c8d 100644 --- a/greenmine/documents/models.py +++ b/greenmine/documents/models.py @@ -1,4 +1,5 @@ # -* coding: utf-8 -*- + from django.db import models from django.utils.translation import ugettext_lazy as _ diff --git a/greenmine/documents/permissions.py b/greenmine/documents/permissions.py index a9c3a330..1e43a59d 100644 --- a/greenmine/documents/permissions.py +++ b/greenmine/documents/permissions.py @@ -1,5 +1,8 @@ +# -*- coding: utf-8 -*- + from greenmine.base.permissions import BaseDetailPermission + class DocumentDetailPermission(BaseDetailPermission): get_permission = "can_view_document" put_permission = "can_change_document" diff --git a/greenmine/documents/search_indexes.py b/greenmine/documents/search_indexes.py index bc89050e..843483c1 100644 --- a/greenmine/documents/search_indexes.py +++ b/greenmine/documents/search_indexes.py @@ -1,14 +1,17 @@ # -* coding: utf-8 -*- + from haystack import indexes -from .models import Document + +from . import models class DocumentIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/document_text.txt') + text = indexes.CharField(document=True, use_template=True, + template_name='search/indexes/document_text.txt') title = indexes.CharField(model_attr='title') def get_model(self): - return Document + return models.Document def index_queryset(self, using=None): return self.get_model().objects.all() diff --git a/greenmine/documents/serializers.py b/greenmine/documents/serializers.py index 50c4c0d3..fa330d23 100644 --- a/greenmine/documents/serializers.py +++ b/greenmine/documents/serializers.py @@ -1,8 +1,11 @@ +# -*- coding: utf-8 -*- + from rest_framework import serializers -from greenmine.documents.models import Document +from . import models + class DocumentSerializer(serializers.ModelSerializer): class Meta: - model = Document + model = models.Document fields = () diff --git a/greenmine/documents/urls.py b/greenmine/documents/urls.py index e40bffdc..10fdd949 100644 --- a/greenmine/documents/urls.py +++ b/greenmine/documents/urls.py @@ -1,7 +1,10 @@ +# -*- coding: utf-8 -*- + from django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns -from greenmine.documents import api +from . import api + urlpatterns = format_suffix_patterns(patterns('', url(r'^documents/$', api.DocumentList.as_view(), name='document-list'), diff --git a/greenmine/questions/admin.py b/greenmine/questions/admin.py index 2d807148..6e875f72 100644 --- a/greenmine/questions/admin.py +++ b/greenmine/questions/admin.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- + from django.contrib import admin -from greenmine.questions.models import Question, QuestionStatus +from . import models import reversion @@ -9,10 +10,10 @@ import reversion class QuestionAdmin(reversion.VersionAdmin): list_display = ["subject", "project", "owner"] -admin.site.register(Question, QuestionAdmin) +admin.site.register(models.Question, QuestionAdmin) class QuestionStatusAdmin(admin.ModelAdmin): list_display = ["name", "order", "is_closed", "project"] -admin.site.register(QuestionStatus, QuestionStatusAdmin) +admin.site.register(models.QuestionStatus, QuestionStatusAdmin) diff --git a/greenmine/questions/api.py b/greenmine/questions/api.py index 83c8d728..3ba530a2 100644 --- a/greenmine/questions/api.py +++ b/greenmine/questions/api.py @@ -3,29 +3,31 @@ from rest_framework import generics from rest_framework.permissions import IsAuthenticated -from greenmine.questions.serializers import QuestionSerializer -from greenmine.questions.models import Question -from greenmine.questions.permissions import QuestionDetailPermission +from . import serializers +from . import models +from . import permissions import reversion + class QuestionList(generics.ListCreateAPIView): - model = Question - serializer_class = QuestionSerializer + model = models.Question + serializer_class = serializers.QuestionSerializer filter_fields = ('project',) permission_classes = (IsAuthenticated,) def get_queryset(self): - return self.model.objects.filter(project__members=self.request.user) + return super(QuestionList, self).filter(project__members=self.request.user) + def pre_save(self, obj): obj.owner = self.request.user class QuestionDetail(generics.RetrieveUpdateDestroyAPIView): - model = Question - serializer_class = QuestionSerializer - permission_classes = (IsAuthenticated, QuestionDetailPermission,) + model = models.Question + serializer_class = serializers.QuestionSerializer + permission_classes = (IsAuthenticated, permissions.QuestionDetailPermission,) def post_save(self, obj, created=False): with reversion.create_revision(): diff --git a/greenmine/questions/models.py b/greenmine/questions/models.py index b2d0988e..b0b0a654 100644 --- a/greenmine/questions/models.py +++ b/greenmine/questions/models.py @@ -102,9 +102,7 @@ class Question(models.Model): 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): """ diff --git a/greenmine/questions/permissions.py b/greenmine/questions/permissions.py index b276fd41..0cb030e3 100644 --- a/greenmine/questions/permissions.py +++ b/greenmine/questions/permissions.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from greenmine.base.permissions import BaseDetailPermission diff --git a/greenmine/questions/search_indexes.py b/greenmine/questions/search_indexes.py index ecb0bbee..45b00fa8 100644 --- a/greenmine/questions/search_indexes.py +++ b/greenmine/questions/search_indexes.py @@ -1,14 +1,17 @@ # -* coding: utf-8 -*- + from haystack import indexes -from .models import Question + +from . import models class QuestionIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/question_text.txt') + text = indexes.CharField(document=True, use_template=True, + template_name='search/indexes/question_text.txt') title = indexes.CharField(model_attr='subject') def get_model(self): - return Question + return models.Question def index_queryset(self, using=None): return self.get_model().objects.all() diff --git a/greenmine/questions/serializers.py b/greenmine/questions/serializers.py index 412c1652..4e623945 100644 --- a/greenmine/questions/serializers.py +++ b/greenmine/questions/serializers.py @@ -2,10 +2,11 @@ from rest_framework import serializers -from greenmine.questions.models import Question +import reversion + from greenmine.scrum.serializers import PickleField -import reversion +from . import models class QuestionSerializer(serializers.ModelSerializer): @@ -14,7 +15,7 @@ class QuestionSerializer(serializers.ModelSerializer): history = serializers.SerializerMethodField('get_history') class Meta: - model = Question + model = models.Question fields = () def get_comment(self, obj): diff --git a/greenmine/questions/urls.py b/greenmine/questions/urls.py index 9eef951c..3f44bddd 100644 --- a/greenmine/questions/urls.py +++ b/greenmine/questions/urls.py @@ -1,7 +1,10 @@ +# -*- coding: utf-8 -*- + from django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns -from greenmine.questions import api +from . import api + urlpatterns = format_suffix_patterns(patterns('', url(r'^questions/$', api.QuestionList.as_view(), name='question-list'), diff --git a/greenmine/scrum/admin.py b/greenmine/scrum/admin.py index 2f0172a5..2cfef0eb 100644 --- a/greenmine/scrum/admin.py +++ b/greenmine/scrum/admin.py @@ -30,6 +30,7 @@ class UserStoryInline(admin.TabularInline): else: return models.UserStory.objects.none() + class ProjectAdmin(reversion.VersionAdmin): list_display = ["name", "owner"] inlines = [MembershipInline, MilestoneInline, UserStoryInline] @@ -67,30 +68,39 @@ 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) diff --git a/greenmine/scrum/api.py b/greenmine/scrum/api.py index c5e2b219..2ace56c3 100644 --- a/greenmine/scrum/api.py +++ b/greenmine/scrum/api.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import django_filters from rest_framework import generics diff --git a/greenmine/scrum/choices.py b/greenmine/scrum/choices.py index 540fb4b2..9dcfcc7a 100644 --- a/greenmine/scrum/choices.py +++ b/greenmine/scrum/choices.py @@ -2,6 +2,7 @@ from django.utils.translation import ugettext_lazy as _ + PRIORITY_CHOICES = ( (1, _(u'Low')), (3, _(u'Normal')), diff --git a/greenmine/scrum/management/commands/sample_data.py b/greenmine/scrum/management/commands/sample_data.py index 290499da..190efb71 100644 --- a/greenmine/scrum/management/commands/sample_data.py +++ b/greenmine/scrum/management/commands/sample_data.py @@ -13,6 +13,7 @@ from greenmine.base.models import User, Role from greenmine.scrum.models import * from greenmine.questions.models import * + subjects = [ "Fixing templates for Django 1.2.", "get_actions() does not check for 'delete_selected' in actions", @@ -32,13 +33,13 @@ subjects = [ class Command(BaseCommand): def create_user(self, counter): user = User.objects.create( - username='user%d' % (counter), - first_name='user%d' % (counter), - email='foouser%d@domain.com' % (counter), + username='useri{0}'.format(counter), + first_name='user{0}'.format(counter), + email='foouser{0}@domain.com'.format(counter), token=''.join(random.sample('abcdef0123456789', 10)), ) - user.set_password('user%d' % (counter)) + user.set_password('user{0}'.format(counter)) user.save() return user @@ -54,8 +55,8 @@ class Command(BaseCommand): for x in xrange(3): # create project project = Project( - name='Project Example 1 %s' % (x), - description='Project example %s description' % (x), + name='Project Example 1 {0}'.format(x), + description='Project example {0} description'.format(x), owner=random.choice(users), public=True, ) @@ -71,7 +72,7 @@ class Command(BaseCommand): for y in xrange(2): milestone = Milestone.objects.create( project=project, - name='Sprint %s' % (y), + name='Sprint {0}'.format(y), owner=project.owner, created_date=now_date, modified_date=now_date, @@ -102,7 +103,7 @@ class Command(BaseCommand): for w in xrange(3): Task.objects.create( - subject="Task %s" % (w), + subject="Task %s".format(w), description=lorem_ipsum.words(30, common=False), project=project, owner=random.choice(users), diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 431a52bc..10fe908a 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -376,8 +376,7 @@ class Attachment(models.Model): def __unicode__(self): return u'content_type {0} - object_id {1} - attachment {2}'.format( - self.content_type, self.object_id, self.id - ) + self.content_type, self.object_id, self.id) class Task(models.Model): diff --git a/greenmine/scrum/permissions.py b/greenmine/scrum/permissions.py index aa705584..d7b5e390 100644 --- a/greenmine/scrum/permissions.py +++ b/greenmine/scrum/permissions.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from greenmine.base.permissions import BaseDetailPermission diff --git a/greenmine/scrum/search_indexes.py b/greenmine/scrum/search_indexes.py index 064977a8..a3632926 100644 --- a/greenmine/scrum/search_indexes.py +++ b/greenmine/scrum/search_indexes.py @@ -1,10 +1,12 @@ # -* coding: utf-8 -*- + from haystack import indexes from greenmine.scrum.models import UserStory, Task class UserStoryIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/userstory_text.txt') + text = indexes.CharField(document=True, use_template=True, + template_name='search/indexes/userstory_text.txt') title = indexes.CharField(model_attr='subject') def get_model(self): @@ -15,7 +17,8 @@ class UserStoryIndex(indexes.SearchIndex, indexes.Indexable): class TaskIndex(indexes.SearchIndex, indexes.Indexable): - text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/task_text.txt') + text = indexes.CharField(document=True, use_template=True, + template_name='search/indexes/task_text.txt') title = indexes.CharField(model_attr='subject') def get_model(self): diff --git a/greenmine/scrum/serializers.py b/greenmine/scrum/serializers.py index c25f3989..07daa29d 100644 --- a/greenmine/scrum/serializers.py +++ b/greenmine/scrum/serializers.py @@ -7,6 +7,7 @@ from picklefield.fields import dbsafe_encode, dbsafe_decode import json, reversion + class PickleField(serializers.WritableField): """ Pickle objects serializer. diff --git a/greenmine/scrum/urls.py b/greenmine/scrum/urls.py index a2840cbf..164552c0 100644 --- a/greenmine/scrum/urls.py +++ b/greenmine/scrum/urls.py @@ -5,6 +5,7 @@ from rest_framework.urlpatterns import format_suffix_patterns from greenmine.scrum import api + urlpatterns = format_suffix_patterns(patterns('', url(r'^projects/$', api.ProjectList.as_view(), name='project-list'), url(r'^projects/(?P[0-9]+)/$', api.ProjectDetail.as_view(), name='project-detail'), diff --git a/greenmine/wiki/admin.py b/greenmine/wiki/admin.py index 9ad9bf50..8ed443f5 100644 --- a/greenmine/wiki/admin.py +++ b/greenmine/wiki/admin.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + from django.contrib import admin from greenmine.wiki.models import WikiPage, WikiPageAttachment diff --git a/greenmine/wiki/api.py b/greenmine/wiki/api.py index b37dd073..184c8b4f 100644 --- a/greenmine/wiki/api.py +++ b/greenmine/wiki/api.py @@ -37,8 +37,8 @@ class WikiPageDetail(generics.RetrieveUpdateDestroyAPIView): # Get the single item from the filtered queryset obj = queryset.get() except ObjectDoesNotExist: - raise Http404(_("No %(verbose_name)s found matching the query") % - {'verbose_name': queryset.model._meta.verbose_name}) + raise Http404(_("No {verbose_name} found matching the query").format( + verbose_name=queryset.model._meta.verbose_name)) return obj diff --git a/greenmine/wiki/models.py b/greenmine/wiki/models.py index 9ccd67c2..b57dabc4 100644 --- a/greenmine/wiki/models.py +++ b/greenmine/wiki/models.py @@ -59,5 +59,5 @@ class WikiPageAttachment(models.Model): ordering = ['wikipage', 'created_date'] def __unicode__(self): - return u'project {0} - page {1} - attachment {2}'.format(self.wikipage.project_id, self.wikipage.subject, self.id) - + return u'project {0} - page {1} - attachment {2}'.format(self.wikipage.project_id, + self.wikipage.subject, self.id) diff --git a/greenmine/wiki/permissions.py b/greenmine/wiki/permissions.py index 9ee4d38b..73352807 100644 --- a/greenmine/wiki/permissions.py +++ b/greenmine/wiki/permissions.py @@ -1,5 +1,8 @@ +# -*- coding: utf-8 -*- + from greenmine.base.permissions import BaseDetailPermission + class WikiPageDetailPermission(BaseDetailPermission): get_permission = "can_view_wikipage" put_permission = "change_wikipage" @@ -8,6 +11,7 @@ class WikiPageDetailPermission(BaseDetailPermission): safe_methods = ['HEAD', 'OPTIONS'] path_to_project = ['project'] + class WikiPageAttachmentDetailPermission(BaseDetailPermission): get_permission = "can_view_wikipageattachment" put_permission = "change_wikipageattachment" diff --git a/greenmine/wiki/search_indexes.py b/greenmine/wiki/search_indexes.py index 0a14b8f6..fdc6cd2c 100644 --- a/greenmine/wiki/search_indexes.py +++ b/greenmine/wiki/search_indexes.py @@ -1,4 +1,5 @@ # -* coding: utf-8 -*- + from haystack import indexes from .models import WikiPage diff --git a/greenmine/wiki/serializers.py b/greenmine/wiki/serializers.py index 8b354dc9..6879c89a 100644 --- a/greenmine/wiki/serializers.py +++ b/greenmine/wiki/serializers.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from rest_framework import serializers from greenmine.wiki.models import WikiPage, WikiPageAttachment diff --git a/greenmine/wiki/urls.py b/greenmine/wiki/urls.py index 0128e065..251d6a07 100644 --- a/greenmine/wiki/urls.py +++ b/greenmine/wiki/urls.py @@ -1,8 +1,11 @@ +# -*- coding: utf-8 -*- + from django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns from greenmine.wiki import api + urlpatterns = format_suffix_patterns(patterns('', url(r'^pages/$', api.WikiPageList.as_view(), name='wiki-page-list'), url(r'^pages/(?P\d+)-(?P[\w\-\d]+)/$', api.WikiPageDetail.as_view(), name='wiki-page-detail'), From ecd27d822dce373d8cc720cdadb414651db255d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 7 May 2013 10:22:37 +0200 Subject: [PATCH 11/25] Adding auto update user story finish date on all task close and task finished_date on task close --- greenmine/scrum/models.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 10fe908a..395463e9 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from datetime import datetime + from django.db import models from django.utils import timezone from django.dispatch import receiver @@ -569,6 +571,19 @@ def user_story_ref_handler(sender, instance, **kwargs): 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 and sender.objects.get(id=instance.id).status.is_closed == False and instance.status.is_closed == True: + instance.finished_date = datetime.now() + if all([task.status.is_closed for task in instance.user_story.tasks.exclude(id=instance.id)]): + instance.user_story.finish_date = datetime.now() + instance.user_story.save() + # Email alerts signals handlers # TODO: temporary commented (Pending refactor) # from . import sigdispatch From 623597a175b7ac8538bac112f511a0349bad9702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 7 May 2013 16:53:48 +0200 Subject: [PATCH 12/25] Adding improvements for backlog and dashboard graph drawing --- greenmine/scrum/models.py | 40 ++++++++++++++++++++++++++++++---- greenmine/scrum/serializers.py | 1 + 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 395463e9..c3b3c16e 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -159,6 +159,15 @@ class Points(models.Model): def __unicode__(self): return u'project {0} - {1}'.format(self.project_id, self.name) + @property + def value(self): + if self.order == -2: + return 0.5 + elif self.order == -1: + return 1 + else: + return self.order + class Membership(models.Model): user = models.ForeignKey('base.User', null=False, blank=False) @@ -224,6 +233,16 @@ class Project(models.Model): super(Project, self).save(*args, **kwargs) + @property + def list_of_milestones(self): + return [ + { + 'name': milestone.name, + 'finish_date': milestone.estimated_finish, + 'closed_points': milestone.closed_points + } for milestone in self.milestones.all().order_by('estimated_start') + ] + class Milestone(models.Model): uuid = models.CharField( @@ -284,6 +303,11 @@ class Milestone(models.Model): super(Milestone, self).save(*args, **kwargs) + @property + def closed_points(self): + points = [ us.points.value for us in self.user_stories.all() if us.is_closed ] + return sum(points) + class UserStory(models.Model): uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, @@ -578,11 +602,19 @@ def tasks_close_handler(sender, instance, **kwargs): user story if that is not created. """ - if instance.id and sender.objects.get(id=instance.id).status.is_closed == False and instance.status.is_closed == True: - instance.finished_date = datetime.now() - if all([task.status.is_closed for task in instance.user_story.tasks.exclude(id=instance.id)]): - instance.user_story.finish_date = datetime.now() + if instance.id: + if sender.objects.get(id=instance.id).status.is_closed == False and instance.status.is_closed == True: + instance.finished_date = datetime.now() + if all([task.status.is_closed for task in instance.user_story.tasks.exclude(id=instance.id)]): + instance.user_story.finish_date = datetime.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() # Email alerts signals handlers # TODO: temporary commented (Pending refactor) diff --git a/greenmine/scrum/serializers.py b/greenmine/scrum/serializers.py index 07daa29d..bec1ebb8 100644 --- a/greenmine/scrum/serializers.py +++ b/greenmine/scrum/serializers.py @@ -27,6 +27,7 @@ class PointsSerializer(serializers.ModelSerializer): class ProjectSerializer(serializers.ModelSerializer): tags = PickleField() + list_of_milestones = serializers.Field(source='list_of_milestones') class Meta: model = Project From bc6117d6443c5dbace00afaef8ea93ba7195c159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 7 May 2013 17:15:28 +0200 Subject: [PATCH 13/25] Adding backlog increments calculation on backend --- greenmine/scrum/models.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index c3b3c16e..03d81d9c 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -239,7 +239,9 @@ class Project(models.Model): { 'name': milestone.name, 'finish_date': milestone.estimated_finish, - 'closed_points': milestone.closed_points + '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') ] @@ -308,6 +310,39 @@ class Milestone(models.Model): points = [ us.points.value for us in self.user_stories.all() if us.is_closed ] return sum(points) + @property + def client_increment_points(self): + user_stories = UserStory.objects.filter( + created_date__gt=self.estimated_start, + created_date__lt=self.estimated_finish, + client_requirement=True, + team_requirement=False + ) + points = [ us.points.value for us in user_stories ] + return sum(points) + (self.shared_increment_points / 2) + + @property + def team_increment_points(self): + user_stories = UserStory.objects.filter( + created_date__gt=self.estimated_start, + created_date__lt=self.estimated_finish, + client_requirement=False, + team_requirement=True + ) + points = [ us.points.value for us in user_stories ] + return sum(points) + (self.shared_increment_points / 2) + + @property + def shared_increment_points(self): + user_stories = UserStory.objects.filter( + created_date__gt=self.estimated_start, + created_date__lt=self.estimated_finish, + client_requirement=True, + team_requirement=True + ) + points = [ us.points.value for us in user_stories ] + return sum(points) + class UserStory(models.Model): uuid = models.CharField(max_length=40, unique=True, null=False, blank=True, From 1a0b9c90a9577462c3565b6f10fff411c2bb2453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 8 May 2013 11:20:50 +0200 Subject: [PATCH 14/25] Fixed calculation of client an team increments --- greenmine/scrum/models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 03d81d9c..77c8eec2 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -313,8 +313,9 @@ class Milestone(models.Model): @property def client_increment_points(self): user_stories = UserStory.objects.filter( - created_date__gt=self.estimated_start, + created_date__gte=self.estimated_start, created_date__lt=self.estimated_finish, + project_id = self.project_id, client_requirement=True, team_requirement=False ) @@ -324,8 +325,9 @@ class Milestone(models.Model): @property def team_increment_points(self): user_stories = UserStory.objects.filter( - created_date__gt=self.estimated_start, + created_date__gte=self.estimated_start, created_date__lt=self.estimated_finish, + project_id = self.project_id, client_requirement=False, team_requirement=True ) @@ -335,8 +337,9 @@ class Milestone(models.Model): @property def shared_increment_points(self): user_stories = UserStory.objects.filter( - created_date__gt=self.estimated_start, + created_date__gte=self.estimated_start, created_date__lt=self.estimated_finish, + project_id = self.project_id, client_requirement=True, team_requirement=True ) From c27e5e07f307b67644bb22efc1686cd977b1856d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 May 2013 10:52:34 +0200 Subject: [PATCH 15/25] Adding issue is_closed property --- greenmine/scrum/models.py | 4 ++++ greenmine/scrum/serializers.py | 1 + 2 files changed, 5 insertions(+) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 77c8eec2..ae38e802 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -583,6 +583,10 @@ class Issue(models.Model): super(Issue, self).save(*args, **kwargs) + @property + def is_closed(self): + return self.status.is_closed + # Model related signals handlers diff --git a/greenmine/scrum/serializers.py b/greenmine/scrum/serializers.py index bec1ebb8..1950a5dd 100644 --- a/greenmine/scrum/serializers.py +++ b/greenmine/scrum/serializers.py @@ -78,6 +78,7 @@ 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 From 3e4d3034b688a58315d5d19a369d3805781bc914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 May 2013 10:57:35 +0200 Subject: [PATCH 16/25] Now greenmine session middleware is based on SessionMiddleware --- greenmine/base/middleware.py | 40 +++--------------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/greenmine/base/middleware.py b/greenmine/base/middleware.py index 26413c37..7fb410ed 100644 --- a/greenmine/base/middleware.py +++ b/greenmine/base/middleware.py @@ -6,8 +6,10 @@ from django.utils.cache import patch_vary_headers from django.utils.http import cookie_date from django.utils.importlib import import_module +from django.contrib.sessions.middleware import SessionMiddleware -class GreenmineSessionMiddleware(object): + +class GreenmineSessionMiddleware(SessionMiddleware): def process_request(self, request): engine = import_module(settings.SESSION_ENGINE) session_key = request.META.get(settings.SESSION_HEADER_NAME, None) @@ -15,42 +17,6 @@ class GreenmineSessionMiddleware(object): session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None) request.session = engine.SessionStore(session_key) - def process_response(self, request, response): - """ - If request.session was modified, or if the configuration is to save the - session every time, save the changes and set a session cookie. - """ - try: - accessed = request.session.accessed - modified = request.session.modified - except AttributeError: - pass - else: - if accessed: - patch_vary_headers(response, ('Cookie',)) - if modified or settings.SESSION_SAVE_EVERY_REQUEST: - if request.session.get_expire_at_browser_close(): - max_age = None - expires = None - else: - max_age = request.session.get_expiry_age() - expires_time = time.time() + max_age - expires = cookie_date(expires_time) - # Save the session data and refresh the client cookie. - # Skip session save for 500 responses, refs #3881. - if response.status_code != 500: - request.session.save() - response.set_cookie(settings.SESSION_COOKIE_NAME, - request.session.session_key, - max_age=max_age, - expires=expires, - domain=settings.SESSION_COOKIE_DOMAIN, - path=settings.SESSION_COOKIE_PATH, - secure=settings.SESSION_COOKIE_SECURE or None, - httponly=settings.SESSION_COOKIE_HTTPONLY or None) - return response - - COORS_ALLOWED_ORIGINS = getattr(settings, 'COORS_ALLOWED_ORIGINS', '*') COORS_ALLOWED_METHODS = getattr(settings, 'COORS_ALLOWED_METHODS', From 7d3dc0f28ac0136f4a3932fc3301f2e1a60f7f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 May 2013 14:04:40 +0200 Subject: [PATCH 17/25] Changing the usage of now to use the timezone now --- greenmine/scrum/models.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index ae38e802..27f91438 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from datetime import datetime - from django.db import models from django.utils import timezone from django.dispatch import receiver @@ -646,9 +644,9 @@ def tasks_close_handler(sender, instance, **kwargs): if instance.id: if sender.objects.get(id=instance.id).status.is_closed == False and instance.status.is_closed == True: - instance.finished_date = datetime.now() + 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 = datetime.now() + 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 From be8aeff41e69bc6f82f541b65292093c9fe3e04f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 May 2013 14:05:05 +0200 Subject: [PATCH 18/25] Adding is_iocanie flag to tasks --- greenmine/scrum/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 27f91438..7c2a3377 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -479,6 +479,8 @@ class Task(models.Model): 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')) class Meta: verbose_name = u'task' From 74d453a316109c4e9f15007a993ee57a804e2f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 10 May 2013 16:51:56 +0200 Subject: [PATCH 19/25] Better sample data --- .../scrum/management/commands/sample_data.py | 283 ++++++++++-------- requirements.txt | 1 + 2 files changed, 159 insertions(+), 125 deletions(-) diff --git a/greenmine/scrum/management/commands/sample_data.py b/greenmine/scrum/management/commands/sample_data.py index 190efb71..4186a7cc 100644 --- a/greenmine/scrum/management/commands/sample_data.py +++ b/greenmine/scrum/management/commands/sample_data.py @@ -3,6 +3,8 @@ import random import datetime +from sampledatahelper.helper import SampleDataHelper + from django.core.management.base import BaseCommand from django.db import transaction from django.utils.timezone import now @@ -31,136 +33,167 @@ subjects = [ class Command(BaseCommand): + sd = SampleDataHelper(seed=12345678901) + + @transaction.commit_on_success + def handle(self, *args, **options): + self.users = [User.objects.get(is_superuser=True)] + for x in range(10): + self.users.append(self.create_user(x)) + + role = Role.objects.all()[0] + + # projects + for x in xrange(3): + project = self.create_project(x) + + for user in self.users: + Membership.objects.create(project=project, role=role, user=user) + + start_date = now() - datetime.timedelta(35) + + # create random milestones + for y in xrange(self.sd.int(1, 5)): + end_date = start_date + datetime.timedelta(15) + milestone = self.create_milestone(project, start_date, end_date) + + # create uss asociated to milestones + for z in xrange(self.sd.int(3, 7)): + us = self.create_us(project, milestone) + + for w in xrange(self.sd.int(0,6)): + if start_date <= now() and end_date <= now(): + task = self.create_task(project, milestone, us, start_date, end_date, closed=True) + 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 + pass + + start_date = end_date + + # created unassociated uss. + for y in xrange(self.sd.int(8,15)): + us = self.create_us(project) + + # create bugs. + for y in xrange(self.sd.int(15,25)): + bug = self.create_bug(project) + + # create questions. + for y in xrange(self.sd.int(15,25)): + question = self.create_question(project) + + def create_question(self, project): + question = Question.objects.create( + project=project, + subject=self.sd.words(1,5), + content=self.sd.paragraph(), + owner=project.owner, + status=self.sd.db_object_from_queryset(QuestionStatus.objects.filter(project=project)), + tags=[], + ) + + for tag in self.sd.words(1,5).split(" "): + question.tags.append(tag) + + question.save() + + def create_bug(self, project): + bug = Issue.objects.create( + project=project, + subject=self.sd.words(1, 5), + description=self.sd.paragraph(), + owner=project.owner, + severity=self.sd.db_object_from_queryset(Severity.objects.filter(project=project)), + status=self.sd.db_object_from_queryset(IssueStatus.objects.filter(project=project)), + priority=self.sd.db_object_from_queryset(Priority.objects.filter(project=project)), + type=self.sd.db_object_from_queryset(IssueType.objects.filter(project=project)), + tags=[], + ) + + for tag in self.sd.words(1, 5).split(" "): + bug.tags.append(tag) + + bug.save() + return bug + + def create_task(self, project, milestone, us, min_date, max_date, closed=False): + task = Task( + subject="Task {0}".format(self.sd.words(3,4)), + description=self.sd.paragraph(), + project=project, + owner=self.sd.choice(self.users), + milestone=milestone, + user_story=us, + finished_date=None, + ) + if closed: + task.status = TaskStatus.objects.get(project=project, order=4) + else: + task.status = self.sd.db_object_from_queryset(TaskStatus.objects.filter(project=project)) + + if task.status.is_closed: + task.finished_date = self.sd.datetime_between(min_date, max_date) + + task.save() + return task + + def create_us(self, project, milestone=None): + us = UserStory( + subject=self.sd.words(4,9), + project=project, + owner=self.sd.choice(self.users), + description=self.sd.paragraph(), + milestone=milestone, + status=UserStoryStatus.objects.get(project=project, order=2), + points=self.sd.db_object_from_queryset(Points.objects.filter(project=project)), + tags=[] + ) + + for tag in self.sd.words().split(" "): + us.tags.append(tag) + + us.save() + return us + + def create_milestone(self, project, start_date, end_date): + milestone = Milestone( + project=project, + name='Sprint {0}'.format(start_date), + owner=project.owner, + created_date=start_date, + modified_date=start_date, + estimated_start=start_date, + estimated_finish=end_date, + order=10 + ) + milestone.save() + return milestone + + def create_project(self, counter): + # create project + project = Project( + name='Project Example 1 {0}'.format(counter), + description='Project example {0} description'.format(counter), + owner=random.choice(self.users), + public=True, + total_story_points=60, + sprints=4 + ) + + project.save() + return project + def create_user(self, counter): user = User.objects.create( - username='useri{0}'.format(counter), - first_name='user{0}'.format(counter), - email='foouser{0}@domain.com'.format(counter), + username='user-{0}-{1}'.format(counter, self.sd.word()), + first_name=self.sd.name('es'), + last_name=self.sd.surname('es'), + email=self.sd.email(), token=''.join(random.sample('abcdef0123456789', 10)), ) user.set_password('user{0}'.format(counter)) user.save() return user - - @transaction.commit_on_success - def handle(self, *args, **options): - users = [User.objects.get(is_superuser=True)] - for x in range(10): - users.append(self.create_user(x)) - - role = Role.objects.all()[0] - - # projects - for x in xrange(3): - # create project - project = Project( - name='Project Example 1 {0}'.format(x), - description='Project example {0} description'.format(x), - owner=random.choice(users), - public=True, - ) - - project.save() - - for user in users: - Membership.objects.create(project=project, role=role, user=user) - - now_date = now() - datetime.timedelta(30) - - # create random milestones - for y in xrange(2): - milestone = Milestone.objects.create( - project=project, - name='Sprint {0}'.format(y), - owner=project.owner, - created_date=now_date, - modified_date=now_date, - estimated_start=now_date, - estimated_finish=now_date + datetime.timedelta(15), - order=10 - ) - - now_date = now_date + datetime.timedelta(15) - - # create uss asociated to milestones - for z in xrange(5): - us = UserStory.objects.create( - subject=lorem_ipsum.words(random.randint(4, 9), common=False), - project=project, - owner=random.choice(users), - description=lorem_ipsum.words(30, common=False), - milestone=milestone, - status=UserStoryStatus.objects.get(project=project, order=2), - points=Points.objects.get(project=project, order=3), - tags=[] - ) - - for tag in lorem_ipsum.words(random.randint(1, 5), common=True).split(" "): - us.tags.append(tag) - - us.save() - - for w in xrange(3): - Task.objects.create( - subject="Task %s".format(w), - description=lorem_ipsum.words(30, common=False), - project=project, - owner=random.choice(users), - milestone=milestone, - user_story=us, - status=TaskStatus.objects.get(project=project, order=4), - ) - - # created unassociated uss. - for y in xrange(10): - us = UserStory.objects.create( - subject=lorem_ipsum.words(random.randint(4, 9), common=False), - status=UserStoryStatus.objects.get(project=project, order=2), - points=Points.objects.get(project=project, order=3), - owner=random.choice(users), - description=lorem_ipsum.words(30, common=False), - milestone=None, - project=project, - tags=[], - ) - - for tag in lorem_ipsum.words(random.randint(1, 5), common=True).split(" "): - us.tags.append(tag) - - us.save() - - # create bugs. - for y in xrange(20): - bug = Issue.objects.create( - project=project, - subject=lorem_ipsum.words(random.randint(1, 5), common=False), - description=lorem_ipsum.words(random.randint(1, 15), common=False), - owner=project.owner, - severity=Severity.objects.get(project=project, order=2), - status=IssueStatus.objects.get(project=project, order=4), - priority=Priority.objects.get(project=project, order=3), - type=IssueType.objects.get(project=project, order=1), - tags=[], - ) - - for tag in lorem_ipsum.words(random.randint(1, 5), common=True).split(" "): - bug.tags.append(tag) - - bug.save() - - # create questions. - for y in xrange(20): - question = Question.objects.create( - project=project, - subject=lorem_ipsum.words(random.randint(1, 5), common=False), - content=lorem_ipsum.words(random.randint(1, 15), common=False), - owner=project.owner, - status=QuestionStatus.objects.get(project=project, order=1), - tags=[], - ) - - for tag in lorem_ipsum.words(random.randint(1, 5), common=True).split(" "): - question.tags.append(tag) - - question.save() diff --git a/requirements.txt b/requirements.txt index be46d2e6..b77bcf7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ six==1.3.0 djangorestframework==2.2.5 django-filter==0.6 psycopg2==2.4.6 +django-sampledatahelper==0.0.1 From 69bd2f5746a70b514881b3906a845e364285f5b9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 May 2013 09:55:55 +0200 Subject: [PATCH 20/25] Fixed wrong issue history. --- greenmine/scrum/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/greenmine/scrum/serializers.py b/greenmine/scrum/serializers.py index 1950a5dd..83142988 100644 --- a/greenmine/scrum/serializers.py +++ b/greenmine/scrum/serializers.py @@ -117,7 +117,7 @@ class IssueSerializer(serializers.ModelSerializer): for version in reversed(list(reversion.get_for_object(obj))): if current: - issues_diff = self.get_issues_diff(version, current) + issues_diff = self.get_issues_diff(current, version) diff_list.append(issues_diff) current = version From b1b23c9f6bcc018fe43302c066b94af91548a6e5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 May 2013 19:07:37 +0200 Subject: [PATCH 21/25] Fixed wrong increment of ref value on tasks and user stories. --- greenmine/base/models.py | 14 -------------- greenmine/base/utils/slug.py | 3 --- greenmine/scrum/models.py | 25 +++++++++++++------------ 3 files changed, 13 insertions(+), 29 deletions(-) diff --git a/greenmine/base/models.py b/greenmine/base/models.py index 5d2f54f8..6df4c971 100644 --- a/greenmine/base/models.py +++ b/greenmine/base/models.py @@ -30,20 +30,6 @@ def attach_uuid(sender, instance, **kwargs): instance.uuid = unicode(uuid.uuid1()) -# Centraliced reference assignation. -@receiver(signals.pre_save, sender=Task) -@receiver(signals.pre_save, sender=UserStory) -def attach_unique_reference(sender, instance, **kwargs): - project = Project.objects.select_for_update().filter(pk=instance.project_id).get() - if isinstance(instance, Task): - project.last_task_ref += 1 - instance.ref = project.last_task_ref - else: - project.last_us_ref += 1 - instance.ref = project.last_us_ref - - project.save() - class User(AbstractUser): color = models.CharField(max_length=9, null=False, blank=False, default="#669933", diff --git a/greenmine/base/utils/slug.py b/greenmine/base/utils/slug.py index 71b594a6..3e73ff7f 100644 --- a/greenmine/base/utils/slug.py +++ b/greenmine/base/utils/slug.py @@ -25,9 +25,6 @@ def slugify_uniquely(value, model, slugfield="slug"): def ref_uniquely(p, seq_field, model, field='ref'): - """ - Returns a unique reference code based on base64 and time. - """ project = p.__class__.objects.select_for_update().get(pk=p.pk) ref = getattr(project, seq_field) + 1 diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index 7c2a3377..c0383f38 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -505,9 +505,6 @@ class Task(models.Model): if self.id: self.modified_date = timezone.now() - if not self.ref: - self.ref = ref_uniquely(self.project, 'last_task_ref', self.__class__) - super(Task, self).save(*args, **kwargs) @@ -578,9 +575,6 @@ class Issue(models.Model): if self.id: self.modified_date = timezone.now() - if not self.ref: - self.ref = ref_uniquely(self.project, 'last_issue_ref', self.__class__) - super(Issue, self).save(*args, **kwargs) @property @@ -626,13 +620,20 @@ def project_post_save(sender, instance, created, **kwargs): IssueType.objects.create(project=instance, name=name, order=order) -@receiver(models.signals.pre_save, sender=UserStory, dispatch_uid='user_story_ref_handler') -def user_story_ref_handler(sender, instance, **kwargs): - """ - Automatically assignes a seguent reference code to a - user story if that is not created. - """ +@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__) From f2bf3f457cba9e00a5415b92bde47ed244ece510 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 May 2013 19:08:01 +0200 Subject: [PATCH 22/25] Add tasks attachments api views. --- greenmine/base/api.py | 1 + greenmine/scrum/admin.py | 2 +- greenmine/scrum/api.py | 33 ++++++++++++++++++++++++-- greenmine/scrum/serializers.py | 42 ++++++++++++++++++++++++++++++++++ greenmine/scrum/urls.py | 3 ++- 5 files changed, 77 insertions(+), 4 deletions(-) diff --git a/greenmine/base/api.py b/greenmine/base/api.py index e831dcd8..255bc264 100644 --- a/greenmine/base/api.py +++ b/greenmine/base/api.py @@ -40,6 +40,7 @@ class ApiRoot(APIView): 'issues': reverse('issues-list', request=request, format=format), 'tasks': reverse('tasks-list', request=request, format=format), 'tasks/statuses': reverse('tasks-status-list', request=request, format=format), + 'tasks/attachments': reverse('tasks-attachment-list', request=request, format=format), 'severities': reverse('severity-list', request=request, format=format), 'priorities': reverse('priority-list', request=request, format=format), 'documents': reverse('document-list', request=request, format=format), diff --git a/greenmine/scrum/admin.py b/greenmine/scrum/admin.py index 2cfef0eb..00008570 100644 --- a/greenmine/scrum/admin.py +++ b/greenmine/scrum/admin.py @@ -58,7 +58,7 @@ admin.site.register(models.Attachment, AttachmentAdmin) class TaskAdmin(reversion.VersionAdmin): - list_display = ["subject", "user_story", "milestone", "project", "user_story_id"] + list_display = ["subject", "ref", "user_story", "milestone", "project", "user_story_id"] list_filter = ["user_story", "milestone", "project"] def user_story_id(self, instance): diff --git a/greenmine/scrum/api.py b/greenmine/scrum/api.py index 2ace56c3..376b7eef 100644 --- a/greenmine/scrum/api.py +++ b/greenmine/scrum/api.py @@ -102,7 +102,7 @@ class UserStoryDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = (IsAuthenticated, UserStoryDetailPermission,) -class IssuesAttachmentFilter(django_filters.FilterSet): +class AttachmentFilter(django_filters.FilterSet): class Meta: model = Attachment fields = ['project', 'object_id'] @@ -112,7 +112,7 @@ class IssuesAttachmentList(generics.ListCreateAPIView): model = Attachment serializer_class = AttachmentSerializer permission_classes = (IsAuthenticated,) - filter_class = IssuesAttachmentFilter + filter_class = AttachmentFilter def get_queryset(self): ct = ContentType.objects.get_for_model(Issue) @@ -131,6 +131,29 @@ class IssuesAttachmentDetail(generics.RetrieveUpdateDestroyAPIView): permission_classes = (IsAuthenticated, AttachmentDetailPermission,) +class TasksAttachmentList(generics.ListCreateAPIView): + model = Attachment + serializer_class = AttachmentSerializer + permission_classes = (IsAuthenticated,) + filter_class = AttachmentFilter + + def get_queryset(self): + ct = ContentType.objects.get_for_model(Task) + return super(TasksAttachmentList, self).get_queryset()\ + .filter(project__members=self.request.user)\ + .filter(content_type=ct) + + def pre_save(self, obj): + obj.content_type = ContentType.objects.get_for_model(Task) + obj.owner = self.request.user + + +class TasksAttachmentDetail(generics.RetrieveUpdateDestroyAPIView): + model = Attachment + serializer_class = AttachmentSerializer + permission_classes = (IsAuthenticated, AttachmentDetailPermission,) + + class TaskList(generics.ListCreateAPIView): model = Task serializer_class = TaskSerializer @@ -150,6 +173,12 @@ class TaskDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = TaskSerializer permission_classes = (IsAuthenticated, TaskDetailPermission,) + 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']) + class IssueList(generics.ListCreateAPIView): model = Issue diff --git a/greenmine/scrum/serializers.py b/greenmine/scrum/serializers.py index 83142988..2493be45 100644 --- a/greenmine/scrum/serializers.py +++ b/greenmine/scrum/serializers.py @@ -68,11 +68,53 @@ class AttachmentSerializer(serializers.ModelSerializer): 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_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 IssueSerializer(serializers.ModelSerializer): tags = PickleField() diff --git a/greenmine/scrum/urls.py b/greenmine/scrum/urls.py index 164552c0..5c15cbc6 100644 --- a/greenmine/scrum/urls.py +++ b/greenmine/scrum/urls.py @@ -27,6 +27,8 @@ urlpatterns = format_suffix_patterns(patterns('', url(r'^issues/types/(?P[0-9]+)/$', api.IssueTypeDetail.as_view(), name='issues-type-detail'), url(r'^tasks/$', api.TaskList.as_view(), name='tasks-list'), url(r'^tasks/(?P[0-9]+)/$', api.TaskDetail.as_view(), name='tasks-detail'), + url(r'^tasks/attachments/$', api.TasksAttachmentList.as_view(), name='tasks-attachment-list'), + url(r'^tasks/attachments/(?P[0-9]+)/$', api.TasksAttachmentDetail.as_view(), name='tasks-attachment-detail'), url(r'^severities/$', api.SeverityList.as_view(), name='severity-list'), url(r'^severities/(?P[0-9]+)/$', api.SeverityDetail.as_view(), name='severity-detail'), url(r'^tasks/statuses/$', api.TaskStatusList.as_view(), name='tasks-status-list'), @@ -34,4 +36,3 @@ urlpatterns = format_suffix_patterns(patterns('', url(r'^priorities/$', api.PriorityList.as_view(), name='priority-list'), url(r'^priorities/(?P[0-9]+)/$', api.PriorityDetail.as_view(), name='priority-detail'), )) - From ec3a211d7b411861a19fef4a812cce3f3bfbc265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Sun, 2 Jun 2013 10:45:11 +0200 Subject: [PATCH 23/25] Improving a little the sample_data --- greenmine/scrum/management/commands/sample_data.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/greenmine/scrum/management/commands/sample_data.py b/greenmine/scrum/management/commands/sample_data.py index 4186a7cc..8447d05a 100644 --- a/greenmine/scrum/management/commands/sample_data.py +++ b/greenmine/scrum/management/commands/sample_data.py @@ -146,10 +146,13 @@ class Command(BaseCommand): owner=self.sd.choice(self.users), description=self.sd.paragraph(), milestone=milestone, - status=UserStoryStatus.objects.get(project=project, order=2), - points=self.sd.db_object_from_queryset(Points.objects.filter(project=project)), + status=self.sd.db_object_from_queryset(UserStoryStatus.objects.filter(project=project)), tags=[] ) + if milestone: + us.points=self.sd.db_object_from_queryset(Points.objects.filter(project=project).exclude(order=0)) + else: + us.points=self.sd.db_object_from_queryset(Points.objects.filter(project=project)) for tag in self.sd.words().split(" "): us.tags.append(tag) @@ -178,8 +181,8 @@ class Command(BaseCommand): description='Project example {0} description'.format(counter), owner=random.choice(self.users), public=True, - total_story_points=60, - sprints=4 + total_story_points=self.sd.int(100, 150), + sprints=self.sd.int(5,10) ) project.save() From 621a636da2422efab875737e948536403b5b9a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Sun, 2 Jun 2013 10:51:22 +0200 Subject: [PATCH 24/25] Fix GitHub issue #1: Now project owner is null=False and blank=False --- greenmine/scrum/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/greenmine/scrum/models.py b/greenmine/scrum/models.py index c0383f38..23638756 100644 --- a/greenmine/scrum/models.py +++ b/greenmine/scrum/models.py @@ -189,7 +189,7 @@ class Project(models.Model): verbose_name=_('created date')) modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, verbose_name=_('modified date')) - owner = models.ForeignKey('base.User', null=False, blank=True, + owner = models.ForeignKey('base.User', null=False, blank=False, related_name='owned_projects', verbose_name=_('owner')) members = models.ManyToManyField('base.User', related_name='projects', through='Membership', From 328f2bfcf0501fbf513b9265bf56fcea75efa25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20G=C3=B3mez?= Date: Sun, 2 Jun 2013 10:57:37 +0200 Subject: [PATCH 25/25] Return also the projects owned by the logged in user in the projects API endpoint --- greenmine/scrum/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/greenmine/scrum/api.py b/greenmine/scrum/api.py index 376b7eef..9643d27e 100644 --- a/greenmine/scrum/api.py +++ b/greenmine/scrum/api.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- -import django_filters +from django.db.models import Q +import django_filters from rest_framework import generics from rest_framework.permissions import IsAuthenticated @@ -52,7 +53,9 @@ class ProjectList(generics.ListCreateAPIView): permission_classes = (IsAuthenticated,) def get_queryset(self): - return self.model.objects.filter(members=self.request.user) + return self.model.objects.filter( + Q(owner=self.request.user) | Q(members=self.request.user) + ) def pre_save(self, obj): obj.owner = self.request.user