Merge branch 'master' into notifications

Conflicts:
	greenmine/base/models.py
	greenmine/scrum/models.py
remotes/origin/enhancement/email-actions
Andrés Moya 2013-07-15 11:09:23 +02:00
commit 59b9e65fe5
41 changed files with 1083 additions and 641 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
.*.sw* .*.sw*
*.log *.log
greenmine/search
greenmine/settings/local.py greenmine/settings/local.py
database.sqlite database.sqlite
logs logs

View File

@ -13,9 +13,13 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework import status from rest_framework import status
from rest_framework import generics from rest_framework import generics
from haystack.query import SearchQuerySet
from greenmine.base.serializers import LoginSerializer, UserLogged, UserSerializer, RoleSerializer from greenmine.base.serializers import LoginSerializer, UserLogged, UserSerializer, RoleSerializer
from greenmine.base.serializers import SearchSerializer
from greenmine.base.models import User, Role from greenmine.base.models import User, Role
from greenmine.scrum import models from greenmine.scrum import models
from django.conf import settings
import django_filters import django_filters
@ -36,14 +40,15 @@ class ApiRoot(APIView):
'issues': reverse('issues-list', request=request, format=format), 'issues': reverse('issues-list', request=request, format=format),
'tasks': reverse('tasks-list', request=request, format=format), 'tasks': reverse('tasks-list', request=request, format=format),
'tasks/statuses': reverse('tasks-status-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), 'severities': reverse('severity-list', request=request, format=format),
'priorities': reverse('priority-list', request=request, format=format), 'priorities': reverse('priority-list', request=request, format=format),
'documents': reverse('document-list', request=request, format=format), 'documents': reverse('document-list', request=request, format=format),
'questions': reverse('question-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), 'wiki/pages': reverse('wiki-page-list', request=request, format=format),
'users': reverse('user-list', request=request, format=format), 'users': reverse('user-list', request=request, format=format),
'roles': reverse('user-roles', request=request, format=format), 'roles': reverse('user-roles', request=request, format=format),
'search': reverse('search', request=request, format=format),
}) })
@ -65,11 +70,9 @@ class RoleList(generics.ListCreateAPIView):
class UserFilter(django_filters.FilterSet): class UserFilter(django_filters.FilterSet):
is_active = django_filters.BooleanFilter(name="is_active")
class Meta: class Meta:
model = User model = User
fields = ['is_active',] fields = ['is_active']
class UserList(generics.ListCreateAPIView): class UserList(generics.ListCreateAPIView):
@ -93,6 +96,12 @@ class UserList(generics.ListCreateAPIView):
pass pass
class UserDetail(generics.RetrieveUpdateDestroyAPIView):
model = User
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
class Login(APIView): class Login(APIView):
def post(self, request, format=None): def post(self, request, format=None):
username = request.DATA.get('username', None) username = request.DATA.get('username', None)
@ -129,3 +138,17 @@ class Logout(APIView):
def post(self, request, format=None): def post(self, request, format=None):
logout(request) logout(request)
return Response() 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)

View File

@ -134,16 +134,41 @@
"questions", "questions",
"question" "question"
], ],
[
"can_assign_question_to_myself",
"questions",
"question"
],
[
"can_assign_question_to_other",
"questions",
"question"
],
[
"can_change_assigned_question",
"questions",
"question"
],
[ [
"can_change_owned_question", "can_change_owned_question",
"questions", "questions",
"question" "question"
], ],
[
"can_change_question_state",
"questions",
"question"
],
[ [
"can_reply_question", "can_reply_question",
"questions", "questions",
"question" "question"
], ],
[
"can_view_question",
"questions",
"question"
],
[ [
"change_question", "change_question",
"questions", "questions",
@ -155,19 +180,34 @@
"question" "question"
], ],
[ [
"add_questionresponse", "add_questionstatus",
"questions", "questions",
"questionresponse" "questionstatus"
], ],
[ [
"change_questionresponse", "change_questionstatus",
"questions", "questions",
"questionresponse" "questionstatus"
], ],
[ [
"delete_questionresponse", "delete_questionstatus",
"questions", "questions",
"questionresponse" "questionstatus"
],
[
"add_attachment",
"scrum",
"attachment"
],
[
"change_attachment",
"scrum",
"attachment"
],
[
"delete_attachment",
"scrum",
"attachment"
], ],
[ [
"add_issue", "add_issue",

View File

@ -6,8 +6,10 @@ from django.utils.cache import patch_vary_headers
from django.utils.http import cookie_date from django.utils.http import cookie_date
from django.utils.importlib import import_module 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): def process_request(self, request):
engine = import_module(settings.SESSION_ENGINE) engine = import_module(settings.SESSION_ENGINE)
session_key = request.META.get(settings.SESSION_HEADER_NAME, None) 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) session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
request.session = engine.SessionStore(session_key) 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_ORIGINS = getattr(settings, 'COORS_ALLOWED_ORIGINS', '*')
COORS_ALLOWED_METHODS = getattr(settings, 'COORS_ALLOWED_METHODS', COORS_ALLOWED_METHODS = getattr(settings, 'COORS_ALLOWED_METHODS',

View File

@ -31,23 +31,9 @@ def attach_uuid(sender, instance, **kwargs):
instance.uuid = unicode(uuid.uuid1()) 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, WatcherMixin): class User(AbstractUser, WatcherMixin):
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')) verbose_name=_('color'))
description = models.TextField(null=False, blank=True, description = models.TextField(null=False, blank=True,
verbose_name=_('description')) verbose_name=_('description'))

View File

@ -54,12 +54,39 @@ class LoginSerializer(serializers.Serializer):
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
projects = serializers.SerializerMethodField('get_projects')
class Meta: class Meta:
model = User model = User
fields = ('id', 'username', 'first_name', 'last_name', 'color', 'is_active',) fields = ('id', 'username', 'first_name', 'last_name', 'email', 'color', 'description',
'default_language', 'default_timezone', 'is_active', 'photo', 'projects')
def get_projects(self, obj):
return [x.id for x in obj.projects.all()]
class RoleSerializer(serializers.ModelSerializer): class RoleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Role model = Role
fields = ('id', 'name', 'slug', 'permissions',) 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

View File

@ -9,6 +9,8 @@ urlpatterns = format_suffix_patterns(patterns('',
url(r'^auth/login/$', api.Login.as_view(), name='login'), url(r'^auth/login/$', api.Login.as_view(), name='login'),
url(r'^auth/logout/$', api.Logout.as_view(), name='logout'), url(r'^auth/logout/$', api.Logout.as_view(), name='logout'),
url(r'^users/$', api.UserList.as_view(), name="user-list"), url(r'^users/$', api.UserList.as_view(), name="user-list"),
url(r'^users/(?P<pk>[0-9]+)/$', api.UserDetail.as_view(), name="user-detail"),
url(r'^roles/$', api.RoleList.as_view(), name="user-roles"), 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'), url(r'^$', api.ApiRoot.as_view(), name='api_root'),
)) ))

View File

@ -25,9 +25,6 @@ def slugify_uniquely(value, model, slugfield="slug"):
def ref_uniquely(p, seq_field, model, field='ref'): 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) project = p.__class__.objects.select_for_update().get(pk=p.pk)
ref = getattr(project, seq_field) + 1 ref = getattr(project, seq_field) + 1

View File

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib import admin from django.contrib import admin
from greenmine.documents.models import Document from . import models
class DocumentAdmin(admin.ModelAdmin): class DocumentAdmin(admin.ModelAdmin):
list_display = ["title", "project", "owner"] list_display = ["title", "project", "owner"]
admin.site.register(Document, DocumentAdmin) admin.site.register(models.Document, DocumentAdmin)

View File

@ -1,19 +1,21 @@
# -*- coding: utf-8 -*-
from rest_framework import generics from rest_framework import generics
from greenmine.documents.serializers import DocumentSerializer from . import serializers
from greenmine.documents.models import Document from . import models
from greenmine.documents.permissions import DocumentDetailPermission from . import permissions
class DocumentList(generics.ListCreateAPIView): class DocumentList(generics.ListCreateAPIView):
model = Document model = models.Document
serializer_class = DocumentSerializer serializer_class = serializers.DocumentSerializer
def get_queryset(self): 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): class DocumentDetail(generics.RetrieveUpdateDestroyAPIView):
model = Document model = models.Document
serializer_class = DocumentSerializer serializer_class = serializers.DocumentSerializer
permission_classes = (DocumentDetailPermission,) permission_classes = (permissions.DocumentDetailPermission,)

View File

@ -1,4 +1,5 @@
# -* coding: utf-8 -*- # -* coding: utf-8 -*-
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View File

@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
from greenmine.base.permissions import BaseDetailPermission from greenmine.base.permissions import BaseDetailPermission
class DocumentDetailPermission(BaseDetailPermission): class DocumentDetailPermission(BaseDetailPermission):
get_permission = "can_view_document" get_permission = "can_view_document"
put_permission = "can_change_document" put_permission = "can_change_document"

View File

@ -1,13 +1,17 @@
# -* coding: utf-8 -*- # -* coding: utf-8 -*-
from haystack import indexes from haystack import indexes
from .models import Document
from . import models
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') 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): def get_model(self):
return Document return models.Document
def index_queryset(self): def index_queryset(self, using=None):
return self.get_model().objects.all() return self.get_model().objects.all()

View File

@ -1,8 +1,11 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers from rest_framework import serializers
from greenmine.documents.models import Document from . import models
class DocumentSerializer(serializers.ModelSerializer): class DocumentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Document model = models.Document
fields = () fields = ()

View File

