Merge pull request #242 from taigaio/us/55/custom-fields
US #55: Custom fieldsremotes/origin/enhancement/email-actions
commit
336d141656
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -168,3 +168,8 @@ class ModelListViewSet(pagination.HeadersPaginationMixin,
|
|||
mixins.ListModelMixin,
|
||||
GenericViewSet):
|
||||
pass
|
||||
|
||||
class ModelUpdateRetrieveViewSet(mixins.UpdateModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
GenericViewSet):
|
||||
pass
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
|
@ -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
|
|
@ -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')]),
|
||||
),
|
||||
]
|
|
@ -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,),
|
||||
),
|
||||
]
|
|
@ -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;"""
|
||||
)
|
||||
]
|
|
@ -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),
|
||||
]
|
|
@ -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
|
|
@ -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')
|
|
@ -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
|
|
@ -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()
|
|
@ -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":{}})
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
104
taiga/routers.py
104
taiga/routers.py
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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]
|
|
@ -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]
|
|
@ -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]
|
|
@ -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()
|
|
@ -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()
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue