Improving API performance for us, task, milestone and issue listing

remotes/origin/issue/4795/notification_even_they_are_disabled
Alejandro Alonso 2016-06-15 08:52:31 +02:00 committed by David Barragán Merino
parent 117a97f12c
commit 7968c80376
18 changed files with 474 additions and 75 deletions

View File

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

View File

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

View File

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

View File

@ -65,6 +65,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
filters.WatchersFilter,)
filter_fields = ("project",
"project__slug",
"status__is_closed")
order_by_fields = ("type",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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