From 7968c8037660764d9e3405d95587605db8830621 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 15 Jun 2016 08:52:31 +0200 Subject: [PATCH] Improving API performance for us, task, milestone and issue listing --- CHANGELOG.md | 1 + requirements.txt | 1 + taiga/base/api/serializers.py | 9 ++ taiga/projects/issues/api.py | 1 + taiga/projects/issues/serializers.py | 34 +++++-- taiga/projects/milestones/api.py | 48 ++++++---- taiga/projects/milestones/serializers.py | 32 ++++++- taiga/projects/milestones/utils.py | 58 ++++++++++++ taiga/projects/mixins/serializers.py | 55 ++++++++++++ taiga/projects/notifications/mixins.py | 18 +++- taiga/projects/serializers.py | 2 + taiga/projects/tasks/api.py | 24 +++-- taiga/projects/tasks/serializers.py | 46 ++++++++-- taiga/projects/userstories/api.py | 28 ++++-- taiga/projects/userstories/serializers.py | 100 ++++++++++++++++++--- taiga/projects/userstories/utils.py | 56 ++++++++++++ taiga/projects/votes/mixins/serializers.py | 17 +++- taiga/users/serializers.py | 19 ++++ 18 files changed, 474 insertions(+), 75 deletions(-) create mode 100644 taiga/projects/milestones/utils.py create mode 100644 taiga/projects/userstories/utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 439d9bb8..3068820d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/requirements.txt b/requirements.txt index 93d225f0..a1262fdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index a9e5f139..7de82458 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -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) diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index cae23be3..57acfca8 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -65,6 +65,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W filters.WatchersFilter,) filter_fields = ("project", + "project__slug", "status__is_closed") order_by_fields = ("type", diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 83557f20..4243ea31 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -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): diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index f109060b..1520f2c7 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -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", - "project", - "status", - "owner", - "assigned_to", - "generated_from_issue") + 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 diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index 2a52be47..b3df7c8f 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -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 diff --git a/taiga/projects/milestones/utils.py b/taiga/projects/milestones/utils.py new file mode 100644 index 00000000..a32d7684 --- /dev/null +++ b/taiga/projects/milestones/utils.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + + +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 diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py index 07a9b683..2d788298 100644 --- a/taiga/projects/mixins/serializers.py +++ b/taiga/projects/mixins/serializers.py @@ -17,9 +17,12 @@ # along with this program. If not, see . 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 diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index ee1d59f8..62db374e 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -15,6 +15,9 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . + +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) diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index f388528f..9c185a97 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import serpy + from django.utils.translation import ugettext as _ from django.db.models import Q diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 9ebf6dfe..b7c13ab1 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -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", - "milestone", - "project", - "assigned_to", - "status__is_closed" - ] + filter_fields = ["user_story", + "milestone", + "project", + "project__slug", + "assigned_to", + "status__is_closed"] def get_serializer_class(self, *args, **kwargs): if self.action in ["retrieve", "by_ref"]: @@ -95,12 +94,11 @@ 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", - "owner", - "assigned_to", - "status", - "project") + qs = qs.select_related("milestone", + "owner", + "assigned_to", + "status", + "project") return self.attach_watchers_attrs_to_queryset(qs) diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index c0c8334a..d7423e66 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -16,13 +16,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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): diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index b6693c36..dbe5c433 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -16,8 +16,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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) diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index dae58a18..c863c87b 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -16,6 +16,11 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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() diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py new file mode 100644 index 00000000..36a9970d --- /dev/null +++ b/taiga/projects/userstories/utils.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# 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 . + + +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 diff --git a/taiga/projects/votes/mixins/serializers.py b/taiga/projects/votes/mixins/serializers.py index 1a6faeb2..fc7a988e 100644 --- a/taiga/projects/votes/mixins/serializers.py +++ b/taiga/projects/votes/mixins/serializers.py @@ -16,13 +16,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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") diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index e35e56cb..98903ec1 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -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)