Merge pull request #242 from taigaio/us/55/custom-fields

US #55: Custom fields
remotes/origin/enhancement/email-actions
Alejandro 2015-03-04 14:27:22 +01:00
commit 336d141656
44 changed files with 3612 additions and 152 deletions

View File

@ -4,7 +4,7 @@
## 1.6.0 ??? (Unreleased)
### Features
- ...
- Added custom fields per project for user stories, tasks and issues.
### Misc
- New contrib plugin for hipchat (by Δndrea Stagi)

View File

@ -180,6 +180,7 @@ INSTALLED_APPS = [
"taiga.userstorage",
"taiga.projects",
"taiga.projects.references",
"taiga.projects.custom_attributes",
"taiga.projects.history",
"taiga.projects.notifications",
"taiga.projects.attachments",

View File

@ -19,10 +19,12 @@
from .viewsets import ModelListViewSet
from .viewsets import ModelCrudViewSet
from .viewsets import ModelUpdateRetrieveViewSet
from .viewsets import GenericViewSet
from .viewsets import ReadOnlyListViewSet
__all__ = ["ModelCrudViewSet",
"ModelListViewSet",
"ModelUpdateRetrieveViewSet",
"GenericViewSet",
"ReadOnlyListViewSet"]

View File

@ -168,3 +168,8 @@ class ModelListViewSet(pagination.HeadersPaginationMixin,
mixins.ListModelMixin,
GenericViewSet):
pass
class ModelUpdateRetrieveViewSet(mixins.UpdateModelMixin,
mixins.RetrieveModelMixin,
GenericViewSet):
pass

View File

@ -127,6 +127,21 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
"severities" in data):
service.store_default_choices(project_serialized.object, data)
if "userstorycustomattributes" in data:
service.store_custom_attributes(project_serialized.object, data,
"userstorycustomattributes",
serializers.UserStoryCustomAttributeExportSerializer)
if "taskcustomattributes" in data:
service.store_custom_attributes(project_serialized.object, data,
"taskcustomattributes",
serializers.TaskCustomAttributeExportSerializer)
if "issuecustomattributes" in data:
service.store_custom_attributes(project_serialized.object, data,
"issuecustomattributes",
serializers.IssueCustomAttributeExportSerializer)
if "roles" in data:
service.store_roles(project_serialized.object, data)

View File

@ -103,6 +103,16 @@ def dict_to_project(data, owner=None):
if service.get_errors(clear=False):
raise TaigaImportError('error importing default choices')
service.store_custom_attributes(proj, data, "userstorycustomattributes",
serializers.UserStoryCustomAttributeExportSerializer)
service.store_custom_attributes(proj, data, "taskcustomattributes",
serializers.TaskCustomAttributeExportSerializer)
service.store_custom_attributes(proj, data, "issuecustomattributes",
serializers.IssueCustomAttributeExportSerializer)
if service.get_errors(clear=False):
raise TaigaImportError('error importing custom attributes')
service.store_roles(proj, data)
if service.get_errors(clear=False):

View File

@ -20,11 +20,14 @@ from collections import OrderedDict
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from taiga.projects import models as projects_models
from taiga.projects.custom_attributes import models as custom_attributes_models
from taiga.projects.userstories import models as userstories_models
from taiga.projects.tasks import models as tasks_models
from taiga.projects.issues import models as issues_models
@ -81,14 +84,15 @@ class RelatedNoneSafeField(serializers.RelatedField):
return
value = self.get_default_value()
key = self.source or field_name
if value in self.null_values:
if self.required:
raise ValidationError(self.error_messages['required'])
into[(self.source or field_name)] = None
into[key] = None
elif self.many:
into[(self.source or field_name)] = [self.from_native(item) for item in value if self.from_native(item) is not None]
into[key] = [self.from_native(item) for item in value if self.from_native(item) is not None]
else:
into[(self.source or field_name)] = self.from_native(value)
into[key] = self.from_native(value)
class UserRelatedField(RelatedNoneSafeField):
@ -251,7 +255,8 @@ class AttachmentExportSerializerMixin(serializers.ModelSerializer):
def get_attachments(self, obj):
content_type = ContentType.objects.get_for_model(obj.__class__)
attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk, content_type=content_type)
attachments_qs = attachments_models.Attachment.objects.filter(object_id=obj.pk,
content_type=content_type)
return AttachmentExportSerializer(attachments_qs, many=True).data
@ -305,6 +310,114 @@ class RoleExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'project')
class UserStoryCustomAttributeExportSerializer(serializers.ModelSerializer):
modified_date = serializers.DateTimeField(required=False)
class Meta:
model = custom_attributes_models.UserStoryCustomAttribute
exclude = ('id', 'project')
class TaskCustomAttributeExportSerializer(serializers.ModelSerializer):
modified_date = serializers.DateTimeField(required=False)
class Meta:
model = custom_attributes_models.TaskCustomAttribute
exclude = ('id', 'project')
class IssueCustomAttributeExportSerializer(serializers.ModelSerializer):
modified_date = serializers.DateTimeField(required=False)
class Meta:
model = custom_attributes_models.IssueCustomAttribute
exclude = ('id', 'project')
class CustomAttributesValuesExportSerializerMixin(serializers.ModelSerializer):
custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values")
def custom_attributes_queryset(self, project):
raise NotImplementedError()
def get_custom_attributes_values(self, obj):
def _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values):
ret = {}
for attr in custom_attributes:
value = values.get(str(attr["id"]), None)
if value is not None:
ret[attr["name"]] = value
return ret
try:
values = obj.custom_attributes_values.attributes_values
custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name')
return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values)
except ObjectDoesNotExist:
return None
class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer):
attributes_values = JsonField(source="attributes_values",required=True)
_custom_attribute_model = None
_container_field = None
class Meta:
exclude = ("id",)
def validate_attributes_values(self, attrs, source):
# values must be a dict
data_values = attrs.get("attributes_values", None)
if self.object:
data_values = (data_values or self.object.attributes_values)
if type(data_values) is not dict:
raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
# Values keys must be in the container object project
data_container = attrs.get(self._container_field, None)
if data_container:
project_id = data_container.project_id
elif self.object:
project_id = getattr(self.object, self._container_field).project_id
else:
project_id = None
values_ids = list(data_values.keys())
qs = self._custom_attribute_model.objects.filter(project=project_id,
id__in=values_ids)
if qs.count() != len(values_ids):
raise ValidationError(_("It contain invalid custom fields."))
return attrs
class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
_custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute
_container_model = "userstories.UserStory"
_container_field = "user_story"
class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
model = custom_attributes_models.UserStoryCustomAttributesValues
class TaskCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
_custom_attribute_model = custom_attributes_models.TaskCustomAttribute
_container_field = "task"
class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
model = custom_attributes_models.TaskCustomAttributesValues
class IssueCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
_custom_attribute_model = custom_attributes_models.IssueCustomAttribute
_container_field = "issue"
class Meta(BaseCustomAttributesValuesExportSerializer.Meta):
model = custom_attributes_models.IssueCustomAttributesValues
class MembershipExportSerializer(serializers.ModelSerializer):
user = UserRelatedField(required=False)
role = ProjectRelatedField(slug_field="name")
@ -354,7 +467,8 @@ class MilestoneExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'project')
class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer):
class TaskExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer):
owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name")
user_story = ProjectRelatedField(slug_field="ref", required=False)
@ -367,8 +481,12 @@ class TaskExportSerializer(HistoryExportSerializerMixin, AttachmentExportSeriali
model = tasks_models.Task
exclude = ('id', 'project')
def custom_attributes_queryset(self, project):
return project.taskcustomattributes.all()
class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer):
class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer):
role_points = RolePointsExportSerializer(many=True, required=False)
owner = UserRelatedField(required=False)
assigned_to = UserRelatedField(required=False)
@ -382,8 +500,12 @@ class UserStoryExportSerializer(HistoryExportSerializerMixin, AttachmentExportSe
model = userstories_models.UserStory
exclude = ('id', 'project', 'points', 'tasks')
def custom_attributes_queryset(self, project):
return project.userstorycustomattributes.all()
class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer):
class IssueExportSerializer(CustomAttributesValuesExportSerializerMixin, HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, serializers.ModelSerializer):
owner = UserRelatedField(required=False)
status = ProjectRelatedField(slug_field="name")
assigned_to = UserRelatedField(required=False)
@ -395,15 +517,19 @@ class IssueExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerial
votes = serializers.SerializerMethodField("get_votes")
modified_date = serializers.DateTimeField(required=False)
def get_votes(self, obj):
return [x.email for x in votes_service.get_voters(obj)]
class Meta:
model = issues_models.Issue
exclude = ('id', 'project')
def get_votes(self, obj):
return [x.email for x in votes_service.get_voters(obj)]
class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin, serializers.ModelSerializer):
def custom_attributes_queryset(self, project):
return project.issuecustomattributes.all()
class WikiPageExportSerializer(HistoryExportSerializerMixin, AttachmentExportSerializerMixin,
serializers.ModelSerializer):
owner = UserRelatedField(required=False)
last_modifier = UserRelatedField(required=False)
watchers = UserRelatedField(many=True, required=False)
@ -437,6 +563,9 @@ class ProjectExportSerializer(serializers.ModelSerializer):
priorities = PriorityExportSerializer(many=True, required=False)
severities = SeverityExportSerializer(many=True, required=False)
issue_types = IssueTypeExportSerializer(many=True, required=False)
userstorycustomattributes = UserStoryCustomAttributeExportSerializer(many=True, required=False)
taskcustomattributes = TaskCustomAttributeExportSerializer(many=True, required=False)
issuecustomattributes = IssueCustomAttributeExportSerializer(many=True, required=False)
roles = RoleExportSerializer(many=True, required=False)
milestones = MilestoneExportSerializer(many=True, required=False)
wiki_pages = WikiPageExportSerializer(many=True, required=False)

View File

