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*
*.log
greenmine/search
greenmine/settings/local.py
database.sqlite
logs

View File

@ -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
@ -36,14 +40,15 @@ 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),
'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),
'search': reverse('search', request=request, format=format),
})
@ -65,11 +70,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):
@ -93,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)
@ -129,3 +138,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)

View File

@ -134,16 +134,41 @@
"questions",
"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",
"questions",
"question"
],
[
"can_change_question_state",
"questions",
"question"
],
[
"can_reply_question",
"questions",
"question"
],
[
"can_view_question",
"questions",
"question"
],
[
"change_question",
"questions",
@ -155,19 +180,34 @@
"question"
],
[
"add_questionresponse",
"add_questionstatus",
"questions",
"questionresponse"
"questionstatus"
],
[
"change_questionresponse",
"change_questionstatus",
"questions",
"questionresponse"
"questionstatus"
],
[
"delete_questionresponse",
"delete_questionstatus",
"questions",
"questionresponse"
"questionstatus"
],
[
"add_attachment",
"scrum",
"attachment"
],
[
"change_attachment",
"scrum",
"attachment"
],
[
"delete_attachment",
"scrum",
"attachment"
],
[
"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.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',

View File

@ -31,23 +31,9 @@ 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, 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'))
description = models.TextField(null=False, blank=True,
verbose_name=_('description'))

View File

@ -54,12 +54,39 @@ 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', 'email', 'color', 'description',
'default_language', 'default_timezone', 'is_active', 'photo', 'projects')
def get_projects(self, obj):
return [x.id for x in obj.projects.all()]
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = Role
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/logout/$', api.Logout.as_view(), name='logout'),
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'^search/$', api.Search.as_view(), name="search"),
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'):
"""
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,19 @@
# -*- coding: utf-8 -*-
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"]
admin.site.register(Question, QuestionAdmin)
admin.site.register(models.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(models.QuestionStatus, QuestionStatusAdmin)

View File

@ -1,32 +1,36 @@
# -*- 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 . 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 = (QuestionDetailPermission,)
model = models.Question
serializer_class = serializers.QuestionSerializer
permission_classes = (IsAuthenticated, permissions.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'])

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.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,66 @@ 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'))
# Model related signals handlers
@receiver(models.signals.post_save, sender=Project, dispatch_uid='project_post_save_add_question_states')
def project_post_save_add_question_states(sender, instance, created, **kwargs):
"""
Create all project model depences on project is
created.
"""
class Meta:
verbose_name = u'question response'
verbose_name_plural = u'question responses'
ordering = ['question', 'created_date']
def __unicode__(self):
return u'{0} - response {1}'.format(unicode(self.question), self.id)
if not created:
return
# Populate new project dependen default data
for order, name, is_closed in QUESTION_STATUS:
QuestionStatus.objects.create(name=name, order=order,
is_closed=is_closed, project=instance)

View File

@ -1,15 +1,13 @@
# -*- coding: utf-8 -*-
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 = []

View File

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

View File

@ -1,15 +1,59 @@
# -*- coding: utf-8 -*-
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):
tags = PickleField()
comment = serializers.SerializerMethodField('get_comment')
history = serializers.SerializerMethodField('get_history')
class Meta:
model = Question
model = models.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

View File

@ -1,12 +1,13 @@
# -*- 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'),
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:
return models.UserStory.objects.none()
class ProjectAdmin(reversion.VersionAdmin):
list_display = ["name", "owner"]
inlines = [MembershipInline, MilestoneInline, UserStoryInline]
@ -57,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):
@ -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)

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.permissions import IsAuthenticated
@ -50,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
@ -100,7 +105,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']
@ -110,7 +115,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)
@ -129,6 +134,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
@ -140,6 +168,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):
@ -147,6 +176,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

View File

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

View File

@ -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
@ -11,6 +13,8 @@ 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.",
@ -29,122 +33,170 @@ 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),
token=''.join(random.sample('abcdef0123456789', 10)),
)
user.set_password('user%d' % (counter))
user.save()
return user
sd = SampleDataHelper(seed=12345678901)
@transaction.commit_on_success
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):
users.append(self.create_user(x))
self.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 %s' % (x),
description='Project example %s description' % (x),
owner=random.choice(users),
public=True,
)
project = self.create_project(x)
project.save()
for user in users:
for user in self.users:
Membership.objects.create(project=project, role=role, user=user)
now_date = now() - datetime.timedelta(30)
start_date = now() - datetime.timedelta(35)
# create random milestones
for y in xrange(2):
milestone = Milestone.objects.create(
project=project,
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)
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(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 z in xrange(self.sd.int(3, 7)):
us = self.create_us(project, milestone)
for tag in lorem_ipsum.words(random.randint(1, 5), common=True).split(" "):
us.tags.append(tag)
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
us.save()
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),
)
start_date = end_date
# 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()
for y in xrange(self.sd.int(8,15)):
us = self.create_us(project)
# create bugs.
for y in xrange(20):
bug = Issue.objects.create(
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=lorem_ipsum.words(random.randint(1, 5), common=False),
description=lorem_ipsum.words(random.randint(1, 15), common=False),
subject=self.sd.words(1,5),
content=self.sd.paragraph(),
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),
status=self.sd.db_object_from_queryset(QuestionStatus.objects.filter(project=project)),
tags=[],
)
for tag in lorem_ipsum.words(random.randint(1, 5), common=True).split(" "):
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):
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)
@ -181,7 +190,7 @@ class Project(models.Model, WatchedMixin):
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',
@ -240,20 +249,42 @@ class Project(models.Model, WatchedMixin):
'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):
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,
@ -304,6 +335,47 @@ class Milestone(models.Model, WatchedMixin):
'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):
uuid = models.CharField(max_length=40, unique=True, null=False, blank=True,
@ -421,8 +493,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, WatchedMixin):
@ -436,12 +507,6 @@ class Task(models.Model, WatchedMixin):
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'))
@ -469,6 +534,8 @@ class Task(models.Model, WatchedMixin):
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'
@ -493,9 +560,6 @@ class Task(models.Model, WatchedMixin):
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)
def _get_watchers_by_role(self):
@ -574,9 +638,6 @@ class Issue(models.Model, WatchedMixin):
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)
def _get_watchers_by_role(self):
@ -587,6 +648,10 @@ class Issue(models.Model, WatchedMixin):
'project_owner': (self.project, self.project.owner),
}
@property
def is_closed(self):
return self.status.is_closed
# 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)
@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 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
user story if that is not created.
"""
if not instance.id and instance.project:
instance.ref = ref_uniquely(instance.project, 'last_us_ref', instance.__class__)
if instance.id:
if sender.objects.get(id=instance.id).status.is_closed == False and instance.status.is_closed == True:
instance.finished_date = timezone.now()
if all([task.status.is_closed for task in instance.user_story.tasks.exclude(id=instance.id)]):
instance.user_story.finish_date = timezone.now()
instance.user_story.save()
elif sender.objects.get(id=instance.id).status.is_closed == True and instance.status.is_closed == False:
instance.finished_date = None
instance.user_story.finish_date = None
instance.user_story.save()
else:
instance.user_story.finish_date = None
instance.user_story.save()
# Email alerts signals handlers
# TODO: temporary commented (Pending refactor)

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from greenmine.base.permissions import BaseDetailPermission
@ -41,7 +43,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']

