Add initial Epic viewset (+ Voters and watchers)
parent
329a3e5ef3
commit
d5b2bc95ab
|
@ -160,6 +160,10 @@ class CanViewProjectFilterBackend(PermissionBasedFilterBackend):
|
||||||
permission = "view_project"
|
permission = "view_project"
|
||||||
|
|
||||||
|
|
||||||
|
class CanViewEpicsFilterBackend(PermissionBasedFilterBackend):
|
||||||
|
permission = "view_epics"
|
||||||
|
|
||||||
|
|
||||||
class CanViewUsFilterBackend(PermissionBasedFilterBackend):
|
class CanViewUsFilterBackend(PermissionBasedFilterBackend):
|
||||||
permission = "view_us"
|
permission = "view_us"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,223 @@
|
||||||
|
# -*- 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>
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from taiga.base.api.utils import get_object_or_404
|
||||||
|
from taiga.base import filters, response
|
||||||
|
from taiga.base import exceptions as exc
|
||||||
|
from taiga.base.decorators import list_route
|
||||||
|
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
|
from taiga.base.api.mixins import BlockedByProjectMixin
|
||||||
|
|
||||||
|
from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
|
from taiga.projects.models import Project, EpicStatus
|
||||||
|
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
|
from taiga.projects.occ import OCCResourceMixin
|
||||||
|
from taiga.projects.tagging.api import TaggedResourceMixin
|
||||||
|
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
from . import permissions
|
||||||
|
from . import serializers
|
||||||
|
from . import services
|
||||||
|
from . import validators
|
||||||
|
from . import utils as epics_utils
|
||||||
|
|
||||||
|
|
||||||
|
class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin,
|
||||||
|
WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet):
|
||||||
|
validator_class = validators.EpicValidator
|
||||||
|
queryset = models.Epic.objects.all()
|
||||||
|
permission_classes = (permissions.EpicPermission,)
|
||||||
|
filter_backends = (filters.CanViewEpicsFilterBackend,
|
||||||
|
filters.OwnersFilter,
|
||||||
|
filters.AssignedToFilter,
|
||||||
|
filters.StatusesFilter,
|
||||||
|
filters.TagsFilter,
|
||||||
|
filters.WatchersFilter,
|
||||||
|
filters.QFilter)
|
||||||
|
retrieve_exclude_filters = (filters.OwnersFilter,
|
||||||
|
filters.AssignedToFilter,
|
||||||
|
filters.StatusesFilter,
|
||||||
|
filters.TagsFilter,
|
||||||
|
filters.WatchersFilter)
|
||||||
|
filter_fields = ["project",
|
||||||
|
"project__slug",
|
||||||
|
"assigned_to",
|
||||||
|
"status__is_closed"]
|
||||||
|
|
||||||
|
def get_serializer_class(self, *args, **kwargs):
|
||||||
|
if self.action in ["retrieve", "by_ref"]:
|
||||||
|
return serializers.EpicNeighborsSerializer
|
||||||
|
|
||||||
|
if self.action == "list":
|
||||||
|
return serializers.EpicListSerializer
|
||||||
|
|
||||||
|
return serializers.EpicSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
qs = super().get_queryset()
|
||||||
|
qs = qs.select_related("project",
|
||||||
|
"status",
|
||||||
|
"owner",
|
||||||
|
"assigned_to")
|
||||||
|
|
||||||
|
include_attachments = "include_attachments" in self.request.QUERY_PARAMS
|
||||||
|
qs = epics_utils.attach_extra_info(qs, user=self.request.user,
|
||||||
|
include_attachments=include_attachments)
|
||||||
|
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def pre_conditions_on_save(self, obj):
|
||||||
|
super().pre_conditions_on_save(obj)
|
||||||
|
|
||||||
|
if obj.status and obj.status.project != obj.project:
|
||||||
|
raise exc.WrongArguments(_("You don't have permissions to set this status to this epic."))
|
||||||
|
|
||||||
|
def pre_save(self, obj):
|
||||||
|
if not obj.id:
|
||||||
|
obj.owner = self.request.user
|
||||||
|
super().pre_save(obj)
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object_or_none()
|
||||||
|
project_id = request.DATA.get('project', None)
|
||||||
|
if project_id and self.object and self.object.project.id != project_id:
|
||||||
|
try:
|
||||||
|
new_project = Project.objects.get(pk=project_id)
|
||||||
|
self.check_permissions(request, "destroy", self.object)
|
||||||
|
self.check_permissions(request, "create", new_project)
|
||||||
|
|
||||||
|
status_id = request.DATA.get('status', None)
|
||||||
|
if status_id is not None:
|
||||||
|
try:
|
||||||
|
old_status = self.object.project.epic_statuses.get(pk=status_id)
|
||||||
|
new_status = new_project.epic_statuses.get(slug=old_status.slug)
|
||||||
|
request.DATA['status'] = new_status.id
|
||||||
|
except EpicStatus.DoesNotExist:
|
||||||
|
request.DATA['status'] = new_project.default_epic_status.id
|
||||||
|
|
||||||
|
except Project.DoesNotExist:
|
||||||
|
return response.BadRequest(_("The project doesn't exist"))
|
||||||
|
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@list_route(methods=["GET"])
|
||||||
|
def filters_data(self, request, *args, **kwargs):
|
||||||
|
project_id = request.QUERY_PARAMS.get("project", None)
|
||||||
|
project = get_object_or_404(Project, id=project_id)
|
||||||
|
|
||||||
|
filter_backends = self.get_filter_backends()
|
||||||
|
statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter)
|
||||||
|
assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter)
|
||||||
|
owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
|
||||||
|
|
||||||
|
queryset = self.get_queryset()
|
||||||
|
querysets = {
|
||||||
|
"statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends),
|
||||||
|
"assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends),
|
||||||
|
"owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
|
||||||
|
"tags": self.filter_queryset(queryset)
|
||||||
|
}
|
||||||
|
return response.Ok(services.get_epics_filters_data(project, querysets))
|
||||||
|
|
||||||
|
@list_route(methods=["GET"])
|
||||||
|
def by_ref(self, request):
|
||||||
|
retrieve_kwargs = {
|
||||||
|
"ref": request.QUERY_PARAMS.get("ref", None)
|
||||||
|
}
|
||||||
|
project_id = request.QUERY_PARAMS.get("project", None)
|
||||||
|
if project_id is not None:
|
||||||
|
retrieve_kwargs["project_id"] = project_id
|
||||||
|
|
||||||
|
project_slug = request.QUERY_PARAMS.get("project__slug", None)
|
||||||
|
if project_slug is not None:
|
||||||
|
retrieve_kwargs["project__slug"] = project_slug
|
||||||
|
|
||||||
|
return self.retrieve(request, **retrieve_kwargs)
|
||||||
|
|
||||||
|
#@list_route(methods=["GET"])
|
||||||
|
#def csv(self, request):
|
||||||
|
# uuid = request.QUERY_PARAMS.get("uuid", None)
|
||||||
|
# if uuid is None:
|
||||||
|
# return response.NotFound()
|
||||||
|
|
||||||
|
# project = get_object_or_404(Project, epics_csv_uuid=uuid)
|
||||||
|
# queryset = project.epics.all().order_by('ref')
|
||||||
|
# data = services.epics_to_csv(project, queryset)
|
||||||
|
# csv_response = HttpResponse(data.getvalue(), content_type='application/csv; charset=utf-8')
|
||||||
|
# csv_response['Content-Disposition'] = 'attachment; filename="epics.csv"'
|
||||||
|
# return csv_response
|
||||||
|
|
||||||
|
@list_route(methods=["POST"])
|
||||||
|
def bulk_create(self, request, **kwargs):
|
||||||
|
validator = validators.EpicsBulkValidator(data=request.DATA)
|
||||||
|
if validator.is_valid():
|
||||||
|
data = validator.data
|
||||||
|
project = Project.objects.get(id=data["project_id"])
|
||||||
|
self.check_permissions(request, 'bulk_create', project)
|
||||||
|
if project.blocked_code is not None:
|
||||||
|
raise exc.Blocked(_("Blocked element"))
|
||||||
|
|
||||||
|
epics = services.create_epics_in_bulk(
|
||||||
|
data["bulk_epics"], milestone_id=data["sprint_id"], user_story_id=data["us_id"],
|
||||||
|
status_id=data.get("status_id") or project.default_epic_status_id,
|
||||||
|
project=project, owner=request.user, callback=self.post_save, precall=self.pre_save)
|
||||||
|
|
||||||
|
epics = self.get_queryset().filter(id__in=[i.id for i in epics])
|
||||||
|
epics_serialized = self.get_serializer_class()(epics, many=True)
|
||||||
|
|
||||||
|
return response.Ok(epics_serialized.data)
|
||||||
|
|
||||||
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
|
def _bulk_update_order(self, order_field, request, **kwargs):
|
||||||
|
validator = validators.UpdateEpicsOrderBulkValidator(data=request.DATA)
|
||||||
|
if not validator.is_valid():
|
||||||
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
|
data = validator.data
|
||||||
|
project = get_object_or_404(Project, pk=data["project_id"])
|
||||||
|
|
||||||
|
self.check_permissions(request, "bulk_update_order", project)
|
||||||
|
if project.blocked_code is not None:
|
||||||
|
raise exc.Blocked(_("Blocked element"))
|
||||||
|
|
||||||
|
services.update_epics_order_in_bulk(data["bulk_epics"],
|
||||||
|
project=project,
|
||||||
|
field=order_field)
|
||||||
|
services.snapshot_epics_in_bulk(data["bulk_epics"], request.user)
|
||||||
|
|
||||||
|
return response.NoContent()
|
||||||
|
|
||||||
|
@list_route(methods=["POST"])
|
||||||
|
def bulk_update_epic_order(self, request, **kwargs):
|
||||||
|
return self._bulk_update_order("epic_order", request, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EpicVotersViewSet(VotersViewSetMixin, ModelListViewSet):
|
||||||
|
permission_classes = (permissions.EpicVotersPermission,)
|
||||||
|
resource_model = models.Epic
|
||||||
|
|
||||||
|
|
||||||
|
class EpicWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
|
||||||
|
permission_classes = (permissions.EpicWatchersPermission,)
|
||||||
|
resource_model = models.Epic
|
|
@ -0,0 +1,74 @@
|
||||||
|
# -*- 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>
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import Field, MethodField
|
||||||
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
|
|
||||||
|
from taiga.mdrender.service import render as mdrender
|
||||||
|
from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
|
||||||
|
from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
|
||||||
|
from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
|
||||||
|
from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
|
||||||
|
from taiga.projects.notifications.mixins import WatchedResourceSerializer
|
||||||
|
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
|
||||||
|
|
||||||
|
|
||||||
|
class EpicListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
|
||||||
|
OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
|
||||||
|
StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
|
||||||
|
serializers.LightSerializer):
|
||||||
|
|
||||||
|
id = Field()
|
||||||
|
ref = Field()
|
||||||
|
project = Field(attr="project_id")
|
||||||
|
created_date = Field()
|
||||||
|
modified_date = Field()
|
||||||
|
subject = Field()
|
||||||
|
epic_order = Field()
|
||||||
|
client_requirement = Field()
|
||||||
|
team_requirement = Field()
|
||||||
|
version = Field()
|
||||||
|
watchers = Field()
|
||||||
|
is_blocked = Field()
|
||||||
|
blocked_note = Field()
|
||||||
|
tags = Field()
|
||||||
|
is_closed = MethodField()
|
||||||
|
|
||||||
|
def get_is_closed(self, obj):
|
||||||
|
return obj.status is not None and obj.status.is_closed
|
||||||
|
|
||||||
|
|
||||||
|
class EpicSerializer(EpicListSerializer):
|
||||||
|
comment = MethodField()
|
||||||
|
blocked_note_html = MethodField()
|
||||||
|
description = Field()
|
||||||
|
description_html = MethodField()
|
||||||
|
|
||||||
|
def get_comment(self, obj):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def get_blocked_note_html(self, obj):
|
||||||
|
return mdrender(obj.project, obj.blocked_note)
|
||||||
|
|
||||||
|
def get_description_html(self, obj):
|
||||||
|
return mdrender(obj.project, obj.description)
|
||||||
|
|
||||||
|
|
||||||
|
class EpicNeighborsSerializer(NeighborsSerializerMixin, EpicSerializer):
|
||||||
|
pass
|
|
@ -0,0 +1,376 @@
|
||||||
|
# -*- 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>
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
from collections import OrderedDict
|
||||||
|
from operator import itemgetter
|
||||||
|
from contextlib import closing
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from taiga.base.utils import db, text
|
||||||
|
from taiga.projects.history.services import take_snapshot
|
||||||
|
from taiga.projects.epics.apps import connect_epics_signals
|
||||||
|
from taiga.projects.epics.apps import disconnect_epics_signals
|
||||||
|
from taiga.events import events
|
||||||
|
from taiga.projects.votes.utils import attach_total_voters_to_queryset
|
||||||
|
from taiga.projects.notifications.utils import attach_watchers_to_queryset
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
# Bulk actions
|
||||||
|
#####################################################
|
||||||
|
|
||||||
|
def get_epics_from_bulk(bulk_data, **additional_fields):
|
||||||
|
"""Convert `bulk_data` into a list of epics.
|
||||||
|
|
||||||
|
:param bulk_data: List of epics in bulk format.
|
||||||
|
:param additional_fields: Additional fields when instantiating each epic.
|
||||||
|
|
||||||
|
:return: List of `Epic` instances.
|
||||||
|
"""
|
||||||
|
return [models.Epic(subject=line, **additional_fields)
|
||||||
|
for line in text.split_in_lines(bulk_data)]
|
||||||
|
|
||||||
|
|
||||||
|
def create_epics_in_bulk(bulk_data, callback=None, precall=None, **additional_fields):
|
||||||
|
"""Create epics from `bulk_data`.
|
||||||
|
|
||||||
|
:param bulk_data: List of epics in bulk format.
|
||||||
|
:param callback: Callback to execute after each epic save.
|
||||||
|
:param additional_fields: Additional fields when instantiating each epic.
|
||||||
|
|
||||||
|
:return: List of created `Epic` instances.
|
||||||
|
"""
|
||||||
|
epics = get_epics_from_bulk(bulk_data, **additional_fields)
|
||||||
|
|
||||||
|
disconnect_epics_signals()
|
||||||
|
|
||||||
|
try:
|
||||||
|
db.save_in_bulk(epics, callback, precall)
|
||||||
|
finally:
|
||||||
|
connect_epics_signals()
|
||||||
|
|
||||||
|
return epics
|
||||||
|
|
||||||
|
|
||||||
|
def update_epics_order_in_bulk(bulk_data: list, field: str, project: object):
|
||||||
|
"""
|
||||||
|
Update the order of some epics.
|
||||||
|
`bulk_data` should be a list of tuples with the following format:
|
||||||
|
|
||||||
|
[(<epic id>, {<field>: <value>, ...}), ...]
|
||||||
|
"""
|
||||||
|
epic_ids = []
|
||||||
|
new_order_values = []
|
||||||
|
for epic_data in bulk_data:
|
||||||
|
epic_ids.append(epic_data["epic_id"])
|
||||||
|
new_order_values.append({field: epic_data["order"]})
|
||||||
|
|
||||||
|
events.emit_event_for_ids(ids=epic_ids,
|
||||||
|
content_type="epics.epic",
|
||||||
|
projectid=project.pk)
|
||||||
|
|
||||||
|
db.update_in_bulk_with_ids(epic_ids, new_order_values, model=models.Epic)
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_epics_in_bulk(bulk_data, user):
|
||||||
|
for epic_data in bulk_data:
|
||||||
|
try:
|
||||||
|
epic = models.Epic.objects.get(pk=epic_data['epic_id'])
|
||||||
|
take_snapshot(epic, user=user)
|
||||||
|
except models.Epic.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
# CSV
|
||||||
|
#####################################################
|
||||||
|
#
|
||||||
|
#def epics_to_csv(project, queryset):
|
||||||
|
# csv_data = io.StringIO()
|
||||||
|
# fieldnames = ["ref", "subject", "description", "user_story", "sprint", "sprint_estimated_start",
|
||||||
|
# "sprint_estimated_finish", "owner", "owner_full_name", "assigned_to",
|
||||||
|
# "assigned_to_full_name", "status", "is_iocaine", "is_closed", "us_order",
|
||||||
|
# "epicboard_order", "attachments", "external_reference", "tags", "watchers", "voters",
|
||||||
|
# "created_date", "modified_date", "finished_date"]
|
||||||
|
#
|
||||||
|
# custom_attrs = project.epiccustomattributes.all()
|
||||||
|
# for custom_attr in custom_attrs:
|
||||||
|
# fieldnames.append(custom_attr.name)
|
||||||
|
#
|
||||||
|
# queryset = queryset.prefetch_related("attachments",
|
||||||
|
# "custom_attributes_values")
|
||||||
|
# queryset = queryset.select_related("milestone",
|
||||||
|
# "owner",
|
||||||
|
# "assigned_to",
|
||||||
|
# "status",
|
||||||
|
# "project")
|
||||||
|
#
|
||||||
|
# queryset = attach_total_voters_to_queryset(queryset)
|
||||||
|
# queryset = attach_watchers_to_queryset(queryset)
|
||||||
|
#
|
||||||
|
# writer = csv.DictWriter(csv_data, fieldnames=fieldnames)
|
||||||
|
# writer.writeheader()
|
||||||
|
# for epic in queryset:
|
||||||
|
# epic_data = {
|
||||||
|
# "ref": epic.ref,
|
||||||
|
# "subject": epic.subject,
|
||||||
|
# "description": epic.description,
|
||||||
|
# "user_story": epic.user_story.ref if epic.user_story else None,
|
||||||
|
# "sprint": epic.milestone.name if epic.milestone else None,
|
||||||
|
# "sprint_estimated_start": epic.milestone.estimated_start if epic.milestone else None,
|
||||||
|
# "sprint_estimated_finish": epic.milestone.estimated_finish if epic.milestone else None,
|
||||||
|
# "owner": epic.owner.username if epic.owner else None,
|
||||||
|
# "owner_full_name": epic.owner.get_full_name() if epic.owner else None,
|
||||||
|
# "assigned_to": epic.assigned_to.username if epic.assigned_to else None,
|
||||||
|
# "assigned_to_full_name": epic.assigned_to.get_full_name() if epic.assigned_to else None,
|
||||||
|
# "status": epic.status.name if epic.status else None,
|
||||||
|
# "is_iocaine": epic.is_iocaine,
|
||||||
|
# "is_closed": epic.status is not None and epic.status.is_closed,
|
||||||
|
# "us_order": epic.us_order,
|
||||||
|
# "epicboard_order": epic.epicboard_order,
|
||||||
|
# "attachments": epic.attachments.count(),
|
||||||
|
# "external_reference": epic.external_reference,
|
||||||
|
# "tags": ",".join(epic.tags or []),
|
||||||
|
# "watchers": epic.watchers,
|
||||||
|
# "voters": epic.total_voters,
|
||||||
|
# "created_date": epic.created_date,
|
||||||
|
# "modified_date": epic.modified_date,
|
||||||
|
# "finished_date": epic.finished_date,
|
||||||
|
# }
|
||||||
|
# for custom_attr in custom_attrs:
|
||||||
|
# value = epic.custom_attributes_values.attributes_values.get(str(custom_attr.id), None)
|
||||||
|
# epic_data[custom_attr.name] = value
|
||||||
|
#
|
||||||
|
# writer.writerow(epic_data)
|
||||||
|
#
|
||||||
|
# return csv_data
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################
|
||||||
|
# Api filter data
|
||||||
|
#####################################################
|
||||||
|
|
||||||
|
def _get_epics_statuses(project, queryset):
|
||||||
|
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
|
||||||
|
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
|
||||||
|
where = queryset_where_tuple[0]
|
||||||
|
where_params = queryset_where_tuple[1]
|
||||||
|
|
||||||
|
extra_sql = """
|
||||||
|
SELECT "projects_epicstatus"."id",
|
||||||
|
"projects_epicstatus"."name",
|
||||||
|
"projects_epicstatus"."color",
|
||||||
|
"projects_epicstatus"."order",
|
||||||
|
(SELECT count(*)
|
||||||
|
FROM "epics_epic"
|
||||||
|
INNER JOIN "projects_project" ON
|
||||||
|
("epics_epic"."project_id" = "projects_project"."id")
|
||||||
|
WHERE {where} AND "epics_epic"."status_id" = "projects_epicstatus"."id")
|
||||||
|
FROM "projects_epicstatus"
|
||||||
|
WHERE "projects_epicstatus"."project_id" = %s
|
||||||
|
ORDER BY "projects_epicstatus"."order";
|
||||||
|
""".format(where=where)
|
||||||
|
|
||||||
|
with closing(connection.cursor()) as cursor:
|
||||||
|
cursor.execute(extra_sql, where_params + [project.id])
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for id, name, color, order, count in rows:
|
||||||
|
result.append({
|
||||||
|
"id": id,
|
||||||
|
"name": _(name),
|
||||||
|
"color": color,
|
||||||
|
"order": order,
|
||||||
|
"count": count,
|
||||||
|
})
|
||||||
|
return sorted(result, key=itemgetter("order"))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_epics_assigned_to(project, queryset):
|
||||||
|
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
|
||||||
|
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
|
||||||
|
where = queryset_where_tuple[0]
|
||||||
|
where_params = queryset_where_tuple[1]
|
||||||
|
|
||||||
|
extra_sql = """
|
||||||
|
WITH counters AS (
|
||||||
|
SELECT assigned_to_id, count(assigned_to_id) count
|
||||||
|
FROM "epics_epic"
|
||||||
|
INNER JOIN "projects_project" ON ("epics_epic"."project_id" = "projects_project"."id")
|
||||||
|
WHERE {where} AND "epics_epic"."assigned_to_id" IS NOT NULL
|
||||||
|
GROUP BY assigned_to_id
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT "projects_membership"."user_id" user_id,
|
||||||
|
"users_user"."full_name",
|
||||||
|
"users_user"."username",
|
||||||
|
COALESCE("counters".count, 0) count
|
||||||
|
FROM projects_membership
|
||||||
|
LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id")
|
||||||
|
INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
|
||||||
|
WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
|
||||||
|
|
||||||
|
-- unassigned epics
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count
|
||||||
|
FROM "epics_epic"
|
||||||
|
INNER JOIN "projects_project" ON ("epics_epic"."project_id" = "projects_project"."id")
|
||||||
|
WHERE {where} AND "epics_epic"."assigned_to_id" IS NULL
|
||||||
|
GROUP BY assigned_to_id
|
||||||
|
""".format(where=where)
|
||||||
|
|
||||||
|
with closing(connection.cursor()) as cursor:
|
||||||
|
cursor.execute(extra_sql, where_params + [project.id] + where_params)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
none_valued_added = False
|
||||||
|
for id, full_name, username, count in rows:
|
||||||
|
result.append({
|
||||||
|
"id": id,
|
||||||
|
"full_name": full_name or username or "",
|
||||||
|
"count": count,
|
||||||
|
})
|
||||||
|
|
||||||
|
if id is None:
|
||||||
|
none_valued_added = True
|
||||||
|
|
||||||
|
# If there was no epic with null assigned_to we manually add it
|
||||||
|
if not none_valued_added:
|
||||||
|
result.append({
|
||||||
|
"id": None,
|
||||||
|
"full_name": "",
|
||||||
|
"count": 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return sorted(result, key=itemgetter("full_name"))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_epics_owners(project, queryset):
|
||||||
|
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
|
||||||
|
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
|
||||||
|
where = queryset_where_tuple[0]
|
||||||
|
where_params = queryset_where_tuple[1]
|
||||||
|
|
||||||
|
extra_sql = """
|
||||||
|
WITH counters AS (
|
||||||
|
SELECT "epics_epic"."owner_id" owner_id,
|
||||||
|
count(coalesce("epics_epic"."owner_id", -1)) count
|
||||||
|
FROM "epics_epic"
|
||||||
|
INNER JOIN "projects_project" ON ("epics_epic"."project_id" = "projects_project"."id")
|
||||||
|
WHERE {where}
|
||||||
|
GROUP BY "epics_epic"."owner_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT "projects_membership"."user_id" id,
|
||||||
|
"users_user"."full_name",
|
||||||
|
"users_user"."username",
|
||||||
|
COALESCE("counters".count, 0) count
|
||||||
|
FROM projects_membership
|
||||||
|
LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id")
|
||||||
|
INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id")
|
||||||
|
WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
|
||||||
|
|
||||||
|
-- System users
|
||||||
|
UNION
|
||||||
|
|
||||||
|
SELECT "users_user"."id" user_id,
|
||||||
|
"users_user"."full_name" full_name,
|
||||||
|
"users_user"."username" username,
|
||||||
|
COALESCE("counters".count, 0) count
|
||||||
|
FROM users_user
|
||||||
|
LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id")
|
||||||
|
WHERE ("users_user"."is_system" IS TRUE)
|
||||||
|
""".format(where=where)
|
||||||
|
|
||||||
|
with closing(connection.cursor()) as cursor:
|
||||||
|
cursor.execute(extra_sql, where_params + [project.id])
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for id, full_name, username, count in rows:
|
||||||
|
if count > 0:
|
||||||
|
result.append({
|
||||||
|
"id": id,
|
||||||
|
"full_name": full_name or username or "",
|
||||||
|
"count": count,
|
||||||
|
})
|
||||||
|
return sorted(result, key=itemgetter("full_name"))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_epics_tags(project, queryset):
|
||||||
|
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
|
||||||
|
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
|
||||||
|
where = queryset_where_tuple[0]
|
||||||
|
where_params = queryset_where_tuple[1]
|
||||||
|
|
||||||
|
extra_sql = """
|
||||||
|
WITH epics_tags AS (
|
||||||
|
SELECT tag,
|
||||||
|
COUNT(tag) counter FROM (
|
||||||
|
SELECT UNNEST(epics_epic.tags) tag
|
||||||
|
FROM epics_epic
|
||||||
|
INNER JOIN projects_project
|
||||||
|
ON (epics_epic.project_id = projects_project.id)
|
||||||
|
WHERE {where}) tags
|
||||||
|
GROUP BY tag),
|
||||||
|
project_tags AS (
|
||||||
|
SELECT reduce_dim(tags_colors) tag_color
|
||||||
|
FROM projects_project
|
||||||
|
WHERE id=%s)
|
||||||
|
|
||||||
|
SELECT tag_color[1] tag, COALESCE(epics_tags.counter, 0) counter
|
||||||
|
FROM project_tags
|
||||||
|
LEFT JOIN epics_tags ON project_tags.tag_color[1] = epics_tags.tag
|
||||||
|
ORDER BY tag
|
||||||
|
""".format(where=where)
|
||||||
|
|
||||||
|
with closing(connection.cursor()) as cursor:
|
||||||
|
cursor.execute(extra_sql, where_params + [project.id])
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for name, count in rows:
|
||||||
|
result.append({
|
||||||
|
"name": name,
|
||||||
|
"count": count,
|
||||||
|
})
|
||||||
|
return sorted(result, key=itemgetter("name"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_epics_filters_data(project, querysets):
|
||||||
|
"""
|
||||||
|
Given a project and an epics queryset, return a simple data structure
|
||||||
|
of all possible filters for the epics in the queryset.
|
||||||
|
"""
|
||||||
|
data = OrderedDict([
|
||||||
|
("statuses", _get_epics_statuses(project, querysets["statuses"])),
|
||||||
|
("assigned_to", _get_epics_assigned_to(project, querysets["assigned_to"])),
|
||||||
|
("owners", _get_epics_owners(project, querysets["owners"])),
|
||||||
|
("tags", _get_epics_tags(project, querysets["tags"])),
|
||||||
|
])
|
||||||
|
|
||||||
|
return data
|
|
@ -0,0 +1,39 @@
|
||||||
|
# -*- 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/>.
|
||||||
|
|
||||||
|
from taiga.projects.attachments.utils import attach_basic_attachments
|
||||||
|
from taiga.projects.notifications.utils import attach_watchers_to_queryset
|
||||||
|
from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
|
||||||
|
from taiga.projects.notifications.utils import 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
|
||||||
|
|
||||||
|
|
||||||
|
def attach_extra_info(queryset, user=None, include_attachments=False):
|
||||||
|
|
||||||
|
if include_attachments:
|
||||||
|
queryset = attach_basic_attachments(queryset)
|
||||||
|
queryset = queryset.extra(select={"include_attachments": "True"})
|
||||||
|
|
||||||
|
queryset = attach_total_voters_to_queryset(queryset)
|
||||||
|
queryset = attach_watchers_to_queryset(queryset)
|
||||||
|
queryset = attach_total_watchers_to_queryset(queryset)
|
||||||
|
queryset = attach_is_voter_to_queryset(queryset, user)
|
||||||
|
queryset = attach_is_watcher_to_queryset(queryset, user)
|
||||||
|
return queryset
|
|
@ -0,0 +1,67 @@
|
||||||
|
# -*- 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>
|
||||||
|
# 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/>.
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.api import validators
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
|
from taiga.base.fields import PgArrayField
|
||||||
|
from taiga.projects.milestones.validators import MilestoneExistsValidator
|
||||||
|
from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
|
||||||
|
from taiga.projects.notifications.validators import WatchersValidator
|
||||||
|
from taiga.projects.tagging.fields import TagsAndTagsColorsField
|
||||||
|
from taiga.projects.validators import ProjectExistsValidator
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class EpicExistsValidator:
|
||||||
|
def validate_epic_id(self, attrs, source):
|
||||||
|
value = attrs[source]
|
||||||
|
if not models.Epic.objects.filter(pk=value).exists():
|
||||||
|
msg = _("There's no epic with that id")
|
||||||
|
raise ValidationError(msg)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class EpicValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator):
|
||||||
|
tags = TagsAndTagsColorsField(default=[], required=False)
|
||||||
|
external_reference = PgArrayField(required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.Epic
|
||||||
|
read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
|
||||||
|
|
||||||
|
|
||||||
|
class EpicsBulkValidator(ProjectExistsValidator, EpicExistsValidator,
|
||||||
|
validators.Validator):
|
||||||
|
project_id = serializers.IntegerField()
|
||||||
|
status_id = serializers.IntegerField(required=False)
|
||||||
|
bulk_epics = serializers.CharField()
|
||||||
|
|
||||||
|
|
||||||
|
# Order bulk validators
|
||||||
|
|
||||||
|
class _EpicOrderBulkValidator(EpicExistsValidator, validators.Validator):
|
||||||
|
epic_id = serializers.IntegerField()
|
||||||
|
order = serializers.IntegerField()
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateEpicsOrderBulkValidator(ProjectExistsValidator, validators.Validator):
|
||||||
|
project_id = serializers.IntegerField()
|
||||||
|
bulk_epics = _EpicOrderBulkValidator(many=True)
|
|
@ -145,6 +145,10 @@ router.register(r"wiki/attachments", WikiAttachmentViewSet,
|
||||||
from taiga.projects.milestones.api import MilestoneViewSet
|
from taiga.projects.milestones.api import MilestoneViewSet
|
||||||
from taiga.projects.milestones.api import MilestoneWatchersViewSet
|
from taiga.projects.milestones.api import MilestoneWatchersViewSet
|
||||||
|
|
||||||
|
from taiga.projects.epics.api import EpicViewSet
|
||||||
|
from taiga.projects.epics.api import EpicVotersViewSet
|
||||||
|
from taiga.projects.epics.api import EpicWatchersViewSet
|
||||||
|
|
||||||
from taiga.projects.userstories.api import UserStoryViewSet
|
from taiga.projects.userstories.api import UserStoryViewSet
|
||||||
from taiga.projects.userstories.api import UserStoryVotersViewSet
|
from taiga.projects.userstories.api import UserStoryVotersViewSet
|
||||||
from taiga.projects.userstories.api import UserStoryWatchersViewSet
|
from taiga.projects.userstories.api import UserStoryWatchersViewSet
|
||||||
|
@ -166,6 +170,13 @@ router.register(r"milestones", MilestoneViewSet,
|
||||||
router.register(r"milestones/(?P<resource_id>\d+)/watchers", MilestoneWatchersViewSet,
|
router.register(r"milestones/(?P<resource_id>\d+)/watchers", MilestoneWatchersViewSet,
|
||||||
base_name="milestone-watchers")
|
base_name="milestone-watchers")
|
||||||
|
|
||||||
|
router.register(r"epics", EpicViewSet,
|
||||||
|
base_name="epics")
|
||||||
|
router.register(r"epics/(?P<resource_id>\d+)/voters", EpicVotersViewSet,
|
||||||
|
base_name="epic-voters")
|
||||||
|
router.register(r"epics/(?P<resource_id>\d+)/watchers", EpicWatchersViewSet,
|
||||||
|
base_name="epic-watchers")
|
||||||
|
|
||||||
router.register(r"userstories", UserStoryViewSet,
|
router.register(r"userstories", UserStoryViewSet,
|
||||||
base_name="userstories")
|
base_name="userstories")
|
||||||
router.register(r"userstories/(?P<resource_id>\d+)/voters", UserStoryVotersViewSet,
|
router.register(r"userstories/(?P<resource_id>\d+)/voters", UserStoryVotersViewSet,
|
||||||
|
|
Loading…
Reference in New Issue