@ -20,6 +20,7 @@ from unidecode import unidecode
from django.template.defaultfilters import slugify
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from taiga.projects.history.services import make_key_from_model_object
from taiga.projects.references import sequences as seq
@ -57,7 +58,8 @@ def store_project(data):
"default_priority", "default_severity", "default_issue_status",
"default_issue_type", "memberships", "points", "us_statuses",
"task_statuses", "issue_statuses", "priorities", "severities",
"issue_types", "roles", "milestones", "wiki_pages",
"issue_types", "userstorycustomattributes", "taskcustomattributes",
"issuecustomattributes", "roles", "milestones", "wiki_pages",
"wiki_links", "notify_policies", "user_stories", "issues", "tasks",
]
if key not in excluded_fields:
@ -72,7 +74,7 @@ def store_project(data):
return None
def store_choice(project, data, field, serializer):
def _store_choice(project, data, field, serializer):
serialized = serializer(data=data)
if serialized.is_valid():
serialized.object.project = project
@ -86,10 +88,58 @@ def store_choice(project, data, field, serializer):
def store_choices(project, data, field, serializer):
result = []
for choice_data in data.get(field, []):
result.append(store_choice(project, choice_data, field, serializer))
result.append(_store_choice(project, choice_data, field, serializer))
return result
def _store_custom_attribute(project, data, field, serializer):
serialized = serializer(data=data)
if serialized.is_valid():
serialized.object.project = project
serialized.object._importing = True
serialized.save()
return serialized.object
add_errors(field, serialized.errors)
return None
def store_custom_attributes(project, data, field, serializer):
result = []
for custom_attribute_data in data.get(field, []):
result.append(_store_custom_attribute(project, custom_attribute_data, field, serializer))
return result
def store_custom_attributes_values(obj, data_values, obj_field, serializer_class):
data = {
obj_field: obj.id,
"attributes_values": data_values,
}
try:
custom_attributes_values = obj.custom_attributes_values
serializer = serializer_class(custom_attributes_values, data=data)
except ObjectDoesNotExist:
serializer = serializer_class(data=data)
if serializer.is_valid():
serializer.save()
return serializer
add_errors("custom_attributes_values", serializer.errors)
return None
def _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes, values):
ret = {}
for attr in custom_attributes:
value = values.get(attr["name"], None)
if value is not None:
ret[str(attr["id"])] = value
return ret
def store_role(project, role):
serialized = serializers.RoleExportSerializer(data=role)
if serialized.is_valid():
@ -103,7 +153,7 @@ def store_role(project, role):
def store_roles(project, data):
results = []
for role in data.get('roles', []):
for role in data.get("roles", []):
results.append(store_role(project, role))
return results
@ -145,16 +195,16 @@ def store_membership(project, membership):
def store_memberships(project, data):
results = []
for membership in data.get('memberships', []):
for membership in data.get("memberships", []):
results.append(store_membership(project, membership))
return results
def store_task(project, task):
if 'status' not in task and project.default_task_status:
task['status'] = project.default_task_status.name
def store_task(project, data):
if "status" not in data and project.default_task_status:
data["status"] = project.default_task_status.name
serialized = serializers.TaskExportSerializer(data=task, context={"project": project})
serialized = serializers.TaskExportSerializer(data=data, context={"project": project})
if serialized.is_valid():
serialized.object.project = project
if serialized.object.owner is None:
@ -173,12 +223,20 @@ def store_task(project, task):
serialized.object.ref, _ = refs.make_reference(serialized.object, project)
serialized.object.save()
for task_attachment in task.get('attachments', []):
for task_attachment in data.get("attachments", []):
store_attachment(project, serialized.object, task_attachment)
for history in task.get('history', []):
for history in data.get("history", []):
store_history(project, serialized.object, history)
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
custom_attributes = serialized.object.project.taskcustomattributes.all().values('id', 'name')
custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
custom_attributes_values)
store_custom_attributes_values(serialized.object, custom_attributes_values,
"task", serializers.TaskCustomAttributesValuesExportSerializer)
return serialized
add_errors("tasks", serialized.errors)
@ -192,8 +250,8 @@ def store_milestone(project, milestone):
serialized.object._importing = True
serialized.save()
for task_without_us in milestone.get('tasks_without_us', []):
task_without_us['user_story'] = None
for task_without_us in milestone.get("tasks_without_us", []):
task_without_us["user_story"] = None
store_task(project, task_without_us)
return serialized
@ -232,7 +290,7 @@ def store_history(project, obj, history):
def store_wiki_page(project, wiki_page):
wiki_page['slug'] = slugify(unidecode(wiki_page.get('slug', '')))
wiki_page["slug"] = slugify(unidecode(wiki_page.get("slug", "")))
serialized = serializers.WikiPageExportSerializer(data=wiki_page)
if serialized.is_valid():
serialized.object.project = project
@ -242,10 +300,10 @@ def store_wiki_page(project, wiki_page):
serialized.object._not_notify = True
serialized.save()
for attachment in wiki_page.get('attachments', []):
for attachment in wiki_page.get("attachments", []):
store_attachment(project, serialized.object, attachment)
for history in wiki_page.get('history', []):
for history in wiki_page.get("history", []):
store_history(project, serialized.object, history)
return serialized
@ -276,61 +334,12 @@ def store_role_point(project, us, role_point):
return None
def store_user_story(project, userstory):
if 'status' not in userstory and project.default_us_status:
userstory['status'] = project.default_us_status.name
def store_user_story(project, data):
if "status" not in data and project.default_us_status:
data["status"] = project.default_us_status.name
userstory_data = {}
for key, value in userstory.items():
if key != 'role_points':
userstory_data[key] = value
serialized_us = serializers.UserStoryExportSerializer(data=userstory_data, context={"project": project})
if serialized_us.is_valid():
serialized_us.object.project = project
if serialized_us.object.owner is None:
serialized_us.object.owner = serialized_us.object.project.owner
serialized_us.object._importing = True
serialized_us.object._not_notify = True
serialized_us.save()
if serialized_us.object.ref:
sequence_name = refs.make_sequence_name(project)
if not seq.exists(sequence_name):
seq.create(sequence_name)
seq.set_max(sequence_name, serialized_us.object.ref)
else:
serialized_us.object.ref, _ = refs.make_reference(serialized_us.object, project)
serialized_us.object.save()
for us_attachment in userstory.get('attachments', []):
store_attachment(project, serialized_us.object, us_attachment)
for role_point in userstory.get('role_points', []):
store_role_point(project, serialized_us.object, role_point)
for history in userstory.get('history', []):
store_history(project, serialized_us.object, history)
return serialized_us
add_errors("user_stories", serialized_us.errors)
return None
def store_issue(project, data):
serialized = serializers.IssueExportSerializer(data=data, context={"project": project})
if 'type' not in data and project.default_issue_type:
data['type'] = project.default_issue_type.name
if 'status' not in data and project.default_issue_status:
data['status'] = project.default_issue_status.name
if 'priority' not in data and project.default_priority:
data['priority'] = project.default_priority.name
if 'severity' not in data and project.default_severity:
data['severity'] = project.default_severity.name
us_data = {key: value for key, value in data.items() if key not in ["role_points", "custom_attributes_values"]}
serialized = serializers.UserStoryExportSerializer(data=us_data, context={"project": project})
if serialized.is_valid():
serialized.object.project = project
@ -350,10 +359,77 @@ def store_issue(project, data):
serialized.object.ref, _ = refs.make_reference(serialized.object, project)
serialized.object.save()
for attachment in data.get('attachments', []):
store_attachment(project, serialized.object, attachment)
for history in data.get('history', []):
for us_attachment in data.get("attachments", []):
store_attachment(project, serialized.object, us_attachment)
for role_point in data.get("role_points", []):
store_role_point(project, serialized.object, role_point)
for history in data.get("history", []):
store_history(project, serialized.object, history)
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
custom_attributes = serialized.object.project.userstorycustomattributes.all().values('id', 'name')
custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
custom_attributes_values)
store_custom_attributes_values(serialized.object, custom_attributes_values,
"user_story", serializers.UserStoryCustomAttributesValuesExportSerializer)
return serialized
add_errors("user_stories", serialized.errors)
return None
def store_issue(project, data):
serialized = serializers.IssueExportSerializer(data=data, context={"project": project})
if "type" not in data and project.default_issue_type:
data["type"] = project.default_issue_type.name
if "status" not in data and project.default_issue_status:
data["status"] = project.default_issue_status.name
if "priority" not in data and project.default_priority:
data["priority"] = project.default_priority.name
if "severity" not in data and project.default_severity:
data["severity"] = project.default_severity.name
if serialized.is_valid():
serialized.object.project = project
if serialized.object.owner is None:
serialized.object.owner = serialized.object.project.owner
serialized.object._importing = True
serialized.object._not_notify = True
serialized.save()
if serialized.object.ref:
sequence_name = refs.make_sequence_name(project)
if not seq.exists(sequence_name):
seq.create(sequence_name)
seq.set_max(sequence_name, serialized.object.ref)
else:
serialized.object.ref, _ = refs.make_reference(serialized.object, project)
serialized.object.save()
for attachment in data.get("attachments", []):
store_attachment(project, serialized.object, attachment)
for history in data.get("history", []):
store_history(project, serialized.object, history)
custom_attributes_values = data.get("custom_attributes_values", None)
if custom_attributes_values:
custom_attributes = serialized.object.project.issuecustomattributes.all().values('id', 'name')
custom_attributes_values = _use_id_instead_name_as_key_in_custom_attributes_values(custom_attributes,
custom_attributes_values)
store_custom_attributes_values(serialized.object, custom_attributes_values,
"issue", serializers.IssueCustomAttributesValuesExportSerializer)
return serialized
add_errors("issues", serialized.errors)
return None

View File

@ -0,0 +1,71 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.contrib import admin
from . import models
@admin.register(models.UserStoryCustomAttribute)
class UserStoryCustomAttributeAdmin(admin.ModelAdmin):
list_display = ["id", "name", "project", "order"]
list_display_links = ["id", "name"]
fieldsets = (
(None, {
"fields": ("name", "description", ("project", "order"))
}),
("Advanced options", {
"classes": ("collapse",),
"fields": (("created_date", "modified_date"),)
})
)
readonly_fields = ("created_date", "modified_date")
search_fields = ["id", "name", "project__name", "project__slug"]
@admin.register(models.TaskCustomAttribute)
class TaskCustomAttributeAdmin(admin.ModelAdmin):
list_display = ["id", "name", "project", "order"]
list_display_links = ["id", "name"]
fieldsets = (
(None, {
"fields": ("name", "description", ("project", "order"))
}),
("Advanced options", {
"classes": ("collapse",),
"fields": (("created_date", "modified_date"),)
})
)
readonly_fields = ("created_date", "modified_date")
search_fields = ["id", "name", "project__name", "project__slug"]
@admin.register(models.IssueCustomAttribute)
class IssueCustomAttributeAdmin(admin.ModelAdmin):
list_display = ["id", "name", "project", "order"]
list_display_links = ["id", "name"]
fieldsets = (
(None, {
"fields": ("name", "description", ("project", "order"))
}),
("Advanced options", {
"classes": ("collapse",),
"fields": (("created_date", "modified_date"),)
})
)
readonly_fields = ("created_date", "modified_date")
search_fields = ["id", "name", "project__name", "project__slug"]

View File

@ -0,0 +1,119 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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_lazy as _
from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelUpdateRetrieveViewSet
from taiga.base import exceptions as exc
from taiga.base import filters
from taiga.base import response
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.occ.mixins import OCCResourceMixin
from . import models
from . import serializers
from . import permissions
from . import services
######################################################
# Custom Attribute ViewSets
#######################################################
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
model = models.UserStoryCustomAttribute
serializer_class = serializers.UserStoryCustomAttributeSerializer
permission_classes = (permissions.UserStoryCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
bulk_update_param = "bulk_userstory_custom_attributes"
bulk_update_perm = "change_userstory_custom_attributes"
bulk_update_order_action = services.bulk_update_userstory_custom_attribute_order
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
model = models.TaskCustomAttribute
serializer_class = serializers.TaskCustomAttributeSerializer
permission_classes = (permissions.TaskCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
bulk_update_param = "bulk_task_custom_attributes"
bulk_update_perm = "change_task_custom_attributes"
bulk_update_order_action = services.bulk_update_task_custom_attribute_order
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, ModelCrudViewSet):
model = models.IssueCustomAttribute
serializer_class = serializers.IssueCustomAttributeSerializer
permission_classes = (permissions.IssueCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
bulk_update_param = "bulk_issue_custom_attributes"
bulk_update_perm = "change_issue_custom_attributes"
bulk_update_order_action = services.bulk_update_issue_custom_attribute_order
######################################################
# Custom Attributes Values ViewSets
#######################################################
class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
ModelUpdateRetrieveViewSet):
def get_object_for_snapshot(self, obj):
return getattr(obj, self.content_object)
class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.UserStoryCustomAttributesValues
serializer_class = serializers.UserStoryCustomAttributesValuesSerializer
permission_classes = (permissions.UserStoryCustomAttributesValuesPermission,)
lookup_field = "user_story_id"
content_object = "user_story"
def get_queryset(self):
qs = self.model.objects.all()
qs = qs.select_related("user_story", "user_story__project")
return qs
class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.TaskCustomAttributesValues
serializer_class = serializers.TaskCustomAttributesValuesSerializer
permission_classes = (permissions.TaskCustomAttributesValuesPermission,)
lookup_field = "task_id"
content_object = "task"
def get_queryset(self):
qs = self.model.objects.all()
qs = qs.select_related("task", "task__project")
return qs
class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.IssueCustomAttributesValues
serializer_class = serializers.IssueCustomAttributesValuesSerializer
permission_classes = (permissions.IssueCustomAttributesValuesPermission,)
lookup_field = "issue_id"
content_object = "issue"
def get_queryset(self):
qs = self.model.objects.all()
qs = qs.select_related("issue", "issue__project")
return qs

View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('projects', '0015_auto_20141230_1212'),
]
operations = [
migrations.CreateModel(
name='IssueCustomAttribute',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
('name', models.CharField(verbose_name='name', max_length=64)),
('description', models.TextField(blank=True, verbose_name='description')),
('order', models.IntegerField(verbose_name='order', default=10000)),
('created_date', models.DateTimeField(verbose_name='created date', default=django.utils.timezone.now)),
('modified_date', models.DateTimeField(verbose_name='modified date')),
('project', models.ForeignKey(to='projects.Project', verbose_name='project', related_name='issuecustomattributes')),
],
options={
'ordering': ['project', 'order', 'name'],
'verbose_name': 'issue custom attribute',
'verbose_name_plural': 'issue custom attributes',
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='TaskCustomAttribute',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
('name', models.CharField(verbose_name='name', max_length=64)),
('description', models.TextField(blank=True, verbose_name='description')),
('order', models.IntegerField(verbose_name='order', default=10000)),
('created_date', models.DateTimeField(verbose_name='created date', default=django.utils.timezone.now)),
('modified_date', models.DateTimeField(verbose_name='modified date')),
('project', models.ForeignKey(to='projects.Project', verbose_name='project', related_name='taskcustomattributes')),
],
options={
'ordering': ['project', 'order', 'name'],
'verbose_name': 'task custom attribute',
'verbose_name_plural': 'task custom attributes',
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='UserStoryCustomAttribute',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
('name', models.CharField(verbose_name='name', max_length=64)),
('description', models.TextField(blank=True, verbose_name='description')),
('order', models.IntegerField(verbose_name='order', default=10000)),
('created_date', models.DateTimeField(verbose_name='created date', default=django.utils.timezone.now)),
('modified_date', models.DateTimeField(verbose_name='modified date')),
('project', models.ForeignKey(to='projects.Project', verbose_name='project', related_name='userstorycustomattributes')),
],
options={
'ordering': ['project', 'order', 'name'],
'verbose_name': 'user story custom attribute',
'verbose_name_plural': 'user story custom attributes',
'abstract': False,
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='userstorycustomattribute',
unique_together=set([('project', 'name')]),
),
migrations.AlterUniqueTogether(
name='taskcustomattribute',
unique_together=set([('project', 'name')]),
),
migrations.AlterUniqueTogether(
name='issuecustomattribute',
unique_together=set([('project', 'name')]),
),
]

View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django_pgjson.fields
class Migration(migrations.Migration):
dependencies = [
('tasks', '0005_auto_20150114_0954'),
('issues', '0004_auto_20150114_0954'),
('userstories', '0009_remove_userstory_is_archived'),
('custom_attributes', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='IssueCustomAttributesValues',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('version', models.IntegerField(default=1, verbose_name='version')),
('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes_values')),
('issue', models.OneToOneField(verbose_name='issue', to='issues.Issue', related_name='custom_attributes_values')),
],
options={
'verbose_name_plural': 'issue custom attributes values',
'ordering': ['id'],
'verbose_name': 'issue ustom attributes values',
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='TaskCustomAttributesValues',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('version', models.IntegerField(default=1, verbose_name='version')),
('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes_values')),
('task', models.OneToOneField(verbose_name='task', to='tasks.Task', related_name='custom_attributes_values')),
],
options={
'verbose_name_plural': 'task custom attributes values',
'ordering': ['id'],
'verbose_name': 'task ustom attributes values',
'abstract': False,
},
bases=(models.Model,),
),
migrations.CreateModel(
name='UserStoryCustomAttributesValues',
fields=[
('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)),
('version', models.IntegerField(default=1, verbose_name='version')),
('attributes_values', django_pgjson.fields.JsonField(default={}, verbose_name='attributes_values')),
('user_story', models.OneToOneField(verbose_name='user story', to='userstories.UserStory', related_name='custom_attributes_values')),
],
options={
'verbose_name_plural': 'user story custom attributes values',
'ordering': ['id'],
'verbose_name': 'user story ustom attributes values',
'abstract': False,
},
bases=(models.Model,),
),
]

