diff --git a/taiga/front/urls.py b/taiga/front/urls.py index ab1cec8c..77d53dab 100644 --- a/taiga/front/urls.py +++ b/taiga/front/urls.py @@ -33,6 +33,9 @@ urls = { "project": "/project/{0}", # project.slug + "epics": "/project/{0}/epics/", # project.slug + "epic": "/project/{0}/epic/{1}", # project.slug, epic.ref + "backlog": "/project/{0}/backlog/", # project.slug "taskboard": "/project/{0}/taskboard/{1}", # project.slug, milestone.slug "kanban": "/project/{0}/kanban/", # project.slug diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 34920df4..5284f44e 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -243,6 +243,20 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, services.remove_user_from_project(request.user, project) return response.Ok() + def _regenerate_csv_uuid(self, project, field): + uuid_value = uuid.uuid4().hex + setattr(project, field, uuid_value) + project.save() + return uuid_value + + @detail_route(methods=["POST"]) + def regenerate_epics_csv_uuid(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "regenerate_epics_csv_uuid", project) + self.pre_conditions_on_save(project) + data = {"uuid": self._regenerate_csv_uuid(project, "epics_csv_uuid")} + return response.Ok(data) + @detail_route(methods=["POST"]) def regenerate_userstories_csv_uuid(self, request, pk=None): project = self.get_object() @@ -251,14 +265,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, data = {"uuid": self._regenerate_csv_uuid(project, "userstories_csv_uuid")} return response.Ok(data) - @detail_route(methods=["POST"]) - def regenerate_issues_csv_uuid(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "regenerate_issues_csv_uuid", project) - self.pre_conditions_on_save(project) - data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")} - return response.Ok(data) - @detail_route(methods=["POST"]) def regenerate_tasks_csv_uuid(self, request, pk=None): project = self.get_object() @@ -267,6 +273,14 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")} return response.Ok(data) + @detail_route(methods=["POST"]) + def regenerate_issues_csv_uuid(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "regenerate_issues_csv_uuid", project) + self.pre_conditions_on_save(project) + data = {"uuid": self._regenerate_csv_uuid(project, "issues_csv_uuid")} + return response.Ok(data) + @list_route(methods=["GET"]) def by_slug(self, request, *args, **kwargs): slug = request.QUERY_PARAMS.get("slug", None) @@ -293,12 +307,6 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, self.check_permissions(request, "stats", project) return response.Ok(services.get_stats_for_project(project)) - def _regenerate_csv_uuid(self, project, field): - uuid_value = uuid.uuid4().hex - setattr(project, field, uuid_value) - project.save() - return uuid_value - @detail_route(methods=["GET"]) def member_stats(self, request, pk=None): project = self.get_object() diff --git a/taiga/projects/epics/api.py b/taiga/projects/epics/api.py index e71106d1..2c2760ba 100644 --- a/taiga/projects/epics/api.py +++ b/taiga/projects/epics/api.py @@ -154,18 +154,18 @@ class EpicViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, 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() + @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 + 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): diff --git a/taiga/projects/epics/services.py b/taiga/projects/epics/services.py index 01a0f0fa..8674e79f 100644 --- a/taiga/projects/epics/services.py +++ b/taiga/projects/epics/services.py @@ -105,66 +105,57 @@ def snapshot_epics_in_bulk(bulk_data, user): ##################################################### # 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 + +def epics_to_csv(project, queryset): + csv_data = io.StringIO() + fieldnames = ["ref", "subject", "description", "owner", "owner_full_name", "assigned_to", + "assigned_to_full_name", "status", "epics_order", "client_requirement", + "team_requirement", "attachments", "tags", "watchers", "voters", + "created_date", "modified_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("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, + "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, + "epics_order": epic.epics_order, + "client_requirement": epic.client_requirement, + "team_requirement": epic.team_requirement, + "attachments": epic.attachments.count(), + "tags": ",".join(epic.tags or []), + "watchers": epic.watchers, + "voters": epic.total_voters, + "created_date": epic.created_date, + "modified_date": epic.modified_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 ##################################################### diff --git a/taiga/projects/migrations/0050_project_epics_csv_uuid.py b/taiga/projects/migrations/0050_project_epics_csv_uuid.py new file mode 100644 index 00000000..2dc87674 --- /dev/null +++ b/taiga/projects/migrations/0050_project_epics_csv_uuid.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-07-20 17:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0049_auto_20160629_1443'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='epics_csv_uuid', + field=models.CharField(blank=True, db_index=True, default=None, editable=False, max_length=32, null=True), + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 02d24519..080cd411 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -202,6 +202,8 @@ class Project(ProjectDefaults, TaggedMixin, TagsColorsdMixin, models.Model): looking_for_people_note = models.TextField(default="", null=False, blank=True, verbose_name=_("loking for people note")) + epics_csv_uuid = models.CharField(max_length=32, editable=False, null=True, + blank=True, default=None, db_index=True) userstories_csv_uuid = models.CharField(max_length=32, editable=False, null=True, blank=True, default=None, db_index=True) diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 5e3b44db..7c10b5c2 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -62,6 +62,7 @@ class ProjectPermission(TaigaResourcePermission): stats_perms = HasProjectPerm('view_project') member_stats_perms = HasProjectPerm('view_project') issues_stats_perms = HasProjectPerm('view_project') + regenerate_epics_csv_uuid_perms = IsProjectAdmin() regenerate_userstories_csv_uuid_perms = IsProjectAdmin() regenerate_issues_csv_uuid_perms = IsProjectAdmin() regenerate_tasks_csv_uuid_perms = IsProjectAdmin() diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index 58999578..a560ed36 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -373,9 +373,10 @@ class ProjectDetailSerializer(ProjectSerializer): # Admin fields is_private_extra_info = MethodField() max_memberships = MethodField() - issues_csv_uuid = Field() - tasks_csv_uuid = Field() + epics_csv_uuid = Field() userstories_csv_uuid = Field() + tasks_csv_uuid = Field() + issues_csv_uuid = Field() transfer_token = Field() milestones = MethodField() @@ -404,8 +405,8 @@ class ProjectDetailSerializer(ProjectSerializer): ret = super().to_value(instance) admin_fields = [ - "is_private_extra_info", "max_memberships", "issues_csv_uuid", - "tasks_csv_uuid", "userstories_csv_uuid", "transfer_token" + "epics_csv_uuid", "userstories_csv_uuid", "tasks_csv_uuid", "issues_csv_uuid", + "is_private_extra_info", "max_memberships", "transfer_token", ] is_admin_user = False diff --git a/tests/integration/resources_permissions/test_epics_resources.py b/tests/integration/resources_permissions/test_epics_resources.py index ceffe206..f4e33813 100644 --- a/tests/integration/resources_permissions/test_epics_resources.py +++ b/tests/integration/resources_permissions/test_epics_resources.py @@ -59,29 +59,29 @@ def data(): m.public_project = f.ProjectFactory(is_private=False, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - owner=m.project_owner) - #epics_csv_uuid=uuid.uuid4().hex) + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), - owner=m.project_owner) - #epics_csv_uuid=uuid.uuid4().hex) + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], - owner=m.project_owner) - #epics_csv_uuid=uuid.uuid4().hex) + owner=m.project_owner, + epics_csv_uuid=uuid.uuid4().hex) m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, - #epics_csv_uuid=uuid.uuid4().hex, + epics_csv_uuid=uuid.uuid4().hex, blocked_code=project_choices.BLOCKED_BY_STAFF) m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) @@ -873,29 +873,29 @@ def test_epic_watchers_retrieve(client, data): assert results == [401, 403, 403, 200, 200] -#def test_epics_csv(client, data): -# url = reverse('epics-csv') -# csv_public_uuid = data.public_project.epics_csv_uuid -# csv_private1_uuid = data.private_project1.epics_csv_uuid -# csv_private2_uuid = data.private_project1.epics_csv_uuid -# csv_blocked_uuid = data.blocked_project.epics_csv_uuid -# -# users = [ -# None, -# data.registered_user, -# data.project_member_without_perms, -# data.project_member_with_perms, -# data.project_owner -# ] -# -# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) -# assert results == [200, 200, 200, 200, 200] -# -# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) -# assert results == [200, 200, 200, 200, 200] -# -# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) -# assert results == [200, 200, 200, 200, 200] -# -# results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) -# assert results == [200, 200, 200, 200, 200] +def test_epics_csv(client, data): + url = reverse('epics-csv') + csv_public_uuid = data.public_project.epics_csv_uuid + csv_private1_uuid = data.private_project1.epics_csv_uuid + csv_private2_uuid = data.private_project1.epics_csv_uuid + csv_blocked_uuid = data.blocked_project.epics_csv_uuid + + users = [ + None, + data.registered_user, + data.project_member_without_perms, + data.project_member_with_perms, + data.project_owner + ] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_public_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private1_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_private2_uuid), None, users) + assert results == [200, 200, 200, 200, 200] + + results = helper_test_http_method(client, 'get', "{}?uuid={}".format(url, csv_blocked_uuid), None, users) + assert results == [200, 200, 200, 200, 200] diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 1410c86d..3d62dc6e 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -486,6 +486,31 @@ def test_invitations_retrieve(client, data): assert results == [200, 200, 200, 200] +def test_regenerate_epics_csv_uuid(client, data): + public_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.private_project2.pk}) + blocked_url = reverse('projects-regenerate-epics-csv-uuid', kwargs={"pk": data.blocked_project.pk}) + + users = [ + None, + data.registered_user, + data.project_member_with_perms, + data.project_owner + ] + results = helper_test_http_method(client, 'post', public_url, None, users) + assert results == [401, 403, 403, 200] + + results = helper_test_http_method(client, 'post', private1_url, None, users) + assert results == [401, 403, 403, 200] + + results = helper_test_http_method(client, 'post', private2_url, None, users) + assert results == [404, 404, 403, 200] + + results = helper_test_http_method(client, 'post', blocked_url, None, users) + assert results == [404, 404, 403, 451] + + def test_regenerate_userstories_csv_uuid(client, data): public_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.private_project1.pk}) diff --git a/tests/integration/test_epics.py b/tests/integration/test_epics.py new file mode 100644 index 00000000..5f22f62f --- /dev/null +++ b/tests/integration/test_epics.py @@ -0,0 +1,68 @@ +# -*- 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 . + +import uuid +import csv + +from unittest import mock + +from django.core.urlresolvers import reverse + +from taiga.base.utils import json +from taiga.projects.epics import services + +from .. import factories as f + +import pytest +pytestmark = pytest.mark.django_db + + +def test_get_invalid_csv(client): + url = reverse("epics-csv") + + response = client.get(url) + assert response.status_code == 404 + + response = client.get("{}?uuid={}".format(url, "not-valid-uuid")) + assert response.status_code == 404 + + +def test_get_valid_csv(client): + url = reverse("epics-csv") + project = f.ProjectFactory.create(epics_csv_uuid=uuid.uuid4().hex) + + response = client.get("{}?uuid={}".format(url, project.epics_csv_uuid)) + assert response.status_code == 200 + + +def test_custom_fields_csv_generation(): + project = f.ProjectFactory.create(epics_csv_uuid=uuid.uuid4().hex) + attr = f.EpicCustomAttributeFactory.create(project=project, name="attr1", description="desc") + epic = f.EpicFactory.create(project=project) + attr_values = epic.custom_attributes_values + attr_values.attributes_values = {str(attr.id):"val1"} + attr_values.save() + queryset = project.epics.all() + data = services.epics_to_csv(project, queryset) + data.seek(0) + reader = csv.reader(data) + row = next(reader) + assert row[17] == attr.name + row = next(reader) + assert row[17] == "val1"