Improving API performance for us, task, milestone and issue listing
parent
117a97f12c
commit
7968c80376
|
@ -15,6 +15,7 @@
|
|||
- Select a color (or not) to a tag when add it to stories, issues and tasks.
|
||||
|
||||
### Misc
|
||||
- [API] Improve performance of some calls over list.
|
||||
- Lots of small and not so small bugfixes.
|
||||
|
||||
|
||||
|
|
|
@ -34,3 +34,4 @@ git+https://github.com/Xof/django-pglocks.git@dbb8d7375066859f897604132bd437832d
|
|||
pyjwkest==1.1.5
|
||||
python-dateutil==2.4.2
|
||||
netaddr==0.7.18
|
||||
serpy==0.1.1
|
||||
|
|
|
@ -69,6 +69,7 @@ import copy
|
|||
import datetime
|
||||
import inspect
|
||||
import types
|
||||
import serpy
|
||||
|
||||
# Note: We do the following so that users of the framework can use this style:
|
||||
#
|
||||
|
@ -1220,3 +1221,11 @@ class HyperlinkedModelSerializer(ModelSerializer):
|
|||
"model_name": model_meta.object_name.lower()
|
||||
}
|
||||
return self._default_view_name % format_kwargs
|
||||
|
||||
|
||||
class LightSerializer(serpy.Serializer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.pop("read_only", None)
|
||||
kwargs.pop("partial", None)
|
||||
kwargs.pop("files", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
|
|
@ -65,6 +65,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
|||
filters.WatchersFilter,)
|
||||
|
||||
filter_fields = ("project",
|
||||
"project__slug",
|
||||
"status__is_closed")
|
||||
|
||||
order_by_fields = ("type",
|
||||
|
|
|
@ -20,18 +20,24 @@ from taiga.base.api import serializers
|
|||
from taiga.base.fields import PgArrayField
|
||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||
|
||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.validators import WatchersValidator
|
||||
from taiga.projects.serializers import BasicIssueStatusSerializer
|
||||
from taiga.mdrender.service import render as mdrender
|
||||
from taiga.projects.mixins.serializers import OwnerExtraInfoMixin
|
||||
from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin
|
||||
from taiga.projects.mixins.serializers import StatusExtraInfoMixin
|
||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.validators import WatchersValidator
|
||||
from taiga.projects.tagging.fields import TagsAndTagsColorsField
|
||||
from taiga.projects.serializers import BasicIssueStatusSerializer
|
||||
from taiga.projects.validators import ProjectExistsValidator
|
||||
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
||||
from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
|
||||
|
||||
from taiga.users.serializers import UserBasicInfoSerializer
|
||||
|
||||
from . import models
|
||||
|
||||
import serpy
|
||||
|
||||
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
||||
serializers.ModelSerializer):
|
||||
|
@ -68,11 +74,23 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa
|
|||
return mdrender(obj.project, obj.description)
|
||||
|
||||
|
||||
class IssueListSerializer(IssueSerializer):
|
||||
class Meta:
|
||||
model = models.Issue
|
||||
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
||||
exclude = ("description", "description_html")
|
||||
class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
|
||||
OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin,
|
||||
serializers.LightSerializer):
|
||||
id = serpy.Field()
|
||||
ref = serpy.Field()
|
||||
severity = serpy.Field(attr="severity_id")
|
||||
priority = serpy.Field(attr="priority_id")
|
||||
type = serpy.Field(attr="type_id")
|
||||
milestone = serpy.Field(attr="milestone_id")
|
||||
project = serpy.Field(attr="project_id")
|
||||
created_date = serpy.Field()
|
||||
modified_date = serpy.Field()
|
||||
finished_date = serpy.Field()
|
||||
subject = serpy.Field()
|
||||
external_reference = serpy.Field()
|
||||
version = serpy.Field()
|
||||
watchers = serpy.Field()
|
||||
|
||||
|
||||
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
|
||||
|
|
|
@ -22,31 +22,46 @@ from django.db.models import Prefetch
|
|||
from taiga.base import filters
|
||||
from taiga.base import response
|
||||
from taiga.base.decorators import detail_route
|
||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||
from taiga.base.api import ModelCrudViewSet
|
||||
from taiga.base.api import ModelListViewSet
|
||||
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||
from taiga.base.api.utils import get_object_or_404
|
||||
from taiga.base.utils.db import get_object_or_none
|
||||
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
from taiga.projects.votes.utils import attach_total_voters_to_queryset, attach_is_voter_to_queryset
|
||||
from taiga.projects.notifications.utils import attach_watchers_to_queryset, attach_is_watcher_to_queryset
|
||||
from taiga.projects.votes.utils import attach_total_voters_to_queryset
|
||||
from taiga.projects.votes.utils import attach_is_voter_to_queryset
|
||||
from taiga.projects.notifications.utils import attach_watchers_to_queryset
|
||||
from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
|
||||
from taiga.projects.userstories import utils as userstories_utils
|
||||
|
||||
from . import serializers
|
||||
from . import models
|
||||
from . import permissions
|
||||
from . import utils as milestones_utils
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||
BlockedByProjectMixin, ModelCrudViewSet):
|
||||
serializer_class = serializers.MilestoneSerializer
|
||||
permission_classes = (permissions.MilestonePermission,)
|
||||
filter_backends = (filters.CanViewMilestonesFilterBackend,)
|
||||
filter_fields = ("project", "closed")
|
||||
filter_fields = (
|
||||
"project",
|
||||
"project__slug",
|
||||
"closed"
|
||||
)
|
||||
queryset = models.Milestone.objects.all()
|
||||
|
||||
def get_serializer_class(self, *args, **kwargs):
|
||||
if self.action == "list":
|
||||
return serializers.MilestoneListSerializer
|
||||
|
||||
return serializers.MilestoneSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
res = super().list(request, *args, **kwargs)
|
||||
self._add_taiga_info_headers()
|
||||
|
@ -72,17 +87,17 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
|||
|
||||
# Userstories prefetching
|
||||
UserStory = apps.get_model("userstories", "UserStory")
|
||||
us_qs = UserStory.objects.prefetch_related("role_points",
|
||||
"role_points__points",
|
||||
"role_points__role")
|
||||
|
||||
us_qs = us_qs.select_related("milestone",
|
||||
us_qs = UserStory.objects.select_related("milestone",
|
||||
"project",
|
||||
"status",
|
||||
"owner",
|
||||
"assigned_to",
|
||||
"generated_from_issue")
|
||||
|
||||
us_qs = userstories_utils.attach_total_points(us_qs)
|
||||
us_qs = userstories_utils.attach_role_points(us_qs)
|
||||
us_qs = attach_total_voters_to_queryset(us_qs)
|
||||
us_qs = self.attach_watchers_attrs_to_queryset(us_qs)
|
||||
|
||||
if self.request.user.is_authenticated():
|
||||
|
@ -94,7 +109,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
|||
# Milestones prefetching
|
||||
qs = qs.select_related("project", "owner")
|
||||
qs = self.attach_watchers_attrs_to_queryset(qs)
|
||||
|
||||
qs = milestones_utils.attach_total_points(qs)
|
||||
qs = milestones_utils.attach_closed_points(qs)
|
||||
qs = qs.order_by("-estimated_start")
|
||||
return qs
|
||||
|
||||
|
|
|
@ -21,14 +21,18 @@ from django.utils.translation import ugettext as _
|
|||
from taiga.base.api import serializers
|
||||
from taiga.base.utils import json
|
||||
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.validators import WatchersValidator
|
||||
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
|
||||
from ..userstories.serializers import UserStoryListSerializer
|
||||
from taiga.projects.userstories.serializers import UserStoryListSerializer
|
||||
|
||||
from . import models
|
||||
|
||||
import serpy
|
||||
|
||||
class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, ValidateDuplicatedNameInProjectMixin):
|
||||
user_stories = UserStoryListSerializer(many=True, required=False, read_only=True)
|
||||
|
||||
class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer,
|
||||
ValidateDuplicatedNameInProjectMixin):
|
||||
total_points = serializers.SerializerMethodField("get_total_points")
|
||||
closed_points = serializers.SerializerMethodField("get_closed_points")
|
||||
|
||||
|
@ -41,3 +45,25 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, Val
|
|||
|
||||
def get_closed_points(self, obj):
|
||||
return sum(obj.closed_points.values())
|
||||
|
||||
|
||||
class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.LightSerializer):
|
||||
id = serpy.Field()
|
||||
name = serpy.Field()
|
||||
slug = serpy.Field()
|
||||
owner = serpy.Field(attr="owner_id")
|
||||
project = serpy.Field(attr="project_id")
|
||||
estimated_start = serpy.Field()
|
||||
estimated_finish = serpy.Field()
|
||||
created_date = serpy.Field()
|
||||
modified_date = serpy.Field()
|
||||
closed = serpy.Field()
|
||||
disponibility = serpy.Field()
|
||||
order = serpy.Field()
|
||||
watchers = serpy.Field()
|
||||
user_stories = serpy.MethodField("get_user_stories")
|
||||
total_points = serializers.Field(source="total_points_attr")
|
||||
closed_points = serializers.Field(source="closed_points_attr")
|
||||
|
||||
def get_user_stories(self, obj):
|
||||
return UserStoryListSerializer(obj.user_stories.all(), many=True).data
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
def attach_total_points(queryset, as_field="total_points_attr"):
|
||||
"""Attach total of point values to each object of the queryset.
|
||||
|
||||
:param queryset: A Django milestones queryset object.
|
||||
:param as_field: Attach the points as an attribute with this name.
|
||||
|
||||
:return: Queryset object with the additional `as_field` field.
|
||||
"""
|
||||
model = queryset.model
|
||||
sql = """SELECT SUM(projects_points.value)
|
||||
FROM userstories_rolepoints
|
||||
INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id
|
||||
INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
|
||||
WHERE userstories_userstory.milestone_id = {tbl}.id"""
|
||||
|
||||
sql = sql.format(tbl=model._meta.db_table)
|
||||
queryset = queryset.extra(select={as_field: sql})
|
||||
return queryset
|
||||
|
||||
|
||||
def attach_closed_points(queryset, as_field="closed_points_attr"):
|
||||
"""Attach total of closed point values to each object of the queryset.
|
||||
|
||||
:param queryset: A Django milestones queryset object.
|
||||
:param as_field: Attach the points as an attribute with this name.
|
||||
|
||||
:return: Queryset object with the additional `as_field` field.
|
||||
"""
|
||||
model = queryset.model
|
||||
sql = """SELECT SUM(projects_points.value)
|
||||
FROM userstories_rolepoints
|
||||
INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id
|
||||
INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
|
||||
WHERE userstories_userstory.milestone_id = {tbl}.id AND userstories_userstory.is_closed=True"""
|
||||
|
||||
sql = sql.format(tbl=model._meta.db_table)
|
||||
queryset = queryset.extra(select={as_field: sql})
|
||||
return queryset
|
|
@ -17,9 +17,12 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from taiga.base.api import serializers
|
||||
from taiga.users.serializers import ListUserBasicInfoSerializer
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
import serpy
|
||||
|
||||
class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
|
||||
|
||||
def validate_name(self, attrs, source):
|
||||
|
@ -39,3 +42,55 @@ class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
|
|||
raise serializers.ValidationError(_("Name duplicated for the project"))
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
class CachedSerializedUsersMixin(serpy.Serializer):
|
||||
def to_value(self, instance):
|
||||
self._serialized_users = {}
|
||||
return super().to_value(instance)
|
||||
|
||||
def get_user_extra_info(self, user):
|
||||
if user is None:
|
||||
return None
|
||||
|
||||
serialized_user = self._serialized_users.get(user.id, None)
|
||||
if serialized_user is None:
|
||||
serializer_user = ListUserBasicInfoSerializer(user).data
|
||||
self._serialized_users[user.id] = serializer_user
|
||||
|
||||
return serialized_user
|
||||
|
||||
|
||||
class OwnerExtraInfoMixin(CachedSerializedUsersMixin):
|
||||
owner = serpy.Field(attr="owner_id")
|
||||
owner_extra_info = serpy.MethodField()
|
||||
|
||||
def get_owner_extra_info(self, obj):
|
||||
return self.get_user_extra_info(obj.owner)
|
||||
|
||||
|
||||
class AssigedToExtraInfoMixin(CachedSerializedUsersMixin):
|
||||
assigned_to = serpy.Field(attr="assigned_to_id")
|
||||
assigned_to_extra_info = serpy.MethodField()
|
||||
|
||||
def get_assigned_to_extra_info(self, obj):
|
||||
return self.get_user_extra_info(obj.assigned_to)
|
||||
|
||||
|
||||
class StatusExtraInfoMixin(serpy.Serializer):
|
||||
status = serpy.Field(attr="status_id")
|
||||
status_extra_info = serpy.MethodField()
|
||||
def to_value(self, instance):
|
||||
self._serialized_status = {}
|
||||
return super().to_value(instance)
|
||||
|
||||
def get_status_extra_info(self, obj):
|
||||
serialized_status = self._serialized_status.get(obj.status_id, None)
|
||||
if serialized_status is None:
|
||||
serialized_status = {
|
||||
"name": _(obj.status.name),
|
||||
"color": obj.status.color
|
||||
}
|
||||
self._serialized_status[obj.status_id] = serialized_status
|
||||
|
||||
return serialized_status
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import serpy
|
||||
|
||||
from functools import partial
|
||||
from operator import is_not
|
||||
|
||||
|
@ -183,10 +186,7 @@ class WatchedModelMixin(object):
|
|||
return frozenset(filter(is_not_none, participants))
|
||||
|
||||
|
||||
class WatchedResourceModelSerializer(serializers.ModelSerializer):
|
||||
is_watcher = serializers.SerializerMethodField("get_is_watcher")
|
||||
total_watchers = serializers.SerializerMethodField("get_total_watchers")
|
||||
|
||||
class BaseWatchedResourceModelSerializer(object):
|
||||
def get_is_watcher(self, obj):
|
||||
if "request" in self.context:
|
||||
user = self.context["request"].user
|
||||
|
@ -199,6 +199,16 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer):
|
|||
return getattr(obj, "total_watchers", 0) or 0
|
||||
|
||||
|
||||
class WatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serializers.ModelSerializer):
|
||||
is_watcher = serializers.SerializerMethodField("get_is_watcher")
|
||||
total_watchers = serializers.SerializerMethodField("get_total_watchers")
|
||||
|
||||
|
||||
class ListWatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serpy.Serializer):
|
||||
is_watcher = serializers.SerializerMethodField("get_is_watcher")
|
||||
total_watchers = serializers.SerializerMethodField("get_total_watchers")
|
||||
|
||||
|
||||
class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
|
||||
watchers = WatchersField(required=False)
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import serpy
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import Q
|
||||
|
||||
|
|
|
@ -44,13 +44,12 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
|||
permission_classes = (permissions.TaskPermission,)
|
||||
filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
|
||||
retrieve_exclude_filters = (filters.WatchersFilter,)
|
||||
filter_fields = [
|
||||
"user_story",
|
||||
filter_fields = ["user_story",
|
||||
"milestone",
|
||||
"project",
|
||||
"project__slug",
|
||||
"assigned_to",
|
||||
"status__is_closed"
|
||||
]
|
||||
"status__is_closed"]
|
||||
|
||||
def get_serializer_class(self, *args, **kwargs):
|
||||
if self.action in ["retrieve", "by_ref"]:
|
||||
|
@ -95,8 +94,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
|||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
qs = self.attach_votes_attrs_to_queryset(qs)
|
||||
qs = qs.select_related(
|
||||
"milestone",
|
||||
qs = qs.select_related("milestone",
|
||||
"owner",
|
||||
"assigned_to",
|
||||
"status",
|
||||
|
|
|
@ -16,13 +16,19 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from taiga.base.api import serializers
|
||||
from taiga.base.fields import PgArrayField
|
||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||
|
||||
|
||||
from taiga.projects.milestones.validators import SprintExistsValidator
|
||||
from taiga.projects.mixins.serializers import OwnerExtraInfoMixin
|
||||
from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin
|
||||
from taiga.projects.mixins.serializers import StatusExtraInfoMixin
|
||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.validators import WatchersValidator
|
||||
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
|
||||
from taiga.mdrender.service import render as mdrender
|
||||
|
@ -30,11 +36,15 @@ from taiga.projects.tagging.fields import TagsAndTagsColorsField
|
|||
from taiga.projects.tasks.validators import TaskExistsValidator
|
||||
from taiga.projects.validators import ProjectExistsValidator
|
||||
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
||||
from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
|
||||
|
||||
from taiga.users.serializers import UserBasicInfoSerializer
|
||||
from taiga.users.services import get_photo_or_gravatar_url
|
||||
from taiga.users.services import get_big_photo_or_gravatar_url
|
||||
|
||||
from . import models
|
||||
|
||||
import serpy
|
||||
|
||||
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
||||
serializers.ModelSerializer):
|
||||
|
@ -72,11 +82,35 @@ class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWat
|
|||
return obj.status is not None and obj.status.is_closed
|
||||
|
||||
|
||||
class TaskListSerializer(TaskSerializer):
|
||||
class Meta:
|
||||
model = models.Task
|
||||
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
||||
exclude = ("description", "description_html")
|
||||
class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
|
||||
OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin,
|
||||
serializers.LightSerializer):
|
||||
id = serpy.Field()
|
||||
user_story = serpy.Field(attr="user_story_id")
|
||||
ref = serpy.Field()
|
||||
project = serpy.Field(attr="project_id")
|
||||
milestone = serpy.Field(attr="milestone_id")
|
||||
milestone_slug = serpy.MethodField("get_milestone_slug")
|
||||
created_date = serpy.Field()
|
||||
modified_date = serpy.Field()
|
||||
finished_date = serpy.Field()
|
||||
subject = serpy.Field()
|
||||
us_order = serpy.Field()
|
||||
taskboard_order = serpy.Field()
|
||||
is_iocaine = serpy.Field()
|
||||
external_reference = serpy.Field()
|
||||
version = serpy.Field()
|
||||
watchers = serpy.Field()
|
||||
is_blocked = serpy.Field()
|
||||
blocked_note = serpy.Field()
|
||||
tags = serpy.Field()
|
||||
is_closed = serpy.MethodField()
|
||||
|
||||
def get_milestone_slug(self, obj):
|
||||
return obj.milestone.slug if obj.milestone else None
|
||||
|
||||
def get_is_closed(self, obj):
|
||||
return obj.status is not None and obj.status.is_closed
|
||||
|
||||
|
||||
class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
|
||||
|
|
|
@ -16,8 +16,13 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from contextlib import closing
|
||||
from collections import namedtuple
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import transaction
|
||||
from django.db import transaction, connection
|
||||
from django.db.models.sql import datastructures
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
@ -27,17 +32,23 @@ from taiga.base import response
|
|||
from taiga.base import status
|
||||
from taiga.base.decorators import list_route
|
||||
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||
from taiga.base.api import ModelCrudViewSet
|
||||
from taiga.base.api import ModelListViewSet
|
||||
from taiga.base.api.utils import get_object_or_404
|
||||
|
||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||
from taiga.projects.history.services import take_snapshot
|
||||
from taiga.projects.milestones.models import Milestone
|
||||
from taiga.projects.models import Project, UserStoryStatus
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||
from taiga.projects.notifications.mixins import WatchedResourceMixin
|
||||
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||
from taiga.projects.occ import OCCResourceMixin
|
||||
from taiga.projects.tagging.api import TaggedResourceMixin
|
||||
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||
from taiga.projects.userstories.models import RolePoints
|
||||
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin
|
||||
from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
|
||||
from taiga.projects.userstories.utils import attach_total_points
|
||||
from taiga.projects.userstories.utils import attach_role_points
|
||||
|
||||
from . import models
|
||||
from . import permissions
|
||||
|
@ -63,6 +74,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
|||
filters.TagsFilter,
|
||||
filters.WatchersFilter)
|
||||
filter_fields = ["project",
|
||||
"project__slug",
|
||||
"milestone",
|
||||
"milestone__isnull",
|
||||
"is_closed",
|
||||
|
@ -87,9 +99,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
|||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
qs = qs.prefetch_related("role_points",
|
||||
"role_points__points",
|
||||
"role_points__role")
|
||||
qs = qs.select_related("milestone",
|
||||
"project",
|
||||
"status",
|
||||
|
@ -97,7 +106,10 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
|||
"assigned_to",
|
||||
"generated_from_issue")
|
||||
qs = self.attach_votes_attrs_to_queryset(qs)
|
||||
return self.attach_watchers_attrs_to_queryset(qs)
|
||||
qs = self.attach_watchers_attrs_to_queryset(qs)
|
||||
qs = attach_total_points(qs)
|
||||
qs = attach_role_points(qs)
|
||||
return qs
|
||||
|
||||
def pre_conditions_on_save(self, obj):
|
||||
super().pre_conditions_on_save(obj)
|
||||
|
|
|
@ -16,6 +16,11 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from collections import ChainMap
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from taiga.base.api import serializers
|
||||
from taiga.base.api.utils import get_object_or_404
|
||||
from taiga.base.fields import PickledObjectField
|
||||
|
@ -23,21 +28,33 @@ from taiga.base.fields import PgArrayField
|
|||
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||
from taiga.base.utils import json
|
||||
|
||||
from taiga.projects.milestones.validators import SprintExistsValidator
|
||||
from taiga.projects.models import Project
|
||||
from taiga.projects.notifications.validators import WatchersValidator
|
||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||
from taiga.projects.serializers import BasicUserStoryStatusSerializer
|
||||
from taiga.mdrender.service import render as mdrender
|
||||
|
||||
from taiga.projects.milestones.validators import SprintExistsValidator
|
||||
from taiga.projects.models import Project, UserStoryStatus
|
||||
from taiga.projects.mixins.serializers import OwnerExtraInfoMixin
|
||||
from taiga.projects.mixins.serializers import AssigedToExtraInfoMixin
|
||||
from taiga.projects.mixins.serializers import StatusExtraInfoMixin
|
||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
|
||||
from taiga.projects.notifications.validators import WatchersValidator
|
||||
from taiga.projects.serializers import BasicUserStoryStatusSerializer
|
||||
from taiga.projects.tagging.fields import TagsAndTagsColorsField
|
||||
from taiga.projects.userstories.validators import UserStoryExistsValidator
|
||||
from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
|
||||
from taiga.projects.validators import ProjectExistsValidator
|
||||
from taiga.projects.validators import UserStoryStatusExistsValidator
|
||||
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
||||
from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
|
||||
|
||||
from taiga.users.serializers import UserBasicInfoSerializer
|
||||
from taiga.users.serializers import ListUserBasicInfoSerializer
|
||||
from taiga.users.services import get_photo_or_gravatar_url
|
||||
from taiga.users.services import get_big_photo_or_gravatar_url
|
||||
|
||||
from . import models
|
||||
|
||||
import serpy
|
||||
|
||||
|
||||
class RolePointsField(serializers.WritableField):
|
||||
def to_native(self, obj):
|
||||
|
@ -106,12 +123,68 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin,
|
|||
return mdrender(obj.project, obj.description)
|
||||
|
||||
|
||||
class UserStoryListSerializer(UserStorySerializer):
|
||||
class Meta:
|
||||
model = models.UserStory
|
||||
depth = 0
|
||||
read_only_fields = ('created_date', 'modified_date')
|
||||
exclude = ("description", "description_html")
|
||||
class ListOriginIssueSerializer(serializers.LightSerializer):
|
||||
id = serpy.Field()
|
||||
ref = serpy.Field()
|
||||
subject = serpy.Field()
|
||||
|
||||
def to_value(self, instance):
|
||||
if instance is None:
|
||||
return None
|
||||
|
||||
return super().to_value(instance)
|
||||
|
||||
|
||||
class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
|
||||
OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin, serializers.LightSerializer):
|
||||
|
||||
id = serpy.Field()
|
||||
ref = serpy.Field()
|
||||
milestone = serpy.Field(attr="milestone_id")
|
||||
milestone_slug = serpy.MethodField()
|
||||
milestone_name = serpy.MethodField()
|
||||
project = serpy.Field(attr="project_id")
|
||||
is_closed = serpy.Field()
|
||||
points = serpy.MethodField()
|
||||
backlog_order = serpy.Field()
|
||||
sprint_order = serpy.Field()
|
||||
kanban_order = serpy.Field()
|
||||
created_date = serpy.Field()
|
||||
modified_date = serpy.Field()
|
||||
finish_date = serpy.Field()
|
||||
subject = serpy.Field()
|
||||
client_requirement = serpy.Field()
|
||||
team_requirement = serpy.Field()
|
||||
generated_from_issue = serpy.Field(attr="generated_from_issue_id")
|
||||
external_reference = serpy.Field()
|
||||
tribe_gig = serpy.Field()
|
||||
version = serpy.Field()
|
||||
watchers = serpy.Field()
|
||||
is_blocked = serpy.Field()
|
||||
blocked_note = serpy.Field()
|
||||
tags = serpy.Field()
|
||||
total_points = serpy.Field("total_points_attr")
|
||||
comment = serpy.MethodField("get_comment")
|
||||
origin_issue = ListOriginIssueSerializer(attr="generated_from_issue")
|
||||
|
||||
def to_value(self, instance):
|
||||
self._serialized_status = {}
|
||||
return super().to_value(instance)
|
||||
|
||||
def get_milestone_slug(self, obj):
|
||||
return obj.milestone.slug if obj.milestone else None
|
||||
|
||||
def get_milestone_name(self, obj):
|
||||
return obj.milestone.name if obj.milestone else None
|
||||
|
||||
def get_points(self, obj):
|
||||
if obj.role_points_attr is None:
|
||||
return {}
|
||||
|
||||
return dict(ChainMap(*json.loads(obj.role_points_attr)))
|
||||
|
||||
def get_comment(self, obj):
|
||||
return ""
|
||||
|
||||
|
||||
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
|
||||
|
@ -128,7 +201,8 @@ class NeighborUserStorySerializer(serializers.ModelSerializer):
|
|||
depth = 0
|
||||
|
||||
|
||||
class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer):
|
||||
class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator,
|
||||
serializers.Serializer):
|
||||
project_id = serializers.IntegerField()
|
||||
status_id = serializers.IntegerField(required=False)
|
||||
bulk_stories = serializers.CharField()
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
def attach_total_points(queryset, as_field="total_points_attr"):
|
||||
"""Attach total of point values to each object of the queryset.
|
||||
|
||||
:param queryset: A Django user stories queryset object.
|
||||
:param as_field: Attach the points as an attribute with this name.
|
||||
|
||||
:return: Queryset object with the additional `as_field` field.
|
||||
"""
|
||||
model = queryset.model
|
||||
sql = """SELECT SUM(projects_points.value)
|
||||
FROM userstories_rolepoints
|
||||
INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
|
||||
WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
|
||||
|
||||
sql = sql.format(tbl=model._meta.db_table)
|
||||
queryset = queryset.extra(select={as_field: sql})
|
||||
return queryset
|
||||
|
||||
|
||||
def attach_role_points(queryset, as_field="role_points_attr"):
|
||||
"""Attach role point as json column to each object of the queryset.
|
||||
|
||||
:param queryset: A Django user stories queryset object.
|
||||
:param as_field: Attach the role points as an attribute with this name.
|
||||
|
||||
:return: Queryset object with the additional `as_field` field.
|
||||
"""
|
||||
model = queryset.model
|
||||
sql = """SELECT json_agg(json_build_object(userstories_rolepoints.role_id,
|
||||
userstories_rolepoints.points_id))::text
|
||||
FROM userstories_rolepoints
|
||||
WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
|
||||
|
||||
sql = sql.format(tbl=model._meta.db_table)
|
||||
queryset = queryset.extra(select={as_field: sql})
|
||||
return queryset
|
|
@ -16,13 +16,12 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import serpy
|
||||
|
||||
from taiga.base.api import serializers
|
||||
|
||||
|
||||
class VoteResourceSerializerMixin(serializers.ModelSerializer):
|
||||
is_voter = serializers.SerializerMethodField("get_is_voter")
|
||||
total_voters = serializers.SerializerMethodField("get_total_voters")
|
||||
|
||||
class BaseVoteResourceSerializerMixin(object):
|
||||
def get_is_voter(self, obj):
|
||||
# The "is_voted" attribute is attached in the get_queryset of the viewset.
|
||||
return getattr(obj, "is_voter", False) or False
|
||||
|
@ -30,3 +29,13 @@ class VoteResourceSerializerMixin(serializers.ModelSerializer):
|
|||
def get_total_voters(self, obj):
|
||||
# The "total_voters" attribute is attached in the get_queryset of the viewset.
|
||||
return getattr(obj, "total_voters", 0) or 0
|
||||
|
||||
|
||||
class VoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serializers.ModelSerializer):
|
||||
is_voter = serializers.SerializerMethodField("get_is_voter")
|
||||
total_voters = serializers.SerializerMethodField("get_total_voters")
|
||||
|
||||
|
||||
class ListVoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serpy.Serializer):
|
||||
is_voter = serpy.MethodField("get_is_voter")
|
||||
total_voters = serpy.MethodField("get_total_voters")
|
||||
|
|
|
@ -33,6 +33,7 @@ from .gravatar import get_gravatar_url
|
|||
from collections import namedtuple
|
||||
|
||||
import re
|
||||
import serpy
|
||||
|
||||
|
||||
######################################################
|
||||
|
@ -144,6 +145,24 @@ class UserBasicInfoSerializer(UserSerializer):
|
|||
fields = ("username", "full_name_display", "photo", "big_photo", "is_active", "id")
|
||||
|
||||
|
||||
class ListUserBasicInfoSerializer(serpy.Serializer):
|
||||
username = serpy.Field()
|
||||
full_name_display = serpy.MethodField()
|
||||
photo = serpy.MethodField()
|
||||
big_photo = serpy.MethodField()
|
||||
is_active = serpy.Field()
|
||||
id = serpy.Field()
|
||||
|
||||
def get_full_name_display(self, obj):
|
||||
return obj.get_full_name()
|
||||
|
||||
def get_photo(self, obj):
|
||||
return get_photo_or_gravatar_url(obj)
|
||||
|
||||
def get_big_photo(self, obj):
|
||||
return get_big_photo_or_gravatar_url(obj)
|
||||
|
||||
|
||||
class RecoverySerializer(serializers.Serializer):
|
||||
token = serializers.CharField(max_length=200)
|
||||
password = serializers.CharField(min_length=6)
|
||||
|
|
Loading…
Reference in New Issue