View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('custom_attributes', '0002_issuecustomattributesvalues_taskcustomattributesvalues_userstorycustomattributesvalues'),
]
operations = [
# Function: Remove a key in a json field
migrations.RunSQL(
"""
CREATE OR REPLACE FUNCTION "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[])
RETURNS json
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT COALESCE ((SELECT ('{' || string_agg(to_json("key") || ':' || "value", ',') || '}')
FROM json_each("json")
WHERE "key" <> ALL ("keys_to_delete")),
'{}')::json $function$;
""",
reverse_sql="""DROP FUNCTION IF EXISTS "json_object_delete_keys"("json" json, VARIADIC "keys_to_delete" text[])
CASCADE;"""
),
# Function: Romeve a key in the json field of *_custom_attributes_values.values
migrations.RunSQL(
"""
CREATE OR REPLACE FUNCTION "clean_key_in_custom_attributes_values"()
RETURNS trigger
AS $clean_key_in_custom_attributes_values$
DECLARE
key text;
tablename text;
BEGIN
key := OLD.id::text;
tablename := TG_ARGV[0]::text;
EXECUTE 'UPDATE ' || quote_ident(tablename) || '
SET attributes_values = json_object_delete_keys(attributes_values, ' ||
quote_literal(key) || ')';
RETURN NULL;
END; $clean_key_in_custom_attributes_values$
LANGUAGE plpgsql;
""",
reverse_sql="""DROP FUNCTION IF EXISTS "clean_key_in_custom_attributes_values"()
CASCADE;"""
),
# Trigger: Clean userstorycustomattributes values before remove a userstorycustomattribute
migrations.RunSQL(
"""
CREATE TRIGGER "update_userstorycustomvalues_after_remove_userstorycustomattribute"
AFTER DELETE ON custom_attributes_userstorycustomattribute
FOR EACH ROW
EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_userstorycustomattributesvalues');
""",
reverse_sql="""DROP TRIGGER IF EXISTS "update_userstorycustomvalues_after_remove_userstorycustomattribute"
ON custom_attributes_userstorycustomattribute
CASCADE;"""
),
# Trigger: Clean taskcustomattributes values before remove a taskcustomattribute
migrations.RunSQL(
"""
CREATE TRIGGER "update_taskcustomvalues_after_remove_taskcustomattribute"
AFTER DELETE ON custom_attributes_taskcustomattribute
FOR EACH ROW
EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_taskcustomattributesvalues');
""",
reverse_sql="""DROP TRIGGER IF EXISTS "update_taskcustomvalues_after_remove_taskcustomattribute"
ON custom_attributes_taskcustomattribute
CASCADE;"""
),
# Trigger: Clean issuecustomattributes values before remove a issuecustomattribute
migrations.RunSQL(
"""
CREATE TRIGGER "update_issuecustomvalues_after_remove_issuecustomattribute"
AFTER DELETE ON custom_attributes_issuecustomattribute
FOR EACH ROW
EXECUTE PROCEDURE clean_key_in_custom_attributes_values('custom_attributes_issuecustomattributesvalues');
""",
reverse_sql="""DROP TRIGGER IF EXISTS "update_issuecustomvalues_after_remove_issuecustomattribute"
ON custom_attributes_issuecustomattribute
CASCADE;"""
)
]

View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
def create_empty_user_story_custom_attrributes_values(apps, schema_editor):
cav_model = apps.get_model("custom_attributes", "UserStoryCustomAttributesValues")
obj_model = apps.get_model("userstories", "UserStory")
db_alias = schema_editor.connection.alias
data = []
for user_story in obj_model.objects.using(db_alias).all().select_related("custom_attributes_values"):
if not hasattr(user_story, "custom_attributes_values"):
data.append(cav_model(user_story=user_story,attributes_values={}))
cav_model.objects.using(db_alias).bulk_create(data)
def delete_empty_user_story_custom_attrributes_values(apps, schema_editor):
cav_model = apps.get_model("custom_attributes", "UserStoryCustomAttributesValues")
db_alias = schema_editor.connection.alias
cav_model.objects.using(db_alias).extra(where=["attributes_values::text <> '{}'::text"]).delete()
def create_empty_task_custom_attrributes_values(apps, schema_editor):
cav_model = apps.get_model("custom_attributes", "TaskCustomAttributesValues")
obj_model = apps.get_model("tasks", "Task")
db_alias = schema_editor.connection.alias
data = []
for task in obj_model.objects.using(db_alias).all().select_related("custom_attributes_values"):
if not hasattr(task, "custom_attributes_values"):
data.append(cav_model(task=task,attributes_values={}))
cav_model.objects.using(db_alias).bulk_create(data)
def delete_empty_task_custom_attrributes_values(apps, schema_editor):
cav_model = apps.get_model("custom_attributes", "TaskCustomAttributesValues")
db_alias = schema_editor.connection.alias
cav_model.objects.using(db_alias).extra(where=["attributes_values::text <> '{}'::text"]).delete()
def create_empty_issues_custom_attrributes_values(apps, schema_editor):
cav_model = apps.get_model("custom_attributes", "IssueCustomAttributesValues")
obj_model = apps.get_model("issues", "Issue")
db_alias = schema_editor.connection.alias
data = []
for issue in obj_model.objects.using(db_alias).all().select_related("custom_attributes_values"):
if not hasattr(issue, "custom_attributes_values"):
data.append(cav_model(issue=issue,attributes_values={}))
cav_model.objects.using(db_alias).bulk_create(data)
def delete_empty_issue_custom_attrributes_values(apps, schema_editor):
cav_model = apps.get_model("custom_attributes", "IssueCustomAttributesValues")
db_alias = schema_editor.connection.alias
cav_model.objects.using(db_alias).extra(where=["attributes_values::text <> '{}'::text"]).delete()
class Migration(migrations.Migration):
dependencies = [
('custom_attributes', '0003_triggers_on_delete_customattribute'),
]
operations = [
migrations.RunPython(create_empty_user_story_custom_attrributes_values,
reverse_code=delete_empty_user_story_custom_attrributes_values,
atomic=True),
migrations.RunPython(create_empty_task_custom_attrributes_values,
reverse_code=delete_empty_task_custom_attrributes_values,
atomic=True),
migrations.RunPython(create_empty_issues_custom_attrributes_values,
reverse_code=delete_empty_issue_custom_attrributes_values,
atomic=True),
]

View File

@ -0,0 +1,130 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django_pgjson.fields import JsonField
from taiga.projects.occ.mixins import OCCModelMixin
######################################################
# Custom Attribute Models
#######################################################
class AbstractCustomAttribute(models.Model):
name = models.CharField(null=False, blank=False, max_length=64, verbose_name=_("name"))
description = models.TextField(null=False, blank=True, verbose_name=_("description"))
order = models.IntegerField(null=False, blank=False, default=10000, verbose_name=_("order"))
project = models.ForeignKey("projects.Project", null=False, blank=False, related_name="%(class)ss",
verbose_name=_("project"))
created_date = models.DateTimeField(null=False, blank=False, default=timezone.now,
verbose_name=_("created date"))
modified_date = models.DateTimeField(null=False, blank=False,
verbose_name=_("modified date"))
_importing = None
class Meta:
abstract = True
ordering = ["project", "order", "name"]
unique_together = ("project", "name")
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self._importing or not self.modified_date:
self.modified_date = timezone.now()
return super().save(*args, **kwargs)
class UserStoryCustomAttribute(AbstractCustomAttribute):
class Meta(AbstractCustomAttribute.Meta):
verbose_name = "user story custom attribute"
verbose_name_plural = "user story custom attributes"
class TaskCustomAttribute(AbstractCustomAttribute):
class Meta(AbstractCustomAttribute.Meta):
verbose_name = "task custom attribute"
verbose_name_plural = "task custom attributes"
class IssueCustomAttribute(AbstractCustomAttribute):
class Meta(AbstractCustomAttribute.Meta):
verbose_name = "issue custom attribute"
verbose_name_plural = "issue custom attributes"
######################################################
# Custom Attributes Values Models
#######################################################
class AbstractCustomAttributesValues(OCCModelMixin, models.Model):
attributes_values = JsonField(null=False, blank=False, default={}, verbose_name=_("attributes_values"))
class Meta:
abstract = True
ordering = ["id"]
class UserStoryCustomAttributesValues(AbstractCustomAttributesValues):
user_story = models.OneToOneField("userstories.UserStory",
null=False, blank=False, related_name="custom_attributes_values",
verbose_name=_("user story"))
class Meta(AbstractCustomAttributesValues.Meta):
verbose_name = "user story ustom attributes values"
verbose_name_plural = "user story custom attributes values"
@property
def project(self):
# NOTE: This property simplifies checking permissions
return self.user_story.project
class TaskCustomAttributesValues(AbstractCustomAttributesValues):
task = models.OneToOneField("tasks.Task",
null=False, blank=False, related_name="custom_attributes_values",
verbose_name=_("task"))
class Meta(AbstractCustomAttributesValues.Meta):
verbose_name = "task ustom attributes values"
verbose_name_plural = "task custom attributes values"
@property
def project(self):
# NOTE: This property simplifies checking permissions
return self.task.project
class IssueCustomAttributesValues(AbstractCustomAttributesValues):
issue = models.OneToOneField("issues.Issue",
null=False, blank=False, related_name="custom_attributes_values",
verbose_name=_("issue"))
class Meta(AbstractCustomAttributesValues.Meta):
verbose_name = "issue ustom attributes values"
verbose_name_plural = "issue custom attributes values"
@property
def project(self):
# NOTE: This property simplifies checking permissions
return self.issue.project

View File

@ -0,0 +1,83 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.permissions import TaigaResourcePermission
from taiga.base.api.permissions import HasProjectPerm
from taiga.base.api.permissions import IsProjectOwner
from taiga.base.api.permissions import AllowAny
from taiga.base.api.permissions import IsSuperUser
######################################################
# Custom Attribute Permissions
#######################################################
class UserStoryCustomAttributePermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
create_perms = IsProjectOwner()
update_perms = IsProjectOwner()
destroy_perms = IsProjectOwner()
list_perms = AllowAny()
bulk_update_order_perms = IsProjectOwner()
class TaskCustomAttributePermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
create_perms = IsProjectOwner()
update_perms = IsProjectOwner()
destroy_perms = IsProjectOwner()
list_perms = AllowAny()
bulk_update_order_perms = IsProjectOwner()
class IssueCustomAttributePermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_project')
create_perms = IsProjectOwner()
update_perms = IsProjectOwner()
destroy_perms = IsProjectOwner()
list_perms = AllowAny()
bulk_update_order_perms = IsProjectOwner()
######################################################
# Custom Attributes Values Permissions
#######################################################
class UserStoryCustomAttributesValuesPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_us')
update_perms = HasProjectPerm('modify_us')
class TaskCustomAttributesValuesPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_tasks')
update_perms = HasProjectPerm('modify_task')
class IssueCustomAttributesValuesPermission(TaigaResourcePermission):
enought_perms = IsProjectOwner() | IsSuperUser()
global_perms = None
retrieve_perms = HasProjectPerm('view_issues')
update_perms = HasProjectPerm('modify_issue')

View File

@ -0,0 +1,146 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.apps import apps
from django.utils.translation import ugettext_lazy as _
from rest_framework.serializers import ValidationError
from taiga.base.serializers import ModelSerializer
from taiga.base.serializers import JsonField
from . import models
######################################################
# Custom Attribute Serializer
#######################################################
class BaseCustomAttributeSerializer(ModelSerializer):
class Meta:
read_only_fields = ('id',)
exclude = ('created_date', 'modified_date')
def _validate_integrity_between_project_and_name(self, attrs, source):
"""
Check the name is not duplicated in the project. Check when:
- create a new one
- update the name
- update the project (move to another project)
"""
data_id = attrs.get("id", None)
data_name = attrs.get("name", None)
data_project = attrs.get("project", None)
if self.object:
data_id = data_id or self.object.id
data_name = data_name or self.object.name
data_project = data_project or self.object.project
model = self.Meta.model
qs = (model.objects.filter(project=data_project, name=data_name)
.exclude(id=data_id))
if qs.exists():
raise ValidationError(_("Already exists one with the same name."))
return attrs
def validate_name(self, attrs, source):
return self._validate_integrity_between_project_and_name(attrs, source)
def validate_project(self, attrs, source):
return self._validate_integrity_between_project_and_name(attrs, source)
class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer):
class Meta(BaseCustomAttributeSerializer.Meta):
model = models.UserStoryCustomAttribute
class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer):
class Meta(BaseCustomAttributeSerializer.Meta):
model = models.TaskCustomAttribute
class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer):
class Meta(BaseCustomAttributeSerializer.Meta):
model = models.IssueCustomAttribute
######################################################
# Custom Attribute Serializer
#######################################################
class BaseCustomAttributesValuesSerializer(ModelSerializer):
attributes_values = JsonField(source="attributes_values", label="attributes values")
_custom_attribute_model = None
_container_field = None
class Meta:
exclude = ("id",)
def validate_attributes_values(self, attrs, source):
# values must be a dict
data_values = attrs.get("attributes_values", None)
if self.object:
data_values = (data_values or self.object.attributes_values)
if type(data_values) is not dict:
raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
# Values keys must be in the container object project
data_container = attrs.get(self._container_field, None)
if data_container:
project_id = data_container.project_id
elif self.object:
project_id = getattr(self.object, self._container_field).project_id
else:
project_id = None
values_ids = list(data_values.keys())
qs = self._custom_attribute_model.objects.filter(project=project_id,
id__in=values_ids)
if qs.count() != len(values_ids):
raise ValidationError(_("It contain invalid custom fields."))
return attrs
class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
_custom_attribute_model = models.UserStoryCustomAttribute
_container_model = "userstories.UserStory"
_container_field = "user_story"
class Meta(BaseCustomAttributesValuesSerializer.Meta):
model = models.UserStoryCustomAttributesValues
class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer):
_custom_attribute_model = models.TaskCustomAttribute
_container_field = "task"
class Meta(BaseCustomAttributesValuesSerializer.Meta):
model = models.TaskCustomAttributesValues
class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer):
_custom_attribute_model = models.IssueCustomAttribute
_container_field = "issue"
class Meta(BaseCustomAttributesValuesSerializer.Meta):
model = models.IssueCustomAttributesValues

View File

@ -0,0 +1,69 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.db import transaction
from django.db import connection
@transaction.atomic
def bulk_update_userstory_custom_attribute_order(project, user, data):
cursor = connection.cursor()
sql = """
prepare bulk_update_order as update custom_attributes_userstorycustomattribute set "order" = $1
where custom_attributes_userstorycustomattribute.id = $2 and
custom_attributes_userstorycustomattribute.project_id = $3;
"""
cursor.execute(sql)
for id, order in data:
cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);",
(order, id, project.id))
cursor.execute("DEALLOCATE bulk_update_order")
cursor.close()
@transaction.atomic
def bulk_update_task_custom_attribute_order(project, user, data):
cursor = connection.cursor()
sql = """
prepare bulk_update_order as update custom_attributes_taskcustomattribute set "order" = $1
where custom_attributes_taskcustomattribute.id = $2 and
custom_attributes_taskcustomattribute.project_id = $3;
"""
cursor.execute(sql)
for id, order in data:
cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);",
(order, id, project.id))
cursor.execute("DEALLOCATE bulk_update_order")
cursor.close()
@transaction.atomic
def bulk_update_issue_custom_attribute_order(project, user, data):
cursor = connection.cursor()
sql = """
prepare bulk_update_order as update custom_attributes_issuecustomattribute set "order" = $1
where custom_attributes_issuecustomattribute.id = $2 and
custom_attributes_issuecustomattribute.project_id = $3;
"""
cursor.execute(sql)
for id, order in data:
cursor.execute("EXECUTE bulk_update_order (%s, %s, %s);",
(order, id, project.id))
cursor.execute("DEALLOCATE bulk_update_order")
cursor.close()

View File

@ -0,0 +1,35 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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 . import models
def create_custom_attribute_value_when_create_user_story(sender, instance, created, **kwargs):
if created:
models.UserStoryCustomAttributesValues.objects.get_or_create(user_story=instance,
defaults={"attributes_values":{}})
def create_custom_attribute_value_when_create_task(sender, instance, created, **kwargs):
if created:
models.TaskCustomAttributesValues.objects.get_or_create(task=instance,
defaults={"attributes_values":{}})
def create_custom_attribute_value_when_create_issue(sender, instance, created, **kwargs):
if created:
models.IssueCustomAttributesValues.objects.get_or_create(issue=instance,
defaults={"attributes_values":{}})

View File

@ -14,9 +14,13 @@
# 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 contextlib import suppress
from functools import partial
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from taiga.base.utils.iterators import as_tuple
from taiga.base.utils.iterators import as_dict
from taiga.mdrender.service import render as mdrender
@ -181,6 +185,42 @@ def extract_attachments(obj) -> list:
"order": attach.order}
@as_tuple
def extract_user_story_custom_attributes(obj) -> list:
with suppress(ObjectDoesNotExist):
custom_attributes_values = obj.custom_attributes_values.attributes_values
for attr in obj.project.userstorycustomattributes.all():
with suppress(KeyError):
value = custom_attributes_values[str(attr.id)]
yield {"id": attr.id,
"name": attr.name,
"value": value}
@as_tuple
def extract_task_custom_attributes(obj) -> list:
with suppress(ObjectDoesNotExist):
custom_attributes_values = obj.custom_attributes_values.attributes_values
for attr in obj.project.taskcustomattributes.all():
with suppress(KeyError):
value = custom_attributes_values[str(attr.id)]
yield {"id": attr.id,
"name": attr.name,
"value": value}
@as_tuple
def extract_issue_custom_attributes(obj) -> list:
with suppress(ObjectDoesNotExist):
custom_attributes_values = obj.custom_attributes_values.attributes_values
for attr in obj.project.issuecustomattributes.all():
with suppress(KeyError):
value = custom_attributes_values[str(attr.id)]
yield {"id": attr.id,
"name": attr.name,
"value": value}
def project_freezer(project) -> dict:
fields = ("name",
"slug",
@ -243,6 +283,7 @@ def userstory_freezer(us) -> dict:
"is_blocked": us.is_blocked,
"blocked_note": us.blocked_note,
"blocked_note_html": mdrender(us.project, us.blocked_note),
"custom_attributes": extract_user_story_custom_attributes(us),
}
return snapshot
@ -267,6 +308,7 @@ def issue_freezer(issue) -> dict:
"is_blocked": issue.is_blocked,
"blocked_note": issue.blocked_note,
"blocked_note_html": mdrender(issue.project, issue.blocked_note),
"custom_attributes": extract_issue_custom_attributes(issue),
}
return snapshot
@ -292,6 +334,7 @@ def task_freezer(task) -> dict:
"is_blocked": task.is_blocked,
"blocked_note": task.blocked_note,
"blocked_note_html": mdrender(task.project, task.blocked_note),
"custom_attributes": extract_task_custom_attributes(task),
}
return snapshot

View File

@ -197,6 +197,35 @@ class HistoryEntry(models.Model):
if attachments["new"] or attachments["changed"] or attachments["deleted"]:
value = attachments
elif key == "custom_attributes":
custom_attributes = {
"new": [],
"changed": [],
"deleted": [],
}
oldcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][0] or []}
newcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][1] or []}
for aid in set(tuple(oldcustattrs.keys()) + tuple(newcustattrs.keys())):
if aid in oldcustattrs and aid in newcustattrs:
changes = make_diff_from_dicts(oldcustattrs[aid], newcustattrs[aid],
excluded_keys=("name"))
if changes:
change = {
"name": newcustattrs.get(aid, {}).get("name", ""),
"changes": changes
}
custom_attributes["changed"].append(change)
elif aid in oldcustattrs and aid not in newcustattrs:
custom_attributes["deleted"].append(oldcustattrs[aid])
elif aid not in oldcustattrs and aid in newcustattrs:
custom_attributes["new"].append(newcustattrs[aid])
if custom_attributes["new"] or custom_attributes["changed"] or custom_attributes["deleted"]:
value = custom_attributes
elif key in self.values:
value = [resolve_value(key, x) for x in self.diff[key]]
else:

View File

@ -6,7 +6,8 @@
"backlog_order",
"kanban_order",
"taskboard_order",
"us_order"
"us_order",
"custom_attributes"
] %}
{% for field_name, values in changed_fields.items() %}
@ -80,9 +81,7 @@
<tr>
<td colspan="2">
<h3>{{ _("Deleted attachment") }}</h3>
{% if att.changes.description %}
<p>{{ att.filename|linebreaksbr }}</p>
{% endif %}
</td>
</tr>
{% endfor %}
@ -155,7 +154,6 @@
</tr>
{# * #}
{% else %}
<tr>
<td valign="middle" rowspan="2" class="update-row-name">
<h3>{{ verbose_name(obj_class, field_name) }}</h3>
@ -172,5 +170,52 @@
</td>
</tr>
{% endif %}
{% elif field_name == "custom_attributes" %}
{# CUSTOM ATTRIBUTES #}
{% if values.new %}
{% for attr in values['new']%}
<tr>
<td valign="middle" rowspan="2" class="update-row-name">
<h3>{{ attr.name }}</h3>
</td>
</tr>
<tr>
<td valign="top">
<span>{{ _("to") }}</span><br>
<strong>{{ attr.value|linebreaksbr }}</strong>
</td>
</tr>
{% endfor %}
{% endif %}
{% if values.changed %}
{% for attr in values['changed'] %}
<tr>
<td valign="middle" rowspan="2" class="update-row-name">
<h3>{{ attr.name }}</h3>
</td>
<td valign="top" class="update-row-from">
<span>{{ _("from") }}</span><br>
<strong>{{ attr.changes.value.0|linebreaksbr }}</strong>
</td>
</tr>
<tr>
<td valign="top">
<span>{{ _("to") }}</span><br>
<strong>{{ attr.changes.value.1|linebreaksbr }}</strong>
</td>
</tr>
{% endfor %}
{% endif %}
{% if values.deleted %}
{% for attr in values['deleted']%}
<tr>
<td colspan="2">
<h3>{{ attr.name }}</h3>
<p>{{ _("-deleted-") }}</p>
</td>
</tr>
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}

View File

@ -8,7 +8,8 @@
"taskboard_order",
"us_order",
"blocked_note_diff",
"blocked_note_html"
"blocked_note_html",
"custom_attributes"
] %}
{% for field_name, values in changed_fields.items() %}
{% if field_name not in excluded_fields %}
@ -18,6 +19,7 @@
{% for role, points in values.items() %}
* {{ role }} {{ _("to:") }} {{ points.1 }} {{ _("from:") }} {{ points.0 }}
{% endfor %}
{# ATTACHMENTS #}
{% elif field_name == "attachments" %}
{% if values.new %}
@ -40,6 +42,7 @@
- {{ att.filename }}
{% endfor %}
{% endif %}
{# TAGS AND WATCHERS #}
{% elif field_name in ["tags", "watchers"] %}
{% set values_from = values.0 or [] %}
@ -53,6 +56,36 @@
{% if values_removed %}
* {{ _("removed:") }} {{ ', '.join(values_removed) }}
{% endif %}
{# * #}
{% else %}
* {{ _("From:") }} {{ values.0 }}
* {{ _("To:") }} {{ values.1 }}
{% endif %}
{% elif field_name == "custom_attributes" %}
{# CUSTOM ATTRIBUTES #}
{% elif field_name == "attachments" %}
{% if values.new %}
{% for attr in values['new']%}
- {{ attr.name }}:
* {{ attr.value }}
{% endfor %}
{% endif %}
{% if values.changed %}
{% for attr in values['changed'] %}
- {{ attr.name }}:
* {{ _("From:") }} {{ attr.changes.value.0 }}
* {{ _("To:") }} {{ attr.changes.value.1 }}
{% endfor %}
{% endif %}
{% if values.deleted %}
{% for attr in values['deleted']%}
- {{ attr.name }}: {{ _("-deleted-") }}
* {{ attr.value }}
{% endfor %}
{% endif %}
{% endif %}
{% endfor %}

View File

@ -19,6 +19,7 @@ from django.apps import apps
from django.db.models import signals
from taiga.projects import signals as generic_handlers
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
from . import signals as handlers
@ -39,3 +40,8 @@ class IssuesAppConfig(AppConfig):
sender=apps.get_model("issues", "Issue"))
signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
sender=apps.get_model("issues", "Issue"))
# Custom Attributes
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_issue,
sender=apps.get_model("issues", "Issue"),
dispatch_uid="create_custom_attribute_value_when_create_issue")

View File

@ -16,7 +16,7 @@
from rest_framework import serializers
from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin,
from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin,
PgArrayField, ModelSerializer)
from taiga.mdrender.service import render as mdrender

View File

@ -34,6 +34,7 @@ from taiga.projects.tasks.models import *
from taiga.projects.issues.models import *
from taiga.projects.wiki.models import *
from taiga.projects.attachments.models import *
from taiga.projects.custom_attributes.models import *
from taiga.projects.history.services import take_snapshot
from taiga.events.apps import disconnect_events_signals
@ -150,6 +151,27 @@ class Command(BaseCommand):
if role.computable:
computable_project_roles.add(role)
# added custom attributes
if self.sd.boolean:
for i in range(1, 4):
UserStoryCustomAttribute.objects.create(name=self.sd.words(1, 3),
description=self.sd.words(3, 12),
project=project,
order=i)
if self.sd.boolean:
for i in range(1, 4):
TaskCustomAttribute.objects.create(name=self.sd.words(1, 3),
description=self.sd.words(3, 12),
project=project,
order=i)
if self.sd.boolean:
for i in range(1, 4):
IssueCustomAttribute.objects.create(name=self.sd.words(1, 3),
description=self.sd.words(3, 12),
project=project,
order=i)
if x < NUM_PROJECTS:
start_date = now() - datetime.timedelta(55)
@ -248,6 +270,14 @@ class Command(BaseCommand):
project=project)),
tags=self.sd.words(1, 10).split(" "))
bug.save()
custom_attributes_values = {str(ca.id): self.sd.words(1, 12) for ca in project.issuecustomattributes.all()
if self.sd.boolean()}
if custom_attributes_values:
bug.custom_attributes_values.attributes_values = custom_attributes_values
bug.custom_attributes_values.save()
for i in range(self.sd.int(*NUM_ATTACHMENTS)):
attachment = self.create_attachment(bug, i+1)
@ -291,6 +321,12 @@ class Command(BaseCommand):
task.save()
custom_attributes_values = {str(ca.id): self.sd.words(1, 12) for ca in project.taskcustomattributes.all()
if self.sd.boolean()}
if custom_attributes_values:
task.custom_attributes_values.attributes_values = custom_attributes_values
task.custom_attributes_values.save()
for i in range(self.sd.int(*NUM_ATTACHMENTS)):
attachment = self.create_attachment(task, i+1)
@ -328,6 +364,15 @@ class Command(BaseCommand):
role_points.save()
us.save()
custom_attributes_values = {str(ca.id): self.sd.words(1, 12) for ca in project.userstorycustomattributes.all()
if self.sd.boolean()}
if custom_attributes_values:
us.custom_attributes_values.attributes_values = custom_attributes_values
us.custom_attributes_values.save()
for i in range(self.sd.int(*NUM_ATTACHMENTS)):
attachment = self.create_attachment(us, i+1)
@ -345,7 +390,7 @@ class Command(BaseCommand):
take_snapshot(us,
comment=self.sd.paragraph(),
user=us.owner)
return us
def create_milestone(self, project, start_date, end_date):
@ -375,9 +420,9 @@ class Command(BaseCommand):
def create_user(self, counter=None, username=None, full_name=None, email=None):
counter = counter or self.sd.int()
username = username or 'user{0}'.format(counter)
username = username or "user{0}".format(counter)
full_name = full_name or "{} {}".format(self.sd.name('es'), self.sd.surname('es', number=1))
email = email or self.sd.email()
email = email or "user{0}@taigaio.demo".format(counter)
user = User.objects.create(username=username,
full_name=full_name,

View File

@ -34,7 +34,10 @@ from taiga.permissions.service import is_project_owner
from . import models
from . import services
from . validators import ProjectExistsValidator
from .validators import ProjectExistsValidator
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
from .custom_attributes.serializers import TaskCustomAttributeSerializer
from .custom_attributes.serializers import IssueCustomAttributeSerializer
######################################################
@ -298,7 +301,6 @@ 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")
@ -309,6 +311,12 @@ class ProjectDetailSerializer(ProjectSerializer):
issue_types = IssueTypeSerializer(many=True, required=False)
priorities = PrioritySerializer(many=True, required=False) # Issues
severities = SeveritySerializer(many=True, required=False)
userstory_custom_attributes = UserStoryCustomAttributeSerializer(source="userstorycustomattributes",
many=True, required=False)
task_custom_attributes = TaskCustomAttributeSerializer(source="taskcustomattributes",
many=True, required=False)
issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes",
many=True, required=False)
def get_memberships(self, obj):
qs = obj.memberships.filter(user__isnull=False)

