Adding questions support

remotes/origin/enhancement/email-actions
Alejandro Alonso 2013-04-26 10:49:18 +02:00
parent 7df355d92e
commit bce92c42f0
11 changed files with 554 additions and 408 deletions

View File

@ -44,7 +44,6 @@ class ApiRoot(APIView):
'priorities': reverse('priority-list', request=request, format=format),
'documents': reverse('document-list', request=request, format=format),
'questions': reverse('question-list', request=request, format=format),
'question_responses': reverse('question-response-list', request=request, format=format),
'wiki/pages': reverse('wiki-page-list', request=request, format=format),
'users': reverse('user-list', request=request, format=format),
'roles': reverse('user-roles', request=request, format=format),

View File

@ -1,561 +1,601 @@
[
{
"pk": 1,
"model": "base.role",
"pk": 1,
"model": "base.role",
"fields": {
"permissions": [
[
"add_logentry",
"admin",
"add_logentry",
"admin",
"logentry"
],
],
[
"change_logentry",
"admin",
"change_logentry",
"admin",
"logentry"
],
],
[
"delete_logentry",
"admin",
"delete_logentry",
"admin",
"logentry"
],
],
[
"add_group",
"auth",
"add_group",
"auth",
"group"
],
],
[
"change_group",
"auth",
"change_group",
"auth",
"group"
],
],
[
"delete_group",
"auth",
"delete_group",
"auth",
"group"
],
],
[
"add_permission",
"auth",
"add_permission",
"auth",
"permission"
],
],
[
"change_permission",
"auth",
"change_permission",
"auth",
"permission"
],
],
[
"delete_permission",
"auth",
"delete_permission",
"auth",
"permission"
],
],
[
"add_role",
"base",
"add_role",
"base",
"role"
],
],
[
"change_role",
"base",
"change_role",
"base",
"role"
],
],
[
"delete_role",
"base",
"delete_role",
"base",
"role"
],
],
[
"add_user",
"base",
"add_user",
"base",
"user"
],
],
[
"change_user",
"base",
"change_user",
"base",
"user"
],
],
[
"delete_user",
"base",
"delete_user",
"base",
"user"
],
],
[
"add_contenttype",
"contenttypes",
"add_contenttype",
"contenttypes",
"contenttype"
],
],
[
"change_contenttype",
"contenttypes",
"change_contenttype",
"contenttypes",
"contenttype"
],
],
[
"delete_contenttype",
"contenttypes",
"delete_contenttype",
"contenttypes",
"contenttype"
],
],
[
"add_document",
"documents",
"add_document",
"documents",
"document"
],
],
[
"can_change_owned_documents",
"documents",
"can_change_owned_documents",
"documents",
"document"
],
],
[
"can_download_from_my_projects",
"documents",
"can_download_from_my_projects",
"documents",
"document"
],
],
[
"can_download_from_other_projects",
"documents",
"can_download_from_other_projects",
"documents",
"document"
],
],
[
"can_view_documents",
"documents",
"can_view_documents",
"documents",
"document"
],
],
[
"change_document",
"documents",
"change_document",
"documents",
"document"
],
],
[
"delete_document",
"documents",
"delete_document",
"documents",
"document"
],
],
[
"add_question",
"questions",
"add_question",
"questions",
"question"
],
],
[
"can_change_owned_question",
"questions",
"can_assign_question_to_myself",
"questions",
"question"
],
],
[
"can_reply_question",
"questions",
"can_assign_question_to_other",
"questions",
"question"
],
],
[
"change_question",
"questions",
"can_change_assigned_question",
"questions",
"question"
],
],
[
"delete_question",
"questions",
"can_change_owned_question",
"questions",
"question"
],
],
[
"add_questionresponse",
"questions",
"questionresponse"
],
"can_change_question_state",
"questions",
"question"
],
[
"change_questionresponse",
"questions",
"questionresponse"
],
"can_reply_question",
"questions",
"question"
],
[
"delete_questionresponse",
"questions",
"questionresponse"
],
"can_view_question",
"questions",
"question"
],
[
"add_issue",
"scrum",
"change_question",
"questions",
"question"
],
[
"delete_question",
"questions",
"question"
],
[
"add_questionstatus",
"questions",
"questionstatus"
],
[
"change_questionstatus",
"questions",
"questionstatus"
],
[
"delete_questionstatus",
"questions",
"questionstatus"
],
[
"add_attachment",
"scrum",
"attachment"
],
[
"change_attachment",
"scrum",
"attachment"
],
[
"delete_attachment",
"scrum",
"attachment"
],
[
"add_issue",
"scrum",
"issue"
],
],
[
"can_assign_issue_to_myself",
"scrum",
"can_assign_issue_to_myself",
"scrum",
"issue"
],
],
[
"can_assign_issue_to_other",
"scrum",
"can_assign_issue_to_other",
"scrum",
"issue"
],
],
[
"can_change_assigned_issue",
"scrum",
"can_change_assigned_issue",
"scrum",
"issue"
],
],
[
"can_change_issue_state",
"scrum",
"can_change_issue_state",
"scrum",
"issue"
],
],
[
"can_change_owned_issue",
"scrum",
"can_change_owned_issue",
"scrum",
"issue"
],
],
[
"can_comment_issue",
"scrum",
"can_comment_issue",
"scrum",
"issue"
],
],
[
"can_view_issue",
"scrum",
"can_view_issue",
"scrum",
"issue"
],
],
[
"change_issue",
"scrum",
"change_issue",
"scrum",
"issue"
],
],
[
"delete_issue",
"scrum",
"delete_issue",
"scrum",
"issue"
],
],
[
"add_issuestatus",
"scrum",
"add_issuestatus",
"scrum",
"issuestatus"
],
],
[
"change_issuestatus",
"scrum",
"change_issuestatus",
"scrum",
"issuestatus"
],
],
[
"delete_issuestatus",
"scrum",
"delete_issuestatus",
"scrum",
"issuestatus"
],
],
[
"add_issuetype",
"scrum",
"add_issuetype",
"scrum",
"issuetype"
],
],
[
"change_issuetype",
"scrum",
"change_issuetype",
"scrum",
"issuetype"
],
],
[
"delete_issuetype",
"scrum",
"delete_issuetype",
"scrum",
"issuetype"
],
],
[
"add_membership",
"scrum",
"add_membership",
"scrum",
"membership"
],
],
[
"change_membership",
"scrum",
"change_membership",
"scrum",
"membership"
],
],
[
"delete_membership",
"scrum",
"delete_membership",
"scrum",
"membership"
],
],
[
"add_milestone",
"scrum",
"add_milestone",
"scrum",
"milestone"
],
],
[
"can_view_milestone",
"scrum",
"can_view_milestone",
"scrum",
"milestone"
],
],
[
"change_milestone",
"scrum",
"change_milestone",
"scrum",
"milestone"
],
],
[
"delete_milestone",
"scrum",
"delete_milestone",
"scrum",
"milestone"
],
],
[
"add_points",
"scrum",
"add_points",
"scrum",
"points"
],
],
[
"change_points",
"scrum",
"change_points",
"scrum",
"points"
],
],
[
"delete_points",
"scrum",
"delete_points",
"scrum",
"points"
],
],
[
"add_priority",
"scrum",
"add_priority",
"scrum",
"priority"
],
],
[
"change_priority",
"scrum",
"change_priority",
"scrum",
"priority"
],
],
[
"delete_priority",
"scrum",
"delete_priority",
"scrum",
"priority"
],
],
[
"add_project",
"scrum",
"add_project",
"scrum",
"project"
],
],
[
"can_list_projects",
"scrum",
"can_list_projects",
"scrum",
"project"
],
],
[
"can_manage_users",
"scrum",
"can_manage_users",
"scrum",
"project"
],
],
[
"can_view_project",
"scrum",
"can_view_project",
"scrum",
"project"
],
],
[
"change_project",
"scrum",
"change_project",
"scrum",
"project"
],
],
[
"delete_project",
"scrum",
"delete_project",
"scrum",
"project"
],
],
[
"add_severity",
"scrum",
"add_severity",
"scrum",
"severity"
],
],
[
"change_severity",
"scrum",
"change_severity",
"scrum",
"severity"
],
],
[
"delete_severity",
"scrum",
"delete_severity",
"scrum",
"severity"
],
],
[
"add_task",
"scrum",
"add_task",
"scrum",
"task"
],
],
[
"can_add_task_to_us",
"scrum",
"can_add_task_to_us",
"scrum",
"task"
],
],
[
"can_assign_task_to_myself",
"scrum",
"can_assign_task_to_myself",
"scrum",
"task"
],
],
[
"can_assign_task_to_other",
"scrum",
"can_assign_task_to_other",
"scrum",
"task"
],
],
[
"can_change_assigned_task",
"scrum",
"can_change_assigned_task",
"scrum",
"task"
],
],
[
"can_change_owned_task",
"scrum",
"can_change_owned_task",
"scrum",
"task"
],
],
[
"can_change_task_state",
"scrum",
"can_change_task_state",
"scrum",
"task"
],
],
[
"can_comment_task",
"scrum",
"can_comment_task",
"scrum",
"task"
],
],
[
"can_view_task",
"scrum",
"can_view_task",
"scrum",
"task"
],
],
[
"change_task",
"scrum",
"change_task",
"scrum",
"task"
],
],
[
"delete_task",
"scrum",
"delete_task",
"scrum",
"task"
],
],
[
"add_taskstatus",
"scrum",
"add_taskstatus",
"scrum",
"taskstatus"
],
],
[
"change_taskstatus",
"scrum",
"change_taskstatus",
"scrum",
"taskstatus"
],
],
[
"delete_taskstatus",
"scrum",
"delete_taskstatus",
"scrum",
"taskstatus"
],
],
[
"add_userstory",
"scrum",
"add_userstory",
"scrum",
"userstory"
],
],
[
"can_add_userstory_to_milestones",
"scrum",
"can_add_userstory_to_milestones",
"scrum",
"userstory"
],
],
[
"can_change_owned_userstory",
"scrum",
"can_change_owned_userstory",
"scrum",
"userstory"
],
],
[
"can_comment_userstory",
"scrum",
"can_comment_userstory",
"scrum",
"userstory"
],
],
[
"can_delete_userstory",
"scrum",
"can_delete_userstory",
"scrum",
"userstory"
],
],
[
"can_view_userstory",
"scrum",
"can_view_userstory",
"scrum",
"userstory"
],
],
[
"change_userstory",
"scrum",
"change_userstory",
"scrum",
"userstory"
],
],
[
"delete_userstory",
"scrum",
"delete_userstory",
"scrum",
"userstory"
],
],
[
"add_userstorystatus",
"scrum",
"add_userstorystatus",
"scrum",
"userstorystatus"
],
],
[
"change_userstorystatus",
"scrum",
"change_userstorystatus",
"scrum",
"userstorystatus"
],
],
[
"delete_userstorystatus",
"scrum",
"delete_userstorystatus",
"scrum",
"userstorystatus"
],
],
[
"add_session",
"sessions",
"add_session",
"sessions",
"session"
],
],
[
"change_session",
"sessions",
"change_session",
"sessions",
"session"
],
],
[
"delete_session",
"sessions",
"delete_session",
"sessions",
"session"
],
],
[
"add_migrationhistory",
"south",
"add_migrationhistory",
"south",
"migrationhistory"
],
],
[
"change_migrationhistory",
"south",
"change_migrationhistory",
"south",
"migrationhistory"
],
],
[
"delete_migrationhistory",
"south",
"delete_migrationhistory",
"south",
"migrationhistory"
],
],
[
"add_wikipage",
"wiki",
"add_wikipage",
"wiki",
"wikipage"
],
],
[
"can_change_owned_wikipage",
"wiki",
"can_change_owned_wikipage",
"wiki",
"wikipage"
],
],
[
"can_view_wikipage",
"wiki",
"can_view_wikipage",
"wiki",
"wikipage"
],
],
[
"change_wikipage",
"wiki",
"change_wikipage",
"wiki",
"wikipage"
],
],
[
"delete_wikipage",
"wiki",
"delete_wikipage",
"wiki",
"wikipage"
],
],
[
"add_wikipageattachment",
"wiki",
"add_wikipageattachment",
"wiki",
"wikipageattachment"
],
],
[
"change_wikipageattachment",
"wiki",
"change_wikipageattachment",
"wiki",
"wikipageattachment"
],
],
[
"delete_wikipageattachment",
"wiki",
"delete_wikipageattachment",
"wiki",
"wikipageattachment"
]
],
"name": "Developer",
],
"name": "Developer",
"slug": ""
}
}

