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.
|
- Select a color (or not) to a tag when add it to stories, issues and tasks.
|
||||||
|
|
||||||
### Misc
|
### Misc
|
||||||
|
- [API] Improve performance of some calls over list.
|
||||||
- Lots of small and not so small bugfixes.
|
- 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
|
pyjwkest==1.1.5
|
||||||
python-dateutil==2.4.2
|
python-dateutil==2.4.2
|
||||||
netaddr==0.7.18
|
netaddr==0.7.18
|
||||||
|
serpy==0.1.1
|
||||||
|
|
|
@ -69,6 +69,7 @@ import copy
|
||||||
import datetime
|
import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import types
|
import types
|
||||||
|
import serpy
|
||||||
|
|
||||||
# Note: We do the following so that users of the framework can use this style:
|
# 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()
|
"model_name": model_meta.object_name.lower()
|
||||||
}
|
}
|
||||||
return self._default_view_name % format_kwargs
|
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,)
|
filters.WatchersFilter,)
|
||||||
|
|
||||||
filter_fields = ("project",
|
filter_fields = ("project",
|
||||||
|
"project__slug",
|
||||||
"status__is_closed")
|
"status__is_closed")
|
||||||
|
|
||||||
order_by_fields = ("type",
|
order_by_fields = ("type",
|
||||||
|
|
|
@ -20,18 +20,24 @@ from taiga.base.api import serializers
|
||||||
from taiga.base.fields import PgArrayField
|
from taiga.base.fields import PgArrayField
|
||||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
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.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.tagging.fields import TagsAndTagsColorsField
|
||||||
|
from taiga.projects.serializers import BasicIssueStatusSerializer
|
||||||
from taiga.projects.validators import ProjectExistsValidator
|
from taiga.projects.validators import ProjectExistsValidator
|
||||||
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
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 UserBasicInfoSerializer
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
import serpy
|
||||||
|
|
||||||
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
||||||
serializers.ModelSerializer):
|
serializers.ModelSerializer):
|
||||||
|
@ -68,11 +74,23 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa
|
||||||
return mdrender(obj.project, obj.description)
|
return mdrender(obj.project, obj.description)
|
||||||
|
|
||||||
|
|
||||||
class IssueListSerializer(IssueSerializer):
|
class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
|
||||||
class Meta:
|
OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin,
|
||||||
model = models.Issue
|
serializers.LightSerializer):
|
||||||
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
id = serpy.Field()
|
||||||
exclude = ("description", "description_html")
|
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):
|
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
|
||||||
|
|
|
@ -22,31 +22,46 @@ from django.db.models import Prefetch
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
from taiga.base.decorators import detail_route
|
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.mixins import BlockedByProjectMixin
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.base.utils.db import get_object_or_none
|
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.history.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.votes.utils import attach_total_voters_to_queryset, attach_is_voter_to_queryset
|
from taiga.projects.votes.utils import attach_total_voters_to_queryset
|
||||||
from taiga.projects.notifications.utils import attach_watchers_to_queryset, attach_is_watcher_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 serializers
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
from . import utils as milestones_utils
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||||
BlockedByProjectMixin, ModelCrudViewSet):
|
BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
serializer_class = serializers.MilestoneSerializer
|
|
||||||
permission_classes = (permissions.MilestonePermission,)
|
permission_classes = (permissions.MilestonePermission,)
|
||||||
filter_backends = (filters.CanViewMilestonesFilterBackend,)
|
filter_backends = (filters.CanViewMilestonesFilterBackend,)
|
||||||
filter_fields = ("project", "closed")
|
filter_fields = (
|
||||||
|
"project",
|
||||||
|
"project__slug",
|
||||||
|
"closed"
|
||||||
|
)
|
||||||
queryset = models.Milestone.objects.all()
|
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):
|
def list(self, request, *args, **kwargs):
|
||||||
res = super().list(request, *args, **kwargs)
|
res = super().list(request, *args, **kwargs)
|
||||||
self._add_taiga_info_headers()
|
self._add_taiga_info_headers()
|
||||||
|
@ -72,17 +87,17 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||||
|
|
||||||
# Userstories prefetching
|
# Userstories prefetching
|
||||||
UserStory = apps.get_model("userstories", "UserStory")
|
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",
|
"project",
|
||||||
"status",
|
"status",
|
||||||
"owner",
|
"owner",
|
||||||
"assigned_to",
|
"assigned_to",
|
||||||
"generated_from_issue")
|
"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)
|
us_qs = self.attach_watchers_attrs_to_queryset(us_qs)
|
||||||
|
|
||||||
if self.request.user.is_authenticated():
|
if self.request.user.is_authenticated():
|
||||||
|
@ -94,7 +109,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||||
# Milestones prefetching
|
# Milestones prefetching
|
||||||
qs = qs.select_related("project", "owner")
|
qs = qs.select_related("project", "owner")
|
||||||
qs = self.attach_watchers_attrs_to_queryset(qs)
|
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")
|
qs = qs.order_by("-estimated_start")
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
|
@ -21,14 +21,18 @@ from django.utils.translation import ugettext as _
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
||||||
|
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
|
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
|
||||||
from ..userstories.serializers import UserStoryListSerializer
|
from taiga.projects.userstories.serializers import UserStoryListSerializer
|
||||||
|
|
||||||
from . import models
|
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")
|
total_points = serializers.SerializerMethodField("get_total_points")
|
||||||
closed_points = serializers.SerializerMethodField("get_closed_points")
|
closed_points = serializers.SerializerMethodField("get_closed_points")
|
||||||
|
|
||||||
|
@ -41,3 +45,25 @@ class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, Val
|
||||||
|
|
||||||
def get_closed_points(self, obj):
|
def get_closed_points(self, obj):
|
||||||
return sum(obj.closed_points.values())
|
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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.users.serializers import ListUserBasicInfoSerializer
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
import serpy
|
||||||
|
|
||||||
class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
|
class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
|
||||||
|
|
||||||
def validate_name(self, attrs, source):
|
def validate_name(self, attrs, source):
|
||||||
|
@ -39,3 +42,55 @@ class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
|
||||||
raise serializers.ValidationError(_("Name duplicated for the project"))
|
raise serializers.ValidationError(_("Name duplicated for the project"))
|
||||||
|
|
||||||
return attrs
|
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
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import serpy
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from operator import is_not
|
from operator import is_not
|
||||||
|
|
||||||
|
@ -183,10 +186,7 @@ class WatchedModelMixin(object):
|
||||||
return frozenset(filter(is_not_none, participants))
|
return frozenset(filter(is_not_none, participants))
|
||||||
|
|
||||||
|
|
||||||
class WatchedResourceModelSerializer(serializers.ModelSerializer):
|
class BaseWatchedResourceModelSerializer(object):
|
||||||
is_watcher = serializers.SerializerMethodField("get_is_watcher")
|
|
||||||
total_watchers = serializers.SerializerMethodField("get_total_watchers")
|
|
||||||
|
|
||||||
def get_is_watcher(self, obj):
|
def get_is_watcher(self, obj):
|
||||||
if "request" in self.context:
|
if "request" in self.context:
|
||||||
user = self.context["request"].user
|
user = self.context["request"].user
|
||||||
|
@ -199,6 +199,16 @@ class WatchedResourceModelSerializer(serializers.ModelSerializer):
|
||||||
return getattr(obj, "total_watchers", 0) or 0
|
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):
|
class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
|
||||||
watchers = WatchersField(required=False)
|
watchers = WatchersField(required=False)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import serpy
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
|
@ -44,13 +44,12 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
||||||
permission_classes = (permissions.TaskPermission,)
|
permission_classes = (permissions.TaskPermission,)
|
||||||
filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
|
filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter)
|
||||||
retrieve_exclude_filters = (filters.WatchersFilter,)
|
retrieve_exclude_filters = (filters.WatchersFilter,)
|
||||||
filter_fields = [
|
filter_fields = ["user_story",
|
||||||
"user_story",
|
"milestone",
|
||||||
"milestone",
|
"project",
|
||||||
"project",
|
"project__slug",
|
||||||
"assigned_to",
|
"assigned_to",
|
||||||
"status__is_closed"
|
"status__is_closed"]
|
||||||
]
|
|
||||||
|
|
||||||
def get_serializer_class(self, *args, **kwargs):
|
def get_serializer_class(self, *args, **kwargs):
|
||||||
if self.action in ["retrieve", "by_ref"]:
|
if self.action in ["retrieve", "by_ref"]:
|
||||||
|
@ -95,12 +94,11 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
qs = self.attach_votes_attrs_to_queryset(qs)
|
qs = self.attach_votes_attrs_to_queryset(qs)
|
||||||
qs = qs.select_related(
|
qs = qs.select_related("milestone",
|
||||||
"milestone",
|
"owner",
|
||||||
"owner",
|
"assigned_to",
|
||||||
"assigned_to",
|
"status",
|
||||||
"status",
|
"project")
|
||||||
"project")
|
|
||||||
|
|
||||||
return self.attach_watchers_attrs_to_queryset(qs)
|
return self.attach_watchers_attrs_to_queryset(qs)
|
||||||
|
|
||||||
|
|
|
@ -16,13 +16,19 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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.api import serializers
|
||||||
from taiga.base.fields import PgArrayField
|
from taiga.base.fields import PgArrayField
|
||||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
|
|
||||||
|
|
||||||
from taiga.projects.milestones.validators import SprintExistsValidator
|
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 EditableWatchedResourceModelSerializer
|
||||||
|
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
|
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
|
||||||
from taiga.mdrender.service import render as mdrender
|
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.tasks.validators import TaskExistsValidator
|
||||||
from taiga.projects.validators import ProjectExistsValidator
|
from taiga.projects.validators import ProjectExistsValidator
|
||||||
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
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 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
|
from . import models
|
||||||
|
|
||||||
|
import serpy
|
||||||
|
|
||||||
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
|
||||||
serializers.ModelSerializer):
|
serializers.ModelSerializer):
|
||||||
|
@ -72,11 +82,35 @@ class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWat
|
||||||
return obj.status is not None and obj.status.is_closed
|
return obj.status is not None and obj.status.is_closed
|
||||||
|
|
||||||
|
|
||||||
class TaskListSerializer(TaskSerializer):
|
class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
|
||||||
class Meta:
|
OwnerExtraInfoMixin, AssigedToExtraInfoMixin, StatusExtraInfoMixin,
|
||||||
model = models.Task
|
serializers.LightSerializer):
|
||||||
read_only_fields = ('id', 'ref', 'created_date', 'modified_date')
|
id = serpy.Field()
|
||||||
exclude = ("description", "description_html")
|
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):
|
class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
|
||||||
|
|
|
@ -16,8 +16,13 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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.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.utils.translation import ugettext as _
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
@ -27,17 +32,23 @@ from taiga.base import response
|
||||||
from taiga.base import status
|
from taiga.base import status
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base.decorators import list_route
|
||||||
from taiga.base.api.mixins import BlockedByProjectMixin
|
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.base.api.utils import get_object_or_404
|
||||||
|
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.history.services import take_snapshot
|
from taiga.projects.history.services import take_snapshot
|
||||||
from taiga.projects.milestones.models import Milestone
|
from taiga.projects.milestones.models import Milestone
|
||||||
from taiga.projects.models import Project, UserStoryStatus
|
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.occ import OCCResourceMixin
|
||||||
from taiga.projects.tagging.api import TaggedResourceMixin
|
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 models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
@ -63,6 +74,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
filters.TagsFilter,
|
filters.TagsFilter,
|
||||||
filters.WatchersFilter)
|
filters.WatchersFilter)
|
||||||
filter_fields = ["project",
|
filter_fields = ["project",
|
||||||
|
"project__slug",
|
||||||
"milestone",
|
"milestone",
|
||||||
"milestone__isnull",
|
"milestone__isnull",
|
||||||
"is_closed",
|
"is_closed",
|
||||||
|
@ -87,9 +99,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
qs = qs.prefetch_related("role_points",
|
|
||||||
"role_points__points",
|
|
||||||
"role_points__role")
|
|
||||||
qs = qs.select_related("milestone",
|
qs = qs.select_related("milestone",
|
||||||
"project",
|
"project",
|
||||||
"status",
|
"status",
|
||||||
|
@ -97,7 +106,10 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
"assigned_to",
|
"assigned_to",
|
||||||
"generated_from_issue")
|
"generated_from_issue")
|
||||||
qs = self.attach_votes_attrs_to_queryset(qs)
|
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):
|
def pre_conditions_on_save(self, obj):
|
||||||
super().pre_conditions_on_save(obj)
|
super().pre_conditions_on_save(obj)
|
||||||
|
|
|
@ -16,6 +16,11 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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 import serializers
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.base.fields import PickledObjectField
|
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.neighbors import NeighborsSerializerMixin
|
||||||
from taiga.base.utils import json
|
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.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.tagging.fields import TagsAndTagsColorsField
|
||||||
from taiga.projects.userstories.validators import UserStoryExistsValidator
|
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 VoteResourceSerializerMixin
|
||||||
|
from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
|
||||||
|
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
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
|
from . import models
|
||||||
|
|
||||||
|
import serpy
|
||||||
|
|
||||||
|
|
||||||
class RolePointsField(serializers.WritableField):
|
class RolePointsField(serializers.WritableField):
|
||||||
def to_native(self, obj):
|
def to_native(self, obj):
|
||||||
|
@ -106,12 +123,68 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin,
|
||||||
return mdrender(obj.project, obj.description)
|
return mdrender(obj.project, obj.description)
|
||||||
|
|
||||||
|
|
||||||
class UserStoryListSerializer(UserStorySerializer):
|
class ListOriginIssueSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = serpy.Field()
|
||||||
model = models.UserStory
|
ref = serpy.Field()
|
||||||
depth = 0
|
subject = serpy.Field()
|
||||||
read_only_fields = ('created_date', 'modified_date')
|
|
||||||
exclude = ("description", "description_html")
|
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):
|
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
|
||||||
|
@ -128,7 +201,8 @@ class NeighborUserStorySerializer(serializers.ModelSerializer):
|
||||||
depth = 0
|
depth = 0
|
||||||
|
|
||||||
|
|
||||||
class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, serializers.Serializer):
|
class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator,
|
||||||
|
serializers.Serializer):
|
||||||
project_id = serializers.IntegerField()
|
project_id = serializers.IntegerField()
|
||||||
status_id = serializers.IntegerField(required=False)
|
status_id = serializers.IntegerField(required=False)
|
||||||
bulk_stories = serializers.CharField()
|
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
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import serpy
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
|
|
||||||
class VoteResourceSerializerMixin(serializers.ModelSerializer):
|
class BaseVoteResourceSerializerMixin(object):
|
||||||
is_voter = serializers.SerializerMethodField("get_is_voter")
|
|
||||||
total_voters = serializers.SerializerMethodField("get_total_voters")
|
|
||||||
|
|
||||||
def get_is_voter(self, obj):
|
def get_is_voter(self, obj):
|
||||||
# The "is_voted" attribute is attached in the get_queryset of the viewset.
|
# The "is_voted" attribute is attached in the get_queryset of the viewset.
|
||||||
return getattr(obj, "is_voter", False) or False
|
return getattr(obj, "is_voter", False) or False
|
||||||
|
@ -30,3 +29,13 @@ class VoteResourceSerializerMixin(serializers.ModelSerializer):
|
||||||
def get_total_voters(self, obj):
|
def get_total_voters(self, obj):
|
||||||
# The "total_voters" attribute is attached in the get_queryset of the viewset.
|
# The "total_voters" attribute is attached in the get_queryset of the viewset.
|
||||||
return getattr(obj, "total_voters", 0) or 0
|
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
|
from collections import namedtuple
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import serpy
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
|
@ -144,6 +145,24 @@ class UserBasicInfoSerializer(UserSerializer):
|
||||||
fields = ("username", "full_name_display", "photo", "big_photo", "is_active", "id")
|
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):
|
class RecoverySerializer(serializers.Serializer):
|
||||||
token = serializers.CharField(max_length=200)
|
token = serializers.CharField(max_length=200)
|
||||||
password = serializers.CharField(min_length=6)
|
password = serializers.CharField(min_length=6)
|
||||||
|
|
Loading…
Reference in New Issue