diff --git a/CHANGELOG.md b/CHANGELOG.md index c7da191e..a9322385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Added custom fields per project for user stories, tasks and issues. +- Support of export to CSV user stories, tasks and issues. - Allow public projects. ### Misc diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 92ce7e28..2cb34264 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -53,6 +53,7 @@ from .votes.utils import attach_votescount_to_queryset class ProjectViewSet(ModelCrudViewSet): serializer_class = serializers.ProjectDetailSerializer + admin_serializer_class = serializers.ProjectDetailAdminSerializer list_serializer_class = serializers.ProjectSerializer permission_classes = (permissions.ProjectPermission, ) filter_backends = (filters.CanViewProjectObjFilterBackend,) @@ -61,6 +62,23 @@ class ProjectViewSet(ModelCrudViewSet): qs = models.Project.objects.all() return attach_votescount_to_queryset(qs, as_field="stars_count") + def get_serializer_class(self): + if self.action == "list": + return self.list_serializer_class + elif self.action == "create": + return self.serializer_class + + if self.action == "by_slug": + slug = self.request.QUERY_PARAMS.get("slug", None) + project = get_object_or_404(models.Project, slug=slug) + else: + project = self.get_object() + + if permissions_service.is_project_owner(self.request.user, project): + return self.admin_serializer_class + + return self.serializer_class + @list_route(methods=["GET"]) def by_slug(self, request): slug = request.QUERY_PARAMS.get("slug", None) @@ -87,6 +105,33 @@ class ProjectViewSet(ModelCrudViewSet): 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=["POST"]) + def regenerate_userstories_csv_uuid(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, "regenerate_userstories_csv_uuid", project) + 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) + 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() + self.check_permissions(request, "regenerate_tasks_csv_uuid", project) + data = {"uuid": self._regenerate_csv_uuid(project, "tasks_csv_uuid")} + return response.Ok(data) + @detail_route(methods=["GET"]) def member_stats(self, request, pk=None): project = self.get_object() diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index c09ad0f4..d7879640 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -16,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _ from django.db.models import Q -from django.http import Http404 +from django.http import Http404, HttpResponse from taiga.base import filters from taiga.base import exceptions as exc @@ -24,7 +24,6 @@ from taiga.base import response from taiga.base.decorators import detail_route, list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.utils import get_object_or_404 -from taiga.base import tags from taiga.users.models import User @@ -163,6 +162,19 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, issue = get_object_or_404(models.Issue, ref=ref, project_id=project_id) return self.retrieve(request, pk=issue.pk) + @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, issues_csv_uuid=uuid) + queryset = project.issues.all().order_by('ref') + data = services.issues_to_csv(project, queryset) + csv_response = HttpResponse(data.getvalue(), content_type='application/csv') + csv_response['Content-Disposition'] = 'attachment; filename="issues.csv"' + return csv_response + @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): serializer = serializers.IssuesBulkSerializer(data=request.DATA) diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py index b5689a7e..0a106092 100644 --- a/taiga/projects/issues/permissions.py +++ b/taiga/projects/issues/permissions.py @@ -28,6 +28,7 @@ class IssuePermission(TaigaResourcePermission): update_perms = HasProjectPerm('modify_issue') destroy_perms = HasProjectPerm('delete_issue') list_perms = AllowAny() + csv_perms = AllowAny() upvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') downvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') bulk_create_perms = HasProjectPerm('add_issue') diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index ac2f98ce..0733bc87 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -14,6 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import io +import csv + from taiga.base.utils import db, text from . import models @@ -58,3 +61,42 @@ def update_issues_order_in_bulk(bulk_data): issue_ids.append(issue_id) new_order_values.append({"order": new_order_value}) db.update_in_bulk_with_ids(issue_ids, new_order_values, model=models.Issue) + + +def issues_to_csv(project, queryset): + csv_data = io.StringIO() + fieldnames = ["ref", "subject", "description", "milestone", "owner", + "owner_full_name", "assigned_to", "assigned_to_full_name", + "status", "severity", "priority", "type", "is_closed", + "attachments", "external_reference"] + for custom_attr in project.issuecustomattributes.all(): + fieldnames.append(custom_attr.name) + + writer = csv.DictWriter(csv_data, fieldnames=fieldnames) + writer.writeheader() + for issue in queryset: + issue_data = { + "ref": issue.ref, + "subject": issue.subject, + "description": issue.description, + "milestone": issue.milestone.name if issue.milestone else None, + "owner": issue.owner.username, + "owner_full_name": issue.owner.get_full_name(), + "assigned_to": issue.assigned_to.username if issue.assigned_to else None, + "assigned_to_full_name": issue.assigned_to.get_full_name() if issue.assigned_to else None, + "status": issue.status.name, + "severity": issue.severity.name, + "priority": issue.priority.name, + "type": issue.type.name, + "is_closed": issue.is_closed, + "attachments": issue.attachments.count(), + "external_reference": issue.external_reference, + } + + for custom_attr in project.issuecustomattributes.all(): + value = issue.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) + issue_data[custom_attr.name] = value + + writer.writerow(issue_data) + + return csv_data diff --git a/taiga/projects/migrations/0018_auto_20150219_1606.py b/taiga/projects/migrations/0018_auto_20150219_1606.py new file mode 100644 index 00000000..a5f326b4 --- /dev/null +++ b/taiga/projects/migrations/0018_auto_20150219_1606.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('projects', '0017_fix_is_private_for_projects'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='issues_csv_uuid', + field=models.CharField(editable=False, max_length=32, default=None, null=True, db_index=True, blank=True), + preserve_default=True, + ), + migrations.AddField( + model_name='project', + name='tasks_csv_uuid', + field=models.CharField(editable=False, max_length=32, default=None, null=True, db_index=True, blank=True), + preserve_default=True, + ), + migrations.AddField( + model_name='project', + name='userstories_csv_uuid', + field=models.CharField(editable=False, max_length=32, default=None, null=True, db_index=True, blank=True), + preserve_default=True, + ), + ] diff --git a/taiga/projects/models.py b/taiga/projects/models.py index 6932715d..39d294db 100644 --- a/taiga/projects/models.py +++ b/taiga/projects/models.py @@ -15,6 +15,8 @@ # along with this program. If not, see . import itertools +import uuid + from django.core.exceptions import ValidationError from django.db import models @@ -163,6 +165,15 @@ class Project(ProjectDefaults, TaggedMixin, models.Model): is_private = models.BooleanField(default=True, null=False, blank=True, verbose_name=_("is private")) + userstories_csv_uuid = models.CharField(max_length=32, editable=False, + null=True, blank=True, + default=None, db_index=True) + tasks_csv_uuid = models.CharField(max_length=32, editable=False, null=True, + blank=True, default=None, db_index=True) + issues_csv_uuid = models.CharField(max_length=32, editable=False, + null=True, blank=True, default=None, + db_index=True) + tags_colors = TextArrayField(dimension=2, null=False, blank=True, verbose_name=_("tags colors"), default=[]) _importing = None diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index fc9ef228..08b900a7 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -54,6 +54,9 @@ class ProjectPermission(TaigaResourcePermission): list_perms = AllowAny() stats_perms = HasProjectPerm('view_project') member_stats_perms = HasProjectPerm('view_project') + regenerate_userstories_csv_uuid_perms = IsProjectOwner() + regenerate_issues_csv_uuid_perms = IsProjectOwner() + regenerate_tasks_csv_uuid_perms = IsProjectOwner() star_perms = IsAuthenticated() unstar_perms = IsAuthenticated() issues_stats_perms = HasProjectPerm('view_project') diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index b0332be9..b8634380 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -269,7 +269,8 @@ class ProjectSerializer(ModelSerializer): class Meta: model = models.Project read_only_fields = ("created_date", "modified_date", "owner") - exclude = ("last_us_ref", "last_task_ref", "last_issue_ref") + exclude = ("last_us_ref", "last_task_ref", "last_issue_ref", + "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid") def get_stars_number(self, obj): # The "stars_count" attribute is attached in the get_queryset of the viewset. @@ -301,6 +302,7 @@ class ProjectSerializer(ModelSerializer): raise serializers.ValidationError("Total milestones must be major or equal to zero") return attrs + class ProjectDetailSerializer(ProjectSerializer): roles = serializers.SerializerMethodField("get_roles") memberships = serializers.SerializerMethodField("get_memberships") @@ -331,6 +333,13 @@ class ProjectDetailSerializer(ProjectSerializer): return serializer.data +class ProjectDetailAdminSerializer(ProjectDetailSerializer): + class Meta: + model = models.Project + read_only_fields = ("created_date", "modified_date", "owner") + exclude = ("last_us_ref", "last_task_ref", "last_issue_ref") + + ###################################################### ## Starred ###################################################### diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index a2adf9e9..01bfa11f 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -22,6 +22,7 @@ from taiga.base import exceptions as exc from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet from taiga.projects.models import Project +from django.http import HttpResponse from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin @@ -71,6 +72,19 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, task = get_object_or_404(models.Task, ref=ref, project_id=project_id) return self.retrieve(request, pk=task.pk) + @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, tasks_csv_uuid=uuid) + queryset = project.tasks.all().order_by('ref') + data = services.tasks_to_csv(project, queryset) + csv_response = HttpResponse(data.getvalue(), content_type='application/csv') + csv_response['Content-Disposition'] = 'attachment; filename="tasks.csv"' + return csv_response + @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): serializer = serializers.TasksBulkSerializer(data=request.DATA) @@ -98,8 +112,8 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, self.check_permissions(request, "bulk_update_order", project) services.update_tasks_order_in_bulk(data["bulk_tasks"], - project=project, - field=order_field) + project=project, + field=order_field) services.snapshot_tasks_in_bulk(data["bulk_tasks"], request.user) return response.NoContent() diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 1983d7a6..f97215b8 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -26,5 +26,6 @@ class TaskPermission(TaigaResourcePermission): update_perms = HasProjectPerm('modify_task') destroy_perms = HasProjectPerm('delete_task') list_perms = AllowAny() + csv_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_task') bulk_update_order_perms = HasProjectPerm('modify_task') diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 379d1321..d4a00a1e 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -14,6 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import io +import csv + from taiga.base.utils import db, text from taiga.projects.history.services import take_snapshot from taiga.events import events @@ -75,3 +78,42 @@ def snapshot_tasks_in_bulk(bulk_data, user): take_snapshot(task, user=user) except models.UserStory.DoesNotExist: pass + + +def tasks_to_csv(project, queryset): + csv_data = io.StringIO() + fieldnames = ["ref", "subject", "description", "user_story", "milestone", "owner", + "owner_full_name", "assigned_to", "assigned_to_full_name", + "status", "is_iocaine", "is_closed", "us_order", + "taskboard_order", "attachments", "external_reference"] + for custom_attr in project.taskcustomattributes.all(): + fieldnames.append(custom_attr.name) + + writer = csv.DictWriter(csv_data, fieldnames=fieldnames) + writer.writeheader() + for task in queryset: + task_data = { + "ref": task.ref, + "subject": task.subject, + "description": task.description, + "user_story": task.user_story.ref if task.user_story else None, + "milestone": task.milestone.name if task.milestone else None, + "owner": task.owner.username, + "owner_full_name": task.owner.get_full_name(), + "assigned_to": task.assigned_to.username if task.assigned_to else None, + "assigned_to_full_name": task.assigned_to.get_full_name() if task.assigned_to else None, + "status": task.status.name, + "is_iocaine": task.is_iocaine, + "is_closed": task.status.is_closed, + "us_order": task.us_order, + "taskboard_order": task.taskboard_order, + "attachments": task.attachments.count(), + "external_reference": task.external_reference, + } + for custom_attr in project.taskcustomattributes.all(): + value = task.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) + task_data[custom_attr.name] = value + + writer.writerow(task_data) + + return csv_data diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 32d40242..1e270dc1 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -22,6 +22,7 @@ from django.apps import apps from django.db import transaction from django.utils.translation import ugettext as _ from django.core.exceptions import ObjectDoesNotExist +from django.http import HttpResponse from taiga.base import filters from taiga.base import exceptions as exc @@ -102,6 +103,19 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi userstory = get_object_or_404(models.UserStory, ref=ref, project_id=project_id) return self.retrieve(request, pk=userstory.pk) + @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, userstories_csv_uuid=uuid) + queryset = project.user_stories.all().order_by('ref') + data = services.userstories_to_csv(project, queryset) + csv_response = HttpResponse(data.getvalue(), content_type='application/csv') + csv_response['Content-Disposition'] = 'attachment; filename="userstories.csv"' + return csv_response + @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): serializer = serializers.UserStoriesBulkSerializer(data=request.DATA) diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index c0a7a5bc..3b836cb6 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -25,5 +25,6 @@ class UserStoryPermission(TaigaResourcePermission): update_perms = HasProjectPerm('modify_us') destroy_perms = HasProjectPerm('delete_us') list_perms = AllowAny() + csv_perms = AllowAny() bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) bulk_update_order_perms = HasProjectPerm('modify_us') diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 0d70cb1e..fefc15c3 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -14,6 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import csv +import io + from django.utils import timezone from taiga.base.utils import db, text @@ -104,3 +107,64 @@ def open_userstory(us): us.is_closed = False us.finish_date = None us.save(update_fields=["is_closed", "finish_date"]) + + +def userstories_to_csv(project,queryset): + csv_data = io.StringIO() + fieldnames = ["ref", "subject", "description", "milestone", "owner", + "owner_full_name", "assigned_to", "assigned_to_full_name", + "status", "is_closed"] + for role in project.roles.filter(computable=True).order_by('name'): + fieldnames.append("{}-points".format(role.slug)) + fieldnames.append("total-points") + + fieldnames += ["backlog_order", "sprint_order", "kanban_order", + "created_date", "modified_date", "finish_date", + "client_requirement", "team_requirement", "attachments", + "generated_from_issue", "external_reference", "tasks"] + + for custom_attr in project.userstorycustomattributes.all(): + fieldnames.append(custom_attr.name) + + writer = csv.DictWriter(csv_data, fieldnames=fieldnames) + writer.writeheader() + for us in queryset: + row = { + "ref": us.ref, + "subject": us.subject, + "description": us.description, + "milestone": us.milestone.name if us.milestone else None, + "owner": us.owner.username, + "owner_full_name": us.owner.get_full_name(), + "assigned_to": us.assigned_to.username if us.assigned_to else None, + "assigned_to_full_name": us.assigned_to.get_full_name() if us.assigned_to else None, + "status": us.status.name, + "is_closed": us.is_closed, + "backlog_order": us.backlog_order, + "sprint_order": us.sprint_order, + "kanban_order": us.kanban_order, + "created_date": us.created_date, + "modified_date": us.modified_date, + "finish_date": us.finish_date, + "client_requirement": us.client_requirement, + "team_requirement": us.team_requirement, + "attachments": us.attachments.count(), + "generated_from_issue": us.generated_from_issue.ref if us.generated_from_issue else None, + "external_reference": us.external_reference, + "tasks": ",".join([str(task.ref) for task in us.tasks.all()]), + } + + for role in us.project.roles.filter(computable=True).order_by('name'): + if us.role_points.filter(role_id=role.id).count() == 1: + row["{}-points".format(role.slug)] = us.role_points.get(role_id=role.id).points.value + else: + row["{}-points".format(role.slug)] = 0 + row['total-points'] = us.get_total_points() + + for custom_attr in project.userstorycustomattributes.all(): + value = us.custom_attributes_values.attributes_values.get(str(custom_attr.id), None) + row[custom_attr.name] = value + + writer.writerow(row) + + return csv_data diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index 7b8ec066..c6c99f2d 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -1,3 +1,5 @@ +import uuid + from django.core.urlresolvers import reverse from taiga.projects.issues.serializers import IssueSerializer @@ -36,15 +38,18 @@ 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], USER_PERMISSIONS)), - owner=m.project_owner) + owner=m.project_owner, + issues_csv_uuid=uuid.uuid4().hex) 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], USER_PERMISSIONS)), - owner=m.project_owner) + owner=m.project_owner, + issues_csv_uuid=uuid.uuid4().hex) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], - owner=m.project_owner) + owner=m.project_owner, + issues_csv_uuid=uuid.uuid4().hex) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -437,3 +442,27 @@ def test_issue_voters_retrieve(client, data): results = helper_test_http_method(client, 'get', private_url2, None, users) assert results == [401, 403, 403, 200, 200] + + +def test_issues_csv(client, data): + url = reverse('issues-csv') + csv_public_uuid = data.public_project.issues_csv_uuid + csv_private1_uuid = data.private_project1.issues_csv_uuid + csv_private2_uuid = data.private_project1.issues_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] diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 6518734b..0c97c952 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -369,3 +369,66 @@ def test_invitations_retrieve(client, data): ] results = helper_test_http_method(client, 'get', url, None, users) assert results == [200, 200, 200, 200] + + +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}) + private2_url = reverse('projects-regenerate-userstories-csv-uuid', kwargs={"pk": data.private_project2.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] + + +def test_regenerate_tasks_csv_uuid(client, data): + public_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-regenerate-tasks-csv-uuid', kwargs={"pk": data.private_project2.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] + + +def test_regenerate_issues_csv_uuid(client, data): + public_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.public_project.pk}) + private1_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.private_project1.pk}) + private2_url = reverse('projects-regenerate-issues-csv-uuid', kwargs={"pk": data.private_project2.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] diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 272d0dde..22bb719f 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -1,3 +1,5 @@ +import uuid + from django.core.urlresolvers import reverse from taiga.base.utils import json @@ -35,15 +37,18 @@ 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], USER_PERMISSIONS)), - owner=m.project_owner) + owner=m.project_owner, + tasks_csv_uuid=uuid.uuid4().hex) 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], USER_PERMISSIONS)), - owner=m.project_owner) + owner=m.project_owner, + tasks_csv_uuid=uuid.uuid4().hex) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], - owner=m.project_owner) + owner=m.project_owner, + tasks_csv_uuid=uuid.uuid4().hex) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -307,3 +312,27 @@ def test_task_action_bulk_create(client, data): }) results = helper_test_http_method(client, 'post', url, bulk_data, users) assert results == [401, 403, 403, 200, 200] + + +def test_tasks_csv(client, data): + url = reverse('tasks-csv') + csv_public_uuid = data.public_project.tasks_csv_uuid + csv_private1_uuid = data.private_project1.tasks_csv_uuid + csv_private2_uuid = data.private_project1.tasks_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] diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index 9725ff28..3a718cc7 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -1,3 +1,5 @@ +import uuid + from django.core.urlresolvers import reverse from taiga.base.utils import json @@ -35,15 +37,18 @@ 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], USER_PERMISSIONS)), - owner=m.project_owner) + owner=m.project_owner, + userstories_csv_uuid=uuid.uuid4().hex) 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], USER_PERMISSIONS)), - owner=m.project_owner) + owner=m.project_owner, + userstories_csv_uuid=uuid.uuid4().hex) m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], - owner=m.project_owner) + owner=m.project_owner, + userstories_csv_uuid=uuid.uuid4().hex) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -308,3 +313,27 @@ def test_user_story_action_bulk_update_order(client, data): }) results = helper_test_http_method(client, 'post', url, post_data, users) assert results == [401, 403, 403, 204, 204] + + +def test_user_stories_csv(client, data): + url = reverse('userstories-csv') + csv_public_uuid = data.public_project.userstories_csv_uuid + csv_private1_uuid = data.private_project1.userstories_csv_uuid + csv_private2_uuid = data.private_project1.userstories_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] diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index daee511a..9cd3d2af 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -1,3 +1,6 @@ +import uuid +import csv + from unittest import mock from django.core.urlresolvers import reverse @@ -152,7 +155,6 @@ def test_api_filter_by_text_6(client): issue = f.create_issue(subject="test", owner=user) issue.ref = 123 issue.save() - print(issue.ref, issue.subject) url = reverse("issues-list") + "?q=%s" % (issue.ref) client.login(issue.owner) @@ -161,3 +163,38 @@ def test_api_filter_by_text_6(client): assert response.status_code == 200 assert number_of_issues == 1 + + +def test_get_invalid_csv(client): + url = reverse("issues-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("issues-csv") + project = f.ProjectFactory.create(issues_csv_uuid=uuid.uuid4().hex) + + response = client.get("{}?uuid={}".format(url, project.issues_csv_uuid)) + assert response.status_code == 200 + + +def test_custom_fields_csv_generation(): + project = f.ProjectFactory.create(issues_csv_uuid=uuid.uuid4().hex) + attr = f.IssueCustomAttributeFactory.create(project=project, name="attr1", description="desc") + issue = f.IssueFactory.create(project=project) + attr_values = issue.custom_attributes_values + attr_values.attributes_values = {str(attr.id):"val1"} + attr_values.save() + queryset = project.issues.all() + data = services.issues_to_csv(project, queryset) + data.seek(0) + reader = csv.reader(data) + row = next(reader) + assert row[15] == attr.name + row = next(reader) + assert row[15] == "val1" diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index 2dfcabef..63fe177e 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -1,3 +1,6 @@ +import uuid +import csv + from unittest import mock from django.core.urlresolvers import reverse @@ -110,3 +113,38 @@ def test_api_update_order_in_bulk(client): assert response1.status_code == 204, response1.data assert response2.status_code == 204, response2.data + + +def test_get_invalid_csv(client): + url = reverse("tasks-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("tasks-csv") + project = f.ProjectFactory.create(tasks_csv_uuid=uuid.uuid4().hex) + + response = client.get("{}?uuid={}".format(url, project.tasks_csv_uuid)) + assert response.status_code == 200 + + +def test_custom_fields_csv_generation(): + project = f.ProjectFactory.create(tasks_csv_uuid=uuid.uuid4().hex) + attr = f.TaskCustomAttributeFactory.create(project=project, name="attr1", description="desc") + task = f.TaskFactory.create(project=project) + attr_values = task.custom_attributes_values + attr_values.attributes_values = {str(attr.id):"val1"} + attr_values.save() + queryset = project.tasks.all() + data = services.tasks_to_csv(project, queryset) + data.seek(0) + reader = csv.reader(data) + row = next(reader) + assert row[16] == attr.name + row = next(reader) + assert row[16] == "val1" diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index f0715bac..ceb0cc12 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -1,4 +1,7 @@ import copy +import uuid +import csv + from unittest import mock from django.core.urlresolvers import reverse @@ -241,3 +244,38 @@ def test_get_total_points(client): f.RolePointsFactory.create(user_story=us_mixed, role=role2, points=points2) assert us_mixed.get_total_points() == 1.0 + + +def test_get_invalid_csv(client): + url = reverse("userstories-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("userstories-csv") + project = f.ProjectFactory.create(userstories_csv_uuid=uuid.uuid4().hex) + + response = client.get("{}?uuid={}".format(url, project.userstories_csv_uuid)) + assert response.status_code == 200 + + +def test_custom_fields_csv_generation(): + project = f.ProjectFactory.create(userstories_csv_uuid=uuid.uuid4().hex) + attr = f.UserStoryCustomAttributeFactory.create(project=project, name="attr1", description="desc") + us = f.UserStoryFactory.create(project=project) + attr_values = us.custom_attributes_values + attr_values.attributes_values = {str(attr.id):"val1"} + attr_values.save() + queryset = project.user_stories.all() + data = services.userstories_to_csv(project, queryset) + data.seek(0) + reader = csv.reader(data) + row = next(reader) + assert row[23] == attr.name + row = next(reader) + assert row[23] == "val1"