View File

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

View File

@ -1,32 +1,34 @@
# -*- coding: utf-8 -*-
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from greenmine.questions.serializers import QuestionSerializer, QuestionResponseSerializer
from greenmine.questions.models import Question, QuestionResponse
from greenmine.questions.permissions import QuestionDetailPermission, QuestionResponseDetailPermission
from greenmine.questions.serializers import QuestionSerializer
from greenmine.questions.models import Question
from greenmine.questions.permissions import QuestionDetailPermission
import reversion
class QuestionList(generics.ListCreateAPIView):
model = Question
serializer_class = QuestionSerializer
filter_fields = ('project',)
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return self.model.objects.filter(project__members=self.request.user)
def pre_save(self, obj):
obj.owner = self.request.user
class QuestionDetail(generics.RetrieveUpdateDestroyAPIView):
model = Question
serializer_class = QuestionSerializer
permission_classes = (QuestionDetailPermission,)
permission_classes = (IsAuthenticated, QuestionDetailPermission,)
class QuestionResponseList(generics.ListCreateAPIView):
model = QuestionResponse
serializer_class = QuestionResponseSerializer
def get_queryset(self):
return self.model.objects.filter(question__project__members=self.request.user)
class QuestionResponseDetail(generics.RetrieveUpdateDestroyAPIView):
model = QuestionResponse
serializer_class = QuestionResponseSerializer
permission_classes = (QuestionResponseDetailPermission,)
def post_save(self, obj, created=False):
with reversion.create_revision():
if "comment" in self.request.DATA:
# Update the comment in the last version
reversion.set_comment(self.request.DATA['comment'])

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,68 @@ class Question(models.Model):
upload_to='messages',
verbose_name=_('attached_file'))
project = models.ForeignKey('scrum.Project', null=False, blank=False,
related_name='questions')
related_name='questions',
verbose_name=_('project'))
milestone = models.ForeignKey('scrum.Milestone', null=True, blank=True, default=None,
related_name='questions',
verbose_name=_('milestone'))
assigned_to = models.ForeignKey('base.User', null=False, blank=False,
finished_date = models.DateTimeField(null=True, blank=True,
verbose_name=_('finished date'))
assigned_to = models.ForeignKey('base.User', null=True, blank=True, default=None,
related_name='questions_assigned_to_me',
verbose_name=_('assigned_to'))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_('created date'))
modified_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_('modified date'))
owner = models.ForeignKey('base.User', null=False, blank=False,
related_name='owned_questions')
watchers = models.ManyToManyField('base.User', null=True, blank=True,
related_name='watched_questions',
verbose_name=_('watchers'))
tags = DictField(null=False, blank=True,
tags = PickledObjectField(null=False, blank=True,
verbose_name=_('tags'))
class Meta:
verbose_name = u'question'
verbose_name_plural = u'questions'
ordering = ['project', 'subject', 'id']
#TODO: permissions
permissions = (
('can_reply_question', 'Can reply questions'),
('can_change_owned_question', 'Can modify owned questions'),
('can_change_assigned_question', 'Can modify assigned questions'),
('can_assign_question_to_other', 'Can assign questions to others'),
('can_assign_question_to_myself', 'Can assign questions to myself'),
('can_change_question_state', 'Can change the question state'),
('can_view_question', 'Can view the question'),
)
def __unicode__(self):
return self.subject
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify_uniquely(self.subject, self.__class__)
if self.id:
self.modified_date = timezone.now()
if not self.ref:
self.ref = ref_uniquely(self.project, 'last_issue_ref', self.__class__)
super(Question, self).save(*args, **kwargs)
class QuestionResponse(models.Model):
content = models.TextField(null=False, blank=False,
verbose_name=_('content'))
created_date = models.DateTimeField(auto_now_add=True, null=False, blank=False,
verbose_name=_('created date'))
modified_date = models.DateTimeField(auto_now=True, null=False, blank=False,
verbose_name=_('modified date'))
attached_file = models.FileField(max_length=500, null=True, blank=True,
upload_to='messages',
verbose_name=_('attached file'))
question = models.ForeignKey('Question', null=False, blank=False,
related_name='responses',
verbose_name=_('question'))
owner = models.ForeignKey('base.User', null=False, blank=False,
related_name='question_responses')
tags = DictField(null=False, blank=True,
verbose_name=_('tags'))
class Meta:
verbose_name = u'question response'
verbose_name_plural = u'question responses'
ordering = ['question', 'created_date']
# Model related signals handlers
def __unicode__(self):
return u'{0} - response {1}'.format(unicode(self.question), self.id)
@receiver(models.signals.post_save, sender=Project, dispatch_uid='project_post_save_add_question_states')
def project_post_save_add_question_states(sender, instance, created, **kwargs):
"""
Create all project model depences on project is
created.
"""
if not created:
return
# Populate new project dependen default data
for order, name, is_closed in QUESTION_STATUS:
QuestionStatus.objects.create(name=name, order=order,
is_closed=is_closed, project=instance)

View File

@ -1,15 +1,11 @@
from greenmine.base.permissions import BaseDetailPermission
class QuestionDetailPermission(BaseDetailPermission):
get_permission = "can_view_question"
put_permission = "can_change_question"
delete_permission = "can_delete_question"
put_permission = "change_question"
patch_permission = "change_question"
delete_permission = "delete_question"
safe_methods = ['HEAD', 'OPTIONS']
path_to_document = []
path_to_project = []
class QuestionResponseDetailPermission(BaseDetailPermission):
get_permission = "can_view_questionresponse"
put_permission = "can_change_questionresponse"
delete_permission = "can_delete_questionresponse"
safe_methods = ['HEAD', 'OPTIONS']
path_to_document = []

View File

@ -1,15 +1,58 @@
# -*- coding: utf-8 -*-
from rest_framework import serializers
from greenmine.questions.models import Question, QuestionResponse
from greenmine.questions.models import Question
from greenmine.scrum.serializers import PickleField
import reversion
class QuestionSerializer(serializers.ModelSerializer):
tags = PickleField()
comment = serializers.SerializerMethodField('get_comment')
history = serializers.SerializerMethodField('get_history')
class Meta:
model = Question
fields = ()
def get_comment(self, obj):
return ''
class QuestionResponseSerializer(serializers.ModelSerializer):
class Meta:
model = QuestionResponse
fields = ()
def get_questions_diff(self, old_question_version, new_question_version):
old_obj = old_question_version.field_dict
new_obj = new_question_version.field_dict
diff_dict = {
'modified_date': new_obj['modified_date'],
'by': old_question_version.revision.user,
'comment': old_question_version.revision.comment,
}
for key in old_obj.keys():
if key == 'modified_date':
continue
if old_obj[key] == new_obj[key]:
continue
diff_dict[key] = {
'old': old_obj[key],
'new': new_obj[key],
}
return diff_dict
def get_history(self, obj):
diff_list = []
current = None
for version in reversed(list(reversion.get_for_object(obj))):
if current:
questions_diff = self.get_questions_diff(version, current)
diff_list.append(questions_diff)
current = version
return diff_list

View File

@ -6,7 +6,5 @@ from greenmine.questions import api
urlpatterns = format_suffix_patterns(patterns('',
url(r'^questions/$', api.QuestionList.as_view(), name='question-list'),
url(r'^questions/(?P<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

@ -11,6 +11,7 @@ from django.contrib.webdesign import lorem_ipsum
from greenmine.base.models import User, Role
from greenmine.scrum.models import *
from greenmine.questions.models import *
subjects = [
"Fixing templates for Django 1.2.",
@ -146,3 +147,19 @@ class Command(BaseCommand):
bug.tags.append(tag)
bug.save()
# create questions.
for y in xrange(20):
question = Question.objects.create(
project=project,
subject=lorem_ipsum.words(random.randint(1, 5), common=False),
content=lorem_ipsum.words(random.randint(1, 15), common=False),
owner=project.owner,
status=QuestionStatus.objects.get(project=project, order=1),
tags=[],
)
for tag in lorem_ipsum.words(random.randint(1, 5), common=True).split(" "):
question.tags.append(tag)
question.save()

View File

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