View File

@ -19,6 +19,7 @@ from django.apps import apps
from django.db.models import signals
from taiga.projects import signals as generic_handlers
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
from . import signals as handlers
@ -44,3 +45,8 @@ class TasksAppConfig(AppConfig):
sender=apps.get_model("tasks", "Task"))
signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
sender=apps.get_model("tasks", "Task"))
# Custom Attributes
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_task,
sender=apps.get_model("tasks", "Task"),
dispatch_uid="create_custom_attribute_value_when_create_task")

View File

@ -18,7 +18,7 @@ from rest_framework import serializers
from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin,
PgArrayField, ModelSerializer)
from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.milestones.validators import SprintExistsValidator

View File

@ -19,6 +19,7 @@ from django.apps import apps
from django.db.models import signals
from taiga.projects import signals as generic_handlers
from taiga.projects.custom_attributes import signals as custom_attributes_handlers
from . import signals as handlers
@ -52,3 +53,8 @@ class UserStoriesAppConfig(AppConfig):
sender=apps.get_model("userstories", "UserStory"))
signals.post_delete.connect(generic_handlers.update_project_tags_when_delete_taggable_item,
sender=apps.get_model("userstories", "UserStory"))
# Custom Attributes
signals.post_save.connect(custom_attributes_handlers.create_custom_attribute_value_when_create_user_story,
sender=apps.get_model("userstories", "UserStory"),
dispatch_uid="create_custom_attribute_value_when_create_user_story")

View File

@ -22,10 +22,10 @@ from django.utils import timezone
from djorm_pgarray.fields import TextArrayField
from taiga.base.tags import TaggedMixin
from taiga.projects.occ import OCCModelMixin
from taiga.projects.notifications.mixins import WatchedModelMixin
from taiga.projects.mixins.blocked import BlockedMixin
from taiga.base.tags import TaggedMixin
class RolePoints(models.Model):

View File

@ -14,15 +14,19 @@
# 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 json
from django.apps import apps
from rest_framework import serializers
from taiga.base.serializers import (Serializer, TagsField, NeighborsSerializerMixin,
PgArrayField, ModelSerializer)
from taiga.base.serializers import Serializer
from taiga.base.serializers import TagsField
from taiga.base.serializers import NeighborsSerializerMixin
from taiga.base.serializers import PgArrayField
from taiga.base.serializers import ModelSerializer
from taiga.base.utils import json
from taiga.mdrender.service import render as mdrender
from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.validators import UserStoryStatusExistsValidator
from taiga.projects.userstories.validators import UserStoryExistsValidator
from taiga.projects.notifications.validators import WatchersValidator
@ -92,7 +96,6 @@ class UserStorySerializer(WatchersValidator, ModelSerializer):
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
def serialize_neighbor(self, neighbor):
return NeighborUserStorySerializer(neighbor).data
@ -104,8 +107,7 @@ class NeighborUserStorySerializer(ModelSerializer):
depth = 0
class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator,
Serializer):
class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, Serializer):
project_id = serializers.IntegerField()
status_id = serializers.IntegerField(required=False)
bulk_stories = serializers.CharField()
@ -118,8 +120,6 @@ class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, Serializer):
order = serializers.IntegerField()
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator,
UserStoryStatusExistsValidator,
Serializer):
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, Serializer):
project_id = serializers.IntegerField()
bulk_stories = _UserStoryOrderBulkSerializer(many=True)

View File

@ -35,23 +35,10 @@ from taiga.userstorage.api import StorageEntriesViewSet
router.register(r"user-storage", StorageEntriesViewSet, base_name="user-storage")
# Resolver
from taiga.projects.references.api import ResolverViewSet
# Notify policies
from taiga.projects.notifications.api import NotifyPolicyViewSet
router.register(r"resolver", ResolverViewSet, base_name="resolver")
# Search
from taiga.searches.api import SearchViewSet
router.register(r"search", SearchViewSet, base_name="search")
# Importer
from taiga.export_import.api import ProjectImporterViewSet, ProjectExporterViewSet
router.register(r"importer", ProjectImporterViewSet, base_name="importer")
router.register(r"exporter", ProjectExporterViewSet, base_name="exporter")
router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications")
# Projects & Selectors
@ -80,6 +67,41 @@ router.register(r"priorities", PriorityViewSet, base_name="priorities")
router.register(r"severities",SeverityViewSet , base_name="severities")
# Custom Attributes
from taiga.projects.custom_attributes.api import UserStoryCustomAttributeViewSet
from taiga.projects.custom_attributes.api import TaskCustomAttributeViewSet
from taiga.projects.custom_attributes.api import IssueCustomAttributeViewSet
from taiga.projects.custom_attributes.api import UserStoryCustomAttributesValuesViewSet
from taiga.projects.custom_attributes.api import TaskCustomAttributesValuesViewSet
from taiga.projects.custom_attributes.api import IssueCustomAttributesValuesViewSet
router.register(r"userstory-custom-attributes", UserStoryCustomAttributeViewSet,
base_name="userstory-custom-attributes")
router.register(r"task-custom-attributes", TaskCustomAttributeViewSet,
base_name="task-custom-attributes")
router.register(r"issue-custom-attributes", IssueCustomAttributeViewSet,
base_name="issue-custom-attributes")
router.register(r"userstories/custom-attributes-values", UserStoryCustomAttributesValuesViewSet,
base_name="userstory-custom-attributes-values")
router.register(r"tasks/custom-attributes-values", TaskCustomAttributesValuesViewSet,
base_name="task-custom-attributes-values")
router.register(r"issues/custom-attributes-values", IssueCustomAttributesValuesViewSet,
base_name="issue-custom-attributes-values")
# Search
from taiga.searches.api import SearchViewSet
router.register(r"search", SearchViewSet, base_name="search")
# Resolver
from taiga.projects.references.api import ResolverViewSet
router.register(r"resolver", ResolverViewSet, base_name="resolver")
# Attachments
from taiga.projects.attachments.api import UserStoryAttachmentViewSet
from taiga.projects.attachments.api import IssueAttachmentViewSet
@ -93,11 +115,21 @@ router.register(r"issues/attachments", IssueAttachmentViewSet, base_name="issue-
router.register(r"wiki/attachments", WikiAttachmentViewSet, base_name="wiki-attachments")
# Webhooks
from taiga.webhooks.api import WebhookViewSet, WebhookLogViewSet
# Project components
from taiga.projects.milestones.api import MilestoneViewSet
from taiga.projects.userstories.api import UserStoryViewSet
from taiga.projects.tasks.api import TaskViewSet
from taiga.projects.issues.api import IssueViewSet
from taiga.projects.issues.api import VotersViewSet
from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet
router.register(r"webhooks", WebhookViewSet, base_name="webhooks")
router.register(r"webhooklogs", WebhookLogViewSet, base_name="webhooklogs")
router.register(r"milestones", MilestoneViewSet, base_name="milestones")
router.register(r"userstories", UserStoryViewSet, base_name="userstories")
router.register(r"tasks", TaskViewSet, base_name="tasks")
router.register(r"issues", IssueViewSet, base_name="issues")
router.register(r"issues/(?P<issue_id>\d+)/voters", VotersViewSet, base_name="issue-voters")
router.register(r"wiki", WikiViewSet, base_name="wiki")
router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")
# History & Components
@ -120,27 +152,12 @@ router.register(r"timeline/user", UserTimeline, base_name="user-timeline")
router.register(r"timeline/project", ProjectTimeline, base_name="project-timeline")
# Project components
from taiga.projects.milestones.api import MilestoneViewSet
from taiga.projects.userstories.api import UserStoryViewSet
from taiga.projects.tasks.api import TaskViewSet
from taiga.projects.issues.api import IssueViewSet
from taiga.projects.issues.api import VotersViewSet
from taiga.projects.wiki.api import WikiViewSet, WikiLinkViewSet
# Webhooks
from taiga.webhooks.api import WebhookViewSet
from taiga.webhooks.api import WebhookLogViewSet
router.register(r"milestones", MilestoneViewSet, base_name="milestones")
router.register(r"userstories", UserStoryViewSet, base_name="userstories")
router.register(r"tasks", TaskViewSet, base_name="tasks")
router.register(r"issues", IssueViewSet, base_name="issues")
router.register(r"issues/(?P<issue_id>\d+)/voters", VotersViewSet, base_name="issue-voters")
router.register(r"wiki", WikiViewSet, base_name="wiki")
router.register(r"wiki-links", WikiLinkViewSet, base_name="wiki-links")
# Notify policies
from taiga.projects.notifications.api import NotifyPolicyViewSet
router.register(r"notify-policies", NotifyPolicyViewSet, base_name="notifications")
router.register(r"webhooks", WebhookViewSet, base_name="webhooks")
router.register(r"webhooklogs", WebhookLogViewSet, base_name="webhooklogs")
# GitHub webhooks
@ -161,5 +178,12 @@ from taiga.hooks.bitbucket.api import BitBucketViewSet
router.register(r"bitbucket-hook", BitBucketViewSet, base_name="bitbucket-hook")
# Importer
from taiga.export_import.api import ProjectImporterViewSet, ProjectExporterViewSet
router.register(r"importer", ProjectImporterViewSet, base_name="importer")
router.register(r"exporter", ProjectExporterViewSet, base_name="exporter")
# feedback
# - see taiga.feedback.routers and taiga.feedback.apps

View File

@ -14,6 +14,8 @@
# 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.core.exceptions import ObjectDoesNotExist
from rest_framework import serializers
from taiga.base.serializers import TagsField, PgArrayField, JsonField
@ -63,6 +65,30 @@ class UserSerializer(serializers.Serializer):
return obj.full_name
class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values")
def custom_attributes_queryset(self, project):
raise NotImplementedError()
def get_custom_attributes_values(self, obj):
def _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values):
ret = {}
for attr in custom_attributes:
value = values.get(str(attr["id"]), None)
if value is not None:
ret[attr["name"]] = value
return ret
try:
values = obj.custom_attributes_values.attributes_values
custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name')
return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values)
except ObjectDoesNotExist:
return None
class PointSerializer(serializers.Serializer):
id = serializers.SerializerMethodField("get_pk")
name = serializers.SerializerMethodField("get_name")
@ -78,7 +104,7 @@ class PointSerializer(serializers.Serializer):
return obj.value
class UserStorySerializer(serializers.ModelSerializer):
class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
external_reference = PgArrayField(required=False)
owner = UserSerializer()
@ -90,8 +116,11 @@ class UserStorySerializer(serializers.ModelSerializer):
model = us_models.UserStory
exclude = ("backlog_order", "sprint_order", "kanban_order", "version")
def custom_attributes_queryset(self, project):
return project.userstorycustomattributes.all()
class TaskSerializer(serializers.ModelSerializer):
class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
owner = UserSerializer()
assigned_to = UserSerializer()
@ -100,8 +129,11 @@ class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = task_models.Task
def custom_attributes_queryset(self, project):
return project.taskcustomattributes.all()
class IssueSerializer(serializers.ModelSerializer):
class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.ModelSerializer):
tags = TagsField(default=[], required=False)
owner = UserSerializer()
assigned_to = UserSerializer()
@ -110,6 +142,9 @@ class IssueSerializer(serializers.ModelSerializer):
class Meta:
model = issue_models.Issue
def custom_attributes_queryset(self, project):
return project.issuecustomattributes.all()
class WikiPageSerializer(serializers.ModelSerializer):
owner = UserSerializer()

View File