@ -1,7 +1,10 @@
# -*- coding: utf-8 -*-
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
from greenmine.documents import api from . import api
urlpatterns = format_suffix_patterns(patterns('', urlpatterns = format_suffix_patterns(patterns('',
url(r'^documents/$', api.DocumentList.as_view(), name='document-list'), url(r'^documents/$', api.DocumentList.as_view(), name='document-list'),

View File

@ -1,16 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib import admin from django.contrib import admin
from greenmine.questions.models import Question, QuestionResponse from . import models
import reversion
class QuestionAdmin(admin.ModelAdmin): class QuestionAdmin(reversion.VersionAdmin):
list_display = ["subject", "project", "owner"] list_display = ["subject", "project", "owner"]
admin.site.register(Question, QuestionAdmin) admin.site.register(models.Question, QuestionAdmin)
class QuestionResponseAdmin(admin.ModelAdmin): class QuestionStatusAdmin(admin.ModelAdmin):
list_display = ["id", "question", "owner"] list_display = ["name", "order", "is_closed", "project"]
admin.site.register(QuestionResponse, QuestionResponseAdmin) admin.site.register(models.QuestionStatus, QuestionStatusAdmin)

View File

@ -1,32 +1,36 @@
# -*- coding: utf-8 -*-
from rest_framework import generics from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from greenmine.questions.serializers import QuestionSerializer, QuestionResponseSerializer from . import serializers
from greenmine.questions.models import Question, QuestionResponse from . import models
from greenmine.questions.permissions import QuestionDetailPermission, QuestionResponseDetailPermission from . import permissions
import reversion
class QuestionList(generics.ListCreateAPIView): class QuestionList(generics.ListCreateAPIView):
model = Question model = models.Question
serializer_class = QuestionSerializer serializer_class = serializers.QuestionSerializer
filter_fields = ('project',)
permission_classes = (IsAuthenticated,)
def get_queryset(self): 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): class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
model = Question model = models.Question
serializer_class = QuestionSerializer serializer_class = serializers.QuestionSerializer
permission_classes = (QuestionDetailPermission,) permission_classes = (IsAuthenticated, permissions.QuestionDetailPermission,)
def post_save(self, obj, created=False):
class QuestionResponseList(generics.ListCreateAPIView): with reversion.create_revision():
model = QuestionResponse if "comment" in self.request.DATA:
serializer_class = QuestionResponseSerializer # Update the comment in the last version
reversion.set_comment(self.request.DATA['comment'])
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,)

View File

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

View File

@ -1,15 +1,51 @@
# -*- coding: utf-8 -*-
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ 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.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): 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')) 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, content = models.TextField(null=False, blank=True,
verbose_name=_('content')) verbose_name=_('content'))
closed = models.BooleanField(default=False, null=False, blank=True, closed = models.BooleanField(default=False, null=False, blank=True,
@ -18,66 +54,66 @@ class Question(models.Model):
upload_to='messages', upload_to='messages',
verbose_name=_('attached_file')) verbose_name=_('attached_file'))
project = models.ForeignKey('scrum.Project', null=False, blank=False, project = models.ForeignKey('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, milestone = models.ForeignKey('scrum.Milestone', null=True, blank=True, default=None,
related_name='questions', related_name='questions',
verbose_name=_('milestone')) 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', related_name='questions_assigned_to_me',
verbose_name=_('assigned_to')) verbose_name=_('assigned_to'))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_('created date')) verbose_name=_('created date'))
modified_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, modified_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_('modified date')) 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, watchers = models.ManyToManyField('base.User', null=True, blank=True,
related_name='watched_questions', related_name='watched_questions',
verbose_name=_('watchers')) verbose_name=_('watchers'))
tags = DictField(null=False, blank=True, tags = PickledObjectField(null=False, blank=True,
verbose_name=_('tags')) verbose_name=_('tags'))
class Meta: class Meta:
verbose_name = u'question' verbose_name = u'question'
verbose_name_plural = u'questions' verbose_name_plural = u'questions'
ordering = ['project', 'subject', 'id'] ordering = ['project', 'subject', 'id']
#TODO: permissions
permissions = ( permissions = (
('can_reply_question', 'Can reply questions'), ('can_reply_question', 'Can reply questions'),
('can_change_owned_question', 'Can modify owned 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): def __unicode__(self):
return self.subject return self.subject
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.slug: if self.id:
self.slug = slugify_uniquely(self.subject, self.__class__) 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) super(Question, self).save(*args, **kwargs)
class QuestionResponse(models.Model): # Model related signals handlers
content = models.TextField(null=False, blank=False, @receiver(models.signals.post_save, sender=Project, dispatch_uid='project_post_save_add_question_states')
verbose_name=_('content')) def project_post_save_add_question_states(sender, instance, created, **kwargs):
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False, """
verbose_name=_('created date')) Create all project model depences on project is
modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, created.
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: if not created:
verbose_name = u'question response' return
verbose_name_plural = u'question responses'
ordering = ['question', 'created_date']
def __unicode__(self):
return u'{0} - response {1}'.format(unicode(self.question), self.id)
# Populate new project dependen default data
for order, name, is_closed in QUESTION_STATUS:
QuestionStatus.objects.create(name=name, order=order,
is_closed=is_closed, project=instance)

View File

@ -1,15 +1,13 @@
# -*- coding: utf-8 -*-
from greenmine.base.permissions import BaseDetailPermission from greenmine.base.permissions import BaseDetailPermission
class QuestionDetailPermission(BaseDetailPermission): class QuestionDetailPermission(BaseDetailPermission):
get_permission = "can_view_question" get_permission = "can_view_question"
put_permission = "can_change_question" put_permission = "change_question"
delete_permission = "can_delete_question" patch_permission = "change_question"
delete_permission = "delete_question"
safe_methods = ['HEAD', 'OPTIONS'] 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 = []

View File

@ -1,13 +1,17 @@
# -* coding: utf-8 -*- # -* coding: utf-8 -*-
from haystack import indexes from haystack import indexes
from .models import Question
from . import models
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') 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): def get_model(self):
return Question return models.Question
def index_queryset(self): def index_queryset(self, using=None):
return self.get_model().objects.all() return self.get_model().objects.all()

View File

@ -1,15 +1,59 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers from rest_framework import serializers
from greenmine.questions.models import Question, QuestionResponse import reversion
from greenmine.scrum.serializers import PickleField
from . import models
class QuestionSerializer(serializers.ModelSerializer): class QuestionSerializer(serializers.ModelSerializer):
tags = PickleField()
comment = serializers.SerializerMethodField('get_comment')
history = serializers.SerializerMethodField('get_history')
class Meta: class Meta:
model = Question model = models.Question
fields = () fields = ()
def get_comment(self, obj):
return ''
class QuestionResponseSerializer(serializers.ModelSerializer): def get_questions_diff(self, old_question_version, new_question_version):
class Meta: old_obj = old_question_version.field_dict
model = QuestionResponse new_obj = new_question_version.field_dict
fields = ()
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

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
from greenmine.questions import api from . import api
urlpatterns = format_suffix_patterns(patterns('', urlpatterns = format_suffix_patterns(patterns('',
url(r'^questions/$', api.QuestionList.as_view(), name='question-list'), url(r'^questions/$', api.QuestionList.as_view(), name='question-list'),
url(r'^questions/(?P<pk>[0-9]+)/$', api.QuestionDetail.as_view(), name='question-detail'), url(r'^questions/(?P<pk>[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<pk>[0-9]+)/$', api.QuestionResponseDetail.as_view(), name='question-response-detail'),
)) ))

View File

@ -30,6 +30,7 @@ class UserStoryInline(admin.TabularInline):
else: else:
return models.UserStory.objects.none() return models.UserStory.objects.none()
class ProjectAdmin(reversion.VersionAdmin): class ProjectAdmin(reversion.VersionAdmin):
list_display = ["name", "owner"] list_display = ["name", "owner"]
inlines = [MembershipInline, MilestoneInline, UserStoryInline] inlines = [MembershipInline, MilestoneInline, UserStoryInline]
@ -57,7 +58,7 @@ admin.site.register(models.Attachment, AttachmentAdmin)
class TaskAdmin(reversion.VersionAdmin): 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"] list_filter = ["user_story", "milestone", "project"]
def user_story_id(self, instance): def user_story_id(self, instance):
@ -67,30 +68,39 @@ class MembershipAdmin(admin.ModelAdmin):
list_display = ['project', 'role', 'user'] list_display = ['project', 'role', 'user']
list_filter = ['project', 'role'] list_filter = ['project', 'role']
class IssueAdmin(reversion.VersionAdmin): class IssueAdmin(reversion.VersionAdmin):
list_display = ["subject", "type"] list_display = ["subject", "type"]
class SeverityAdmin(admin.ModelAdmin): class SeverityAdmin(admin.ModelAdmin):
list_display = ["name", "order", "project"] list_display = ["name", "order", "project"]
class PriorityAdmin(admin.ModelAdmin): class PriorityAdmin(admin.ModelAdmin):
list_display = ["name", "order", "project"] list_display = ["name", "order", "project"]
class PointsAdmin(admin.ModelAdmin): class PointsAdmin(admin.ModelAdmin):
list_display = ["name", "order", "project"] list_display = ["name", "order", "project"]
class IssueTypeAdmin(admin.ModelAdmin): class IssueTypeAdmin(admin.ModelAdmin):
list_display = ["name", "order", "project"] list_display = ["name", "order", "project"]
class IssueStatusAdmin(admin.ModelAdmin): class IssueStatusAdmin(admin.ModelAdmin):
list_display = ["name", "order", "is_closed", "project"] list_display = ["name", "order", "is_closed", "project"]
class TaskStatusAdmin(admin.ModelAdmin): class TaskStatusAdmin(admin.ModelAdmin):
list_display = ["name", "order", "is_closed", "project"] list_display = ["name", "order", "is_closed", "project"]
class UserStoryStatusAdmin(admin.ModelAdmin): class UserStoryStatusAdmin(admin.ModelAdmin):
list_display = ["name", "order", "is_closed", "project"] list_display = ["name", "order", "is_closed", "project"]
admin.site.register(models.Task, TaskAdmin) admin.site.register(models.Task, TaskAdmin)
admin.site.register(models.Issue, IssueAdmin) admin.site.register(models.Issue, IssueAdmin)

View File

@ -1,5 +1,8 @@
import django_filters # -*- coding: utf-8 -*-
from django.db.models import Q
import django_filters
from rest_framework import generics from rest_framework import generics
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@ -50,7 +53,9 @@ class ProjectList(generics.ListCreateAPIView):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
def get_queryset(self): 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): def pre_save(self, obj):
obj.owner = self.request.user obj.owner = self.request.user
@ -100,7 +105,7 @@ class UserStoryDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsAuthenticated, UserStoryDetailPermission,) permission_classes = (IsAuthenticated, UserStoryDetailPermission,)
class IssuesAttachmentFilter(django_filters.FilterSet): class AttachmentFilter(django_filters.FilterSet):
class Meta: class Meta:
model = Attachment model = Attachment
fields = ['project', 'object_id'] fields = ['project', 'object_id']
@ -110,7 +115,7 @@ class IssuesAttachmentList(generics.ListCreateAPIView):
model = Attachment model = Attachment
serializer_class = AttachmentSerializer serializer_class = AttachmentSerializer
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
filter_class = IssuesAttachmentFilter filter_class = AttachmentFilter
def get_queryset(self): def get_queryset(self):
ct = ContentType.objects.get_for_model(Issue) ct = ContentType.objects.get_for_model(Issue)
@ -129,6 +134,29 @@ class IssuesAttachmentDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsAuthenticated, AttachmentDetailPermission,) 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): class TaskList(generics.ListCreateAPIView):
model = Task model = Task
serializer_class = TaskSerializer serializer_class = TaskSerializer
@ -140,6 +168,7 @@ class TaskList(generics.ListCreateAPIView):
def pre_save(self, obj): def pre_save(self, obj):
obj.owner = self.request.user obj.owner = self.request.user
obj.milestone = obj.user_story.milestone
class TaskDetail(generics.RetrieveUpdateDestroyAPIView): class TaskDetail(generics.RetrieveUpdateDestroyAPIView):
@ -147,6 +176,12 @@ class TaskDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = TaskSerializer serializer_class = TaskSerializer
permission_classes = (IsAuthenticated, TaskDetailPermission,) 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): class IssueList(generics.ListCreateAPIView):
model = Issue model = Issue

View File

@ -2,6 +2,7 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
PRIORITY_CHOICES = ( PRIORITY_CHOICES = (
(1, _(u'Low')), (1, _(u'Low')),
(3, _(u'Normal')), (3, _(u'Normal')),

View File

@ -3,6 +3,8 @@
import random import random
import datetime import datetime
from sampledatahelper.helper import SampleDataHelper
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from django.utils.timezone import now from django.utils.timezone import now
@ -11,6 +13,8 @@ from django.contrib.webdesign import lorem_ipsum
from greenmine.base.models import User, Role from greenmine.base.models import User, Role
from greenmine.scrum.models import * from greenmine.scrum.models import *
from greenmine.questions.models import *
subjects = [ subjects = [
"Fixing templates for Django 1.2.", "Fixing templates for Django 1.2.",
@ -29,122 +33,170 @@ subjects = [
class Command(BaseCommand): class Command(BaseCommand):
def create_user(self, counter): sd = SampleDataHelper(seed=12345678901)
user = User.objects.create(
username='user%d' % (counter),
first_name='user%d' % (counter),
email='foouser%d@domain.com' % (counter),
token=''.join(random.sample('abcdef0123456789', 10)),
)
user.set_password('user%d' % (counter))
user.save()
return user
@transaction.commit_on_success @transaction.commit_on_success
def handle(self, *args, **options): def handle(self, *args, **options):
users = [User.objects.get(is_superuser=True)] self.users = [User.objects.get(is_superuser=True)]
for x in range(10): for x in range(10):
users.append(self.create_user(x)) self.users.append(self.create_user(x))
role = Role.objects.all()[0] role = Role.objects.all()[0]
# projects # projects
for x in xrange(3): for x in xrange(3):
# create project project = self.create_project(x)
project = Project(
name='Project Example 1 %s' % (x),
description='Project example %s description' % (x),
owner=random.choice(users),
public=True,
)
project.save() for user in self.users:
for user in users:
Membership.objects.create(project=project, role=role, user=user) Membership.objects.create(project=project, role=role, user=user)
now_date = now() - datetime.timedelta(30) start_date = now() - datetime.timedelta(35)
# create random milestones # create random milestones
for y in xrange(2): for y in xrange(self.sd.int(1, 5)):
milestone = Milestone.objects.create( end_date = start_date + datetime.timedelta(15)
project=project, milestone = self.create_milestone(project, start_date, end_date)
name='Sprint %s' % (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 # create uss asociated to milestones
for z in xrange(5): for z in xrange(self.sd.int(3, 7)):
us = UserStory.objects.create( us = self.create_us(project, milestone)
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(" "): for w in xrange(self.sd.int(0,6)):
us.tags.append(tag) 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
us.save() start_date = end_date
for w in xrange(3):
Task.objects.create(
subject="Task %s" % (w),
description=lorem_ipsum.words(30, common=False),
project=project,
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. # created unassociated uss.
for y in xrange(10): for y in xrange(self.sd.int(8,15)):
us = UserStory.objects.create( us = self.create_us(project)
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. # create bugs.
for y in xrange(20): for y in xrange(self.sd.int(15,25)):
bug = Issue.objects.create( bug = self.create_bug(project)
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(" "): # create questions.
bug.tags.append(tag) for y in xrange(self.sd.int(15,25)):
question = self.create_question(project)
bug.save() 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=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)
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=self.sd.int(100, 150),
sprints=self.sd.int(5,10)
)
project.save()
return project
def create_user(self, counter):
user = User.objects.create(
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

View File

@ -158,6 +158,15 @@ class Points(models.Model):
def __unicode__(self): def __unicode__(self):
return u'project {0} - {1}'.format(self.project_id, self.name) 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): class Membership(models.Model):
user = models.ForeignKey('base.User', null=False, blank=False) user = models.ForeignKey('base.User', null=False, blank=False)
@ -181,7 +190,7 @@ class Project(models.Model, WatchedMixin):
verbose_name=_('created date')) verbose_name=_('created date'))
modified_date = models.DateTimeField(auto_now=True, null=False, blank=False, modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
verbose_name=_('modified date')) 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', related_name='owned_projects',
verbose_name=_('owner')) verbose_name=_('owner'))
members = models.ManyToManyField('base.User', related_name='projects', through='Membership', members = models.ManyToManyField('base.User', related_name='projects', through='Membership',
@ -240,20 +249,42 @@ class Project(models.Model, WatchedMixin):
'tags': self.tags, 'tags': self.tags,
} }
@property
def list_of_milestones(self):
return [
{
'name': milestone.name,
'finish_date': milestone.estimated_finish,
'closed_points': milestone.closed_points,
'client_increment_points': milestone.client_increment_points,
'team_increment_points': milestone.team_increment_points
} for milestone in self.milestones.all().order_by('estimated_start')
]
class Milestone(models.Model, WatchedMixin): class Milestone(models.Model, WatchedMixin):
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')) 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')) verbose_name=_('name'))
slug = models.SlugField(max_length=250, unique=True, null=False, blank=True, slug = models.SlugField(max_length=250, unique=True, null=False, blank=True,
verbose_name=_('slug')) verbose_name=_('slug'))
owner = models.ForeignKey('base.User', null=True, blank=False,
related_name='owned_milestones', owner = models.ForeignKey(
verbose_name=_('owner')) 'base.User',
project = models.ForeignKey('Project', null=False, blank=False, null=True, blank=True,
related_name='owned_milestones', verbose_name=_('owner'))
project = models.ForeignKey(
'Project',
null=False, blank=False,
related_name='milestones', related_name='milestones',
verbose_name=_('project')) verbose_name=_('project'))
estimated_start = models.DateField(null=True, blank=True, default=None, estimated_start = models.DateField(null=True, blank=True, default=None,
verbose_name=_('estimated start')) verbose_name=_('estimated start'))
estimated_finish = models.DateField(null=True, blank=True, default=None, estimated_finish = models.DateField(null=True, blank=True, default=None,
@ -304,6 +335,47 @@ class Milestone(models.Model, WatchedMixin):
'modified_date': self.modified_date, 'modified_date': self.modified_date,
} }
@property
def closed_points(self):
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__gte=self.estimated_start,
created_date__lt=self.estimated_finish,
project_id = self.project_id,
client_requirement=True,
team_requirement=False
)
points = [ us.points.value for us in user_stories ]
return sum(points) + (self.shared_increment_points / 2)
@property
def team_increment_points(self):
user_stories = UserStory.objects.filter(
created_date__gte=self.estimated_start,
created_date__lt=self.estimated_finish,
project_id = self.project_id,
client_requirement=False,
team_requirement=True
)
points = [ us.points.value for us in user_stories ]
return sum(points) + (self.shared_increment_points / 2)
@property
def shared_increment_points(self):
user_stories = UserStory.objects.filter(
created_date__gte=self.estimated_start,
created_date__lt=self.estimated_finish,
project_id = self.project_id,
client_requirement=True,
team_requirement=True
)
points = [ us.points.value for us in user_stories ]
return sum(points)
class UserStory(WatchedMixin, models.Model): class UserStory(WatchedMixin, 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,
@ -421,8 +493,7 @@ class Attachment(models.Model):
def __unicode__(self): def __unicode__(self):
return u'content_type {0} - object_id {1} - attachment {2}'.format( 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, WatchedMixin): class Task(models.Model, WatchedMixin):
@ -436,12 +507,6 @@ class Task(models.Model, WatchedMixin):
owner = models.ForeignKey('base.User', null=True, blank=True, default=None, owner = models.ForeignKey('base.User', null=True, blank=True, default=None,
related_name='owned_tasks', related_name='owned_tasks',
verbose_name=_('owner')) 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, status = models.ForeignKey('TaskStatus', null=False, blank=False,
related_name='tasks', related_name='tasks',
verbose_name=_('status')) verbose_name=_('status'))
@ -469,6 +534,8 @@ class Task(models.Model, WatchedMixin):
verbose_name=_('watchers')) verbose_name=_('watchers'))
tags = PickledObjectField(null=False, blank=True, tags = PickledObjectField(null=False, blank=True,
verbose_name=_('tags')) verbose_name=_('tags'))
is_iocaine = models.BooleanField(default=False, null=False, blank=True,
verbose_name=_('is iocaine'))
class Meta: class Meta:
verbose_name = u'task' verbose_name = u'task'
@ -493,9 +560,6 @@ class Task(models.Model, WatchedMixin):
if self.id: if self.id:
self.modified_date = timezone.now() 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) super(Task, self).save(*args, **kwargs)
def _get_watchers_by_role(self): def _get_watchers_by_role(self):
@ -574,9 +638,6 @@ class Issue(models.Model, WatchedMixin):
if self.id: if self.id:
self.modified_date = timezone.now() 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) super(Issue, self).save(*args, **kwargs)
def _get_watchers_by_role(self): def _get_watchers_by_role(self):
@ -587,6 +648,10 @@ class Issue(models.Model, WatchedMixin):
'project_owner': (self.project, self.project.owner), 'project_owner': (self.project, self.project.owner),
} }
@property
def is_closed(self):
return self.status.is_closed
# Model related signals handlers # Model related signals handlers
@ -626,16 +691,44 @@ def project_post_save(sender, instance, created, **kwargs):
IssueType.objects.create(project=instance, name=name, order=order) IssueType.objects.create(project=instance, name=name, order=order)
@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') @receiver(models.signals.pre_save, sender=UserStory, dispatch_uid='user_story_ref_handler')
def user_story_ref_handler(sender, instance, **kwargs): def us_ref_handler(sender, instance, **kwargs):
if not instance.id and instance.project:
instance.ref = ref_uniquely(instance.project, 'last_us_ref', instance.__class__)
@receiver(models.signals.pre_save, sender=Task, dispatch_uid='tasks_close_handler')
def tasks_close_handler(sender, instance, **kwargs):
""" """
Automatically assignes a seguent reference code to a Automatically assignes a seguent reference code to a
user story if that is not created. user story if that is not created.
""" """
if not instance.id and instance.project: if instance.id:
instance.ref = ref_uniquely(instance.project, 'last_us_ref', instance.__class__) if sender.objects.get(id=instance.id).status.is_closed == False and instance.status.is_closed == True:
instance.finished_date = timezone.now()
if all([task.status.is_closed for task in instance.user_story.tasks.exclude(id=instance.id)]):
instance.user_story.finish_date = timezone.now()
instance.user_story.save()
elif sender.objects.get(id=instance.id).status.is_closed == True and instance.status.is_closed == False:
instance.finished_date = None
instance.user_story.finish_date = None
instance.user_story.save()
else:
instance.user_story.finish_date = None
instance.user_story.save()
# Email alerts signals handlers # Email alerts signals handlers
# TODO: temporary commented (Pending refactor) # TODO: temporary commented (Pending refactor)

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from greenmine.base.permissions import BaseDetailPermission from greenmine.base.permissions import BaseDetailPermission
@ -41,7 +43,7 @@ class IssueDetailPermission(BaseDetailPermission):
get_permission = "can_view_issue" get_permission = "can_view_issue"
put_permission = "change_issue" put_permission = "change_issue"
patch_permission = "change_issue" patch_permission = "change_issue"
delete_permission = "can_delete_issue" delete_permission = "delete_issue"
safe_methods = ['HEAD', 'OPTIONS'] safe_methods = ['HEAD', 'OPTIONS']
path_to_project = ['project'] path_to_project = ['project']

View File

@ -1,23 +1,28 @@
# -* coding: utf-8 -*- # -* coding: utf-8 -*-
from haystack import indexes from haystack import indexes
from greenmine.scrum.models import UserStory, Task from greenmine.scrum.models import UserStory, Task
class UserStoryIndex(indexes.SearchIndex, indexes.Indexable): 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): def get_model(self):
return UserStory return UserStory
def index_queryset(self): def index_queryset(self, using=None):
return self.get_model().objects.all() return self.get_model().objects.all()
class TaskIndex(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): def get_model(self):
return Task return Task
def index_queryset(self): def index_queryset(self, using=None):
return self.get_model().objects.all() return self.get_model().objects.all()

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers from rest_framework import serializers
from greenmine.scrum.models import * from greenmine.scrum.models import *
@ -5,6 +7,7 @@ from picklefield.fields import dbsafe_encode, dbsafe_decode
import json, reversion import json, reversion
class PickleField(serializers.WritableField): class PickleField(serializers.WritableField):
""" """
Pickle objects serializer. Pickle objects serializer.
@ -24,6 +27,7 @@ class PointsSerializer(serializers.ModelSerializer):
class ProjectSerializer(serializers.ModelSerializer): class ProjectSerializer(serializers.ModelSerializer):
tags = PickleField() tags = PickleField()
list_of_milestones = serializers.Field(source='list_of_milestones')
class Meta: class Meta:
model = Project model = Project
@ -63,17 +67,60 @@ class AttachmentSerializer(serializers.ModelSerializer):
class TaskSerializer(serializers.ModelSerializer): class TaskSerializer(serializers.ModelSerializer):
tags = PickleField() tags = PickleField(blank=True, default=[])
comment = serializers.SerializerMethodField('get_comment')
history = serializers.SerializerMethodField('get_history')
class Meta: class Meta:
model = Task model = Task
fields = () 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): class IssueSerializer(serializers.ModelSerializer):
tags = PickleField() tags = PickleField()
comment = serializers.SerializerMethodField('get_comment') comment = serializers.SerializerMethodField('get_comment')
history = serializers.SerializerMethodField('get_history') history = serializers.SerializerMethodField('get_history')
is_closed = serializers.Field(source='is_closed')
class Meta: class Meta:
model = Issue model = Issue
@ -112,7 +159,7 @@ class IssueSerializer(serializers.ModelSerializer):
for version in reversed(list(reversion.get_for_object(obj))): for version in reversed(list(reversion.get_for_object(obj))):
if current: if current:
issues_diff = self.get_issues_diff(version, current) issues_diff = self.get_issues_diff(current, version)
diff_list.append(issues_diff) diff_list.append(issues_diff)
current = version current = version

View File

@ -5,6 +5,7 @@ from rest_framework.urlpatterns import format_suffix_patterns
from greenmine.scrum import api from greenmine.scrum import api
urlpatterns = format_suffix_patterns(patterns('', urlpatterns = format_suffix_patterns(patterns('',
url(r'^projects/$', api.ProjectList.as_view(), name='project-list'), url(r'^projects/$', api.ProjectList.as_view(), name='project-list'),
url(r'^projects/(?P<pk>[0-9]+)/$', api.ProjectDetail.as_view(), name='project-detail'), url(r'^projects/(?P<pk>[0-9]+)/$', api.ProjectDetail.as_view(), name='project-detail'),
@ -26,6 +27,8 @@ urlpatterns = format_suffix_patterns(patterns('',
url(r'^issues/types/(?P<pk>[0-9]+)/$', api.IssueTypeDetail.as_view(), name='issues-type-detail'), url(r'^issues/types/(?P<pk>[0-9]+)/$', api.IssueTypeDetail.as_view(), name='issues-type-detail'),
url(r'^tasks/$', api.TaskList.as_view(), name='tasks-list'), url(r'^tasks/$', api.TaskList.as_view(), name='tasks-list'),
url(r'^tasks/(?P<pk>[0-9]+)/$', api.TaskDetail.as_view(), name='tasks-detail'), url(r'^tasks/(?P<pk>[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<pk>[0-9]+)/$', api.TasksAttachmentDetail.as_view(), name='tasks-attachment-detail'),
url(r'^severities/$', api.SeverityList.as_view(), name='severity-list'), url(r'^severities/$', api.SeverityList.as_view(), name='severity-list'),
url(r'^severities/(?P<pk>[0-9]+)/$', api.SeverityDetail.as_view(), name='severity-detail'), url(r'^severities/(?P<pk>[0-9]+)/$', api.SeverityDetail.as_view(), name='severity-detail'),
url(r'^tasks/statuses/$', api.TaskStatusList.as_view(), name='tasks-status-list'), url(r'^tasks/statuses/$', api.TaskStatusList.as_view(), name='tasks-status-list'),
@ -33,4 +36,3 @@ urlpatterns = format_suffix_patterns(patterns('',
url(r'^priorities/$', api.PriorityList.as_view(), name='priority-list'), url(r'^priorities/$', api.PriorityList.as_view(), name='priority-list'),
url(r'^priorities/(?P<pk>[0-9]+)/$', api.PriorityDetail.as_view(), name='priority-detail'), url(r'^priorities/(?P<pk>[0-9]+)/$', api.PriorityDetail.as_view(), name='priority-detail'),
)) ))

View File

@ -317,6 +317,9 @@ HAYSTACK_CONNECTIONS = {
HAYSTACK_DEFAULT_OPERATOR = 'AND' HAYSTACK_DEFAULT_OPERATOR = 'AND'
MAX_SEARCH_RESULTS = 100
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.contrib import admin from django.contrib import admin
from greenmine.wiki.models import WikiPage, WikiPageAttachment from greenmine.wiki.models import WikiPage, WikiPageAttachment

View File

@ -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 rest_framework import generics
from greenmine.wiki.serializers import WikiPageSerializer, WikiPageAttachmentSerializer from greenmine.wiki.serializers import WikiPageSerializer, WikiPageAttachmentSerializer
@ -21,6 +27,20 @@ class WikiPageDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = WikiPageSerializer serializer_class = WikiPageSerializer
permission_classes = (WikiPageDetailPermission,) 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} found matching the query").format(
verbose_name=queryset.model._meta.verbose_name))
return obj
class WikiPageAttachmentList(generics.ListCreateAPIView): class WikiPageAttachmentList(generics.ListCreateAPIView):
model = WikiPageAttachment model = WikiPageAttachment

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from greenmine.base.fields import DictField
class WikiPage(models.Model): class WikiPage(models.Model):
project = models.ForeignKey('scrum.Project', null=False, blank=False, project = models.ForeignKey('scrum.Project', null=False, blank=False,
@ -27,6 +27,8 @@ class WikiPage(models.Model):
verbose_name = u'wiki page' verbose_name = u'wiki page'
verbose_name_plural = u'wiki pages' verbose_name_plural = u'wiki pages'
ordering = ['project', 'slug'] ordering = ['project', 'slug']
unique_together = ('project', 'slug',)
permissions = ( permissions = (
('can_view_wikipage', 'Can modify owned wiki pages'), ('can_view_wikipage', 'Can modify owned wiki pages'),
('can_change_owned_wikipage', 'Can modify owned wiki pages'), ('can_change_owned_wikipage', 'Can modify owned wiki pages'),
@ -57,5 +59,5 @@ class WikiPageAttachment(models.Model):
ordering = ['wikipage', 'created_date'] ordering = ['wikipage', 'created_date']
def __unicode__(self): 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)

View File

@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
from greenmine.base.permissions import BaseDetailPermission from greenmine.base.permissions import BaseDetailPermission
class WikiPageDetailPermission(BaseDetailPermission): class WikiPageDetailPermission(BaseDetailPermission):
get_permission = "can_view_wikipage" get_permission = "can_view_wikipage"
put_permission = "change_wikipage" put_permission = "change_wikipage"
@ -8,6 +11,7 @@ class WikiPageDetailPermission(BaseDetailPermission):
safe_methods = ['HEAD', 'OPTIONS'] safe_methods = ['HEAD', 'OPTIONS']
path_to_project = ['project'] path_to_project = ['project']
class WikiPageAttachmentDetailPermission(BaseDetailPermission): class WikiPageAttachmentDetailPermission(BaseDetailPermission):
get_permission = "can_view_wikipageattachment" get_permission = "can_view_wikipageattachment"
put_permission = "change_wikipageattachment" put_permission = "change_wikipageattachment"

View File

@ -1,13 +1,14 @@
# -* coding: utf-8 -*- # -* coding: utf-8 -*-
from haystack import indexes from haystack import indexes
from .models import WikiPage 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') text = indexes.CharField(document=True, use_template=True, template_name='search/indexes/wikipage_text.txt')
def get_model(self): def get_model(self):
return WikiPage return WikiPage
def index_queryset(self): def index_queryset(self, using=None):
return self.get_model().objects.all() return self.get_model().objects.all()

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers from rest_framework import serializers
from greenmine.wiki.models import WikiPage, WikiPageAttachment from greenmine.wiki.models import WikiPage, WikiPageAttachment

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
from greenmine.wiki import api from greenmine.wiki import api
urlpatterns = format_suffix_patterns(patterns('', urlpatterns = format_suffix_patterns(patterns('',
url(r'^wiki/pages/$', api.WikiPageList.as_view(), name='wiki-page-list'), url(r'^pages/$', api.WikiPageList.as_view(), name='wiki-page-list'),
url(r'^wiki/pages/(?P<slug>[\w\-\d]+)/$', api.WikiPageDetail.as_view(), name='wiki-page-detail'), url(r'^pages/(?P<projectid>\d+)-(?P<slug>[\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/$', api.WikiPageAttachmentList.as_view(), name='wiki-page-attachment-list'),
#url(r'^wiki_page_attachments/(?P<pk>[0-9]+)/$', api.WikiPageAttachmentDetail.as_view(), name='wiki-page-attachment-detail'), #url(r'^wiki_page_attachments/(?P<pk>[0-9]+)/$', api.WikiPageAttachmentDetail.as_view(), name='wiki-page-attachment-detail'),
)) ))

View File

@ -19,3 +19,4 @@ six==1.3.0
djangorestframework==2.2.5 djangorestframework==2.2.5
django-filter==0.6 django-filter==0.6
psycopg2==2.4.6 psycopg2==2.4.6
django-sampledatahelper==0.0.1