View File

@ -1,23 +1,28 @@
# -* 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):
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')
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()

View File

@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers
from greenmine.scrum.models import *
@ -5,6 +7,7 @@ from picklefield.fields import dbsafe_encode, dbsafe_decode
import json, reversion
class PickleField(serializers.WritableField):
"""
Pickle objects serializer.
@ -24,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
@ -63,17 +67,60 @@ class AttachmentSerializer(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:
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()
comment = serializers.SerializerMethodField('get_comment')
history = serializers.SerializerMethodField('get_history')
is_closed = serializers.Field(source='is_closed')
class Meta:
model = Issue
@ -112,7 +159,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

View File

@ -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<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'^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/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/(?P<pk>[0-9]+)/$', api.SeverityDetail.as_view(), name='severity-detail'),
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/(?P<pk>[0-9]+)/$', api.PriorityDetail.as_view(), name='priority-detail'),
))

View File

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

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
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 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} found matching the query").format(
verbose_name=queryset.model._meta.verbose_name))
return obj
class WikiPageAttachmentList(generics.ListCreateAPIView):
model = WikiPageAttachment

View File

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

View File

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

View File

@ -1,13 +1,14 @@
# -* coding: utf-8 -*-
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()

View File

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

View File

@ -1,11 +1,14 @@
# -*- 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'^wiki/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/$', api.WikiPageList.as_view(), name='wiki-page-list'),
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/(?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
django-filter==0.6
psycopg2==2.4.6
django-sampledatahelper==0.0.1