@ -354,6 +354,63 @@ class IssueTypeFactory(Factory):
project = factory.SubFactory("tests.factories.ProjectFactory")
class UserStoryCustomAttributeFactory(Factory):
class Meta:
model = "custom_attributes.UserStoryCustomAttribute"
strategy = factory.CREATE_STRATEGY
name = factory.Sequence(lambda n: "UserStory Custom Attribute {}".format(n))
description = factory.Sequence(lambda n: "Description for UserStory Custom Attribute {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class TaskCustomAttributeFactory(Factory):
class Meta:
model = "custom_attributes.TaskCustomAttribute"
strategy = factory.CREATE_STRATEGY
name = factory.Sequence(lambda n: "Task Custom Attribute {}".format(n))
description = factory.Sequence(lambda n: "Description for Task Custom Attribute {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class IssueCustomAttributeFactory(Factory):
class Meta:
model = "custom_attributes.IssueCustomAttribute"
strategy = factory.CREATE_STRATEGY
name = factory.Sequence(lambda n: "Issue Custom Attribute {}".format(n))
description = factory.Sequence(lambda n: "Description for Issue Custom Attribute {}".format(n))
project = factory.SubFactory("tests.factories.ProjectFactory")
class UserStoryCustomAttributesValuesFactory(Factory):
class Meta:
model = "custom_attributes.UserStoryCustomAttributesValues"
strategy = factory.CREATE_STRATEGY
attributes_values = {}
user_story = factory.SubFactory("tests.factories.UserStoryFactory")
class TaskCustomAttributesValuesFactory(Factory):
class Meta:
model = "custom_attributes.TaskCustomAttributesValues"
strategy = factory.CREATE_STRATEGY
attributes_values = {}
task = factory.SubFactory("tests.factories.TaskFactory")
class IssueCustomAttributesValuesFactory(Factory):
class Meta:
model = "custom_attributes.IssueCustomAttributesValues"
strategy = factory.CREATE_STRATEGY
attributes_values = {}
issue = factory.SubFactory("tests.factories.IssueFactory")
# class FanFactory(Factory):
# project = factory.SubFactory("tests.factories.ProjectFactory")
# user = factory.SubFactory("tests.factories.UserFactory")

View File

@ -0,0 +1,398 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects.custom_attributes import serializers
from taiga.permissions.permissions import (MEMBERS_PERMISSIONS,
ANON_PERMISSIONS, USER_PERMISSIONS)
from tests import factories as f
from tests.utils import helper_test_http_method
import pytest
pytestmark = pytest.mark.django_db
@pytest.fixture
def data():
m = type("Models", (object,), {})
m.registered_user = f.UserFactory.create()
m.project_member_with_perms = f.UserFactory.create()
m.project_member_without_perms = f.UserFactory.create()
m.project_owner = f.UserFactory.create()
m.other_user = f.UserFactory.create()
m.superuser = f.UserFactory.create(is_superuser=True)
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)
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)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.public_project,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
m.private_membership1 = f.MembershipFactory(project=m.private_project1,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.private_project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.private_project1,
user=m.project_member_without_perms,
email=m.project_member_without_perms.email,
role__project=m.private_project1,
role__permissions=[])
m.private_membership2 = f.MembershipFactory(project=m.private_project2,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.private_project2,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.private_project2,
user=m.project_member_without_perms,
email=m.project_member_without_perms.email,
role__project=m.private_project2,
role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
is_owner=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
is_owner=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
is_owner=True)
m.public_issue_ca = f.IssueCustomAttributeFactory(project=m.public_project)
m.private_issue_ca1 = f.IssueCustomAttributeFactory(project=m.private_project1)
m.private_issue_ca2 = f.IssueCustomAttributeFactory(project=m.private_project2)
m.public_issue = f.IssueFactory(project=m.public_project,
status__project=m.public_project,
severity__project=m.public_project,
priority__project=m.public_project,
type__project=m.public_project,
milestone__project=m.public_project)
m.private_issue1 = f.IssueFactory(project=m.private_project1,
status__project=m.private_project1,
severity__project=m.private_project1,
priority__project=m.private_project1,
type__project=m.private_project1,
milestone__project=m.private_project1)
m.private_issue2 = f.IssueFactory(project=m.private_project2,
status__project=m.private_project2,
severity__project=m.private_project2,
priority__project=m.private_project2,
type__project=m.private_project2,
milestone__project=m.private_project2)
m.public_issue_cav = m.public_issue.custom_attributes_values
m.private_issue_cav1 = m.private_issue1.custom_attributes_values
m.private_issue_cav2 = m.private_issue2.custom_attributes_values
return m
#########################################################
# Issue Custom Attribute
#########################################################
def test_issue_custom_attribute_retrieve(client, data):
public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk})
private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk})
private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk})
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', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private1_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
def test_issue_custom_attribute_create(client, data):
public_url = reverse('issue-custom-attributes-list')
private1_url = reverse('issue-custom-attributes-list')
private2_url = reverse('issue-custom-attributes-list')
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
issue_ca_data = {"name": "test-new", "project": data.public_project.id}
issue_ca_data = json.dumps(issue_ca_data)
results = helper_test_http_method(client, 'post', public_url, issue_ca_data, users)
assert results == [401, 403, 403, 403, 201]
issue_ca_data = {"name": "test-new", "project": data.private_project1.id}
issue_ca_data = json.dumps(issue_ca_data)
results = helper_test_http_method(client, 'post', private1_url, issue_ca_data, users)
assert results == [401, 403, 403, 403, 201]
issue_ca_data = {"name": "test-new", "project": data.private_project2.id}
issue_ca_data = json.dumps(issue_ca_data)
results = helper_test_http_method(client, 'post', private2_url, issue_ca_data, users)
assert results == [401, 403, 403, 403, 201]
def test_issue_custom_attribute_update(client, data):
public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk})
private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk})
private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
issue_ca_data = serializers.IssueCustomAttributeSerializer(data.public_issue_ca).data
issue_ca_data["name"] = "test"
issue_ca_data = json.dumps(issue_ca_data)
results = helper_test_http_method(client, 'put', public_url, issue_ca_data, users)
assert results == [401, 403, 403, 403, 200]
issue_ca_data = serializers.IssueCustomAttributeSerializer(data.private_issue_ca1).data
issue_ca_data["name"] = "test"
issue_ca_data = json.dumps(issue_ca_data)
results = helper_test_http_method(client, 'put', private1_url, issue_ca_data, users)
assert results == [401, 403, 403, 403, 200]
issue_ca_data = serializers.IssueCustomAttributeSerializer(data.private_issue_ca2).data
issue_ca_data["name"] = "test"
issue_ca_data = json.dumps(issue_ca_data)
results = helper_test_http_method(client, 'put', private2_url, issue_ca_data, users)
assert results == [401, 403, 403, 403, 200]
def test_issue_custom_attribute_delete(client, data):
public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk})
private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk})
private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'delete', public_url, None, users)
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private1_url, None, users)
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
def test_issue_custom_attribute_list(client, data):
url = reverse('issue-custom-attributes-list')
response = client.json.get(url)
assert len(response.data) == 2
assert response.status_code == 200
client.login(data.registered_user)
response = client.json.get(url)
assert len(response.data) == 2
assert response.status_code == 200
client.login(data.project_member_without_perms)
response = client.json.get(url)
assert len(response.data) == 2
assert response.status_code == 200
client.login(data.project_member_with_perms)
response = client.json.get(url)
assert len(response.data) == 3
assert response.status_code == 200
client.login(data.project_owner)
response = client.json.get(url)
assert len(response.data) == 3
assert response.status_code == 200
def test_issue_custom_attribute_patch(client, data):
public_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.public_issue_ca.pk})
private1_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca1.pk})
private2_url = reverse('issue-custom-attributes-detail', kwargs={"pk": data.private_issue_ca2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
def test_issue_custom_attribute_action_bulk_update_order(client, data):
url = reverse('issue-custom-attributes-bulk-update-order')
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
post_data = json.dumps({
"bulk_issue_custom_attributes": [(1,2)],
"project": data.public_project.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
post_data = json.dumps({
"bulk_issue_custom_attributes": [(1,2)],
"project": data.private_project1.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
post_data = json.dumps({
"bulk_issue_custom_attributes": [(1,2)],
"project": data.private_project2.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
#########################################################
# Issue Custom Attribute
#########################################################
def test_issue_custom_attributes_values_retrieve(client, data):
public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk})
private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk})
private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk})
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', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_issue_custom_attributes_values_update(client, data):
public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk})
private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk})
private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
issue_data = serializers.IssueCustomAttributesValuesSerializer(data.public_issue_cav).data
issue_data["attributes_values"] = {str(data.public_issue_ca.pk): "test"}
issue_data = json.dumps(issue_data)
results = helper_test_http_method(client, 'put', public_url, issue_data, users)
assert results == [401, 403, 403, 200, 200]
issue_data = serializers.IssueCustomAttributesValuesSerializer(data.private_issue_cav1).data
issue_data["attributes_values"] = {str(data.private_issue_ca1.pk): "test"}
issue_data = json.dumps(issue_data)
results = helper_test_http_method(client, 'put', private_url1, issue_data, users)
assert results == [401, 403, 403, 200, 200]
issue_data = serializers.IssueCustomAttributesValuesSerializer(data.private_issue_cav2).data
issue_data["attributes_values"] = {str(data.private_issue_ca2.pk): "test"}
issue_data = json.dumps(issue_data)
results = helper_test_http_method(client, 'put', private_url2, issue_data, users)
assert results == [401, 403, 403, 200, 200]
def test_issue_custom_attributes_values_patch(client, data):
public_url = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.public_issue.pk})
private_url1 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue1.pk})
private_url2 = reverse('issue-custom-attributes-values-detail', kwargs={"issue_id": data.private_issue2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
patch_data = json.dumps({"attributes_values": {str(data.public_issue_ca.pk): "test"},
"version": data.public_issue.version})
results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
assert results == [401, 403, 403, 200, 200]
patch_data = json.dumps({"attributes_values": {str(data.private_issue_ca1.pk): "test"},
"version": data.private_issue1.version})
results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
assert results == [401, 403, 403, 200, 200]
patch_data = json.dumps({"attributes_values": {str(data.private_issue_ca2.pk): "test"},
"version": data.private_issue2.version})
results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
assert results == [401, 403, 403, 200, 200]

View File

@ -0,0 +1,392 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects.custom_attributes import serializers
from taiga.permissions.permissions import (MEMBERS_PERMISSIONS,
ANON_PERMISSIONS, USER_PERMISSIONS)
from tests import factories as f
from tests.utils import helper_test_http_method
import pytest
pytestmark = pytest.mark.django_db
@pytest.fixture
def data():
m = type("Models", (object,), {})
m.registered_user = f.UserFactory.create()
m.project_member_with_perms = f.UserFactory.create()
m.project_member_without_perms = f.UserFactory.create()
m.project_owner = f.UserFactory.create()
m.other_user = f.UserFactory.create()
m.superuser = f.UserFactory.create(is_superuser=True)
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)
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)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.public_project,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
m.private_membership1 = f.MembershipFactory(project=m.private_project1,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.private_project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.private_project1,
user=m.project_member_without_perms,
email=m.project_member_without_perms.email,
role__project=m.private_project1,
role__permissions=[])
m.private_membership2 = f.MembershipFactory(project=m.private_project2,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.private_project2,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.private_project2,
user=m.project_member_without_perms,
email=m.project_member_without_perms.email,
role__project=m.private_project2,
role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
is_owner=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
is_owner=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
is_owner=True)
m.public_task_ca = f.TaskCustomAttributeFactory(project=m.public_project)
m.private_task_ca1 = f.TaskCustomAttributeFactory(project=m.private_project1)
m.private_task_ca2 = f.TaskCustomAttributeFactory(project=m.private_project2)
m.public_task = f.TaskFactory(project=m.public_project,
status__project=m.public_project,
milestone__project=m.public_project,
user_story__project=m.public_project)
m.private_task1 = f.TaskFactory(project=m.private_project1,
status__project=m.private_project1,
milestone__project=m.private_project1,
user_story__project=m.private_project1)
m.private_task2 = f.TaskFactory(project=m.private_project2,
status__project=m.private_project2,
milestone__project=m.private_project2,
user_story__project=m.private_project2)
m.public_task_cav = m.public_task.custom_attributes_values
m.private_task_cav1 = m.private_task1.custom_attributes_values
m.private_task_cav2 = m.private_task2.custom_attributes_values
return m
#########################################################
# Task Custom Attribute
#########################################################
def test_task_custom_attribute_retrieve(client, data):
public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk})
private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk})
private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk})
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', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private1_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
def test_task_custom_attribute_create(client, data):
public_url = reverse('task-custom-attributes-list')
private1_url = reverse('task-custom-attributes-list')
private2_url = reverse('task-custom-attributes-list')
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
task_ca_data = {"name": "test-new", "project": data.public_project.id}
task_ca_data = json.dumps(task_ca_data)
results = helper_test_http_method(client, 'post', public_url, task_ca_data, users)
assert results == [401, 403, 403, 403, 201]
task_ca_data = {"name": "test-new", "project": data.private_project1.id}
task_ca_data = json.dumps(task_ca_data)
results = helper_test_http_method(client, 'post', private1_url, task_ca_data, users)
assert results == [401, 403, 403, 403, 201]
task_ca_data = {"name": "test-new", "project": data.private_project2.id}
task_ca_data = json.dumps(task_ca_data)
results = helper_test_http_method(client, 'post', private2_url, task_ca_data, users)
assert results == [401, 403, 403, 403, 201]
def test_task_custom_attribute_update(client, data):
public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk})
private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk})
private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
task_ca_data = serializers.TaskCustomAttributeSerializer(data.public_task_ca).data
task_ca_data["name"] = "test"
task_ca_data = json.dumps(task_ca_data)
results = helper_test_http_method(client, 'put', public_url, task_ca_data, users)
assert results == [401, 403, 403, 403, 200]
task_ca_data = serializers.TaskCustomAttributeSerializer(data.private_task_ca1).data
task_ca_data["name"] = "test"
task_ca_data = json.dumps(task_ca_data)
results = helper_test_http_method(client, 'put', private1_url, task_ca_data, users)
assert results == [401, 403, 403, 403, 200]
task_ca_data = serializers.TaskCustomAttributeSerializer(data.private_task_ca2).data
task_ca_data["name"] = "test"
task_ca_data = json.dumps(task_ca_data)
results = helper_test_http_method(client, 'put', private2_url, task_ca_data, users)
assert results == [401, 403, 403, 403, 200]
def test_task_custom_attribute_delete(client, data):
public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk})
private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk})
private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'delete', public_url, None, users)
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private1_url, None, users)
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
def test_task_custom_attribute_list(client, data):
url = reverse('task-custom-attributes-list')
response = client.json.get(url)
assert len(response.data) == 2
assert response.status_code == 200
client.login(data.registered_user)
response = client.json.get(url)
assert len(response.data) == 2
assert response.status_code == 200
client.login(data.project_member_without_perms)
response = client.json.get(url)
assert len(response.data) == 2
assert response.status_code == 200
client.login(data.project_member_with_perms)
response = client.json.get(url)
assert len(response.data) == 3
assert response.status_code == 200
client.login(data.project_owner)
response = client.json.get(url)
assert len(response.data) == 3
assert response.status_code == 200
def test_task_custom_attribute_patch(client, data):
public_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.public_task_ca.pk})
private1_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca1.pk})
private2_url = reverse('task-custom-attributes-detail', kwargs={"pk": data.private_task_ca2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
def test_task_custom_attribute_action_bulk_update_order(client, data):
url = reverse('task-custom-attributes-bulk-update-order')
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
post_data = json.dumps({
"bulk_task_custom_attributes": [(1,2)],
"project": data.public_project.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
post_data = json.dumps({
"bulk_task_custom_attributes": [(1,2)],
"project": data.private_project1.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
post_data = json.dumps({
"bulk_task_custom_attributes": [(1,2)],
"project": data.private_project2.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
#########################################################
# Task Custom Attribute
#########################################################
def test_task_custom_attributes_values_retrieve(client, data):
public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk})
private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk})
private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk})
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', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_task_custom_attributes_values_update(client, data):
public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk})
private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk})
private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
task_data = serializers.TaskCustomAttributesValuesSerializer(data.public_task_cav).data
task_data["attributes_values"] = {str(data.public_task_ca.pk): "test"}
task_data = json.dumps(task_data)
results = helper_test_http_method(client, 'put', public_url, task_data, users)
assert results == [401, 403, 403, 200, 200]
task_data = serializers.TaskCustomAttributesValuesSerializer(data.private_task_cav1).data
task_data["attributes_values"] = {str(data.private_task_ca1.pk): "test"}
task_data = json.dumps(task_data)
results = helper_test_http_method(client, 'put', private_url1, task_data, users)
assert results == [401, 403, 403, 200, 200]
task_data = serializers.TaskCustomAttributesValuesSerializer(data.private_task_cav2).data
task_data["attributes_values"] = {str(data.private_task_ca2.pk): "test"}
task_data = json.dumps(task_data)
results = helper_test_http_method(client, 'put', private_url2, task_data, users)
assert results == [401, 403, 403, 200, 200]
def test_task_custom_attributes_values_patch(client, data):
public_url = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.public_task.pk})
private_url1 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task1.pk})
private_url2 = reverse('task-custom-attributes-values-detail', kwargs={"task_id": data.private_task2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
patch_data = json.dumps({"attributes_values": {str(data.public_task_ca.pk): "test"},
"version": data.public_task.version})
results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
assert results == [401, 403, 403, 200, 200]
patch_data = json.dumps({"attributes_values": {str(data.private_task_ca1.pk): "test"},
"version": data.private_task1.version})
results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
assert results == [401, 403, 403, 200, 200]
patch_data = json.dumps({"attributes_values": {str(data.private_task_ca2.pk): "test"},
"version": data.private_task2.version})
results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
assert results == [401, 403, 403, 200, 200]

View File

@ -0,0 +1,398 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects.custom_attributes import serializers
from taiga.permissions.permissions import (MEMBERS_PERMISSIONS,
ANON_PERMISSIONS, USER_PERMISSIONS)
from tests import factories as f
from tests.utils import helper_test_http_method
import pytest
pytestmark = pytest.mark.django_db
@pytest.fixture
def data():
m = type("Models", (object,), {})
m.registered_user = f.UserFactory.create()
m.project_member_with_perms = f.UserFactory.create()
m.project_member_without_perms = f.UserFactory.create()
m.project_owner = f.UserFactory.create()
m.other_user = f.UserFactory.create()
m.superuser = f.UserFactory.create(is_superuser=True)
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)
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)
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.public_project,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
m.private_membership1 = f.MembershipFactory(project=m.private_project1,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.private_project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.private_project1,
user=m.project_member_without_perms,
email=m.project_member_without_perms.email,
role__project=m.private_project1,
role__permissions=[])
m.private_membership2 = f.MembershipFactory(project=m.private_project2,
user=m.project_member_with_perms,
email=m.project_member_with_perms.email,
role__project=m.private_project2,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.private_project2,
user=m.project_member_without_perms,
email=m.project_member_without_perms.email,
role__project=m.private_project2,
role__permissions=[])
f.MembershipFactory(project=m.public_project,
user=m.project_owner,
is_owner=True)
f.MembershipFactory(project=m.private_project1,
user=m.project_owner,
is_owner=True)
f.MembershipFactory(project=m.private_project2,
user=m.project_owner,
is_owner=True)
m.public_userstory_ca = f.UserStoryCustomAttributeFactory(project=m.public_project)
m.private_userstory_ca1 = f.UserStoryCustomAttributeFactory(project=m.private_project1)
m.private_userstory_ca2 = f.UserStoryCustomAttributeFactory(project=m.private_project2)
m.public_user_story = f.UserStoryFactory(project=m.public_project,
status__project=m.public_project)
m.private_user_story1 = f.UserStoryFactory(project=m.private_project1,
status__project=m.private_project1)
m.private_user_story2 = f.UserStoryFactory(project=m.private_project2,
status__project=m.private_project2)
m.public_user_story_cav = m.public_user_story.custom_attributes_values
m.private_user_story_cav1 = m.private_user_story1.custom_attributes_values
m.private_user_story_cav2 = m.private_user_story2.custom_attributes_values
return m
#########################################################
# User Story Custom Attribute
#########################################################
def test_userstory_custom_attribute_retrieve(client, data):
public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk})
private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk})
private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk})
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', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private1_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private2_url, None, users)
assert results == [401, 403, 403, 200, 200]
def test_userstory_custom_attribute_create(client, data):
public_url = reverse('userstory-custom-attributes-list')
private1_url = reverse('userstory-custom-attributes-list')
private2_url = reverse('userstory-custom-attributes-list')
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
userstory_ca_data = {"name": "test-new", "project": data.public_project.id}
userstory_ca_data = json.dumps(userstory_ca_data)
results = helper_test_http_method(client, 'post', public_url, userstory_ca_data, users)
assert results == [401, 403, 403, 403, 201]
userstory_ca_data = {"name": "test-new", "project": data.private_project1.id}
userstory_ca_data = json.dumps(userstory_ca_data)
results = helper_test_http_method(client, 'post', private1_url, userstory_ca_data, users)
assert results == [401, 403, 403, 403, 201]
userstory_ca_data = {"name": "test-new", "project": data.private_project2.id}
userstory_ca_data = json.dumps(userstory_ca_data)
results = helper_test_http_method(client, 'post', private2_url, userstory_ca_data, users)
assert results == [401, 403, 403, 403, 201]
def test_userstory_custom_attribute_update(client, data):
public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk})
private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk})
private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.public_userstory_ca).data
userstory_ca_data["name"] = "test"
userstory_ca_data = json.dumps(userstory_ca_data)
results = helper_test_http_method(client, 'put', public_url, userstory_ca_data, users)
assert results == [401, 403, 403, 403, 200]
userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.private_userstory_ca1).data
userstory_ca_data["name"] = "test"
userstory_ca_data = json.dumps(userstory_ca_data)
results = helper_test_http_method(client, 'put', private1_url, userstory_ca_data, users)
assert results == [401, 403, 403, 403, 200]
userstory_ca_data = serializers.UserStoryCustomAttributeSerializer(data.private_userstory_ca2).data
userstory_ca_data["name"] = "test"
userstory_ca_data = json.dumps(userstory_ca_data)
results = helper_test_http_method(client, 'put', private2_url, userstory_ca_data, users)
assert results == [401, 403, 403, 403, 200]
def test_userstory_custom_attribute_delete(client, data):
public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk})
private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk})
private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'delete', public_url, None, users)
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private1_url, None, users)
assert results == [401, 403, 403, 403, 204]
results = helper_test_http_method(client, 'delete', private2_url, None, users)
assert results == [401, 403, 403, 403, 204]
def test_userstory_custom_attribute_list(client, data):
url = reverse('userstory-custom-attributes-list')
response = client.json.get(url)
assert len(response.data) == 2
assert response.status_code == 200
client.login(data.registered_user)
response = client.json.get(url)
assert len(response.data) == 2
assert response.status_code == 200
client.login(data.project_member_without_perms)
response = client.json.get(url)
assert len(response.data) == 2
assert response.status_code == 200
client.login(data.project_member_with_perms)
response = client.json.get(url)
assert len(response.data) == 3
assert response.status_code == 200
client.login(data.project_owner)
response = client.json.get(url)
assert len(response.data) == 3
assert response.status_code == 200
def test_userstory_custom_attribute_patch(client, data):
public_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.public_userstory_ca.pk})
private1_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca1.pk})
private2_url = reverse('userstory-custom-attributes-detail', kwargs={"pk": data.private_userstory_ca2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
results = helper_test_http_method(client, 'patch', public_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private1_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
results = helper_test_http_method(client, 'patch', private2_url, '{"name": "Test"}', users)
assert results == [401, 403, 403, 403, 200]
def test_userstory_custom_attribute_action_bulk_update_order(client, data):
url = reverse('userstory-custom-attributes-bulk-update-order')
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
post_data = json.dumps({
"bulk_userstory_custom_attributes": [(1,2)],
"project": data.public_project.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
post_data = json.dumps({
"bulk_userstory_custom_attributes": [(1,2)],
"project": data.private_project1.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
post_data = json.dumps({
"bulk_userstory_custom_attributes": [(1,2)],
"project": data.private_project2.pk
})
results = helper_test_http_method(client, 'post', url, post_data, users)
assert results == [401, 403, 403, 403, 204]
#########################################################
# UserStory Custom Attribute
#########################################################
def test_userstory_custom_attributes_values_retrieve(client, data):
public_url = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.public_user_story.pk})
private_url1 = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.private_user_story2.pk})
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', public_url, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url1, None, users)
assert results == [200, 200, 200, 200, 200]
results = helper_test_http_method(client, 'get', private_url2, None, users)
assert results == [401, 403, 403, 200, 200]
def test_userstory_custom_attributes_values_update(client, data):
public_url = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.public_user_story.pk})
private_url1 = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
user_story_data = serializers.UserStoryCustomAttributesValuesSerializer(data.public_user_story_cav).data
user_story_data["attributes_values"] = {str(data.public_userstory_ca.pk): "test"}
user_story_data = json.dumps(user_story_data)
results = helper_test_http_method(client, 'put', public_url, user_story_data, users)
assert results == [401, 403, 403, 200, 200]
user_story_data = serializers.UserStoryCustomAttributesValuesSerializer(data.private_user_story_cav1).data
user_story_data["attributes_values"] = {str(data.private_userstory_ca1.pk): "test"}
user_story_data = json.dumps(user_story_data)
results = helper_test_http_method(client, 'put', private_url1, user_story_data, users)
assert results == [401, 403, 403, 200, 200]
user_story_data = serializers.UserStoryCustomAttributesValuesSerializer(data.private_user_story_cav2).data
user_story_data["attributes_values"] = {str(data.private_userstory_ca2.pk): "test"}
user_story_data = json.dumps(user_story_data)
results = helper_test_http_method(client, 'put', private_url2, user_story_data, users)
assert results == [401, 403, 403, 200, 200]
def test_userstory_custom_attributes_values_patch(client, data):
public_url = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.public_user_story.pk})
private_url1 = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.private_user_story1.pk})
private_url2 = reverse('userstory-custom-attributes-values-detail', kwargs={
"user_story_id": data.private_user_story2.pk})
users = [
None,
data.registered_user,
data.project_member_without_perms,
data.project_member_with_perms,
data.project_owner
]
patch_data = json.dumps({"attributes_values": {str(data.public_userstory_ca.pk): "test"},
"version": data.public_user_story.version})
results = helper_test_http_method(client, 'patch', public_url, patch_data, users)
assert results == [401, 403, 403, 200, 200]
patch_data = json.dumps({"attributes_values": {str(data.private_userstory_ca1.pk): "test"},
"version": data.private_user_story1.version})
results = helper_test_http_method(client, 'patch', private_url1, patch_data, users)
assert results == [401, 403, 403, 200, 200]
patch_data = json.dumps({"attributes_values": {str(data.private_userstory_ca2.pk): "test"},
"version": data.private_user_story2.version})
results = helper_test_http_method(client, 'patch', private_url2, patch_data, users)
assert results == [401, 403, 403, 200, 200]

