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"