View File

@ -0,0 +1,200 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.db.transaction import atomic
from django.core.urlresolvers import reverse
from taiga.base.utils import json
from .. import factories as f
import pytest
pytestmark = pytest.mark.django_db
#########################################################
# Issue Custom Attributes
#########################################################
def test_issue_custom_attribute_duplicate_name_error_on_create(client):
custom_attr_1 = f.IssueCustomAttributeFactory()
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_owner=True)
url = reverse("issue-custom-attributes-list")
data = {"name": custom_attr_1.name,
"project": custom_attr_1.project.pk}
client.login(member.user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
def test_issue_custom_attribute_duplicate_name_error_on_update(client):
custom_attr_1 = f.IssueCustomAttributeFactory()
custom_attr_2 = f.IssueCustomAttributeFactory(project=custom_attr_1.project)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_owner=True)
url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
data = {"name": custom_attr_1.name}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
def test_issue_custom_attribute_duplicate_name_error_on_move_between_projects(client):
custom_attr_1 = f.IssueCustomAttributeFactory()
custom_attr_2 = f.IssueCustomAttributeFactory(name=custom_attr_1.name)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_owner=True)
f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_2.project,
is_owner=True)
url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
data = {"project": custom_attr_1.project.pk}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
#########################################################
# Issue Custom Attributes Values
#########################################################
def test_issue_custom_attributes_values_when_create_us(client):
issue = f.IssueFactory()
assert issue.custom_attributes_values.attributes_values == {}
def test_issue_custom_attributes_values_update(client):
issue = f.IssueFactory()
member = f.MembershipFactory(user=issue.project.owner,
project=issue.project,
is_owner=True)
custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = issue.custom_attributes_values
url = reverse("issue-custom-attributes-values-detail", args=[issue.id])
data = {
"attributes_values": {
ct1_id: "test_1_updated",
ct2_id: "test_2_updated"
},
"version": custom_attrs_val.version
}
assert issue.custom_attributes_values.attributes_values == {}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200
assert response.data["attributes_values"] == data["attributes_values"]
issue = issue.__class__.objects.get(id=issue.id)
assert issue.custom_attributes_values.attributes_values == data["attributes_values"]
def test_issue_custom_attributes_values_update_with_error_invalid_key(client):
issue = f.IssueFactory()
member = f.MembershipFactory(user=issue.project.owner,
project=issue.project,
is_owner=True)
custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project)
custom_attrs_val = issue.custom_attributes_values
url = reverse("issue-custom-attributes-values-detail", args=[issue.id])
data = {
"attributes_values": {
ct1_id: "test_1_updated",
"123456": "test_2_updated"
},
"version": custom_attrs_val.version
}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
def test_issue_custom_attributes_values_delete_issue(client):
issue = f.IssueFactory()
member = f.MembershipFactory(user=issue.project.owner,
project=issue.project,
is_owner=True)
custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = issue.custom_attributes_values
url = reverse("issues-detail", args=[issue.id])
client.login(member.user)
response = client.json.delete(url)
assert response.status_code == 204
assert not issue.__class__.objects.filter(id=issue.id).exists()
assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists()
#########################################################
# Test tristres triggers :-P
#########################################################
def test_trigger_update_issuecustomvalues_afeter_remove_issuecustomattribute(client):
issue = f.IssueFactory()
member = f.MembershipFactory(user=issue.project.owner,
project=issue.project,
is_owner=True)
custom_attr_1 = f.IssueCustomAttributeFactory(project=issue.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.IssueCustomAttributeFactory(project=issue.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = issue.custom_attributes_values
custom_attrs_val.attributes_values = {ct1_id: "test_1", ct2_id: "test_2"}
custom_attrs_val.save()
assert ct1_id in custom_attrs_val.attributes_values.keys()
assert ct2_id in custom_attrs_val.attributes_values.keys()
url = reverse("issue-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
client.login(member.user)
response = client.json.delete(url)
assert response.status_code == 204
custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id)
assert not custom_attr_2.__class__.objects.filter(pk=custom_attr_2.pk).exists()
assert ct1_id in custom_attrs_val.attributes_values.keys()
assert ct2_id not in custom_attrs_val.attributes_values.keys()

View File

@ -0,0 +1,202 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.core.urlresolvers import reverse
from taiga.base.utils import json
from .. import factories as f
import pytest
pytestmark = pytest.mark.django_db
#########################################################
# Task Custom Attributes
#########################################################
def test_task_custom_attribute_duplicate_name_error_on_create(client):
custom_attr_1 = f.TaskCustomAttributeFactory()
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_owner=True)
url = reverse("task-custom-attributes-list")
data = {"name": custom_attr_1.name,
"project": custom_attr_1.project.pk}
client.login(member.user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
def test_task_custom_attribute_duplicate_name_error_on_update(client):
custom_attr_1 = f.TaskCustomAttributeFactory()
custom_attr_2 = f.TaskCustomAttributeFactory(project=custom_attr_1.project)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_owner=True)
url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
data = {"name": custom_attr_1.name}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
def test_task_custom_attribute_duplicate_name_error_on_move_between_projects(client):
custom_attr_1 = f.TaskCustomAttributeFactory()
custom_attr_2 = f.TaskCustomAttributeFactory(name=custom_attr_1.name)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_owner=True)
f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_2.project,
is_owner=True)
url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
data = {"project": custom_attr_1.project.pk}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
#########################################################
# Task Custom Attributes Values
#########################################################
def test_task_custom_attributes_values_when_create_us(client):
task = f.TaskFactory()
assert task.custom_attributes_values.attributes_values == {}
def test_task_custom_attributes_values_update(client):
task = f.TaskFactory()
member = f.MembershipFactory(user=task.project.owner,
project=task.project,
is_owner=True)
custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = task.custom_attributes_values
url = reverse("task-custom-attributes-values-detail", args=[task.id])
data = {
"attributes_values": {
ct1_id: "test_1_updated",
ct2_id: "test_2_updated"
},
"version": custom_attrs_val.version
}
assert task.custom_attributes_values.attributes_values == {}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200
assert response.data["attributes_values"] == data["attributes_values"]
task = task.__class__.objects.get(id=task.id)
assert task.custom_attributes_values.attributes_values == data["attributes_values"]
def test_task_custom_attributes_values_update_with_error_invalid_key(client):
task = f.TaskFactory()
member = f.MembershipFactory(user=task.project.owner,
project=task.project,
is_owner=True)
custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project)
custom_attrs_val = task.custom_attributes_values
url = reverse("task-custom-attributes-values-detail", args=[task.id])
data = {
"attributes_values": {
ct1_id: "test_1_updated",
"123456": "test_2_updated"
},
"version": custom_attrs_val.version
}
assert task.custom_attributes_values.attributes_values == {}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
def test_task_custom_attributes_values_delete_task(client):
task = f.TaskFactory()
member = f.MembershipFactory(user=task.project.owner,
project=task.project,
is_owner=True)
custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = task.custom_attributes_values
url = reverse("tasks-detail", args=[task.id])
client.login(member.user)
response = client.json.delete(url)
assert response.status_code == 204
assert not task.__class__.objects.filter(id=task.id).exists()
assert not custom_attrs_val.__class__.objects.filter(id=custom_attrs_val.id).exists()
#########################################################
# Test tristres triggers :-P
#########################################################
def test_trigger_update_taskcustomvalues_afeter_remove_taskcustomattribute(client):
task = f.TaskFactory()
member = f.MembershipFactory(user=task.project.owner,
project=task.project,
is_owner=True)
custom_attr_1 = f.TaskCustomAttributeFactory(project=task.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.TaskCustomAttributeFactory(project=task.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = task.custom_attributes_values
custom_attrs_val.attributes_values = {ct1_id: "test_1", ct2_id: "test_2"}
custom_attrs_val.save()
assert ct1_id in custom_attrs_val.attributes_values.keys()
assert ct2_id in custom_attrs_val.attributes_values.keys()
url = reverse("task-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
client.login(member.user)
response = client.json.delete(url)
assert response.status_code == 204
custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id)
assert not custom_attr_2.__class__.objects.filter(pk=custom_attr_2.pk).exists()
assert ct1_id in custom_attrs_val.attributes_values.keys()
assert ct2_id not in custom_attrs_val.attributes_values.keys()

View File

@ -0,0 +1,179 @@
# Copyright (C) 2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2015 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2015 David Barragán <bameda@dbarragan.com>
# 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.core.urlresolvers import reverse
from taiga.base.utils import json
from .. import factories as f
import pytest
pytestmark = pytest.mark.django_db
#########################################################
# User Story Custom Attributes
#########################################################
def test_userstory_custom_attribute_duplicate_name_error_on_create(client):
custom_attr_1 = f.UserStoryCustomAttributeFactory()
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_owner=True)
url = reverse("userstory-custom-attributes-list")
data = {"name": custom_attr_1.name,
"project": custom_attr_1.project.pk}
client.login(member.user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
def test_userstory_custom_attribute_duplicate_name_error_on_update(client):
custom_attr_1 = f.UserStoryCustomAttributeFactory()
custom_attr_2 = f.UserStoryCustomAttributeFactory(project=custom_attr_1.project)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_owner=True)
url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
data = {"name": custom_attr_1.name}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
def test_userstory_custom_attribute_duplicate_name_error_on_move_between_projects(client):
custom_attr_1 = f.UserStoryCustomAttributeFactory()
custom_attr_2 = f.UserStoryCustomAttributeFactory(name=custom_attr_1.name)
member = f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_1.project,
is_owner=True)
f.MembershipFactory(user=custom_attr_1.project.owner,
project=custom_attr_2.project,
is_owner=True)
url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
data = {"project": custom_attr_1.project.pk}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
#########################################################
# User Story Custom Attributes Values
#########################################################
def test_userstory_custom_attributes_values_when_create_us(client):
user_story = f.UserStoryFactory()
assert user_story.custom_attributes_values.attributes_values == {}
def test_userstory_custom_attributes_values_update(client):
user_story = f.UserStoryFactory()
member = f.MembershipFactory(user=user_story.project.owner,
project=user_story.project,
is_owner=True)
custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = user_story.custom_attributes_values
url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id])
data = {
"attributes_values": {
ct1_id: "test_1_updated",
ct2_id: "test_2_updated"
},
"version": custom_attrs_val.version
}
assert user_story.custom_attributes_values.attributes_values == {}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200
assert response.data["attributes_values"] == data["attributes_values"]
user_story = user_story.__class__.objects.get(id=user_story.id)
assert user_story.custom_attributes_values.attributes_values == data["attributes_values"]
def test_userstory_custom_attributes_values_update_with_error_invalid_key(client):
user_story = f.UserStoryFactory()
member = f.MembershipFactory(user=user_story.project.owner,
project=user_story.project,
is_owner=True)
custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project)
custom_attrs_val = user_story.custom_attributes_values
url = reverse("userstory-custom-attributes-values-detail", args=[user_story.id])
data = {
"attributes_values": {
ct1_id: "test_1_updated",
"123456": "test_2_updated"
},
"version": custom_attrs_val.version
}
assert user_story.custom_attributes_values.attributes_values == {}
client.login(member.user)
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
#########################################################
# Test tristres triggers :-P
#########################################################
def test_trigger_update_userstorycustomvalues_afeter_remove_userstorycustomattribute(client):
user_story = f.UserStoryFactory()
member = f.MembershipFactory(user=user_story.project.owner,
project=user_story.project,
is_owner=True)
custom_attr_1 = f.UserStoryCustomAttributeFactory(project=user_story.project)
ct1_id = "{}".format(custom_attr_1.id)
custom_attr_2 = f.UserStoryCustomAttributeFactory(project=user_story.project)
ct2_id = "{}".format(custom_attr_2.id)
custom_attrs_val = user_story.custom_attributes_values
custom_attrs_val.attributes_values = {ct1_id: "test_1", ct2_id: "test_2"}
custom_attrs_val.save()
assert ct1_id in custom_attrs_val.attributes_values.keys()
assert ct2_id in custom_attrs_val.attributes_values.keys()
url = reverse("userstory-custom-attributes-detail", kwargs={"pk": custom_attr_2.pk})
client.login(member.user)
response = client.json.delete(url)
assert response.status_code == 204
custom_attrs_val = custom_attrs_val.__class__.objects.get(id=custom_attrs_val.id)
assert ct1_id in custom_attrs_val.attributes_values.keys()
assert ct2_id not in custom_attrs_val.attributes_values.keys()

View File

@ -22,6 +22,8 @@ from django.core.files.base import ContentFile
from .. import factories as f
from django.apps import apps
from taiga.base.utils import json
from taiga.projects.models import Project
from taiga.projects.issues.models import Issue
@ -167,6 +169,61 @@ def test_invalid_project_import_with_extra_data(client):
assert Project.objects.filter(slug="imported-project").count() == 0
def test_valid_project_import_with_custom_attributes(client):
user = f.UserFactory.create()
url = reverse("importer-list")
data = {
"name": "Imported project",
"description": "Imported project",
"userstorycustomattributes": [{
"name": "custom attribute example 1",
"description": "short description 1",
"order": 1
}],
"taskcustomattributes": [{
"name": "custom attribute example 1",
"description": "short description 1",
"order": 1
}],
"issuecustomattributes": [{
"name": "custom attribute example 1",
"description": "short description 1",
"order": 1
}]
}
must_empty_children = ["issues", "user_stories", "wiki_pages", "milestones", "wiki_links"]
must_one_instance_children = ["userstorycustomattributes", "taskcustomattributes", "issuecustomattributes"]
client.login(user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
assert all(map(lambda x: len(response.data[x]) == 0, must_empty_children))
# Allwais is created at least the owner membership
assert all(map(lambda x: len(response.data[x]) == 1, must_one_instance_children))
assert response.data["owner"] == user.email
def test_invalid_project_import_with_custom_attributes(client):
user = f.UserFactory.create()
url = reverse("importer-list")
data = {
"name": "Imported project",
"description": "Imported project",
"userstorycustomattributes": [{ }],
"taskcustomattributes": [{ }],
"issuecustomattributes": [{ }]
}
client.login(user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert len(response.data) == 3
assert Project.objects.filter(slug="imported-project").count() == 0
def test_invalid_issue_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
@ -201,6 +258,30 @@ def test_valid_user_story_import(client):
assert response_data["finish_date"] == "2014-10-24T00:00:00+0000"
def test_valid_user_story_import_with_custom_attributes_values(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
membership = f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_us_status = f.UserStoryStatusFactory.create(project=project)
project.save()
custom_attr = f.UserStoryCustomAttributeFactory(project=project)
url = reverse("importer-us", args=[project.pk])
data = {
"subject": "Test Custom Attrs Values User Story",
"custom_attributes_values": {
custom_attr.name: "test_value"
}
}
client.login(user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
custom_attributes_values = apps.get_model("custom_attributes.UserStoryCustomAttributesValues").objects.get(
user_story__subject=response.data["subject"])
assert custom_attributes_values.attributes_values == {str(custom_attr.id): "test_value"}
def test_valid_issue_import_without_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
@ -224,6 +305,33 @@ def test_valid_issue_import_without_extra_data(client):
assert response_data["ref"] is not None
def test_valid_issue_import_with_custom_attributes_values(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
membership = f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_issue_type = f.IssueTypeFactory.create(project=project)
project.default_issue_status = f.IssueStatusFactory.create(project=project)
project.default_severity = f.SeverityFactory.create(project=project)
project.default_priority = f.PriorityFactory.create(project=project)
project.save()
custom_attr = f.IssueCustomAttributeFactory(project=project)
url = reverse("importer-issue", args=[project.pk])
data = {
"subject": "Test Custom Attrs Values Issues",
"custom_attributes_values": {
custom_attr.name: "test_value"
}
}
client.login(user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
custom_attributes_values = apps.get_model("custom_attributes.IssueCustomAttributesValues").objects.get(
issue__subject=response.data["subject"])
assert custom_attributes_values.attributes_values == {str(custom_attr.id): "test_value"}
def test_valid_issue_import_with_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
@ -481,6 +589,30 @@ def test_valid_task_import_without_extra_data(client):
assert response_data["ref"] is not None
def test_valid_task_import_with_custom_attributes_values(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
membership = f.MembershipFactory(project=project, user=user, is_owner=True)
project.default_task_status = f.TaskStatusFactory.create(project=project)
project.save()
custom_attr = f.TaskCustomAttributeFactory(project=project)
url = reverse("importer-task", args=[project.pk])
data = {
"subject": "Test Custom Attrs Values Tasks",
"custom_attributes_values": {
custom_attr.name: "test_value"
}
}
client.login(user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
custom_attributes_values = apps.get_model("custom_attributes.TaskCustomAttributesValues").objects.get(
task__subject=response.data["subject"])
assert custom_attributes_values.attributes_values == {str(custom_attr.id): "test_value"}
def test_valid_task_import_with_extra_data(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
@ -680,6 +812,7 @@ def test_valid_wiki_link_import(client):
json.loads(response.content.decode("utf-8"))
def test_invalid_milestone_import(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)
@ -711,6 +844,7 @@ def test_valid_milestone_import(client):
json.loads(response.content.decode("utf-8"))
def test_milestone_import_duplicated_milestone(client):
user = f.UserFactory.create()
project = f.ProjectFactory.create(owner=user)