diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py
index 158d712d..31823945 100644
--- a/taiga/base/api/generics.py
+++ b/taiga/base/api/generics.py
@@ -62,6 +62,7 @@ class GenericAPIView(pagination.PaginationMixin,
# or override `get_queryset()`/`get_serializer_class()`.
queryset = None
serializer_class = None
+ validator_class = None
# This shortcut may be used instead of setting either or both
# of the `queryset`/`serializer_class` attributes, although using
@@ -79,6 +80,7 @@ class GenericAPIView(pagination.PaginationMixin,
# The following attributes may be subject to change,
# and should be considered private API.
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
+ model_validator_class = api_settings.DEFAULT_MODEL_VALIDATOR_CLASS
######################################
# These are pending deprecation...
@@ -88,7 +90,7 @@ class GenericAPIView(pagination.PaginationMixin,
slug_field = 'slug'
allow_empty = True
- def get_serializer_context(self):
+ def get_extra_context(self):
"""
Extra context provided to the serializer class.
"""
@@ -101,14 +103,24 @@ class GenericAPIView(pagination.PaginationMixin,
def get_serializer(self, instance=None, data=None,
files=None, many=False, partial=False):
"""
- Return the serializer instance that should be used for validating and
- deserializing input, and for serializing output.
+ Return the serializer instance that should be used for deserializing
+ input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
- context = self.get_serializer_context()
+ context = self.get_extra_context()
return serializer_class(instance, data=data, files=files,
many=many, partial=partial, context=context)
+ def get_validator(self, instance=None, data=None,
+ files=None, many=False, partial=False):
+ """
+ Return the validator instance that should be used for validating the
+ input, and for serializing output.
+ """
+ validator_class = self.get_validator_class()
+ context = self.get_extra_context()
+ return validator_class(instance, data=data, files=files,
+ many=many, partial=partial, context=context)
def filter_queryset(self, queryset, filter_backends=None):
"""
@@ -119,7 +131,7 @@ class GenericAPIView(pagination.PaginationMixin,
method if you want to apply the configured filtering backend to the
default queryset.
"""
- #NOTE TAIGA: Added filter_backends to overwrite the default behavior.
+ # NOTE TAIGA: Added filter_backends to overwrite the default behavior.
backends = filter_backends or self.get_filter_backends()
for backend in backends:
@@ -160,6 +172,22 @@ class GenericAPIView(pagination.PaginationMixin,
model = self.model
return DefaultSerializer
+ def get_validator_class(self):
+ validator_class = self.validator_class
+ serializer_class = self.get_serializer_class()
+
+ # Situations where the validator is the rest framework serializer
+ if validator_class is None and serializer_class is not None:
+ return serializer_class
+
+ if validator_class is not None:
+ return validator_class
+
+ class DefaultValidator(self.model_validator_class):
+ class Meta:
+ model = self.model
+ return DefaultValidator
+
def get_queryset(self):
"""
Get the list of items for this view.
diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py
index 89af6984..861d77ec 100644
--- a/taiga/base/api/mixins.py
+++ b/taiga/base/api/mixins.py
@@ -57,6 +57,7 @@ from .utils import get_object_or_404
from .. import exceptions as exc
from ..decorators import model_pk_lock
+
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
"""
Given a model instance, and an optional pk and slug field,
@@ -89,19 +90,21 @@ class CreateModelMixin:
Create a model instance.
"""
def create(self, request, *args, **kwargs):
- serializer = self.get_serializer(data=request.DATA, files=request.FILES)
+ validator = self.get_validator(data=request.DATA, files=request.FILES)
- if serializer.is_valid():
- self.check_permissions(request, 'create', serializer.object)
+ if validator.is_valid():
+ self.check_permissions(request, 'create', validator.object)
- self.pre_save(serializer.object)
- self.pre_conditions_on_save(serializer.object)
- self.object = serializer.save(force_insert=True)
+ self.pre_save(validator.object)
+ self.pre_conditions_on_save(validator.object)
+ self.object = validator.save(force_insert=True)
self.post_save(self.object, created=True)
+ instance = self.get_queryset().get(id=self.object.id)
+ serializer = self.get_serializer(instance)
headers = self.get_success_headers(serializer.data)
return response.Created(serializer.data, headers=headers)
- return response.BadRequest(serializer.errors)
+ return response.BadRequest(validator.errors)
def get_success_headers(self, data):
try:
@@ -171,28 +174,32 @@ class UpdateModelMixin:
if self.object is None:
raise Http404
- serializer = self.get_serializer(self.object, data=request.DATA,
- files=request.FILES, partial=partial)
+ validator = self.get_validator(self.object, data=request.DATA,
+ files=request.FILES, partial=partial)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
# Hooks
try:
- self.pre_save(serializer.object)
- self.pre_conditions_on_save(serializer.object)
+ self.pre_save(validator.object)
+ self.pre_conditions_on_save(validator.object)
except ValidationError as err:
# full_clean on model instance may be called in pre_save,
# so we have to handle eventual errors.
return response.BadRequest(err.message_dict)
if self.object is None:
- self.object = serializer.save(force_insert=True)
+ self.object = validator.save(force_insert=True)
self.post_save(self.object, created=True)
+ instance = self.get_queryset().get(id=self.object.id)
+ serializer = self.get_serializer(instance)
return response.Created(serializer.data)
- self.object = serializer.save(force_update=True)
+ self.object = validator.save(force_update=True)
self.post_save(self.object, created=False)
+ instance = self.get_queryset().get(id=self.object.id)
+ serializer = self.get_serializer(instance)
return response.Ok(serializer.data)
def partial_update(self, request, *args, **kwargs):
@@ -251,7 +258,7 @@ class BlockeableModelMixin:
raise NotImplementedError("is_blocked must be overridden")
def pre_conditions_blocked(self, obj):
- #Raises permission exception
+ # Raises permission exception
if obj is not None and self.is_blocked(obj):
raise exc.Blocked(_("Blocked element"))
diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py
index 601c1753..82565b26 100644
--- a/taiga/base/api/serializers.py
+++ b/taiga/base/api/serializers.py
@@ -1229,5 +1229,7 @@ class LightSerializer(serpy.Serializer):
kwargs.pop("partial", None)
kwargs.pop("files", None)
context = kwargs.pop("context", {})
+ view = kwargs.pop("view", {})
super().__init__(*args, **kwargs)
self.context = context
+ self.view = view
diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py
index 1a3d01ba..75d204c9 100644
--- a/taiga/base/api/settings.py
+++ b/taiga/base/api/settings.py
@@ -98,6 +98,8 @@ DEFAULTS = {
# Genric view behavior
"DEFAULT_MODEL_SERIALIZER_CLASS":
"taiga.base.api.serializers.ModelSerializer",
+ "DEFAULT_MODEL_VALIDATOR_CLASS":
+ "taiga.base.api.validators.ModelValidator",
"DEFAULT_FILTER_BACKENDS": (),
# Throttling
diff --git a/taiga/projects/likes/mixins/serializers.py b/taiga/base/api/validators.py
similarity index 72%
rename from taiga/projects/likes/mixins/serializers.py
rename to taiga/base/api/validators.py
index 84d63b4e..3a8d6922 100644
--- a/taiga/projects/likes/mixins/serializers.py
+++ b/taiga/base/api/validators.py
@@ -16,15 +16,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.base.api import serializers
+from . import serializers
-class FanResourceSerializerMixin(serializers.ModelSerializer):
- is_fan = serializers.SerializerMethodField("get_is_fan")
+class Validator(serializers.Serializer):
+ pass
- def get_is_fan(self, obj):
- if "request" in self.context:
- user = self.context["request"].user
- return user.is_authenticated() and user.is_fan(obj)
- return False
+class ModelValidator(serializers.ModelSerializer):
+ pass
diff --git a/taiga/base/fields.py b/taiga/base/fields.py
index 5e5c4b5a..f0cf4ee2 100644
--- a/taiga/base/fields.py
+++ b/taiga/base/fields.py
@@ -20,11 +20,13 @@ from django.forms import widgets
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+import serpy
####################################################################
-## Serializer fields
+# Serializer fields
####################################################################
+
class JsonField(serializers.WritableField):
"""
Json objects serializer.
@@ -38,40 +40,6 @@ class JsonField(serializers.WritableField):
return data
-class I18NJsonField(JsonField):
- """
- Json objects serializer.
- """
- widget = widgets.Textarea
-
- def __init__(self, i18n_fields=(), *args, **kwargs):
- super(I18NJsonField, self).__init__(*args, **kwargs)
- self.i18n_fields = i18n_fields
-
- def translate_values(self, d):
- i18n_d = {}
- if d is None:
- return d
-
- for key, value in d.items():
- if isinstance(value, dict):
- i18n_d[key] = self.translate_values(value)
-
- if key in self.i18n_fields:
- if isinstance(value, list):
- i18n_d[key] = [e is not None and _(str(e)) or e for e in value]
- if isinstance(value, str):
- i18n_d[key] = value is not None and _(value) or value
- else:
- i18n_d[key] = value
-
- return i18n_d
-
- def to_native(self, obj):
- i18n_obj = self.translate_values(obj)
- return i18n_obj
-
-
class PgArrayField(serializers.WritableField):
"""
PgArray objects serializer.
@@ -104,3 +72,49 @@ class WatchersField(serializers.WritableField):
def from_native(self, data):
return data
+
+
+class Field(serpy.Field):
+ pass
+
+
+class MethodField(serpy.MethodField):
+ pass
+
+
+class I18NField(serpy.Field):
+ def to_value(self, value):
+ ret = super(I18NField, self).to_value(value)
+ return _(ret)
+
+
+class I18NJsonField(serpy.Field):
+ """
+ Json objects serializer.
+ """
+ def __init__(self, i18n_fields=(), *args, **kwargs):
+ super(I18NJsonField, self).__init__(*args, **kwargs)
+ self.i18n_fields = i18n_fields
+
+ def translate_values(self, d):
+ i18n_d = {}
+ if d is None:
+ return d
+
+ for key, value in d.items():
+ if isinstance(value, dict):
+ i18n_d[key] = self.translate_values(value)
+
+ if key in self.i18n_fields:
+ if isinstance(value, list):
+ i18n_d[key] = [e is not None and _(str(e)) or e for e in value]
+ if isinstance(value, str):
+ i18n_d[key] = value is not None and _(value) or value
+ else:
+ i18n_d[key] = value
+
+ return i18n_d
+
+ def to_native(self, obj):
+ i18n_obj = self.translate_values(obj)
+ return i18n_obj
diff --git a/taiga/base/neighbors.py b/taiga/base/neighbors.py
index a57d2eeb..c8733ade 100644
--- a/taiga/base/neighbors.py
+++ b/taiga/base/neighbors.py
@@ -23,6 +23,7 @@ from django.db import connection
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.sql.datastructures import EmptyResultSet
from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
Neighbor = namedtuple("Neighbor", "left right")
@@ -71,7 +72,6 @@ def get_neighbors(obj, results_set=None):
if row is None:
return Neighbor(None, None)
- obj_position = row[1] - 1
left_object_id = row[2]
right_object_id = row[3]
@@ -88,13 +88,19 @@ def get_neighbors(obj, results_set=None):
return Neighbor(left, right)
-class NeighborsSerializerMixin:
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.fields["neighbors"] = serializers.SerializerMethodField("get_neighbors")
+class NeighborSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+
+
+class NeighborsSerializerMixin(serializers.LightSerializer):
+ neighbors = MethodField()
def serialize_neighbor(self, neighbor):
- raise NotImplementedError
+ if neighbor:
+ return NeighborSerializer(neighbor).data
+ return None
def get_neighbors(self, obj):
view, request = self.context.get("view", None), self.context.get("request", None)
diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py
index d8453ad5..eaff499d 100644
--- a/taiga/export_import/api.py
+++ b/taiga/export_import/api.py
@@ -34,6 +34,7 @@ from taiga.base import exceptions as exc
from taiga.base import response
from taiga.base.api.mixins import CreateModelMixin
from taiga.base.api.viewsets import GenericViewSet
+from taiga.projects import utils as project_utils
from taiga.projects.models import Project, Membership
from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task
@@ -366,5 +367,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
return response.BadRequest({"error": e.message, "details": e.errors})
else:
# On Success
- response_data = ProjectSerializer(project).data
+ project_from_qs = project_utils.attach_extra_info(Project.objects.all()).get(id=project.id)
+ response_data = ProjectSerializer(project_from_qs).data
+
return response.Created(response_data)
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index c6fbbe0d..c441e419 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -23,9 +23,6 @@ from dateutil.relativedelta import relativedelta
from django.apps import apps
from django.conf import settings
from django.core.exceptions import ValidationError
-from django.db.models import signals, Prefetch
-from django.db.models import Value as V
-from django.db.models.functions import Coalesce
from django.http import Http404
from django.utils.translation import ugettext as _
from django.utils import timezone
@@ -45,8 +42,7 @@ from taiga.permissions import services as permissions_services
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.issues.models import Issue
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
-from taiga.projects.notifications.models import NotifyPolicy
-from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
+from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
@@ -54,21 +50,24 @@ from taiga.projects.tasks.models import Task
from taiga.projects.tagging.api import TagsColorsResourceMixin
from taiga.projects.userstories.models import UserStory, RolePoints
-from taiga.users import services as users_services
from . import filters as project_filters
from . import models
from . import permissions
from . import serializers
+from . import validators
from . import services
from . import utils as project_utils
######################################################
-## Project
+# Project
######################################################
-class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin,
+
+class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
+ BlockeableSaveMixin, BlockeableDeleteMixin,
TagsColorsResourceMixin, ModelCrudViewSet):
+ validator_class = validators.ProjectValidator
queryset = models.Project.objects.all()
permission_classes = (permissions.ProjectPermission, )
filter_backends = (project_filters.UserOrderFilterBackend,
@@ -132,12 +131,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
def get_serializer_class(self):
if self.action == "list":
- return serializers.LightProjectSerializer
+ return serializers.ProjectSerializer
- if self.action in ["retrieve", "by_slug"]:
- return serializers.LightProjectDetailSerializer
-
- return serializers.ProjectSerializer
+ return serializers.ProjectDetailSerializer
@detail_route(methods=["POST"])
def change_logo(self, request, *args, **kwargs):
@@ -200,11 +196,11 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
if self.request.user.is_anonymous():
return response.Unauthorized()
- serializer = serializers.UpdateProjectOrderBulkSerializer(data=request.DATA, many=True)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.UpdateProjectOrderBulkValidator(data=request.DATA, many=True)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
services.update_projects_order_in_bulk(data, "user_order", request.user)
return response.NoContent(data=None)
@@ -346,7 +342,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
return response.BadRequest(_("The user must be already a project member"))
reason = request.DATA.get('reason', None)
- transfer_token = services.start_project_transfer(project, user, reason)
+ services.start_project_transfer(project, user, reason)
return response.Ok()
@detail_route(methods=["POST"])
@@ -455,6 +451,7 @@ class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.Points
serializer_class = serializers.PointsSerializer
+ validator_class = validators.PointsValidator
permission_classes = (permissions.PointsPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',)
@@ -471,6 +468,7 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.UserStoryStatus
serializer_class = serializers.UserStoryStatusSerializer
+ validator_class = validators.UserStoryStatusValidator
permission_classes = (permissions.UserStoryStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',)
@@ -487,6 +485,7 @@ class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.TaskStatus
serializer_class = serializers.TaskStatusSerializer
+ validator_class = validators.TaskStatusValidator
permission_classes = (permissions.TaskStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -503,6 +502,7 @@ class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.Severity
serializer_class = serializers.SeveritySerializer
+ validator_class = validators.SeverityValidator
permission_classes = (permissions.SeverityPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -518,6 +518,7 @@ class PriorityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.Priority
serializer_class = serializers.PrioritySerializer
+ validator_class = validators.PriorityValidator
permission_classes = (permissions.PriorityPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -533,6 +534,7 @@ class IssueTypeViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.IssueType
serializer_class = serializers.IssueTypeSerializer
+ validator_class = validators.IssueTypeValidator
permission_classes = (permissions.IssueTypePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -548,6 +550,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.IssueStatus
serializer_class = serializers.IssueStatusSerializer
+ validator_class = validators.IssueStatusValidator
permission_classes = (permissions.IssueStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -566,6 +569,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
class ProjectTemplateViewSet(ModelCrudViewSet):
model = models.ProjectTemplate
serializer_class = serializers.ProjectTemplateSerializer
+ validator_class = validators.ProjectTemplateValidator
permission_classes = (permissions.ProjectTemplatePermission,)
def get_queryset(self):
@@ -579,7 +583,9 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Membership
admin_serializer_class = serializers.MembershipAdminSerializer
+ admin_validator_class = validators.MembershipAdminValidator
serializer_class = serializers.MembershipSerializer
+ validator_class = validators.MembershipValidator
permission_classes = (permissions.MembershipPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project", "role")
@@ -604,6 +610,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
else:
return self.serializer_class
+ def get_validator_class(self):
+ if self.action == "create":
+ return self.admin_validator_class
+
+ return self.validator_class
+
def _check_if_project_can_have_more_memberships(self, project, total_new_memberships):
(can_add_memberships, error_type) = services.check_if_project_can_have_more_memberships(
project,
@@ -618,11 +630,11 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
@list_route(methods=["POST"])
def bulk_create(self, request, **kwargs):
- serializer = serializers.MembersBulkSerializer(data=request.DATA)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.MembersBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
project = models.Project.objects.get(id=data["project_id"])
invitation_extra_text = data.get("invitation_extra_text", None)
self.check_permissions(request, 'bulk_create', project)
diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py
index 6c5ee05b..45e6be45 100644
--- a/taiga/projects/attachments/serializers.py
+++ b/taiga/projects/attachments/serializers.py
@@ -19,14 +19,12 @@
from django.conf import settings
from taiga.base.api import serializers
+from taiga.base.fields import MethodField
from taiga.base.utils.thumbnails import get_thumbnail_url
from . import services
from . import models
-import json
-import serpy
-
class AttachmentSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField("get_url")
@@ -43,12 +41,11 @@ class AttachmentSerializer(serializers.ModelSerializer):
def get_url(self, obj):
return obj.attached_file.url
-
def get_thumbnail_card_url(self, obj):
return services.get_card_image_thumbnail_url(obj)
-class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer):
+class BasicAttachmentsInfoSerializerMixin(serializers.LightSerializer):
"""
Assumptions:
- The queryset has an attribute called "include_attachments" indicating if the attachments array should contain information
@@ -56,7 +53,7 @@ class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer):
- The method attach_basic_attachments has been used to include the necessary
json data about the attachments in the "attachments_attr" column
"""
- attachments = serpy.MethodField()
+ attachments = MethodField()
def get_attachments(self, obj):
include_attachments = getattr(obj, "include_attachments", False)
diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py
index cbb692b8..fe720f97 100644
--- a/taiga/projects/filters.py
+++ b/taiga/projects/filters.py
@@ -97,12 +97,12 @@ class QFilterBackend(FilterBackend):
tsquery = "to_tsquery('english_nostop', %s)"
tsquery_params = [to_tsquery(q)]
tsvector = """
- setweight(to_tsvector('english_nostop',
- coalesce(projects_project.name, '')), 'A') ||
- setweight(to_tsvector('english_nostop',
- coalesce(inmutable_array_to_string(projects_project.tags), '')), 'B') ||
- setweight(to_tsvector('english_nostop',
- coalesce(projects_project.description, '')), 'C')
+ setweight(to_tsvector('english_nostop',
+ coalesce(projects_project.name, '')), 'A') ||
+ setweight(to_tsvector('english_nostop',
+ coalesce(inmutable_array_to_string(projects_project.tags), '')), 'B') ||
+ setweight(to_tsvector('english_nostop',
+ coalesce(projects_project.description, '')), 'C')
"""
select = {
@@ -111,7 +111,7 @@ class QFilterBackend(FilterBackend):
}
select_params = tsquery_params
where = ["{tsvector} @@ {tsquery}".format(tsquery=tsquery,
- tsvector=tsvector),]
+ tsvector=tsvector), ]
params = tsquery_params
order_by = ["-rank", ]
@@ -142,11 +142,11 @@ class UserOrderFilterBackend(FilterBackend):
model = queryset.model
sql = """SELECT projects_membership.user_order
- FROM projects_membership
- WHERE
- projects_membership.project_id = {tbl}.id AND
- projects_membership.user_id = {user_id}
- """
+ FROM projects_membership
+ WHERE
+ projects_membership.project_id = {tbl}.id AND
+ projects_membership.user_id = {user_id}
+ """
sql = sql.format(tbl=model._meta.db_table, user_id=request.user.id)
queryset = queryset.extra(select={"user_order": sql})
diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py
index a4c8199e..2119239a 100644
--- a/taiga/projects/history/api.py
+++ b/taiga/projects/history/api.py
@@ -23,7 +23,6 @@ from django.utils import timezone
from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import ReadOnlyListViewSet
-from taiga.base.api.utils import get_object_or_404
from taiga.mdrender.service import render as mdrender
from . import permissions
@@ -38,7 +37,7 @@ class HistoryViewSet(ReadOnlyListViewSet):
def get_content_type(self):
app_name, model = self.content_type.split(".", 1)
- return get_object_or_404(ContentType, app_label=app_name, model=model)
+ return ContentType.objects.get_by_natural_key(app_name, model)
def get_queryset(self):
ct = self.get_content_type()
diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py
index 558c5c25..d5c023a9 100644
--- a/taiga/projects/history/models.py
+++ b/taiga/projects/history/models.py
@@ -33,7 +33,8 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts
# This keys has been removed from freeze_impl so we can have objects where the
# previous diff has value for the attribute and we want to prevent their propagation
-IGNORE_DIFF_FIELDS = [ "watchers", "description_diff", "content_diff", "blocked_note_diff"]
+IGNORE_DIFF_FIELDS = ["watchers", "description_diff", "content_diff", "blocked_note_diff"]
+
def _generate_uuid():
return str(uuid.uuid1())
@@ -92,15 +93,15 @@ class HistoryEntry(models.Model):
@cached_property
def is_change(self):
- return self.type == HistoryType.change
+ return self.type == HistoryType.change
@cached_property
def is_create(self):
- return self.type == HistoryType.create
+ return self.type == HistoryType.create
@cached_property
def is_delete(self):
- return self.type == HistoryType.delete
+ return self.type == HistoryType.delete
@property
def owner(self):
@@ -185,7 +186,7 @@ class HistoryEntry(models.Model):
role_name = resolve_value("roles", role_id)
oldpoint_id = pointsold.get(role_id, None)
points[role_name] = [resolve_value("points", oldpoint_id),
- resolve_value("points", point_id)]
+ resolve_value("points", point_id)]
# Process that removes points entries with
# duplicate value.
@@ -204,8 +205,8 @@ class HistoryEntry(models.Model):
"deleted": [],
}
- oldattachs = {x["id"]:x for x in self.diff["attachments"][0]}
- newattachs = {x["id"]:x for x in self.diff["attachments"][1]}
+ oldattachs = {x["id"]: x for x in self.diff["attachments"][0]}
+ newattachs = {x["id"]: x for x in self.diff["attachments"][1]}
for aid in set(tuple(oldattachs.keys()) + tuple(newattachs.keys())):
if aid in oldattachs and aid in newattachs:
@@ -235,8 +236,8 @@ class HistoryEntry(models.Model):
"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 []}
+ 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:
diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py
index f1a10481..8407810f 100644
--- a/taiga/projects/history/serializers.py
+++ b/taiga/projects/history/serializers.py
@@ -17,28 +17,31 @@
# along with this program. If not, see .
from taiga.base.api import serializers
-from taiga.base.fields import JsonField, I18NJsonField
+from taiga.base.fields import I18NJsonField, Field, MethodField
from taiga.users.services import get_photo_or_gravatar_url
-from . import models
+
+HISTORY_ENTRY_I18N_FIELDS = ("points", "status", "severity", "priority", "type")
-HISTORY_ENTRY_I18N_FIELDS=("points", "status", "severity", "priority", "type")
-
-
-class HistoryEntrySerializer(serializers.ModelSerializer):
- diff = JsonField()
- snapshot = JsonField()
- values = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
- values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
- user = serializers.SerializerMethodField("get_user")
- delete_comment_user = JsonField()
- comment_versions = JsonField()
-
- class Meta:
- model = models.HistoryEntry
- exclude = ("comment_versions",)
+class HistoryEntrySerializer(serializers.LightSerializer):
+ id = Field()
+ user = MethodField()
+ created_at = Field()
+ type = Field()
+ key = Field()
+ diff = Field()
+ snapshot = Field()
+ values = Field()
+ values_diff = I18NJsonField()
+ comment = I18NJsonField()
+ comment_html = Field()
+ delete_comment_date = Field()
+ delete_comment_user = Field()
+ edit_comment_date = Field()
+ is_hidden = Field()
+ is_snapshot = Field()
def get_user(self, entry):
user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False}
diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py
index 71b5bcf8..764cca39 100644
--- a/taiga/projects/history/services.py
+++ b/taiga/projects/history/services.py
@@ -34,12 +34,9 @@ from collections import namedtuple
from copy import deepcopy
from functools import partial
from functools import wraps
-from functools import lru_cache
from django.conf import settings
from django.contrib.auth import get_user_model
-from django.contrib.contenttypes.models import ContentType
-from django.core.paginator import Paginator, InvalidPage
from django.apps import apps
from django.db import transaction as tx
from django_pglocks import advisory_lock
@@ -50,6 +47,21 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts
from .models import HistoryType
+# Freeze implementatitions
+from .freeze_impl import project_freezer
+from .freeze_impl import milestone_freezer
+from .freeze_impl import userstory_freezer
+from .freeze_impl import issue_freezer
+from .freeze_impl import task_freezer
+from .freeze_impl import wikipage_freezer
+
+
+from .freeze_impl import project_values
+from .freeze_impl import milestone_values
+from .freeze_impl import userstory_values
+from .freeze_impl import issue_values
+from .freeze_impl import task_values
+from .freeze_impl import wikipage_values
# Type that represents a freezed object
FrozenObj = namedtuple("FrozenObj", ["key", "snapshot"])
@@ -71,7 +83,7 @@ _not_important_fields = {
log = logging.getLogger("taiga.history")
-def make_key_from_model_object(obj:object) -> str:
+def make_key_from_model_object(obj: object) -> str:
"""
Create unique key from model instance.
"""
@@ -79,7 +91,7 @@ def make_key_from_model_object(obj:object) -> str:
return "{0}:{1}".format(tn, obj.pk)
-def get_model_from_key(key:str) -> object:
+def get_model_from_key(key: str) -> object:
"""
Get model from key
"""
@@ -87,7 +99,7 @@ def get_model_from_key(key:str) -> object:
return apps.get_model(class_name)
-def get_pk_from_key(key:str) -> object:
+def get_pk_from_key(key: str) -> object:
"""
Get pk from key
"""
@@ -95,7 +107,7 @@ def get_pk_from_key(key:str) -> object:
return pk
-def get_instance_from_key(key:str) -> object:
+def get_instance_from_key(key: str) -> object:
"""
Get instance from key
"""
@@ -109,7 +121,7 @@ def get_instance_from_key(key:str) -> object:
return None
-def register_values_implementation(typename:str, fn=None):
+def register_values_implementation(typename: str, fn=None):
"""
Register values implementation for specified typename.
This function can be used as decorator.
@@ -128,7 +140,7 @@ def register_values_implementation(typename:str, fn=None):
return _wrapper
-def register_freeze_implementation(typename:str, fn=None):
+def register_freeze_implementation(typename: str, fn=None):
"""
Register freeze implementation for specified typename.
This function can be used as decorator.
@@ -149,7 +161,7 @@ def register_freeze_implementation(typename:str, fn=None):
# Low level api
-def freeze_model_instance(obj:object) -> FrozenObj:
+def freeze_model_instance(obj: object) -> FrozenObj:
"""
Creates a new frozen object from model instance.
@@ -179,7 +191,7 @@ def freeze_model_instance(obj:object) -> FrozenObj:
return FrozenObj(key, snapshot)
-def is_hidden_snapshot(obj:FrozenDiff) -> bool:
+def is_hidden_snapshot(obj: FrozenDiff) -> bool:
"""
Check if frozen object is considered
hidden or not.
@@ -199,7 +211,7 @@ def is_hidden_snapshot(obj:FrozenDiff) -> bool:
return False
-def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff:
+def make_diff(oldobj: FrozenObj, newobj: FrozenObj) -> FrozenDiff:
"""
Compute a diff between two frozen objects.
"""
@@ -217,7 +229,7 @@ def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff:
return FrozenDiff(newobj.key, diff, newobj.snapshot)
-def make_diff_values(typename:str, fdiff:FrozenDiff) -> dict:
+def make_diff_values(typename: str, fdiff: FrozenDiff) -> dict:
"""
Given a typename and diff, build a values dict for it.
If no implementation found for typename, warnig is raised in
@@ -242,7 +254,7 @@ def _rebuild_snapshot_from_diffs(keysnapshot, partials):
return result
-def get_last_snapshot_for_key(key:str) -> FrozenObj:
+def get_last_snapshot_for_key(key: str) -> FrozenObj:
entry_model = apps.get_model("history", "HistoryEntry")
# Search last snapshot
@@ -271,17 +283,16 @@ def get_last_snapshot_for_key(key:str) -> FrozenObj:
# Public api
-def get_modified_fields(obj:object, last_modifications):
+def get_modified_fields(obj: object, last_modifications):
"""
Get the modified fields for an object through his last modifications
"""
key = make_key_from_model_object(obj)
entry_model = apps.get_model("history", "HistoryEntry")
history_entries = (entry_model.objects
- .filter(key=key)
- .order_by("-created_at")
- .values_list("diff", flat=True)
- [0:last_modifications])
+ .filter(key=key)
+ .order_by("-created_at")
+ .values_list("diff", flat=True)[0:last_modifications])
modified_fields = []
for history_entry in history_entries:
@@ -291,7 +302,7 @@ def get_modified_fields(obj:object, last_modifications):
@tx.atomic
-def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
+def take_snapshot(obj: object, *, comment: str="", user=None, delete: bool=False):
"""
Given any model instance with registred content type,
create new history entry of "change" type.
@@ -301,7 +312,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
"""
key = make_key_from_model_object(obj)
- with advisory_lock(key) as acquired_key_lock:
+ with advisory_lock(key):
typename = get_typename_for_model_class(obj.__class__)
new_fobj = freeze_model_instance(obj)
@@ -327,8 +338,8 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
# If diff and comment are empty, do
# not create empty history entry
if (not fdiff.diff and not comment
- and old_fobj is not None
- and entry_type != HistoryType.delete):
+ and old_fobj is not None
+ and entry_type != HistoryType.delete):
return None
@@ -358,7 +369,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
# High level query api
-def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change,),
+def get_history_queryset_by_model_instance(obj: object, types=(HistoryType.change,),
include_hidden=False):
"""
Get one page of history for specified object.
@@ -377,20 +388,12 @@ def prefetch_owners_in_history_queryset(qs):
user_ids = [u["pk"] for u in qs.values_list("user", flat=True)]
users = get_user_model().objects.filter(id__in=user_ids)
users_by_id = {u.id: u for u in users}
- for history_entry in qs:
+ for history_entry in qs:
history_entry.prefetch_owner(users_by_id.get(history_entry.user["pk"], None))
return qs
-# Freeze implementatitions
-from .freeze_impl import project_freezer
-from .freeze_impl import milestone_freezer
-from .freeze_impl import userstory_freezer
-from .freeze_impl import issue_freezer
-from .freeze_impl import task_freezer
-from .freeze_impl import wikipage_freezer
-
register_freeze_implementation("projects.project", project_freezer)
register_freeze_implementation("milestones.milestone", milestone_freezer,)
register_freeze_implementation("userstories.userstory", userstory_freezer)
@@ -398,13 +401,6 @@ register_freeze_implementation("issues.issue", issue_freezer)
register_freeze_implementation("tasks.task", task_freezer)
register_freeze_implementation("wiki.wikipage", wikipage_freezer)
-from .freeze_impl import project_values
-from .freeze_impl import milestone_values
-from .freeze_impl import userstory_values
-from .freeze_impl import issue_values
-from .freeze_impl import task_values
-from .freeze_impl import wikipage_values
-
register_values_implementation("projects.project", project_values)
register_values_implementation("milestones.milestone", milestone_values)
register_values_implementation("userstories.userstory", userstory_values)
diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py
index 8da13476..f204fc13 100644
--- a/taiga/projects/issues/api.py
+++ b/taiga/projects/issues/api.py
@@ -34,14 +34,18 @@ from taiga.projects.occ import OCCResourceMixin
from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
+from .utils import attach_extra_info
+
from . import models
from . import services
from . import permissions
from . import serializers
+from . import validators
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
+ validator_class = validators.IssueValidator
queryset = models.Issue.objects.all()
permission_classes = (permissions.IssuePermission, )
filter_backends = (filters.CanViewIssuesFilterBackend,
@@ -145,8 +149,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
def get_queryset(self):
qs = super().get_queryset()
qs = qs.select_related("owner", "assigned_to", "status", "project")
- qs = self.attach_votes_attrs_to_queryset(qs)
- return self.attach_watchers_attrs_to_queryset(qs)
+ qs = attach_extra_info(qs, user=self.request.user)
+ return qs
def pre_save(self, obj):
if not obj.id:
@@ -181,8 +185,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
def by_ref(self, request):
ref = request.QUERY_PARAMS.get("ref", None)
project_id = request.QUERY_PARAMS.get("project", None)
- issue = get_object_or_404(models.Issue, ref=ref, project_id=project_id)
- return self.retrieve(request, pk=issue.pk)
+ return self.retrieve(request, project_id=project_id, ref=ref)
@list_route(methods=["GET"])
def filters_data(self, request, *args, **kwargs):
@@ -224,9 +227,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
@list_route(methods=["POST"])
def bulk_create(self, request, **kwargs):
- serializer = serializers.IssuesBulkSerializer(data=request.DATA)
- if serializer.is_valid():
- data = serializer.data
+ validator = validators.IssuesBulkValidator(data=request.DATA)
+ if validator.is_valid():
+ data = validator.data
project = Project.objects.get(pk=data["project_id"])
self.check_permissions(request, 'bulk_create', project)
if project.blocked_code is not None:
@@ -237,11 +240,13 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
status=project.default_issue_status, severity=project.default_severity,
priority=project.default_priority, type=project.default_issue_type,
callback=self.post_save, precall=self.pre_save)
+
+ issues = self.get_queryset().filter(id__in=[i.id for i in issues])
issues_serialized = self.get_serializer_class()(issues, many=True)
return response.Ok(data=issues_serialized.data)
- return response.BadRequest(serializer.errors)
+ return response.BadRequest(validator.errors)
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py
index 099171a1..2b773b81 100644
--- a/taiga/projects/issues/serializers.py
+++ b/taiga/projects/issues/serializers.py
@@ -17,56 +17,52 @@
# along with this program. If not, see .
from taiga.base.api import serializers
-from taiga.base.fields import PgArrayField
+from taiga.base.fields import Field, MethodField
from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender
-from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
-from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
-from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
-from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
-from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
-from taiga.projects.notifications.validators import WatchersValidator
-from taiga.projects.tagging.fields import TagsAndTagsColorsField
-from taiga.projects.serializers import BasicIssueStatusSerializer
-from taiga.projects.validators import ProjectExistsValidator
+from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
-from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
-
-from taiga.users.serializers import UserBasicInfoSerializer
-
-from . import models
-
-import serpy
-class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
- tags = TagsAndTagsColorsField(default=[], required=False)
- external_reference = PgArrayField(required=False)
- is_closed = serializers.Field(source="is_closed")
- comment = serializers.SerializerMethodField("get_comment")
- generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories")
- blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
- description_html = serializers.SerializerMethodField("get_description_html")
- status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True)
- assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
- owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
+class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
+ OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
+ StatusExtraInfoSerializerMixin, serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ severity = Field(attr="severity_id")
+ priority = Field(attr="priority_id")
+ type = Field(attr="type_id")
+ milestone = Field(attr="milestone_id")
+ project = Field(attr="project_id")
+ created_date = Field()
+ modified_date = Field()
+ finished_date = Field()
+ subject = Field()
+ external_reference = Field()
+ version = Field()
+ watchers = Field()
+ tags = Field()
+ is_closed = Field()
- class Meta:
- model = models.Issue
- read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
+
+class IssueSerializer(IssueListSerializer):
+ comment = MethodField()
+ generated_user_stories = MethodField()
+ blocked_note_html = MethodField()
+ description = Field()
+ description_html = MethodField()
def get_comment(self, obj):
# NOTE: This method and field is necessary to historical comments work
return ""
def get_generated_user_stories(self, obj):
- return [{
- "id": us.id,
- "ref": us.ref,
- "subject": us.subject,
- } for us in obj.generated_user_stories.all()]
+ assert hasattr(obj, "generated_user_stories_attr"), "instance must have a generated_user_stories_attr attribute"
+ return obj.generated_user_stories_attr
def get_blocked_note_html(self, obj):
return mdrender(obj.project, obj.blocked_note)
@@ -75,39 +71,5 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa
return mdrender(obj.project, obj.description)
-class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
- ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
- ListStatusExtraInfoSerializerMixin, serializers.LightSerializer):
- id = serpy.Field()
- ref = serpy.Field()
- severity = serpy.Field(attr="severity_id")
- priority = serpy.Field(attr="priority_id")
- type = serpy.Field(attr="type_id")
- milestone = serpy.Field(attr="milestone_id")
- project = serpy.Field(attr="project_id")
- created_date = serpy.Field()
- modified_date = serpy.Field()
- finished_date = serpy.Field()
- subject = serpy.Field()
- external_reference = serpy.Field()
- version = serpy.Field()
- watchers = serpy.Field()
-
-
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
- def serialize_neighbor(self, neighbor):
- if neighbor:
- return NeighborIssueSerializer(neighbor).data
- return None
-
-
-class NeighborIssueSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.Issue
- fields = ("id", "ref", "subject")
- depth = 0
-
-
-class IssuesBulkSerializer(ProjectExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- bulk_issues = serializers.CharField()
+ pass
diff --git a/taiga/projects/issues/utils.py b/taiga/projects/issues/utils.py
new file mode 100644
index 00000000..2053d923
--- /dev/null
+++ b/taiga/projects/issues/utils.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.votes.utils import attach_is_voter_to_queryset
+
+
+def attach_generated_user_stories(queryset, as_field="generated_user_stories_attr"):
+ """Attach generated user stories json column to each object of the queryset.
+
+ :param queryset: A Django issues queryset object.
+ :param as_field: Attach the generated user stories as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT json_agg(row_to_json(t))
+ FROM(
+ SELECT
+ userstories_userstory.id,
+ userstories_userstory.ref,
+ userstories_userstory.subject
+ FROM userstories_userstory
+ WHERE generated_from_issue_id = {tbl}.id) t"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_extra_info(queryset, user=None):
+ queryset = attach_generated_user_stories(queryset)
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_voter_to_queryset(queryset, user)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ return queryset
diff --git a/taiga/projects/issues/validators.py b/taiga/projects/issues/validators.py
new file mode 100644
index 00000000..4c900c15
--- /dev/null
+++ b/taiga/projects/issues/validators.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# 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 .
+
+from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.fields import PgArrayField
+from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
+from taiga.projects.notifications.validators import WatchersValidator
+from taiga.projects.tagging.fields import TagsAndTagsColorsField
+from taiga.projects.validators import ProjectExistsValidator
+
+from . import models
+
+
+class IssueValidator(WatchersValidator, EditableWatchedResourceSerializer,
+ validators.ModelValidator):
+
+ tags = TagsAndTagsColorsField(default=[], required=False)
+ external_reference = PgArrayField(required=False)
+
+ class Meta:
+ model = models.Issue
+ read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
+
+
+class IssuesBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ bulk_issues = serializers.CharField()
diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py
index 1520f2c7..2e0047fc 100644
--- a/taiga/projects/milestones/api.py
+++ b/taiga/projects/milestones/api.py
@@ -17,7 +17,6 @@
# along with this program. If not, see .
from django.apps import apps
-from django.db.models import Prefetch
from taiga.base import filters
from taiga.base import response
@@ -31,13 +30,9 @@ from taiga.base.utils.db import get_object_or_none
from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin
-from taiga.projects.votes.utils import attach_total_voters_to_queryset
-from taiga.projects.votes.utils import attach_is_voter_to_queryset
-from taiga.projects.notifications.utils import attach_watchers_to_queryset
-from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
-from taiga.projects.userstories import utils as userstories_utils
from . import serializers
+from . import validators
from . import models
from . import permissions
from . import utils as milestones_utils
@@ -47,6 +42,8 @@ import datetime
class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
BlockedByProjectMixin, ModelCrudViewSet):
+ serializer_class = serializers.MilestoneSerializer
+ validator_class = validators.MilestoneValidator
permission_classes = (permissions.MilestonePermission,)
filter_backends = (filters.CanViewMilestonesFilterBackend,)
filter_fields = (
@@ -56,12 +53,6 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
)
queryset = models.Milestone.objects.all()
- def get_serializer_class(self, *args, **kwargs):
- if self.action == "list":
- return serializers.MilestoneListSerializer
-
- return serializers.MilestoneSerializer
-
def list(self, request, *args, **kwargs):
res = super().list(request, *args, **kwargs)
self._add_taiga_info_headers()
@@ -84,33 +75,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
def get_queryset(self):
qs = super().get_queryset()
-
- # Userstories prefetching
- UserStory = apps.get_model("userstories", "UserStory")
-
- us_qs = UserStory.objects.select_related("milestone",
- "project",
- "status",
- "owner",
- "assigned_to",
- "generated_from_issue")
-
- us_qs = userstories_utils.attach_total_points(us_qs)
- us_qs = userstories_utils.attach_role_points(us_qs)
- us_qs = attach_total_voters_to_queryset(us_qs)
- us_qs = self.attach_watchers_attrs_to_queryset(us_qs)
-
- if self.request.user.is_authenticated():
- us_qs = attach_is_voter_to_queryset(self.request.user, us_qs)
- us_qs = attach_is_watcher_to_queryset(us_qs, self.request.user)
-
- qs = qs.prefetch_related(Prefetch("user_stories", queryset=us_qs))
-
- # Milestones prefetching
qs = qs.select_related("project", "owner")
- qs = self.attach_watchers_attrs_to_queryset(qs)
- qs = milestones_utils.attach_total_points(qs)
- qs = milestones_utils.attach_closed_points(qs)
+ qs = milestones_utils.attach_extra_info(qs, user=self.request.user)
qs = qs.order_by("-estimated_start")
return qs
diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py
index 724126fd..44b3e8f4 100644
--- a/taiga/projects/milestones/serializers.py
+++ b/taiga/projects/milestones/serializers.py
@@ -16,58 +16,29 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.utils.translation import ugettext as _
-
from taiga.base.api import serializers
-from taiga.base.utils import json
-from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
-from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
-from taiga.projects.notifications.validators import WatchersValidator
-from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
+from taiga.base.fields import Field, MethodField
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.projects.userstories.serializers import UserStoryListSerializer
-from . import models
-import serpy
-
-
-class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer,
- ValidateDuplicatedNameInProjectMixin):
- total_points = serializers.SerializerMethodField("get_total_points")
- closed_points = serializers.SerializerMethodField("get_closed_points")
- user_stories = serializers.SerializerMethodField("get_user_stories")
-
- class Meta:
- model = models.Milestone
- read_only_fields = ("id", "created_date", "modified_date")
-
- def get_total_points(self, obj):
- return sum(obj.total_points.values())
-
- def get_closed_points(self, obj):
- return sum(obj.closed_points.values())
-
- def get_user_stories(self, obj):
- return UserStoryListSerializer(obj.user_stories.all(), many=True).data
-
-
-class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.LightSerializer):
- id = serpy.Field()
- name = serpy.Field()
- slug = serpy.Field()
- owner = serpy.Field(attr="owner_id")
- project = serpy.Field(attr="project_id")
- estimated_start = serpy.Field()
- estimated_finish = serpy.Field()
- created_date = serpy.Field()
- modified_date = serpy.Field()
- closed = serpy.Field()
- disponibility = serpy.Field()
- order = serpy.Field()
- watchers = serpy.Field()
- user_stories = serpy.MethodField("get_user_stories")
- total_points = serpy.MethodField()
- closed_points = serpy.MethodField()
+class MilestoneSerializer(WatchedResourceSerializer, serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ slug = Field()
+ owner = Field(attr="owner_id")
+ project = Field(attr="project_id")
+ estimated_start = Field()
+ estimated_finish = Field()
+ created_date = Field()
+ modified_date = Field()
+ closed = Field()
+ disponibility = Field()
+ order = Field()
+ watchers = Field()
+ user_stories = MethodField()
+ total_points = MethodField()
+ closed_points = MethodField()
def get_user_stories(self, obj):
return UserStoryListSerializer(obj.user_stories.all(), many=True).data
diff --git a/taiga/projects/milestones/utils.py b/taiga/projects/milestones/utils.py
index a32d7684..b292b1bd 100644
--- a/taiga/projects/milestones/utils.py
+++ b/taiga/projects/milestones/utils.py
@@ -17,6 +17,16 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.apps import apps
+from django.db.models import Prefetch
+
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+from taiga.projects.userstories import utils as userstories_utils
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.votes.utils import attach_is_voter_to_queryset
+
def attach_total_points(queryset, as_field="total_points_attr"):
"""Attach total of point values to each object of the queryset.
@@ -28,7 +38,7 @@ def attach_total_points(queryset, as_field="total_points_attr"):
"""
model = queryset.model
sql = """SELECT SUM(projects_points.value)
- FROM userstories_rolepoints
+ FROM userstories_rolepoints
INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id
INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
WHERE userstories_userstory.milestone_id = {tbl}.id"""
@@ -48,7 +58,7 @@ def attach_closed_points(queryset, as_field="closed_points_attr"):
"""
model = queryset.model
sql = """SELECT SUM(projects_points.value)
- FROM userstories_rolepoints
+ FROM userstories_rolepoints
INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id
INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
WHERE userstories_userstory.milestone_id = {tbl}.id AND userstories_userstory.is_closed=True"""
@@ -56,3 +66,33 @@ def attach_closed_points(queryset, as_field="closed_points_attr"):
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
+
+
+def attach_extra_info(queryset, user=None):
+ # Userstories prefetching
+ UserStory = apps.get_model("userstories", "UserStory")
+ us_queryset = UserStory.objects.select_related("milestone",
+ "project",
+ "status",
+ "owner",
+ "assigned_to",
+ "generated_from_issue")
+
+ us_queryset = userstories_utils.attach_total_points(us_queryset)
+ us_queryset = userstories_utils.attach_role_points(us_queryset)
+ us_queryset = attach_total_voters_to_queryset(us_queryset)
+ us_queryset = attach_watchers_to_queryset(us_queryset)
+ us_queryset = attach_total_watchers_to_queryset(us_queryset)
+ us_queryset = attach_is_voter_to_queryset(us_queryset, user)
+ us_queryset = attach_is_watcher_to_queryset(us_queryset, user)
+
+ queryset = queryset.prefetch_related(Prefetch("user_stories", queryset=us_queryset))
+ queryset = attach_total_points(queryset)
+ queryset = attach_closed_points(queryset)
+
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_voter_to_queryset(queryset, user)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ return queryset
diff --git a/taiga/projects/milestones/validators.py b/taiga/projects/milestones/validators.py
index 3648a672..8de3174c 100644
--- a/taiga/projects/milestones/validators.py
+++ b/taiga/projects/milestones/validators.py
@@ -19,14 +19,23 @@
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.projects.validators import DuplicatedNameInProjectValidator
+from taiga.projects.notifications.validators import WatchersValidator
from . import models
-class SprintExistsValidator:
+class MilestoneExistsValidator:
def validate_sprint_id(self, attrs, source):
value = attrs[source]
if not models.Milestone.objects.filter(pk=value).exists():
- msg = _("There's no sprint with that id")
+ msg = _("There's no milestone with that id")
raise serializers.ValidationError(msg)
return attrs
+
+
+class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.Milestone
+ read_only_fields = ("id", "created_date", "modified_date")
diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py
index a47d9bed..945c1119 100644
--- a/taiga/projects/mixins/serializers.py
+++ b/taiga/projects/mixins/serializers.py
@@ -17,34 +17,13 @@
# along with this program. If not, see .
from taiga.base.api import serializers
-from taiga.users.serializers import ListUserBasicInfoSerializer
+from taiga.base.fields import Field, MethodField
+from taiga.users.serializers import UserBasicInfoSerializer
from django.utils.translation import ugettext as _
-import serpy
-class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
-
- def validate_name(self, attrs, source):
- """
- Check the points name is not duplicated in the project on creation
- """
- model = self.opts.model
- qs = None
- # If the object exists:
- if self.object and attrs.get(source, None):
- qs = model.objects.filter(project=self.object.project, name=attrs[source]).exclude(id=self.object.id)
-
- if not self.object and attrs.get("project", None) and attrs.get(source, None):
- qs = model.objects.filter(project=attrs["project"], name=attrs[source])
-
- if qs and qs.exists():
- raise serializers.ValidationError(_("Name duplicated for the project"))
-
- return attrs
-
-
-class ListCachedUsersSerializerMixin(serpy.Serializer):
+class CachedUsersSerializerMixin(serializers.LightSerializer):
def to_value(self, instance):
self._serialized_users = {}
return super().to_value(instance)
@@ -55,37 +34,40 @@ class ListCachedUsersSerializerMixin(serpy.Serializer):
serialized_user = self._serialized_users.get(user.id, None)
if serialized_user is None:
- serializer_user = ListUserBasicInfoSerializer(user).data
- self._serialized_users[user.id] = serializer_user
+ serialized_user = UserBasicInfoSerializer(user).data
+ self._serialized_users[user.id] = serialized_user
return serialized_user
-class ListOwnerExtraInfoSerializerMixin(ListCachedUsersSerializerMixin):
- owner = serpy.Field(attr="owner_id")
- owner_extra_info = serpy.MethodField()
+class OwnerExtraInfoSerializerMixin(CachedUsersSerializerMixin):
+ owner = Field(attr="owner_id")
+ owner_extra_info = MethodField()
def get_owner_extra_info(self, obj):
return self.get_user_extra_info(obj.owner)
-class ListAssignedToExtraInfoSerializerMixin(ListCachedUsersSerializerMixin):
- assigned_to = serpy.Field(attr="assigned_to_id")
- assigned_to_extra_info = serpy.MethodField()
+class AssignedToExtraInfoSerializerMixin(CachedUsersSerializerMixin):
+ assigned_to = Field(attr="assigned_to_id")
+ assigned_to_extra_info = MethodField()
def get_assigned_to_extra_info(self, obj):
return self.get_user_extra_info(obj.assigned_to)
-class ListStatusExtraInfoSerializerMixin(serpy.Serializer):
- status = serpy.Field(attr="status_id")
- status_extra_info = serpy.MethodField()
+class StatusExtraInfoSerializerMixin(serializers.LightSerializer):
+ status = Field(attr="status_id")
+ status_extra_info = MethodField()
def to_value(self, instance):
self._serialized_status = {}
return super().to_value(instance)
def get_status_extra_info(self, obj):
+ if obj.status_id is None:
+ return None
+
serialized_status = self._serialized_status.get(obj.status_id, None)
if serialized_status is None:
serialized_status = {
diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py
index 2cad1e97..e9dff950 100644
--- a/taiga/projects/notifications/mixins.py
+++ b/taiga/projects/notifications/mixins.py
@@ -16,8 +16,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import serpy
-
from functools import partial
from operator import is_not
@@ -28,16 +26,12 @@ from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import serializers
from taiga.base.api.utils import get_object_or_404
-from taiga.base.fields import WatchersField
+from taiga.base.fields import WatchersField, MethodField
from taiga.projects.notifications import services
-from taiga.projects.notifications.utils import (attach_watchers_to_queryset,
- attach_is_watcher_to_queryset,
- attach_total_watchers_to_queryset)
from . serializers import WatcherSerializer
-
class WatchedResourceMixin:
"""
Rest Framework resource mixin for resources susceptible
@@ -54,14 +48,6 @@ class WatchedResourceMixin:
_not_notify = False
- def attach_watchers_attrs_to_queryset(self, queryset):
- queryset = attach_watchers_to_queryset(queryset)
- queryset = attach_total_watchers_to_queryset(queryset)
- if self.request.user.is_authenticated():
- queryset = attach_is_watcher_to_queryset(queryset, self.request.user)
-
- return queryset
-
@detail_route(methods=["POST"])
def watch(self, request, pk=None):
obj = self.get_object()
@@ -186,7 +172,10 @@ class WatchedModelMixin(object):
return frozenset(filter(is_not_none, participants))
-class BaseWatchedResourceModelSerializer(object):
+class WatchedResourceSerializer(serializers.LightSerializer):
+ is_watcher = MethodField()
+ total_watchers = MethodField()
+
def get_is_watcher(self, obj):
# The "is_watcher" attribute is attached in the get_queryset of the viewset.
if "request" in self.context:
@@ -200,28 +189,18 @@ class BaseWatchedResourceModelSerializer(object):
return getattr(obj, "total_watchers", 0) or 0
-class WatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serializers.ModelSerializer):
- is_watcher = serializers.SerializerMethodField("get_is_watcher")
- total_watchers = serializers.SerializerMethodField("get_total_watchers")
-
-
-class ListWatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serpy.Serializer):
- is_watcher = serpy.MethodField("get_is_watcher")
- total_watchers = serpy.MethodField("get_total_watchers")
-
-
-class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
+class EditableWatchedResourceSerializer(serializers.ModelSerializer):
watchers = WatchersField(required=False)
def restore_object(self, attrs, instance=None):
- #watchers is not a field from the model but can be attached in the get_queryset of the viewset.
- #If that's the case we need to remove it before calling the super method
- watcher_field = self.fields.pop("watchers", None)
+ # watchers is not a field from the model but can be attached in the get_queryset of the viewset.
+ # If that's the case we need to remove it before calling the super method
+ self.fields.pop("watchers", None)
self.validate_watchers(attrs, "watchers")
new_watcher_ids = attrs.pop("watchers", None)
- obj = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance)
+ obj = super(EditableWatchedResourceSerializer, self).restore_object(attrs, instance)
- #A partial update can exclude the watchers field or if the new instance can still not be saved
+ # A partial update can exclude the watchers field or if the new instance can still not be saved
if instance is None or new_watcher_ids is None:
return obj
@@ -230,7 +209,6 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids))
- User = get_user_model()
adding_users = get_user_model().objects.filter(id__in=adding_watcher_ids)
removing_users = get_user_model().objects.filter(id__in=removing_watcher_ids)
for user in adding_users:
@@ -244,7 +222,7 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
return obj
def to_native(self, obj):
- #if watchers wasn't attached via the get_queryset of the viewset we need to manually add it
+ # if watchers wasn't attached via the get_queryset of the viewset we need to manually add it
if obj is not None:
if not hasattr(obj, "watchers"):
obj.watchers = [user.id for user in obj.get_watchers()]
@@ -254,10 +232,10 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
if user and user.is_authenticated():
obj.is_watcher = user.id in obj.watchers
- return super(WatchedResourceModelSerializer, self).to_native(obj)
+ return super(WatchedResourceSerializer, self).to_native(obj)
def save(self, **kwargs):
- obj = super(EditableWatchedResourceModelSerializer, self).save(**kwargs)
+ obj = super(EditableWatchedResourceSerializer, self).save(**kwargs)
self.fields["watchers"] = WatchersField(required=False)
obj.watchers = [user.id for user in obj.get_watchers()]
return obj
diff --git a/taiga/projects/notifications/utils.py b/taiga/projects/notifications/utils.py
index 00b98d63..ae6bd34c 100644
--- a/taiga/projects/notifications/utils.py
+++ b/taiga/projects/notifications/utils.py
@@ -53,15 +53,18 @@ def attach_is_watcher_to_queryset(queryset, user, as_field="is_watcher"):
"""
model = queryset.model
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
- sql = ("""SELECT CASE WHEN (SELECT count(*)
- FROM notifications_watched
- WHERE notifications_watched.content_type_id = {type_id}
- AND notifications_watched.object_id = {tbl}.id
- AND notifications_watched.user_id = {user_id}) > 0
- THEN TRUE
- ELSE FALSE
- END""")
- sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
+ if user is None or user.is_anonymous():
+ sql = """SELECT false"""
+ else:
+ sql = ("""SELECT CASE WHEN (SELECT count(*)
+ FROM notifications_watched
+ WHERE notifications_watched.content_type_id = {type_id}
+ AND notifications_watched.object_id = {tbl}.id
+ AND notifications_watched.user_id = {user_id}) > 0
+ THEN TRUE
+ ELSE FALSE
+ END""")
+ sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
qs = queryset.extra(select={as_field: sql})
return qs
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index c10a4810..96b8d0ba 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -16,135 +16,121 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import serpy
-
from django.utils.translation import ugettext as _
-from django.db.models import Q
from taiga.base.api import serializers
-from taiga.base.fields import JsonField
-from taiga.base.fields import PgArrayField
+from taiga.base.fields import Field, MethodField, I18NField
from taiga.permissions import services as permissions_services
from taiga.users.services import get_photo_or_gravatar_url
from taiga.users.serializers import UserBasicInfoSerializer
-from taiga.users.serializers import ProjectRoleSerializer
-from taiga.users.serializers import ListUserBasicInfoSerializer
-from taiga.users.validators import RoleExistsValidator
-from taiga.permissions.services import get_user_project_permissions
from taiga.permissions.services import calculate_permissions
from taiga.permissions.services import is_project_admin, is_project_owner
-from . import models
from . import services
-from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
-from .custom_attributes.serializers import TaskCustomAttributeSerializer
-from .custom_attributes.serializers import IssueCustomAttributeSerializer
-from .likes.mixins.serializers import FanResourceSerializerMixin
-from .mixins.serializers import ValidateDuplicatedNameInProjectMixin
from .notifications.choices import NotifyLevel
-from .notifications.mixins import WatchedResourceModelSerializer
-from .tagging.fields import TagsField
-from .tagging.fields import TagsColorsField
-from .validators import ProjectExistsValidator
-
-import serpy
-
-######################################################
-## Custom values for selectors
-######################################################
-
-class PointsSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.Points
- i18n_fields = ("name",)
-
-
-class UserStoryStatusSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.UserStoryStatus
- i18n_fields = ("name",)
-
-
-class BasicUserStoryStatusSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.UserStoryStatus
- i18n_fields = ("name",)
- fields = ("name", "color")
-
-
-class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.TaskStatus
- i18n_fields = ("name",)
-
-
-class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.TaskStatus
- i18n_fields = ("name",)
- fields = ("name", "color")
-
-
-class SeveritySerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.Severity
- i18n_fields = ("name",)
-
-
-class PrioritySerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.Priority
- i18n_fields = ("name",)
-
-
-class IssueStatusSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.IssueStatus
- i18n_fields = ("name",)
-
-
-class BasicIssueStatusSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.IssueStatus
- i18n_fields = ("name",)
- fields = ("name", "color")
-
-
-class IssueTypeSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.IssueType
- i18n_fields = ("name",)
######################################################
-## Members
+# Custom values for selectors
######################################################
-class MembershipSerializer(serializers.ModelSerializer):
- role_name = serializers.CharField(source='role.name', required=False, read_only=True, i18n=True)
- full_name = serializers.CharField(source='user.get_full_name', required=False, read_only=True)
- user_email = serializers.EmailField(source='user.email', required=False, read_only=True)
- is_user_active = serializers.BooleanField(source='user.is_active', required=False,
- read_only=True)
- email = serializers.EmailField(required=True)
- color = serializers.CharField(source='user.color', required=False, read_only=True)
- photo = serializers.SerializerMethodField("get_photo")
- project_name = serializers.SerializerMethodField("get_project_name")
- project_slug = serializers.SerializerMethodField("get_project_slug")
- invited_by = UserBasicInfoSerializer(read_only=True)
- is_owner = serializers.SerializerMethodField("get_is_owner")
+class PointsSerializer(serializers.LightSerializer):
+ name = I18NField()
+ order = Field()
+ value = Field()
+ project = Field(attr="project_id")
- class Meta:
- model = models.Membership
- # IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date
- # with this info (excluding here user_email and email)
- read_only_fields = ("user",)
- exclude = ("token", "user_email", "email")
- def get_photo(self, project):
- return get_photo_or_gravatar_url(project.user)
+class UserStoryStatusSerializer(serializers.LightSerializer):
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ is_archived = Field()
+ color = Field()
+ wip_limit = Field()
+ project = Field(attr="project_id")
+
+
+class TaskStatusSerializer(serializers.LightSerializer):
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
+ project = Field(attr="project_id")
+
+
+class SeveritySerializer(serializers.LightSerializer):
+ name = I18NField()
+ order = Field()
+ color = Field()
+ project = Field(attr="project_id")
+
+
+class PrioritySerializer(serializers.LightSerializer):
+ name = I18NField()
+ order = Field()
+ color = Field()
+ project = Field(attr="project_id")
+
+
+class IssueStatusSerializer(serializers.LightSerializer):
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
+ project = Field(attr="project_id")
+
+
+class IssueTypeSerializer(serializers.LightSerializer):
+ name = I18NField()
+ order = Field()
+ color = Field()
+ project = Field(attr="project_id")
+
+
+######################################################
+# Members
+######################################################
+
+class MembershipSerializer(serializers.LightSerializer):
+ id = Field()
+ user = Field(attr="user_id")
+ project = Field(attr="project_id")
+ role = Field(attr="role_id")
+ is_admin = Field()
+ created_at = Field()
+ invited_by = Field(attr="invited_by_id")
+ invitation_extra_text = Field()
+ user_order = Field()
+ role_name = MethodField()
+ full_name = MethodField()
+ is_user_active = MethodField()
+ color = MethodField()
+ photo = MethodField()
+ project_name = MethodField()
+ project_slug = MethodField()
+ invited_by = UserBasicInfoSerializer()
+ is_owner = MethodField()
+
+ def get_role_name(self, obj):
+ return obj.role.name if obj.role else None
+
+ def get_full_name(self, obj):
+ return obj.user.get_full_name() if obj.user else None
+
+ def get_is_user_active(self, obj):
+ return obj.user.is_active if obj.user else False
+
+ def get_color(self, obj):
+ return obj.user.color if obj.user else None
+
+ def get_photo(self, obj):
+ return get_photo_or_gravatar_url(obj.user)
def get_project_name(self, obj):
return obj.project.name if obj and obj.project else ""
@@ -156,230 +142,84 @@ class MembershipSerializer(serializers.ModelSerializer):
return (obj and obj.user_id and obj.project_id and obj.project.owner_id and
obj.user_id == obj.project.owner_id)
- def validate_email(self, attrs, source):
- project = attrs.get("project", None)
- if project is None:
- project = self.object.project
-
- email = attrs[source]
-
- qs = models.Membership.objects.all()
-
- # If self.object is not None, the serializer is in update
- # mode, and for it, it should exclude self.
- if self.object:
- qs = qs.exclude(pk=self.object.pk)
-
- qs = qs.filter(Q(project_id=project.id, user__email=email) |
- Q(project_id=project.id, email=email))
-
- if qs.count() > 0:
- raise serializers.ValidationError(_("Email address is already taken"))
-
- return attrs
-
- def validate_role(self, attrs, source):
- project = attrs.get("project", None)
- if project is None:
- project = self.object.project
-
- role = attrs[source]
-
- if project.roles.filter(id=role.id).count() == 0:
- raise serializers.ValidationError(_("Invalid role for the project"))
-
- return attrs
-
- def validate_is_admin(self, attrs, source):
- project = attrs.get("project", None)
- if project is None:
- project = self.object.project
-
- if (self.object and self.object.user):
- if self.object.user.id == project.owner_id and attrs[source] != True:
- raise serializers.ValidationError(_("The project owner must be admin."))
-
- if not services.project_has_valid_admins(project, exclude_user=self.object.user):
- raise serializers.ValidationError(_("At least one user must be an active admin for this project."))
-
- return attrs
-
class MembershipAdminSerializer(MembershipSerializer):
- class Meta:
- model = models.Membership
- # IMPORTANT: Maintain the MembershipSerializer Meta up to date
- # with this info (excluding there user_email and email)
- read_only_fields = ("user",)
- exclude = ("token",)
+ email = Field()
+ user_email = MethodField()
+ def get_user_email(self, obj):
+ return obj.user.email if obj.user else None
-class MemberBulkSerializer(RoleExistsValidator, serializers.Serializer):
- email = serializers.EmailField()
- role_id = serializers.IntegerField()
-
-
-class MembersBulkSerializer(ProjectExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- bulk_memberships = MemberBulkSerializer(many=True)
- invitation_extra_text = serializers.CharField(required=False, max_length=255)
-
-
-class ProjectMemberSerializer(serializers.ModelSerializer):
- id = serializers.IntegerField(source="user.id", read_only=True)
- username = serializers.CharField(source='user.username', read_only=True)
- full_name = serializers.CharField(source='user.full_name', read_only=True)
- full_name_display = serializers.CharField(source='user.get_full_name', read_only=True)
- color = serializers.CharField(source='user.color', read_only=True)
- photo = serializers.SerializerMethodField("get_photo")
- is_active = serializers.BooleanField(source='user.is_active', read_only=True)
- role_name = serializers.CharField(source='role.name', read_only=True, i18n=True)
-
- class Meta:
- model = models.Membership
- exclude = ("project", "email", "created_at", "token", "invited_by", "invitation_extra_text",
- "user_order")
-
- def get_photo(self, membership):
- return get_photo_or_gravatar_url(membership.user)
+ # IMPORTANT: Maintain the MembershipSerializer Meta up to date
+ # with this info (excluding there user_email and email)
######################################################
-## Projects
+# Projects
######################################################
-class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializer,
- serializers.ModelSerializer):
- anon_permissions = PgArrayField(required=False)
- public_permissions = PgArrayField(required=False)
- my_permissions = serializers.SerializerMethodField("get_my_permissions")
+class ProjectSerializer(serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ slug = Field()
+ description = Field()
+ created_date = Field()
+ modified_date = Field()
+ owner = MethodField()
+ members = MethodField()
+ total_milestones = Field()
+ total_story_points = Field()
+ is_backlog_activated = Field()
+ is_kanban_activated = Field()
+ is_wiki_activated = Field()
+ is_issues_activated = Field()
+ videoconferences = Field()
+ videoconferences_extra_data = Field()
+ creation_template = Field(attr="creation_template_id")
+ is_private = Field()
+ anon_permissions = Field()
+ public_permissions = Field()
+ is_featured = Field()
+ is_looking_for_people = Field()
+ looking_for_people_note = Field()
+ blocked_code = Field()
+ totals_updated_datetime = Field()
+ total_fans = Field()
+ total_fans_last_week = Field()
+ total_fans_last_month = Field()
+ total_fans_last_year = Field()
+ total_activity = Field()
+ total_activity_last_week = Field()
+ total_activity_last_month = Field()
+ total_activity_last_year = Field()
- owner = UserBasicInfoSerializer(read_only=True)
- i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
- i_am_admin = serializers.SerializerMethodField("get_i_am_admin")
- i_am_member = serializers.SerializerMethodField("get_i_am_member")
+ tags = Field()
+ tags_colors = MethodField()
- tags = TagsField(default=[], required=False)
- tags_colors = TagsColorsField(required=False, read_only=True)
+ default_points = Field(attr="default_points_id")
+ default_us_status = Field(attr="default_us_status_id")
+ default_task_status = Field(attr="default_task_status_id")
+ default_priority = Field(attr="default_priority_id")
+ default_severity = Field(attr="default_severity_id")
+ default_issue_status = Field(attr="default_issue_status_id")
+ default_issue_type = Field(attr="default_issue_type_id")
- notify_level = serializers.SerializerMethodField("get_notify_level")
- total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
- total_watchers = serializers.SerializerMethodField("get_total_watchers")
+ my_permissions = MethodField()
- logo_small_url = serializers.SerializerMethodField("get_logo_small_url")
- logo_big_url = serializers.SerializerMethodField("get_logo_big_url")
+ i_am_owner = MethodField()
+ i_am_admin = MethodField()
+ i_am_member = MethodField()
- class Meta:
- model = models.Project
- read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
- exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref",
- "issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid",
- "transfer_token")
+ notify_level = MethodField()
+ total_closed_milestones = MethodField()
- def get_my_permissions(self, obj):
- if "request" in self.context:
- return get_user_project_permissions(self.context["request"].user, obj)
- return []
+ is_watcher = MethodField()
+ total_watchers = MethodField()
- def get_i_am_owner(self, obj):
- if "request" in self.context:
- return is_project_owner(self.context["request"].user, obj)
- return False
+ logo_small_url = MethodField()
+ logo_big_url = MethodField()
- def get_i_am_admin(self, obj):
- if "request" in self.context:
- return is_project_admin(self.context["request"].user, obj)
- return False
-
- def get_i_am_member(self, obj):
- if "request" in self.context:
- user = self.context["request"].user
- if not user.is_anonymous() and user.cached_membership_for_project(obj):
- return True
- return False
-
- def get_total_closed_milestones(self, obj):
- return obj.milestones.filter(closed=True).count()
-
- def get_notify_level(self, obj):
- if "request" in self.context:
- user = self.context["request"].user
- return user.is_authenticated() and user.get_notify_level(obj)
-
- return None
-
- def get_total_watchers(self, obj):
- return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count()
-
- def get_logo_small_url(self, obj):
- return services.get_logo_small_thumbnail_url(obj)
-
- def get_logo_big_url(self, obj):
- return services.get_logo_big_thumbnail_url(obj)
-
-
-class LightProjectSerializer(serializers.LightSerializer):
- id = serpy.Field()
- name = serpy.Field()
- slug = serpy.Field()
- description = serpy.Field()
- created_date = serpy.Field()
- modified_date = serpy.Field()
- owner = serpy.MethodField()
- members = serpy.MethodField()
- total_milestones = serpy.Field()
- total_story_points = serpy.Field()
- is_backlog_activated = serpy.Field()
- is_kanban_activated = serpy.Field()
- is_wiki_activated = serpy.Field()
- is_issues_activated = serpy.Field()
- videoconferences = serpy.Field()
- videoconferences_extra_data = serpy.Field()
- creation_template = serpy.Field(attr="creation_template_id")
- is_private = serpy.Field()
- anon_permissions = serpy.Field()
- public_permissions = serpy.Field()
- is_featured = serpy.Field()
- is_looking_for_people = serpy.Field()
- looking_for_people_note = serpy.Field()
- blocked_code = serpy.Field()
- totals_updated_datetime = serpy.Field()
- total_fans = serpy.Field()
- total_fans_last_week = serpy.Field()
- total_fans_last_month = serpy.Field()
- total_fans_last_year = serpy.Field()
- total_activity = serpy.Field()
- total_activity_last_week = serpy.Field()
- total_activity_last_month = serpy.Field()
- total_activity_last_year = serpy.Field()
-
- tags = serpy.Field()
- tags_colors = serpy.MethodField()
-
- default_points = serpy.Field(attr="default_points_id")
- default_us_status = serpy.Field(attr="default_us_status_id")
- default_task_status = serpy.Field(attr="default_task_status_id")
- default_priority = serpy.Field(attr="default_priority_id")
- default_severity = serpy.Field(attr="default_severity_id")
- default_issue_status = serpy.Field(attr="default_issue_status_id")
- default_issue_type = serpy.Field(attr="default_issue_type_id")
-
- my_permissions = serpy.MethodField()
-
- i_am_owner = serpy.MethodField()
- i_am_admin = serpy.MethodField()
- i_am_member = serpy.MethodField()
-
- notify_level = serpy.MethodField("get_notify_level")
- total_closed_milestones = serpy.MethodField()
-
- is_watcher = serpy.MethodField()
- total_watchers = serpy.MethodField()
-
- logo_small_url = serpy.MethodField()
- logo_big_url = serpy.MethodField()
-
- is_fan = serpy.Field(attr="is_fan_attr")
+ is_fan = Field(attr="is_fan_attr")
def get_members(self, obj):
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
@@ -395,7 +235,8 @@ class LightProjectSerializer(serializers.LightSerializer):
if "request" in self.context:
user = self.context["request"].user
- if not user.is_anonymous() and user.id in [m.get("id") for m in obj.members_attr if m["id"] is not None]:
+ user_ids = [m.get("id") for m in obj.members_attr if m["id"] is not None]
+ if not user.is_anonymous() and user.id in user_ids:
return True
return False
@@ -407,17 +248,17 @@ class LightProjectSerializer(serializers.LightSerializer):
if "request" in self.context:
user = self.context["request"].user
return calculate_permissions(
- is_authenticated = user.is_authenticated(),
- is_superuser = user.is_superuser,
- is_member = self.get_i_am_member(obj),
- is_admin = self.get_i_am_admin(obj),
- role_permissions = obj.my_role_permissions_attr,
- anon_permissions = obj.anon_permissions,
- public_permissions = obj.public_permissions)
+ is_authenticated=user.is_authenticated(),
+ is_superuser=user.is_superuser,
+ is_member=self.get_i_am_member(obj),
+ is_admin=self.get_i_am_admin(obj),
+ role_permissions=obj.my_role_permissions_attr,
+ anon_permissions=obj.anon_permissions,
+ public_permissions=obj.public_permissions)
return []
def get_owner(self, obj):
- return ListUserBasicInfoSerializer(obj.owner).data
+ return UserBasicInfoSerializer(obj.owner).data
def get_i_am_owner(self, obj):
if "request" in self.context:
@@ -436,7 +277,7 @@ class LightProjectSerializer(serializers.LightSerializer):
def get_is_watcher(self, obj):
assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
np = self.get_notify_level(obj)
- return np != None and np != NotifyLevel.none
+ return np is not None and np != NotifyLevel.none
def get_total_watchers(self, obj):
assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
@@ -466,36 +307,36 @@ class LightProjectSerializer(serializers.LightSerializer):
return services.get_logo_big_thumbnail_url(obj)
-class LightProjectDetailSerializer(LightProjectSerializer):
- us_statuses = serpy.Field(attr="userstory_statuses_attr")
- points = serpy.Field(attr="points_attr")
- task_statuses = serpy.Field(attr="task_statuses_attr")
- issue_statuses = serpy.Field(attr="issue_statuses_attr")
- issue_types = serpy.Field(attr="issue_types_attr")
- priorities = serpy.Field(attr="priorities_attr")
- severities = serpy.Field(attr="severities_attr")
- userstory_custom_attributes = serpy.Field(attr="userstory_custom_attributes_attr")
- task_custom_attributes = serpy.Field(attr="task_custom_attributes_attr")
- issue_custom_attributes = serpy.Field(attr="issue_custom_attributes_attr")
- roles = serpy.Field(attr="roles_attr")
- members = serpy.MethodField()
- total_memberships = serpy.MethodField()
- is_out_of_owner_limits = serpy.MethodField()
+class ProjectDetailSerializer(ProjectSerializer):
+ us_statuses = Field(attr="userstory_statuses_attr")
+ points = Field(attr="points_attr")
+ task_statuses = Field(attr="task_statuses_attr")
+ issue_statuses = Field(attr="issue_statuses_attr")
+ issue_types = Field(attr="issue_types_attr")
+ priorities = Field(attr="priorities_attr")
+ severities = Field(attr="severities_attr")
+ userstory_custom_attributes = Field(attr="userstory_custom_attributes_attr")
+ task_custom_attributes = Field(attr="task_custom_attributes_attr")
+ issue_custom_attributes = Field(attr="issue_custom_attributes_attr")
+ roles = Field(attr="roles_attr")
+ members = MethodField()
+ total_memberships = MethodField()
+ is_out_of_owner_limits = MethodField()
- #Admin fields
- is_private_extra_info = serpy.MethodField()
- max_memberships = serpy.MethodField()
- issues_csv_uuid = serpy.Field()
- tasks_csv_uuid = serpy.Field()
- userstories_csv_uuid = serpy.Field()
- transfer_token = serpy.Field()
+ # Admin fields
+ is_private_extra_info = MethodField()
+ max_memberships = MethodField()
+ issues_csv_uuid = Field()
+ tasks_csv_uuid = Field()
+ userstories_csv_uuid = Field()
+ transfer_token = Field()
def to_value(self, instance):
# Name attributes must be translated
- for attr in ["userstory_statuses_attr","points_attr", "task_statuses_attr",
+ for attr in ["userstory_statuses_attr", "points_attr", "task_statuses_attr",
"issue_statuses_attr", "issue_types_attr", "priorities_attr",
"severities_attr", "userstory_custom_attributes_attr",
- "task_custom_attributes_attr","issue_custom_attributes_attr", "roles_attr"]:
+ "task_custom_attributes_attr", "issue_custom_attributes_attr", "roles_attr"]:
assert hasattr(instance, attr), "instance must have a {} attribute".format(attr)
val = getattr(instance, attr)
@@ -547,8 +388,9 @@ class LightProjectDetailSerializer(LightProjectSerializer):
def get_is_out_of_owner_limits(self, obj):
assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
- return services.check_if_project_is_out_of_owner_limits(obj,
- current_memberships = self.get_total_memberships(obj),
+ return services.check_if_project_is_out_of_owner_limits(
+ obj,
+ current_memberships=self.get_total_memberships(obj),
current_private_projects=obj.private_projects_same_owner_attr,
current_public_projects=obj.public_projects_same_owner_attr
)
@@ -556,8 +398,9 @@ class LightProjectDetailSerializer(LightProjectSerializer):
def get_is_private_extra_info(self, obj):
assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
- return services.check_if_project_privacity_can_be_changed(obj,
- current_memberships = self.get_total_memberships(obj),
+ return services.check_if_project_privacity_can_be_changed(
+ obj,
+ current_memberships=self.get_total_memberships(obj),
current_private_projects=obj.private_projects_same_owner_attr,
current_public_projects=obj.public_projects_same_owner_attr
)
@@ -565,41 +408,32 @@ class LightProjectDetailSerializer(LightProjectSerializer):
def get_max_memberships(self, obj):
return services.get_max_memberships_for_project(obj)
-######################################################
-## Liked
-######################################################
-
-class LikedSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.Project
- fields = ['id', 'name', 'slug']
-
-
######################################################
-## Project Templates
+# Project Templates
######################################################
-class ProjectTemplateSerializer(serializers.ModelSerializer):
- default_options = JsonField(required=False, label=_("Default options"))
- us_statuses = JsonField(required=False, label=_("User story's statuses"))
- points = JsonField(required=False, label=_("Points"))
- task_statuses = JsonField(required=False, label=_("Task's statuses"))
- issue_statuses = JsonField(required=False, label=_("Issue's statuses"))
- issue_types = JsonField(required=False, label=_("Issue's types"))
- priorities = JsonField(required=False, label=_("Priorities"))
- severities = JsonField(required=False, label=_("Severities"))
- roles = JsonField(required=False, label=_("Roles"))
-
- class Meta:
- model = models.ProjectTemplate
- read_only_fields = ("created_date", "modified_date")
- i18n_fields = ("name", "description")
-
-######################################################
-## Project order bulk serializers
-######################################################
-
-class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- order = serializers.IntegerField()
+class ProjectTemplateSerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ slug = Field()
+ description = I18NField()
+ order = Field()
+ created_date = Field()
+ modified_date = Field()
+ default_owner_role = Field()
+ is_backlog_activated = Field()
+ is_kanban_activated = Field()
+ is_wiki_activated = Field()
+ is_issues_activated = Field()
+ videoconferences = Field()
+ videoconferences_extra_data = Field()
+ default_options = Field()
+ us_statuses = Field()
+ points = Field()
+ task_statuses = Field()
+ issue_statuses = Field()
+ issue_types = Field()
+ priorities = Field()
+ severities = Field()
+ roles = Field()
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index 01ae057e..bec134c5 100644
--- a/taiga/projects/tasks/api.py
+++ b/taiga/projects/tasks/api.py
@@ -26,7 +26,6 @@ from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
-from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, TaskStatus
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
@@ -38,10 +37,14 @@ from . import models
from . import permissions
from . import serializers
from . import services
+from . import validators
+from . import utils as tasks_utils
-class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
- TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
+class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin,
+ WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin,
+ ModelCrudViewSet):
+ validator_class = validators.TaskValidator
queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,)
filter_backends = (filters.CanViewTasksFilterBackend,
@@ -74,17 +77,15 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
def get_queryset(self):
qs = super().get_queryset()
- qs = self.attach_votes_attrs_to_queryset(qs)
qs = qs.select_related("milestone",
"project",
"status",
"owner",
"assigned_to")
- qs = self.attach_watchers_attrs_to_queryset(qs)
- if "include_attachments" in self.request.QUERY_PARAMS:
- qs = attach_basic_attachments(qs)
- qs = qs.extra(select={"include_attachments": "True"})
+ include_attachments = "include_attachments" in self.request.QUERY_PARAMS
+ qs = tasks_utils.attach_extra_info(qs, user=self.request.user,
+ include_attachments=include_attachments)
return qs
@@ -164,8 +165,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
def by_ref(self, request):
ref = request.QUERY_PARAMS.get("ref", None)
project_id = request.QUERY_PARAMS.get("project", None)
- task = get_object_or_404(models.Task, ref=ref, project_id=project_id)
- return self.retrieve(request, pk=task.pk)
+ return self.retrieve(request, project_id=project_id, ref=ref)
@list_route(methods=["GET"])
def csv(self, request):
@@ -182,9 +182,9 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
@list_route(methods=["POST"])
def bulk_create(self, request, **kwargs):
- serializer = serializers.TasksBulkSerializer(data=request.DATA)
- if serializer.is_valid():
- data = serializer.data
+ validator = validators.TasksBulkValidator(data=request.DATA)
+ if validator.is_valid():
+ data = validator.data
project = Project.objects.get(id=data["project_id"])
self.check_permissions(request, 'bulk_create', project)
if project.blocked_code is not None:
@@ -194,18 +194,20 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
data["bulk_tasks"], milestone_id=data["sprint_id"], user_story_id=data["us_id"],
status_id=data.get("status_id") or project.default_task_status_id,
project=project, owner=request.user, callback=self.post_save, precall=self.pre_save)
+
+ tasks = self.get_queryset().filter(id__in=[i.id for i in tasks])
tasks_serialized = self.get_serializer_class()(tasks, many=True)
return response.Ok(tasks_serialized.data)
- return response.BadRequest(serializer.errors)
+ return response.BadRequest(validator.errors)
def _bulk_update_order(self, order_field, request, **kwargs):
- serializer = serializers.UpdateTasksOrderBulkSerializer(data=request.DATA)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
self.check_permissions(request, "bulk_update_order", project)
diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py
index ac82c570..cd649424 100644
--- a/taiga/projects/tasks/serializers.py
+++ b/taiga/projects/tasks/serializers.py
@@ -16,101 +16,44 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.contrib.auth import get_user_model
-from django.utils.translation import ugettext_lazy as _
-
from taiga.base.api import serializers
-from taiga.base.fields import PgArrayField
+from taiga.base.fields import Field, MethodField
from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender
-from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin
-from taiga.projects.milestones.validators import SprintExistsValidator
-from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
-from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
-from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
-from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
-from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
-from taiga.projects.notifications.validators import WatchersValidator
-from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
-from taiga.mdrender.service import render as mdrender
-from taiga.projects.tagging.fields import TagsAndTagsColorsField
-from taiga.projects.tasks.validators import TaskExistsValidator
-from taiga.projects.validators import ProjectExistsValidator
+from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
+from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
-from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
-
-from taiga.users.serializers import UserBasicInfoSerializer
-from taiga.users.services import get_photo_or_gravatar_url
-from taiga.users.services import get_big_photo_or_gravatar_url
-
-from . import models
-
-import serpy
-class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
-
- tags = TagsAndTagsColorsField(default=[], required=False)
- external_reference = PgArrayField(required=False)
- comment = serializers.SerializerMethodField("get_comment")
- milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
- blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
- description_html = serializers.SerializerMethodField("get_description_html")
- is_closed = serializers.SerializerMethodField("get_is_closed")
- status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True)
- assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
- owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
-
- class Meta:
- model = models.Task
- read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
-
- def get_comment(self, obj):
- return ""
-
- def get_milestone_slug(self, obj):
- if obj.milestone:
- return obj.milestone.slug
- else:
- return None
-
- def get_blocked_note_html(self, obj):
- return mdrender(obj.project, obj.blocked_note)
-
- def get_description_html(self, obj):
- return mdrender(obj.project, obj.description)
-
- def get_is_closed(self, obj):
- return obj.status is not None and obj.status.is_closed
-
-
-class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
- ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
- ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin,
+class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
+ OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
+ StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
serializers.LightSerializer):
- id = serpy.Field()
- user_story = serpy.Field(attr="user_story_id")
- ref = serpy.Field()
- project = serpy.Field(attr="project_id")
- milestone = serpy.Field(attr="milestone_id")
- milestone_slug = serpy.MethodField("get_milestone_slug")
- created_date = serpy.Field()
- modified_date = serpy.Field()
- finished_date = serpy.Field()
- subject = serpy.Field()
- us_order = serpy.Field()
- taskboard_order = serpy.Field()
- is_iocaine = serpy.Field()
- external_reference = serpy.Field()
- version = serpy.Field()
- watchers = serpy.Field()
- is_blocked = serpy.Field()
- blocked_note = serpy.Field()
- tags = serpy.Field()
- is_closed = serpy.MethodField()
+ id = Field()
+ user_story = Field(attr="user_story_id")
+ ref = Field()
+ project = Field(attr="project_id")
+ milestone = Field(attr="milestone_id")
+ milestone_slug = MethodField()
+ created_date = Field()
+ modified_date = Field()
+ finished_date = Field()
+ subject = Field()
+ us_order = Field()
+ taskboard_order = Field()
+ is_iocaine = Field()
+ external_reference = Field()
+ version = Field()
+ watchers = Field()
+ is_blocked = Field()
+ blocked_note = Field()
+ tags = Field()
+ is_closed = MethodField()
def get_milestone_slug(self, obj):
return obj.milestone.slug if obj.milestone else None
@@ -119,36 +62,21 @@ class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceMod
return obj.status is not None and obj.status.is_closed
+class TaskSerializer(TaskListSerializer):
+ comment = MethodField()
+ blocked_note_html = MethodField()
+ description = Field()
+ description_html = MethodField()
+
+ def get_comment(self, obj):
+ return ""
+
+ def get_blocked_note_html(self, obj):
+ return mdrender(obj.project, obj.blocked_note)
+
+ def get_description_html(self, obj):
+ return mdrender(obj.project, obj.description)
+
+
class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
- def serialize_neighbor(self, neighbor):
- if neighbor:
- return NeighborTaskSerializer(neighbor).data
- return None
-
-
-class NeighborTaskSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.Task
- fields = ("id", "ref", "subject")
- depth = 0
-
-
-class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator,
- TaskExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- sprint_id = serializers.IntegerField()
- status_id = serializers.IntegerField(required=False)
- us_id = serializers.IntegerField(required=False)
- bulk_tasks = serializers.CharField()
-
-
-## Order bulk serializers
-
-class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer):
- task_id = serializers.IntegerField()
- order = serializers.IntegerField()
-
-
-class UpdateTasksOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- bulk_tasks = _TaskOrderBulkSerializer(many=True)
+ pass
diff --git a/taiga/projects/tasks/utils.py b/taiga/projects/tasks/utils.py
new file mode 100644
index 00000000..d10dddab
--- /dev/null
+++ b/taiga/projects/tasks/utils.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.projects.attachments.utils import attach_basic_attachments
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.votes.utils import attach_is_voter_to_queryset
+
+
+def attach_extra_info(queryset, user=None, include_attachments=False):
+
+ if include_attachments:
+ queryset = attach_basic_attachments(queryset)
+ queryset = queryset.extra(select={"include_attachments": "True"})
+
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_voter_to_queryset(queryset, user)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ return queryset
diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py
index 4a100779..7f71636c 100644
--- a/taiga/projects/tasks/validators.py
+++ b/taiga/projects/tasks/validators.py
@@ -19,7 +19,13 @@
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
-
+from taiga.base.api import validators
+from taiga.base.fields import PgArrayField
+from taiga.projects.milestones.validators import MilestoneExistsValidator
+from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
+from taiga.projects.notifications.validators import WatchersValidator
+from taiga.projects.tagging.fields import TagsAndTagsColorsField
+from taiga.projects.validators import ProjectExistsValidator
from . import models
@@ -30,3 +36,33 @@ class TaskExistsValidator:
msg = _("There's no task with that id")
raise serializers.ValidationError(msg)
return attrs
+
+
+class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator):
+ tags = TagsAndTagsColorsField(default=[], required=False)
+ external_reference = PgArrayField(required=False)
+
+ class Meta:
+ model = models.Task
+ read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
+
+
+class TasksBulkValidator(ProjectExistsValidator, MilestoneExistsValidator,
+ TaskExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ sprint_id = serializers.IntegerField()
+ status_id = serializers.IntegerField(required=False)
+ us_id = serializers.IntegerField(required=False)
+ bulk_tasks = serializers.CharField()
+
+
+# Order bulk validators
+
+class _TaskOrderBulkValidator(TaskExistsValidator, validators.Validator):
+ task_id = serializers.IntegerField()
+ order = serializers.IntegerField()
+
+
+class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ bulk_tasks = _TaskOrderBulkValidator(many=True)
diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py
index 87ecf18b..47487e88 100644
--- a/taiga/projects/userstories/api.py
+++ b/taiga/projects/userstories/api.py
@@ -16,12 +16,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from contextlib import closing
-from collections import namedtuple
-
from django.apps import apps
-from django.db import transaction, connection
-from django.db.models.sql import datastructures
+from django.db import transaction
from django.utils.translation import ugettext as _
from django.http import HttpResponse
@@ -36,7 +32,6 @@ from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet
from taiga.base.api.utils import get_object_or_404
-from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.history.services import take_snapshot
from taiga.projects.milestones.models import Milestone
@@ -45,21 +40,20 @@ from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.tagging.api import TaggedResourceMixin
-from taiga.projects.userstories.models import RolePoints
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
-from taiga.projects.userstories.utils import attach_total_points
-from taiga.projects.userstories.utils import attach_role_points
-from taiga.projects.userstories.utils import attach_tasks
+from taiga.projects.userstories.utils import attach_extra_info
from . import models
from . import permissions
from . import serializers
from . import services
+from . import validators
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
+ validator_class = validators.UserStoryValidator
queryset = models.UserStory.objects.all()
permission_classes = (permissions.UserStoryPermission,)
filter_backends = (filters.CanViewUsFilterBackend,
@@ -105,18 +99,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
"assigned_to",
"generated_from_issue")
- qs = self.attach_votes_attrs_to_queryset(qs)
- qs = self.attach_watchers_attrs_to_queryset(qs)
- qs = attach_total_points(qs)
- qs = attach_role_points(qs)
-
- if "include_attachments" in self.request.QUERY_PARAMS:
- qs = attach_basic_attachments(qs)
- qs = qs.extra(select={"include_attachments": "True"})
-
- if "include_tasks" in self.request.QUERY_PARAMS:
- qs = attach_tasks(qs)
- qs = qs.extra(select={"include_tasks": "True"})
+ include_attachments = "include_attachments" in self.request.QUERY_PARAMS
+ include_tasks = "include_tasks" in self.request.QUERY_PARAMS
+ qs = attach_extra_info(qs, user=self.request.user,
+ include_attachments=include_attachments,
+ include_tasks=include_tasks)
return qs
@@ -239,8 +226,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
def by_ref(self, request):
ref = request.QUERY_PARAMS.get("ref", None)
project_id = request.QUERY_PARAMS.get("project", None)
- userstory = get_object_or_404(models.UserStory, ref=ref, project_id=project_id)
- return self.retrieve(request, pk=userstory.pk)
+ return self.retrieve(request, project_id=project_id, ref=ref)
@list_route(methods=["GET"])
def csv(self, request):
@@ -257,9 +243,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
@list_route(methods=["POST"])
def bulk_create(self, request, **kwargs):
- serializer = serializers.UserStoriesBulkSerializer(data=request.DATA)
- if serializer.is_valid():
- data = serializer.data
+ validator = validators.UserStoriesBulkValidator(data=request.DATA)
+ if validator.is_valid():
+ data = validator.data
project = Project.objects.get(id=data["project_id"])
self.check_permissions(request, 'bulk_create', project)
if project.blocked_code is not None:
@@ -269,17 +255,20 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
data["bulk_stories"], project=project, owner=request.user,
status_id=data.get("status_id") or project.default_us_status_id,
callback=self.post_save, precall=self.pre_save)
+
+ user_stories = self.get_queryset().filter(id__in=[i.id for i in user_stories])
user_stories_serialized = self.get_serializer_class()(user_stories, many=True)
+
return response.Ok(user_stories_serialized.data)
- return response.BadRequest(serializer.errors)
+ return response.BadRequest(validator.errors)
@list_route(methods=["POST"])
def bulk_update_milestone(self, request, **kwargs):
- serializer = serializers.UpdateMilestoneBulkSerializer(data=request.DATA)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.UpdateMilestoneBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
milestone = get_object_or_404(Milestone, pk=data["milestone_id"])
@@ -291,11 +280,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
return response.NoContent()
def _bulk_update_order(self, order_field, request, **kwargs):
- serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.UpdateUserStoriesOrderBulkValidator(data=request.DATA)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
project = get_object_or_404(Project, pk=data["project_id"])
self.check_permissions(request, "bulk_update_order", project)
diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py
index 236a54d8..ef15eec6 100644
--- a/taiga/projects/userstories/serializers.py
+++ b/taiga/projects/userstories/serializers.py
@@ -16,96 +16,111 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from collections import ChainMap
-
-from django.contrib.auth import get_user_model
-from django.utils.translation import ugettext_lazy as _
-
from taiga.base.api import serializers
-from taiga.base.api.utils import get_object_or_404
-from taiga.base.fields import PickledObjectField
-from taiga.base.fields import PgArrayField
+from taiga.base.fields import Field, MethodField
from taiga.base.neighbors import NeighborsSerializerMixin
-from taiga.base.utils import json
from taiga.mdrender.service import render as mdrender
-from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin
-from taiga.projects.milestones.validators import SprintExistsValidator
-from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
-from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
-from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
-from taiga.projects.models import Project, UserStoryStatus
-from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
-from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
-from taiga.projects.notifications.validators import WatchersValidator
-from taiga.projects.serializers import BasicUserStoryStatusSerializer
-from taiga.projects.tagging.fields import TagsAndTagsColorsField
-from taiga.projects.userstories.validators import UserStoryExistsValidator
-from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
+from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
+from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
+from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
-from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
-
-from taiga.users.serializers import UserBasicInfoSerializer
-from taiga.users.serializers import ListUserBasicInfoSerializer
-from taiga.users.services import get_photo_or_gravatar_url
-from taiga.users.services import get_big_photo_or_gravatar_url
-
-from . import models
-
-import serpy
-class RolePointsField(serializers.WritableField):
- def to_native(self, obj):
- return {str(o.role.id): o.points.id for o in obj.all()}
+class OriginIssueSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
- def from_native(self, obj):
- if isinstance(obj, dict):
- return obj
- return json.loads(obj)
+ def to_value(self, instance):
+ if instance is None:
+ return None
+
+ return super().to_value(instance)
-class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin,
- EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
- tags = TagsAndTagsColorsField(default=[], required=False)
- external_reference = PgArrayField(required=False)
- points = RolePointsField(source="role_points", required=False)
- total_points = serializers.SerializerMethodField("get_total_points")
- comment = serializers.SerializerMethodField("get_comment")
- milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
- milestone_name = serializers.SerializerMethodField("get_milestone_name")
- origin_issue = serializers.SerializerMethodField("get_origin_issue")
- blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
- description_html = serializers.SerializerMethodField("get_description_html")
- status_extra_info = BasicUserStoryStatusSerializer(source="status", required=False, read_only=True)
- assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
- owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
- tribe_gig = PickledObjectField(required=False)
+class UserStoryListSerializer(
+ VoteResourceSerializerMixin, WatchedResourceSerializer,
+ OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
+ StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
+ serializers.LightSerializer):
- class Meta:
- model = models.UserStory
- depth = 0
- read_only_fields = ('created_date', 'modified_date', 'owner')
+ id = Field()
+ ref = Field()
+ milestone = Field(attr="milestone_id")
+ milestone_slug = MethodField()
+ milestone_name = MethodField()
+ project = Field(attr="project_id")
+ is_closed = Field()
+ points = MethodField()
+ backlog_order = Field()
+ sprint_order = Field()
+ kanban_order = Field()
+ created_date = Field()
+ modified_date = Field()
+ finish_date = Field()
+ subject = Field()
+ client_requirement = Field()
+ team_requirement = Field()
+ generated_from_issue = Field(attr="generated_from_issue_id")
+ external_reference = Field()
+ tribe_gig = Field()
+ version = Field()
+ watchers = Field()
+ is_blocked = Field()
+ blocked_note = Field()
+ tags = Field()
+ total_points = MethodField()
+ comment = MethodField()
+ origin_issue = OriginIssueSerializer(attr="generated_from_issue")
+
+ tasks = MethodField()
+
+ def get_milestone_slug(self, obj):
+ return obj.milestone.slug if obj.milestone else None
+
+ def get_milestone_name(self, obj):
+ return obj.milestone.name if obj.milestone else None
def get_total_points(self, obj):
- return obj.get_total_points()
+ assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute"
+ return obj.total_points_attr
+
+ def get_points(self, obj):
+ assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute"
+ if obj.role_points_attr is None:
+ return {}
+
+ return obj.role_points_attr
+
+ def get_comment(self, obj):
+ return ""
+
+ def get_tasks(self, obj):
+ include_tasks = getattr(obj, "include_tasks", False)
+
+ if include_tasks:
+ assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute"
+
+ if not include_tasks or obj.tasks_attr is None:
+ return []
+
+ return obj.tasks_attr
+
+
+class UserStorySerializer(UserStoryListSerializer):
+ comment = MethodField()
+ origin_issue = MethodField()
+ blocked_note_html = MethodField()
+ description = Field()
+ description_html = MethodField()
def get_comment(self, obj):
# NOTE: This method and field is necessary to historical comments work
return ""
- def get_milestone_slug(self, obj):
- if obj.milestone:
- return obj.milestone.slug
- else:
- return None
-
- def get_milestone_name(self, obj):
- if obj.milestone:
- return obj.milestone.name
- else:
- return None
-
def get_origin_issue(self, obj):
if obj.generated_from_issue:
return {
@@ -122,142 +137,5 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin,
return mdrender(obj.project, obj.description)
-class ListOriginIssueSerializer(serializers.LightSerializer):
- id = serpy.Field()
- ref = serpy.Field()
- subject = serpy.Field()
-
- def to_value(self, instance):
- if instance is None:
- return None
-
- return super().to_value(instance)
-
-
-class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
- ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
- ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin,
- serializers.LightSerializer):
-
- id = serpy.Field()
- ref = serpy.Field()
- milestone = serpy.Field(attr="milestone_id")
- milestone_slug = serpy.MethodField()
- milestone_name = serpy.MethodField()
- project = serpy.Field(attr="project_id")
- is_closed = serpy.Field()
- points = serpy.MethodField()
- backlog_order = serpy.Field()
- sprint_order = serpy.Field()
- kanban_order = serpy.Field()
- created_date = serpy.Field()
- modified_date = serpy.Field()
- finish_date = serpy.Field()
- subject = serpy.Field()
- client_requirement = serpy.Field()
- team_requirement = serpy.Field()
- generated_from_issue = serpy.Field(attr="generated_from_issue_id")
- external_reference = serpy.Field()
- tribe_gig = serpy.Field()
- version = serpy.Field()
- watchers = serpy.Field()
- is_blocked = serpy.Field()
- blocked_note = serpy.Field()
- tags = serpy.Field()
- total_points = serpy.MethodField()
- comment = serpy.MethodField("get_comment")
- origin_issue = ListOriginIssueSerializer(attr="generated_from_issue")
-
- tasks = serpy.MethodField()
-
- def get_milestone_slug(self, obj):
- return obj.milestone.slug if obj.milestone else None
-
- def get_milestone_name(self, obj):
- return obj.milestone.name if obj.milestone else None
-
- def get_total_points(self, obj):
- assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute"
- return obj.total_points_attr
-
- def get_points(self, obj):
- assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute"
- if obj.role_points_attr is None:
- return {}
-
- return dict(ChainMap(*obj.role_points_attr))
-
- def get_comment(self, obj):
- return ""
-
- def get_tasks(self, obj):
- include_tasks = getattr(obj, "include_tasks", False)
-
- if include_tasks:
- assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute"
-
- if not include_tasks or obj.tasks_attr is None:
- return []
-
- return obj.tasks_attr
-
-
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
- def serialize_neighbor(self, neighbor):
- if neighbor:
- return NeighborUserStorySerializer(neighbor).data
- return None
-
-
-class NeighborUserStorySerializer(serializers.ModelSerializer):
- class Meta:
- model = models.UserStory
- fields = ("id", "ref", "subject")
- depth = 0
-
-
-class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator,
- serializers.Serializer):
- project_id = serializers.IntegerField()
- status_id = serializers.IntegerField(required=False)
- bulk_stories = serializers.CharField()
-
-
-## Order bulk serializers
-
-class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serializer):
- us_id = serializers.IntegerField()
- order = serializers.IntegerField()
-
-
-class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator,
- serializers.Serializer):
- project_id = serializers.IntegerField()
- bulk_stories = _UserStoryOrderBulkSerializer(many=True)
-
-
-## Milestone bulk serializers
-
-class _UserStoryMilestoneBulkSerializer(UserStoryExistsValidator, serializers.Serializer):
- us_id = serializers.IntegerField()
-
-
-class UpdateMilestoneBulkSerializer(ProjectExistsValidator, SprintExistsValidator, serializers.Serializer):
- project_id = serializers.IntegerField()
- milestone_id = serializers.IntegerField()
- bulk_stories = _UserStoryMilestoneBulkSerializer(many=True)
-
- def validate(self, data):
- """
- All the userstories and the milestone are from the same project
- """
- user_story_ids = [us["us_id"] for us in data["bulk_stories"]]
- project = get_object_or_404(Project, pk=data["project_id"])
-
- if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids):
- raise serializers.ValidationError("all the user stories must be from the same project")
-
- if project.milestones.filter(id=data["milestone_id"]).count() != 1:
- raise serializers.ValidationError("the milestone isn't valid for the project")
-
- return data
+ pass
diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py
index 809248f7..87b8e094 100644
--- a/taiga/projects/userstories/utils.py
+++ b/taiga/projects/userstories/utils.py
@@ -17,6 +17,13 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from taiga.projects.attachments.utils import attach_basic_attachments
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+from taiga.projects.votes.utils import attach_total_voters_to_queryset
+from taiga.projects.votes.utils import attach_is_voter_to_queryset
+
def attach_total_points(queryset, as_field="total_points_attr"):
"""Attach total of point values to each object of the queryset.
@@ -28,7 +35,7 @@ def attach_total_points(queryset, as_field="total_points_attr"):
"""
model = queryset.model
sql = """SELECT SUM(projects_points.value)
- FROM userstories_rolepoints
+ FROM userstories_rolepoints
INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
@@ -46,10 +53,15 @@ def attach_role_points(queryset, as_field="role_points_attr"):
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
- sql = """SELECT json_agg((userstories_rolepoints.role_id, userstories_rolepoints.points_id))
- FROM userstories_rolepoints
+ sql = """SELECT FORMAT('{{%%s}}',
+ STRING_AGG(format(
+ '"%%s":%%s',
+ TO_JSON(userstories_rolepoints.role_id),
+ TO_JSON(userstories_rolepoints.points_id)
+ ), ',')
+ )::json
+ FROM userstories_rolepoints
WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
-
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
@@ -82,3 +94,23 @@ def attach_tasks(queryset, as_field="tasks_attr"):
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
+
+
+def attach_extra_info(queryset, user=None, include_attachments=False, include_tasks=False):
+ queryset = attach_total_points(queryset)
+ queryset = attach_role_points(queryset)
+
+ if include_attachments:
+ queryset = attach_basic_attachments(queryset)
+ queryset = queryset.extra(select={"include_attachments": "True"})
+
+ if include_tasks:
+ queryset = attach_tasks(queryset)
+ queryset = queryset.extra(select={"include_tasks": "True"})
+
+ queryset = attach_total_voters_to_queryset(queryset)
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_voter_to_queryset(queryset, user)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ return queryset
diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py
index 5ad5e7f4..4ea0b24a 100644
--- a/taiga/projects/userstories/validators.py
+++ b/taiga/projects/userstories/validators.py
@@ -19,9 +19,21 @@
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.api.utils import get_object_or_404
+from taiga.base.fields import PgArrayField
+from taiga.base.fields import PickledObjectField
+from taiga.projects.milestones.validators import MilestoneExistsValidator
+from taiga.projects.models import Project
+from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
+from taiga.projects.notifications.validators import WatchersValidator
+from taiga.projects.tagging.fields import TagsAndTagsColorsField
+from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
from . import models
+import json
+
class UserStoryExistsValidator:
def validate_us_id(self, attrs, source):
@@ -30,3 +42,72 @@ class UserStoryExistsValidator:
msg = _("There's no user story with that id")
raise serializers.ValidationError(msg)
return attrs
+
+
+class RolePointsField(serializers.WritableField):
+ def to_native(self, obj):
+ return {str(o.role.id): o.points.id for o in obj.all()}
+
+ def from_native(self, obj):
+ if isinstance(obj, dict):
+ return obj
+ return json.loads(obj)
+
+
+class UserStoryValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator):
+ tags = TagsAndTagsColorsField(default=[], required=False)
+ external_reference = PgArrayField(required=False)
+ points = RolePointsField(source="role_points", required=False)
+ tribe_gig = PickledObjectField(required=False)
+
+ class Meta:
+ model = models.UserStory
+ depth = 0
+ read_only_fields = ('created_date', 'modified_date', 'owner')
+
+
+class UserStoriesBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator,
+ validators.Validator):
+ project_id = serializers.IntegerField()
+ status_id = serializers.IntegerField(required=False)
+ bulk_stories = serializers.CharField()
+
+
+# Order bulk validators
+
+class _UserStoryOrderBulkValidator(UserStoryExistsValidator, validators.Validator):
+ us_id = serializers.IntegerField()
+ order = serializers.IntegerField()
+
+
+class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator,
+ validators.Validator):
+ project_id = serializers.IntegerField()
+ bulk_stories = _UserStoryOrderBulkValidator(many=True)
+
+
+# Milestone bulk validators
+
+class _UserStoryMilestoneBulkValidator(UserStoryExistsValidator, validators.Validator):
+ us_id = serializers.IntegerField()
+
+
+class UpdateMilestoneBulkValidator(ProjectExistsValidator, MilestoneExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ milestone_id = serializers.IntegerField()
+ bulk_stories = _UserStoryMilestoneBulkValidator(many=True)
+
+ def validate(self, data):
+ """
+ All the userstories and the milestone are from the same project
+ """
+ user_story_ids = [us["us_id"] for us in data["bulk_stories"]]
+ project = get_object_or_404(Project, pk=data["project_id"])
+
+ if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids):
+ raise serializers.ValidationError("all the user stories must be from the same project")
+
+ if project.milestones.filter(id=data["milestone_id"]).count() != 1:
+ raise serializers.ValidationError("the milestone isn't valid for the project")
+
+ return data
diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py
index a05d8476..ee552136 100644
--- a/taiga/projects/utils.py
+++ b/taiga/projects/utils.py
@@ -375,7 +375,7 @@ def attach_private_projects_same_owner(queryset, user, as_field="private_project
"""
model = queryset.model
if user is None or user.is_anonymous():
- sql = """SELECT '0'"""
+ sql = """SELECT 0"""
else:
sql = """SELECT COUNT(id)
FROM projects_project p_aux
@@ -399,7 +399,7 @@ def attach_public_projects_same_owner(queryset, user, as_field="public_projects_
"""
model = queryset.model
if user is None or user.is_anonymous():
- sql = """SELECT '0'"""
+ sql = """SELECT 0"""
else:
sql = """SELECT COUNT(id)
FROM projects_project p_aux
diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py
index 05866b66..c8ab21bb 100644
--- a/taiga/projects/validators.py
+++ b/taiga/projects/validators.py
@@ -16,11 +16,42 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from django.db.models import Q
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.fields import JsonField
+from taiga.base.fields import PgArrayField
+from taiga.users.validators import RoleExistsValidator
+
+from .tagging.fields import TagsField
from . import models
+from . import services
+
+
+class DuplicatedNameInProjectValidator:
+
+ def validate_name(self, attrs, source):
+ """
+ Check the points name is not duplicated in the project on creation
+ """
+ model = self.opts.model
+ qs = None
+ # If the object exists:
+ if self.object and attrs.get(source, None):
+ qs = model.objects.filter(
+ project=self.object.project,
+ name=attrs[source]).exclude(id=self.object.id)
+
+ if not self.object and attrs.get("project", None) and attrs.get(source, None):
+ qs = model.objects.filter(project=attrs["project"], name=attrs[source])
+
+ if qs and qs.exists():
+ raise serializers.ValidationError(_("Name duplicated for the project"))
+
+ return attrs
class ProjectExistsValidator:
@@ -48,3 +79,170 @@ class TaskStatusExistsValidator:
msg = _("There's no task status with that id")
raise serializers.ValidationError(msg)
return attrs
+
+
+######################################################
+# Custom values for selectors
+######################################################
+
+class PointsValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.Points
+
+
+class UserStoryStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.UserStoryStatus
+
+
+class TaskStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.TaskStatus
+
+
+class SeverityValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.Severity
+
+
+class PriorityValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.Priority
+
+
+class IssueStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.IssueStatus
+
+
+class IssueTypeValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
+ class Meta:
+ model = models.IssueType
+
+
+######################################################
+# Members
+######################################################
+
+class MembershipValidator(validators.ModelValidator):
+ email = serializers.EmailField(required=True)
+
+ class Meta:
+ model = models.Membership
+ # IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date
+ # with this info (excluding here user_email and email)
+ read_only_fields = ("user",)
+ exclude = ("token", "email")
+
+ def validate_email(self, attrs, source):
+ project = attrs.get("project", None)
+ if project is None:
+ project = self.object.project
+
+ email = attrs[source]
+
+ qs = models.Membership.objects.all()
+
+ # If self.object is not None, the serializer is in update
+ # mode, and for it, it should exclude self.
+ if self.object:
+ qs = qs.exclude(pk=self.object.pk)
+
+ qs = qs.filter(Q(project_id=project.id, user__email=email) |
+ Q(project_id=project.id, email=email))
+
+ if qs.count() > 0:
+ raise serializers.ValidationError(_("Email address is already taken"))
+
+ return attrs
+
+ def validate_role(self, attrs, source):
+ project = attrs.get("project", None)
+ if project is None:
+ project = self.object.project
+
+ role = attrs[source]
+
+ if project.roles.filter(id=role.id).count() == 0:
+ raise serializers.ValidationError(_("Invalid role for the project"))
+
+ return attrs
+
+ def validate_is_admin(self, attrs, source):
+ project = attrs.get("project", None)
+ if project is None:
+ project = self.object.project
+
+ if (self.object and self.object.user):
+ if self.object.user.id == project.owner_id and not attrs[source]:
+ raise serializers.ValidationError(_("The project owner must be admin."))
+
+ if not services.project_has_valid_admins(project, exclude_user=self.object.user):
+ raise serializers.ValidationError(
+ _("At least one user must be an active admin for this project.")
+ )
+
+ return attrs
+
+
+class MembershipAdminValidator(MembershipValidator):
+ class Meta:
+ model = models.Membership
+ # IMPORTANT: Maintain the MembershipSerializer Meta up to date
+ # with this info (excluding there user_email and email)
+ read_only_fields = ("user",)
+ exclude = ("token",)
+
+
+class MemberBulkValidator(RoleExistsValidator, validators.Validator):
+ email = serializers.EmailField()
+ role_id = serializers.IntegerField()
+
+
+class MembersBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ bulk_memberships = MemberBulkValidator(many=True)
+ invitation_extra_text = serializers.CharField(required=False, max_length=255)
+
+
+######################################################
+# Projects
+######################################################
+
+class ProjectValidator(validators.ModelValidator):
+ anon_permissions = PgArrayField(required=False)
+ public_permissions = PgArrayField(required=False)
+ tags = TagsField(default=[], required=False)
+
+ class Meta:
+ model = models.Project
+ read_only_fields = ("created_date", "modified_date", "slug", "blocked_code", "owner")
+
+
+######################################################
+# Project Templates
+######################################################
+
+class ProjectTemplateValidator(validators.ModelValidator):
+ default_options = JsonField(required=False, label=_("Default options"))
+ us_statuses = JsonField(required=False, label=_("User story's statuses"))
+ points = JsonField(required=False, label=_("Points"))
+ task_statuses = JsonField(required=False, label=_("Task's statuses"))
+ issue_statuses = JsonField(required=False, label=_("Issue's statuses"))
+ issue_types = JsonField(required=False, label=_("Issue's types"))
+ priorities = JsonField(required=False, label=_("Priorities"))
+ severities = JsonField(required=False, label=_("Severities"))
+ roles = JsonField(required=False, label=_("Roles"))
+
+ class Meta:
+ model = models.ProjectTemplate
+ read_only_fields = ("created_date", "modified_date")
+
+
+######################################################
+# Project order bulk serializers
+######################################################
+
+class UpdateProjectOrderBulkValidator(ProjectExistsValidator, validators.Validator):
+ project_id = serializers.IntegerField()
+ order = serializers.IntegerField()
diff --git a/taiga/projects/votes/mixins/serializers.py b/taiga/projects/votes/mixins/serializers.py
index fc7a988e..9f9d1049 100644
--- a/taiga/projects/votes/mixins/serializers.py
+++ b/taiga/projects/votes/mixins/serializers.py
@@ -16,12 +16,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import serpy
-
from taiga.base.api import serializers
+from taiga.base.fields import MethodField
-class BaseVoteResourceSerializerMixin(object):
+class VoteResourceSerializerMixin(serializers.LightSerializer):
+ is_voter = MethodField()
+ total_voters = MethodField()
+
def get_is_voter(self, obj):
# The "is_voted" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "is_voter", False) or False
@@ -29,13 +31,3 @@ class BaseVoteResourceSerializerMixin(object):
def get_total_voters(self, obj):
# The "total_voters" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "total_voters", 0) or 0
-
-
-class VoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serializers.ModelSerializer):
- is_voter = serializers.SerializerMethodField("get_is_voter")
- total_voters = serializers.SerializerMethodField("get_total_voters")
-
-
-class ListVoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serpy.Serializer):
- is_voter = serpy.MethodField("get_is_voter")
- total_voters = serpy.MethodField("get_total_voters")
diff --git a/taiga/projects/votes/mixins/viewsets.py b/taiga/projects/votes/mixins/viewsets.py
index 2456e375..50490ba7 100644
--- a/taiga/projects/votes/mixins/viewsets.py
+++ b/taiga/projects/votes/mixins/viewsets.py
@@ -39,14 +39,6 @@ class VotedResourceMixin:
def pre_conditions_on_save(self, obj)
"""
- def attach_votes_attrs_to_queryset(self, queryset):
- qs = attach_total_voters_to_queryset(queryset)
-
- if self.request.user.is_authenticated():
- qs = attach_is_voter_to_queryset(self.request.user, qs)
-
- return qs
-
@detail_route(methods=["POST"])
def upvote(self, request, pk=None):
obj = self.get_object()
diff --git a/taiga/projects/votes/utils.py b/taiga/projects/votes/utils.py
index 291ee284..077abd46 100644
--- a/taiga/projects/votes/utils.py
+++ b/taiga/projects/votes/utils.py
@@ -48,7 +48,7 @@ def attach_total_voters_to_queryset(queryset, as_field="total_voters"):
return qs
-def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"):
+def attach_is_voter_to_queryset(queryset, user, as_field="is_voter"):
"""Attach is_vote boolean to each object of the queryset.
Because of laziness of vote objects creation, this makes much simpler and more efficient to
@@ -57,22 +57,26 @@ def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"):
(The other way was to do it in the serializer with some try/except blocks and additional
queries)
- :param user: A users.User object model
:param queryset: A Django queryset object.
+ :param user: A users.User object model
:param as_field: Attach the boolean as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
- sql = ("""SELECT CASE WHEN (SELECT count(*)
- FROM votes_vote
- WHERE votes_vote.content_type_id = {type_id}
- AND votes_vote.object_id = {tbl}.id
- AND votes_vote.user_id = {user_id}) > 0
- THEN TRUE
- ELSE FALSE
- END""")
- sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
+ if user is None or user.is_anonymous():
+ sql = """SELECT false"""
+ else:
+ sql = ("""SELECT CASE WHEN (SELECT count(*)
+ FROM votes_vote
+ WHERE votes_vote.content_type_id = {type_id}
+ AND votes_vote.object_id = {tbl}.id
+ AND votes_vote.user_id = {user_id}) > 0
+ THEN TRUE
+ ELSE FALSE
+ END""")
+ sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
+
qs = queryset.extra(select={as_field: sql})
return qs
diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py
index 807d86c6..d6a3d44a 100644
--- a/taiga/projects/wiki/api.py
+++ b/taiga/projects/wiki/api.py
@@ -24,7 +24,6 @@ from taiga.base import response
from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
-from taiga.base.api.permissions import IsAuthenticated
from taiga.base.api.utils import get_object_or_404
from taiga.base.decorators import list_route
@@ -42,6 +41,8 @@ from taiga.projects.occ import OCCResourceMixin
from . import models
from . import permissions
from . import serializers
+from . import validators
+from . import utils as wiki_utils
class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
@@ -49,6 +50,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
model = models.WikiPage
serializer_class = serializers.WikiPageSerializer
+ validator_class = validators.WikiPageValidator
permission_classes = (permissions.WikiPagePermission,)
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
filter_fields = ("project", "slug")
@@ -56,7 +58,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
def get_queryset(self):
qs = super().get_queryset()
- qs = self.attach_watchers_attrs_to_queryset(qs)
+ qs = wiki_utils.attach_extra_info(qs, user=self.request.user)
return qs
@list_route(methods=["GET"])
@@ -100,6 +102,7 @@ class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.WikiLink
serializer_class = serializers.WikiLinkSerializer
+ validator_class = validators.WikiLinkValidator
permission_classes = (permissions.WikiLinkPermission,)
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
filter_fields = ["project"]
@@ -120,7 +123,7 @@ class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet):
wiki_page, created = models.WikiPage.objects.get_or_create(
slug=wiki_link.href,
project=wiki_link.project,
- defaults={"owner": self.request.user,"last_modifier": self.request.user})
+ defaults={"owner": self.request.user, "last_modifier": self.request.user})
if created:
# Creaste the new history entre, sSet watcher for the new wiki page
diff --git a/taiga/projects/wiki/serializers.py b/taiga/projects/wiki/serializers.py
index 16de19df..a7e36c60 100644
--- a/taiga/projects/wiki/serializers.py
+++ b/taiga/projects/wiki/serializers.py
@@ -17,21 +17,26 @@
# along with this program. If not, see .
from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
from taiga.projects.history import services as history_service
-from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
-from taiga.projects.notifications.validators import WatchersValidator
+from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.mdrender.service import render as mdrender
-from . import models
+class WikiPageSerializer(WatchedResourceSerializer, serializers.LightSerializer):
+ id = Field()
+ project = Field(attr="project_id")
+ slug = Field()
+ content = Field()
+ owner = Field(attr="owner_id")
+ last_modifier = Field(attr="last_modifier_id")
+ created_date = Field()
+ modified_date = Field()
-class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer):
- html = serializers.SerializerMethodField("get_html")
- editions = serializers.SerializerMethodField("get_editions")
+ html = MethodField()
+ editions = MethodField()
- class Meta:
- model = models.WikiPage
- read_only_fields = ('modified_date', 'created_date', 'owner')
+ version = Field()
def get_html(self, obj):
return mdrender(obj.project, obj.content)
@@ -40,7 +45,9 @@ class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, seri
return history_service.get_history_queryset_by_model_instance(obj).count() + 1 # +1 for creation
-class WikiLinkSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.WikiLink
- read_only_fields = ('href',)
+class WikiLinkSerializer(serializers.LightSerializer):
+ id = Field()
+ project = Field(attr="project_id")
+ title = Field()
+ href = Field()
+ order = Field()
diff --git a/taiga/projects/wiki/utils.py b/taiga/projects/wiki/utils.py
new file mode 100644
index 00000000..ecbf7602
--- /dev/null
+++ b/taiga/projects/wiki/utils.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# Copyright (C) 2014-2016 Anler Hernández
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from taiga.projects.notifications.utils import attach_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
+from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
+
+
+def attach_extra_info(queryset, user=None, include_attachments=False):
+ queryset = attach_watchers_to_queryset(queryset)
+ queryset = attach_total_watchers_to_queryset(queryset)
+ queryset = attach_is_watcher_to_queryset(queryset, user)
+ return queryset
diff --git a/taiga/projects/wiki/validators.py b/taiga/projects/wiki/validators.py
new file mode 100644
index 00000000..033fac1b
--- /dev/null
+++ b/taiga/projects/wiki/validators.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# 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 .
+
+from taiga.base.api import validators
+from taiga.projects.notifications.validators import WatchersValidator
+
+from . import models
+
+
+class WikiPageValidator(WatchersValidator, validators.ModelValidator):
+ class Meta:
+ model = models.WikiPage
+ read_only_fields = ('modified_date', 'created_date', 'owner')
+
+
+class WikiLinkValidator(validators.ModelValidator):
+ class Meta:
+ model = models.WikiLink
+ read_only_fields = ('href',)
diff --git a/taiga/searches/serializers.py b/taiga/searches/serializers.py
index edc2d1ca..e96e1131 100644
--- a/taiga/searches/serializers.py
+++ b/taiga/searches/serializers.py
@@ -16,37 +16,48 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from taiga.projects.issues.serializers import IssueSerializer
-from taiga.projects.userstories.serializers import UserStorySerializer
-from taiga.projects.tasks.serializers import TaskSerializer
-from taiga.projects.wiki.serializers import WikiPageSerializer
-
-from taiga.projects.issues.models import Issue
-from taiga.projects.userstories.models import UserStory
-from taiga.projects.tasks.models import Task
-from taiga.projects.wiki.models import WikiPage
+from taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
-class IssueSearchResultsSerializer(IssueSerializer):
- class Meta:
- model = Issue
- fields = ('id', 'ref', 'subject', 'status', 'assigned_to')
+class IssueSearchResultsSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+ status = Field(attr="status_id")
+ assigned_to = Field(attr="assigned_to_id")
-class TaskSearchResultsSerializer(TaskSerializer):
- class Meta:
- model = Task
- fields = ('id', 'ref', 'subject', 'status', 'assigned_to')
+class TaskSearchResultsSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+ status = Field(attr="status_id")
+ assigned_to = Field(attr="assigned_to_id")
-class UserStorySearchResultsSerializer(UserStorySerializer):
- class Meta:
- model = UserStory
- fields = ('id', 'ref', 'subject', 'status', 'total_points',
- 'milestone_name', 'milestone_slug')
+class UserStorySearchResultsSerializer(serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ subject = Field()
+ status = Field(attr="status_id")
+ total_points = MethodField()
+ milestone_name = MethodField()
+ milestone_slug = MethodField()
+
+ def get_milestone_name(self, obj):
+ return obj.milestone.name if obj.milestone else None
+
+ def get_milestone_slug(self, obj):
+ return obj.milestone.slug if obj.milestone else None
+
+ def get_total_points(self, obj):
+ assert hasattr(obj, "total_points_attr"), \
+ "instance must have a total_points_attr attribute"
+
+ return obj.total_points_attr
-class WikiPageSearchResultsSerializer(WikiPageSerializer):
- class Meta:
- model = WikiPage
- fields = ('id', 'slug')
+class WikiPageSearchResultsSerializer(serializers.LightSerializer):
+ id = Field()
+ slug = Field()
diff --git a/taiga/searches/services.py b/taiga/searches/services.py
index f393844f..4dcda86f 100644
--- a/taiga/searches/services.py
+++ b/taiga/searches/services.py
@@ -19,6 +19,7 @@
from django.apps import apps
from django.conf import settings
from taiga.base.utils.db import to_tsquery
+from taiga.projects.userstories.utils import attach_total_points
MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150)
@@ -30,11 +31,13 @@ def search_user_stories(project, text):
"coalesce(userstories_userstory.description, '')) "
"@@ to_tsquery('english_nostop', %s)")
- if text:
- return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)])
- .filter(project_id=project.pk)[:MAX_RESULTS])
+ queryset = model_cls.objects.filter(project_id=project.pk)
- return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS]
+ if text:
+ queryset = queryset.extra(where=[where_clause], params=[to_tsquery(text)])
+
+ queryset = attach_total_points(queryset)
+ return queryset[:MAX_RESULTS]
def search_tasks(project, text):
diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py
index b0bf8e13..3e3bd6f4 100644
--- a/taiga/timeline/api.py
+++ b/taiga/timeline/api.py
@@ -18,10 +18,8 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
-from django.apps import apps
from taiga.base import response
-from taiga.base.api.utils import get_object_or_404
from taiga.base.api import ReadOnlyListViewSet
from . import serializers
@@ -36,7 +34,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
def get_content_type(self):
app_name, model = self.content_type.split(".", 1)
- return get_object_or_404(ContentType, app_label=app_name, model=model)
+ return ContentType.objects.get_by_natural_key(app_name, model)
def get_queryset(self):
ct = self.get_content_type()
diff --git a/taiga/timeline/migrations/0005_auto_20160706_0723.py b/taiga/timeline/migrations/0005_auto_20160706_0723.py
new file mode 100644
index 00000000..7ac9fa9c
--- /dev/null
+++ b/taiga/timeline/migrations/0005_auto_20160706_0723.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.2 on 2016-07-06 07:23
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('timeline', '0004_auto_20150603_1312'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='timeline',
+ name='created',
+ field=models.DateTimeField(db_index=True, default=django.utils.timezone.now),
+ ),
+ ]
diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py
index c71188f7..ebee7da5 100644
--- a/taiga/timeline/models.py
+++ b/taiga/timeline/models.py
@@ -20,13 +20,12 @@ from django.db import models
from django_pgjson.fields import JsonField
from django.utils import timezone
-from django.core.exceptions import ValidationError
-
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from taiga.projects.models import Project
+
class Timeline(models.Model):
content_type = models.ForeignKey(ContentType, related_name="content_type_timelines")
object_id = models.PositiveIntegerField()
@@ -36,12 +35,11 @@ class Timeline(models.Model):
project = models.ForeignKey(Project, null=True)
data = JsonField()
data_content_type = models.ForeignKey(ContentType, related_name="data_timelines")
- created = models.DateTimeField(default=timezone.now)
+ created = models.DateTimeField(default=timezone.now, db_index=True)
class Meta:
index_together = [('content_type', 'object_id', 'namespace'), ]
-
# Register all implementations
from .timeline_implementations import *
diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py
index a6be6944..07b1985a 100644
--- a/taiga/timeline/serializers.py
+++ b/taiga/timeline/serializers.py
@@ -16,26 +16,32 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.apps import apps
from django.contrib.auth import get_user_model
-from django.forms import widgets
from taiga.base.api import serializers
-from taiga.base.fields import JsonField
+from taiga.base.fields import Field, MethodField
from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
from . import models
-from . import service
-class TimelineSerializer(serializers.ModelSerializer):
+class TimelineSerializer(serializers.LightSerializer):
data = serializers.SerializerMethodField("get_data")
+ id = Field()
+ content_type = Field(attr="content_type_id")
+ object_id = Field()
+ namespace = Field()
+ event_type = Field()
+ project = Field(attr="project_id")
+ data = MethodField()
+ data_content_type = Field(attr="data_content_type_id")
+ created = Field()
class Meta:
model = models.Timeline
def get_data(self, obj):
- #Updates the data user info saved if the user exists
+ # Updates the data user info saved if the user exists
if hasattr(obj, "_prefetched_user"):
user = obj._prefetched_user
else:
diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py
index f99f795e..d3e81976 100644
--- a/taiga/timeline/service.py
+++ b/taiga/timeline/service.py
@@ -27,33 +27,32 @@ from functools import partial, wraps
from taiga.base.utils.db import get_typename_for_model_class
from taiga.celery import app
-from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
_timeline_impl_map = {}
-def _get_impl_key_from_model(model:Model, event_type:str):
+def _get_impl_key_from_model(model: Model, event_type: str):
if issubclass(model, Model):
typename = get_typename_for_model_class(model)
return _get_impl_key_from_typename(typename, event_type)
raise Exception("Not valid model parameter")
-def _get_impl_key_from_typename(typename:str, event_type:str):
+def _get_impl_key_from_typename(typename: str, event_type: str):
if isinstance(typename, str):
return "{0}.{1}".format(typename, event_type)
raise Exception("Not valid typename parameter")
-def build_user_namespace(user:object):
+def build_user_namespace(user: object):
return "{0}:{1}".format("user", user.id)
-def build_project_namespace(project:object):
+def build_project_namespace(project: object):
return "{0}:{1}".format("project", project.id)
-def _add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
+def _add_to_object_timeline(obj: object, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}):
assert isinstance(obj, Model), "obj must be a instance of Model"
assert isinstance(instance, Model), "instance must be a instance of Model"
from .models import Timeline
@@ -75,12 +74,12 @@ def _add_to_object_timeline(obj:object, instance:object, event_type:str, created
)
-def _add_to_objects_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
+def _add_to_objects_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}):
for obj in objects:
_add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data)
-def _push_to_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}):
+def _push_to_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}):
if isinstance(objects, Model):
_add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data)
elif isinstance(objects, QuerySet) or isinstance(objects, list):
@@ -111,10 +110,10 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id
except projectModel.DoesNotExist:
return
- ## Project timeline
+ # Project timeline
_push_to_timeline(project, obj, event_type, created_datetime,
- namespace=build_project_namespace(project),
- extra_data=extra_data)
+ namespace=build_project_namespace(project),
+ extra_data=extra_data)
project.refresh_totals()
@@ -122,14 +121,14 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id
related_people = obj.get_related_people()
_push_to_timeline(related_people, obj, event_type, created_datetime,
- namespace=build_user_namespace(user),
- extra_data=extra_data)
+ namespace=build_user_namespace(user),
+ extra_data=extra_data)
else:
# Actions not related with a project
- ## - Me
+ # - Me
_push_to_timeline(user, obj, event_type, created_datetime,
- namespace=build_user_namespace(user),
- extra_data=extra_data)
+ namespace=build_user_namespace(user),
+ extra_data=extra_data)
def get_timeline(obj, namespace=None):
@@ -141,7 +140,6 @@ def get_timeline(obj, namespace=None):
if namespace is not None:
timeline = timeline.filter(namespace=namespace)
- timeline = timeline.select_related("project")
timeline = timeline.order_by("-created", "-id")
return timeline
@@ -156,22 +154,22 @@ def filter_timeline_for_user(timeline, user):
# Filtering private project with some public parts
content_types = {
- "view_project": ContentType.objects.get(app_label="projects", model="project"),
- "view_milestones": ContentType.objects.get(app_label="milestones", model="milestone"),
- "view_us": ContentType.objects.get(app_label="userstories", model="userstory"),
- "view_tasks": ContentType.objects.get(app_label="tasks", model="task"),
- "view_issues": ContentType.objects.get(app_label="issues", model="issue"),
- "view_wiki_pages": ContentType.objects.get(app_label="wiki", model="wikipage"),
- "view_wiki_links": ContentType.objects.get(app_label="wiki", model="wikilink"),
+ "view_project": ContentType.objects.get_by_natural_key("projects", "project"),
+ "view_milestones": ContentType.objects.get_by_natural_key("milestones", "milestone"),
+ "view_us": ContentType.objects.get_by_natural_key("userstories", "userstory"),
+ "view_tasks": ContentType.objects.get_by_natural_key("tasks", "task"),
+ "view_issues": ContentType.objects.get_by_natural_key("issues", "issue"),
+ "view_wiki_pages": ContentType.objects.get_by_natural_key("wiki", "wikipage"),
+ "view_wiki_links": ContentType.objects.get_by_natural_key("wiki", "wikilink"),
}
for content_type_key, content_type in content_types.items():
tl_filter |= Q(project__is_private=True,
- project__anon_permissions__contains=[content_type_key],
- data_content_type=content_type)
+ project__anon_permissions__contains=[content_type_key],
+ data_content_type=content_type)
# There is no specific permission for seeing new memberships
- membership_content_type = ContentType.objects.get(app_label="projects", model="membership")
+ membership_content_type = ContentType.objects.get_by_natural_key(app_label="projects", model="membership")
tl_filter |= Q(project__is_private=True,
project__anon_permissions__contains=["view_project"],
data_content_type=membership_content_type)
@@ -214,7 +212,7 @@ def get_project_timeline(project, accessing_user=None):
return timeline
-def register_timeline_implementation(typename:str, event_type:str, fn=None):
+def register_timeline_implementation(typename: str, event_type: str, fn=None):
assert isinstance(typename, str), "typename must be a string"
assert isinstance(event_type, str), "event_type must be a string"
@@ -231,7 +229,6 @@ def register_timeline_implementation(typename:str, event_type:str, fn=None):
return _wrapper
-
def extract_project_info(instance):
return {
"id": instance.pk,
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index 98903ec1..3d584c53 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -22,7 +22,8 @@ from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers
-from taiga.base.fields import PgArrayField
+from taiga.base.fields import PgArrayField, Field, MethodField
+
from taiga.base.utils.thumbnails import get_thumbnail_url
from taiga.projects.models import Project
@@ -33,11 +34,10 @@ from .gravatar import get_gravatar_url
from collections import namedtuple
import re
-import serpy
######################################################
-## User
+# User
######################################################
class ContactProjectDetailSerializer(serializers.ModelSerializer):
@@ -139,19 +139,13 @@ class UserAdminSerializer(UserSerializer):
return user.owned_projects.filter(is_private=False).count()
-class UserBasicInfoSerializer(UserSerializer):
- class Meta:
- model = User
- fields = ("username", "full_name_display", "photo", "big_photo", "is_active", "id")
-
-
-class ListUserBasicInfoSerializer(serpy.Serializer):
- username = serpy.Field()
- full_name_display = serpy.MethodField()
- photo = serpy.MethodField()
- big_photo = serpy.MethodField()
- is_active = serpy.Field()
- id = serpy.Field()
+class UserBasicInfoSerializer(serializers.LightSerializer):
+ username = Field()
+ full_name_display = MethodField()
+ photo = MethodField()
+ big_photo = MethodField()
+ is_active = Field()
+ id = Field()
def get_full_name_display(self, obj):
return obj.get_full_name()
@@ -162,6 +156,12 @@ class ListUserBasicInfoSerializer(serpy.Serializer):
def get_big_photo(self, obj):
return get_big_photo_or_gravatar_url(obj)
+ def to_value(self, instance):
+ if instance is None:
+ return None
+
+ return super().to_value(instance)
+
class RecoverySerializer(serializers.Serializer):
token = serializers.CharField(max_length=200)
@@ -177,7 +177,7 @@ class CancelAccountSerializer(serializers.Serializer):
######################################################
-## Role
+# Role
######################################################
class RoleSerializer(serializers.ModelSerializer):
@@ -201,7 +201,7 @@ class ProjectRoleSerializer(serializers.ModelSerializer):
######################################################
-## Like
+# Like
######################################################
class HighLightedContentSerializer(serializers.Serializer):
diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py
index f15021a0..4648a73a 100644
--- a/taiga/webhooks/api.py
+++ b/taiga/webhooks/api.py
@@ -30,6 +30,7 @@ from taiga.base.decorators import detail_route
from . import models
from . import serializers
+from . import validators
from . import permissions
from . import tasks
@@ -37,6 +38,7 @@ from . import tasks
class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Webhook
serializer_class = serializers.WebhookSerializer
+ validator_class = validators.WebhookValidator
permission_classes = (permissions.WebhookPermission,)
filter_backends = (filters.IsProjectAdminFilterBackend,)
filter_fields = ("project",)
diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py
index ee0d8308..624e107c 100644
--- a/taiga/webhooks/serializers.py
+++ b/taiga/webhooks/serializers.py
@@ -19,63 +19,55 @@
from django.core.exceptions import ObjectDoesNotExist
from taiga.base.api import serializers
-from taiga.base.fields import PgArrayField, JsonField
-
+from taiga.base.fields import Field, MethodField
from taiga.front.templatetags.functions import resolve as resolve_front_url
-from taiga.projects.history import models as history_models
-from taiga.projects.issues import models as issue_models
-from taiga.projects.milestones import models as milestone_models
-from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.services import get_logo_big_thumbnail_url
-from taiga.projects.tasks import models as task_models
-from taiga.projects.tagging.fields import TagsField
-from taiga.projects.userstories import models as us_models
-from taiga.projects.wiki import models as wiki_models
from taiga.users.gravatar import get_gravatar_url
from taiga.users.services import get_photo_or_gravatar_url
-from .models import Webhook, WebhookLog
-
########################################################################
-## WebHooks
+# WebHooks
########################################################################
-class WebhookSerializer(serializers.ModelSerializer):
- logs_counter = serializers.SerializerMethodField("get_logs_counter")
-
- class Meta:
- model = Webhook
+class WebhookSerializer(serializers.LightSerializer):
+ id = Field()
+ project = Field(attr="project_id")
+ name = Field()
+ url = Field()
+ key = Field()
+ logs_counter = MethodField()
def get_logs_counter(self, obj):
return obj.logs.count()
-class WebhookLogSerializer(serializers.ModelSerializer):
- request_data = JsonField()
- request_headers = JsonField()
- response_headers = JsonField()
-
- class Meta:
- model = WebhookLog
+class WebhookLogSerializer(serializers.LightSerializer):
+ id = Field()
+ webhook = Field(attr="webhook_id")
+ url = Field()
+ status = Field()
+ request_data = Field()
+ request_headers = Field()
+ response_data = Field()
+ response_headers = Field()
+ duration = Field()
+ created = Field()
########################################################################
-## User
+# User
########################################################################
-class UserSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- permalink = serializers.SerializerMethodField("get_permalink")
- gravatar_url = serializers.SerializerMethodField("get_gravatar_url")
- username = serializers.SerializerMethodField("get_username")
- full_name = serializers.SerializerMethodField("get_full_name")
- photo = serializers.SerializerMethodField("get_photo")
-
- def get_pk(self, obj):
- return obj.pk
+class UserSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ permalink = MethodField()
+ gravatar_url = MethodField()
+ username = MethodField()
+ full_name = MethodField()
+ photo = MethodField()
def get_permalink(self, obj):
return resolve_front_url("user", obj.username)
@@ -84,7 +76,7 @@ class UserSerializer(serializers.Serializer):
return get_gravatar_url(obj.email)
def get_username(self, obj):
- return obj.get_username
+ return obj.get_username()
def get_full_name(self, obj):
return obj.get_full_name()
@@ -92,18 +84,22 @@ class UserSerializer(serializers.Serializer):
def get_photo(self, obj):
return get_photo_or_gravatar_url(obj)
+ def to_value(self, instance):
+ if instance is None:
+ return None
+
+ return super().to_value(instance)
+
+
########################################################################
-## Project
+# Project
########################################################################
-class ProjectSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- permalink = serializers.SerializerMethodField("get_permalink")
- name = serializers.SerializerMethodField("get_name")
- logo_big_url = serializers.SerializerMethodField("get_logo_big_url")
-
- def get_pk(self, obj):
- return obj.pk
+class ProjectSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ permalink = MethodField()
+ name = MethodField()
+ logo_big_url = MethodField()
def get_permalink(self, obj):
return resolve_front_url("project", obj.slug)
@@ -116,11 +112,11 @@ class ProjectSerializer(serializers.Serializer):
########################################################################
-## History Serializer
+# History Serializer
########################################################################
-class HistoryDiffField(serializers.Field):
- def to_native(self, value):
+class HistoryDiffField(Field):
+ def to_value(self, value):
# Tip: 'value' is the object returned by
# taiga.projects.history.models.HistoryEntry.values_diff()
@@ -137,21 +133,21 @@ class HistoryDiffField(serializers.Field):
return ret
-class HistoryEntrySerializer(serializers.ModelSerializer):
- diff = HistoryDiffField(source="values_diff")
-
- class Meta:
- model = history_models.HistoryEntry
- exclude = ("id", "type", "key", "is_hidden", "is_snapshot", "snapshot", "user", "delete_comment_user",
- "values", "created_at")
+class HistoryEntrySerializer(serializers.LightSerializer):
+ comment = Field()
+ comment_html = Field()
+ delete_comment_date = Field()
+ comment_versions = Field()
+ edit_comment_date = Field()
+ diff = HistoryDiffField(attr="values_diff")
########################################################################
-## _Misc_
+# _Misc_
########################################################################
-class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
- custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values")
+class CustomAttributesValuesWebhookSerializerMixin(serializers.LightSerializer):
+ custom_attributes_values = MethodField()
def custom_attributes_queryset(self, project):
raise NotImplementedError()
@@ -161,13 +157,13 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
ret = {}
for attr in custom_attributes:
value = values.get(str(attr["id"]), None)
- if value is not None:
+ if value is not None:
ret[attr["name"]] = value
return ret
try:
- values = obj.custom_attributes_values.attributes_values
+ 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)
@@ -175,10 +171,10 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
return None
-class RolePointsSerializer(serializers.Serializer):
- role = serializers.SerializerMethodField("get_role")
- name = serializers.SerializerMethodField("get_name")
- value = serializers.SerializerMethodField("get_value")
+class RolePointsSerializer(serializers.LightSerializer):
+ role = MethodField()
+ name = MethodField()
+ value = MethodField()
def get_role(self, obj):
return obj.role.name
@@ -190,16 +186,13 @@ class RolePointsSerializer(serializers.Serializer):
return obj.points.value
-class UserStoryStatusSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- slug = serializers.SerializerMethodField("get_slug")
- color = serializers.SerializerMethodField("get_color")
- is_closed = serializers.SerializerMethodField("get_is_closed")
- is_archived = serializers.SerializerMethodField("get_is_archived")
-
- def get_pk(self, obj):
- return obj.pk
+class UserStoryStatusSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ slug = MethodField()
+ color = MethodField()
+ is_closed = MethodField()
+ is_archived = MethodField()
def get_name(self, obj):
return obj.name
@@ -217,15 +210,12 @@ class UserStoryStatusSerializer(serializers.Serializer):
return obj.is_archived
-class TaskStatusSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- slug = serializers.SerializerMethodField("get_slug")
- color = serializers.SerializerMethodField("get_color")
- is_closed = serializers.SerializerMethodField("get_is_closed")
-
- def get_pk(self, obj):
- return obj.pk
+class TaskStatusSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ slug = MethodField()
+ color = MethodField()
+ is_closed = MethodField()
def get_name(self, obj):
return obj.name
@@ -240,15 +230,12 @@ class TaskStatusSerializer(serializers.Serializer):
return obj.is_closed
-class IssueStatusSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- slug = serializers.SerializerMethodField("get_slug")
- color = serializers.SerializerMethodField("get_color")
- is_closed = serializers.SerializerMethodField("get_is_closed")
-
- def get_pk(self, obj):
- return obj.pk
+class IssueStatusSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ slug = MethodField()
+ color = MethodField()
+ is_closed = MethodField()
def get_name(self, obj):
return obj.name
@@ -263,13 +250,10 @@ class IssueStatusSerializer(serializers.Serializer):
return obj.is_closed
-class IssueTypeSerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- color = serializers.SerializerMethodField("get_color")
-
- def get_pk(self, obj):
- return obj.pk
+class IssueTypeSerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ color = MethodField()
def get_name(self, obj):
return obj.name
@@ -278,13 +262,10 @@ class IssueTypeSerializer(serializers.Serializer):
return obj.color
-class PrioritySerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- color = serializers.SerializerMethodField("get_color")
-
- def get_pk(self, obj):
- return obj.pk
+class PrioritySerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ color = MethodField()
def get_name(self, obj):
return obj.name
@@ -293,13 +274,10 @@ class PrioritySerializer(serializers.Serializer):
return obj.color
-class SeveritySerializer(serializers.Serializer):
- id = serializers.SerializerMethodField("get_pk")
- name = serializers.SerializerMethodField("get_name")
- color = serializers.SerializerMethodField("get_color")
-
- def get_pk(self, obj):
- return obj.pk
+class SeveritySerializer(serializers.LightSerializer):
+ id = Field(attr="pk")
+ name = MethodField()
+ color = MethodField()
def get_name(self, obj):
return obj.name
@@ -309,57 +287,90 @@ class SeveritySerializer(serializers.Serializer):
########################################################################
-## Milestone
+# Milestone
########################################################################
-class MilestoneSerializer(serializers.ModelSerializer):
+class MilestoneSerializer(serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ slug = Field()
+ estimated_start = Field()
+ estimated_finish = Field()
+ created_date = Field()
+ modified_date = Field()
+ closed = Field()
+ disponibility = Field()
permalink = serializers.SerializerMethodField("get_permalink")
project = ProjectSerializer()
owner = UserSerializer()
- class Meta:
- model = milestone_models.Milestone
- exclude = ("order", "watchers")
-
def get_permalink(self, obj):
return resolve_front_url("taskboard", obj.project.slug, obj.slug)
########################################################################
-## User Story
+# User Story
########################################################################
-class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
- permalink = serializers.SerializerMethodField("get_permalink")
- tags = TagsField(default=[], required=False)
- external_reference = PgArrayField(required=False)
+class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
+ id = Field()
+ ref = Field()
project = ProjectSerializer()
+ is_closed = Field()
+ created_date = Field()
+ modified_date = Field()
+ finish_date = Field()
+ subject = Field()
+ client_requirement = Field()
+ team_requirement = Field()
+ generated_from_issue = Field(attr="generated_from_issue_id")
+ external_reference = Field()
+ tribe_gig = Field()
+ watchers = MethodField()
+ is_blocked = Field()
+ blocked_note = Field()
+ tags = Field()
+ permalink = serializers.SerializerMethodField("get_permalink")
owner = UserSerializer()
assigned_to = UserSerializer()
- points = RolePointsSerializer(source="role_points", many=True)
+ points = MethodField()
status = UserStoryStatusSerializer()
milestone = MilestoneSerializer()
- class Meta:
- model = us_models.UserStory
- exclude = ("backlog_order", "sprint_order", "kanban_order", "version", "total_watchers", "is_watcher")
-
def get_permalink(self, obj):
return resolve_front_url("userstory", obj.project.slug, obj.ref)
def custom_attributes_queryset(self, project):
return project.userstorycustomattributes.all()
+ def get_watchers(self, obj):
+ return list(obj.get_watchers().values_list("id", flat=True))
+
+ def get_points(self, obj):
+ return RolePointsSerializer(obj.role_points.all(), many=True).data
+
########################################################################
-## Task
+# Task
########################################################################
-class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
+class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ created_date = Field()
+ modified_date = Field()
+ finished_date = Field()
+ subject = Field()
+ us_order = Field()
+ taskboard_order = Field()
+ is_iocaine = Field()
+ external_reference = Field()
+ watchers = MethodField()
+ is_blocked = Field()
+ blocked_note = Field()
+ description = Field()
+ tags = Field()
permalink = serializers.SerializerMethodField("get_permalink")
- tags = TagsField(default=[], required=False)
project = ProjectSerializer()
owner = UserSerializer()
assigned_to = UserSerializer()
@@ -367,25 +378,32 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatch
user_story = UserStorySerializer()
milestone = MilestoneSerializer()
- class Meta:
- model = task_models.Task
- exclude = ("version", "total_watchers", "is_watcher")
-
def get_permalink(self, obj):
return resolve_front_url("task", obj.project.slug, obj.ref)
def custom_attributes_queryset(self, project):
return project.taskcustomattributes.all()
+ def get_watchers(self, obj):
+ return list(obj.get_watchers().values_list("id", flat=True))
+
########################################################################
-## Issue
+# Issue
########################################################################
-class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
- serializers.ModelSerializer):
+class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
+ id = Field()
+ ref = Field()
+ created_date = Field()
+ modified_date = Field()
+ finished_date = Field()
+ subject = Field()
+ external_reference = Field()
+ watchers = MethodField()
+ description = Field()
+ tags = Field()
permalink = serializers.SerializerMethodField("get_permalink")
- tags = TagsField(default=[], required=False)
project = ProjectSerializer()
milestone = MilestoneSerializer()
owner = UserSerializer()
@@ -395,30 +413,30 @@ class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatc
priority = PrioritySerializer()
severity = SeveritySerializer()
- class Meta:
- model = issue_models.Issue
- exclude = ("version", "total_watchers", "is_watcher")
-
def get_permalink(self, obj):
return resolve_front_url("issue", obj.project.slug, obj.ref)
def custom_attributes_queryset(self, project):
return project.issuecustomattributes.all()
+ def get_watchers(self, obj):
+ return list(obj.get_watchers().values_list("id", flat=True))
+
########################################################################
-## Wiki Page
+# Wiki Page
########################################################################
-class WikiPageSerializer(serializers.ModelSerializer):
+class WikiPageSerializer(serializers.LightSerializer):
+ id = Field()
+ slug = Field()
+ content = Field()
+ created_date = Field()
+ modified_date = Field()
permalink = serializers.SerializerMethodField("get_permalink")
project = ProjectSerializer()
owner = UserSerializer()
last_modifier = UserSerializer()
- class Meta:
- model = wiki_models.WikiPage
- exclude = ("watchers", "total_watchers", "is_watcher", "version")
-
def get_permalink(self, obj):
return resolve_front_url("wiki", obj.project.slug, obj.slug)
diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py
index 7990b928..334cd52d 100644
--- a/taiga/webhooks/tasks.py
+++ b/taiga/webhooks/tasks.py
@@ -149,5 +149,4 @@ def test_webhook(webhook_id, url, key, by, date):
data['by'] = UserSerializer(by).data
data['date'] = date
data['data'] = {"test": "test"}
-
return _send_request(webhook_id, url, key, data)
diff --git a/taiga/webhooks/validators.py b/taiga/webhooks/validators.py
new file mode 100644
index 00000000..b95e2e64
--- /dev/null
+++ b/taiga/webhooks/validators.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2014-2016 Andrey Antukh
+# Copyright (C) 2014-2016 Jesús Espino
+# Copyright (C) 2014-2016 David Barragán
+# Copyright (C) 2014-2016 Alejandro Alonso
+# 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 .
+
+from taiga.base.api import validators
+
+from .models import Webhook
+
+
+class WebhookValidator(validators.ModelValidator):
+ class Meta:
+ model = Webhook
diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py
index d9b391f8..4f93ea0c 100644
--- a/tests/integration/resources_permissions/test_issues_resources.py
+++ b/tests/integration/resources_permissions/test_issues_resources.py
@@ -22,7 +22,11 @@ import uuid
from django.core.urlresolvers import reverse
from taiga.projects import choices as project_choices
+from taiga.projects.models import Project
+from taiga.projects.utils import attach_extra_info as attach_project_extra_info
+from taiga.projects.issues.models import Issue
from taiga.projects.issues.serializers import IssueSerializer
+from taiga.projects.issues.utils import attach_extra_info as attach_issue_extra_info
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.base.utils import json
@@ -61,22 +65,29 @@ def data():
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex)
+ m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id)
+
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex)
+ m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
+
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex)
+ m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id)
+
m.blocked_project = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex,
blocked_code=project_choices.BLOCKED_BY_STAFF)
+ m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id)
m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms,
@@ -129,24 +140,31 @@ def data():
priority__project=m.public_project,
type__project=m.public_project,
milestone__project=m.public_project)
+ m.public_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.public_issue.id)
+
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_issue1 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue1.id)
+
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.private_issue2 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue2.id)
+
m.blocked_issue = f.IssueFactory(project=m.blocked_project,
status__project=m.blocked_project,
severity__project=m.blocked_project,
priority__project=m.blocked_project,
type__project=m.blocked_project,
milestone__project=m.blocked_project)
+ m.blocked_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.blocked_issue.id)
return m
@@ -443,24 +461,28 @@ def test_issue_put_update_with_project_change(client):
project1.save()
project2.save()
- membership1 = f.MembershipFactory(project=project1,
- user=user1,
- role__project=project1,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
- membership2 = f.MembershipFactory(project=project2,
- user=user1,
- role__project=project2,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
- membership3 = f.MembershipFactory(project=project1,
- user=user2,
- role__project=project1,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
- membership4 = f.MembershipFactory(project=project2,
- user=user3,
- role__project=project2,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id)
+ project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id)
+
+ f.MembershipFactory(project=project1,
+ user=user1,
+ role__project=project1,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=project2,
+ user=user1,
+ role__project=project2,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=project1,
+ user=user2,
+ role__project=project1,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=project2,
+ user=user3,
+ role__project=project2,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
issue = f.IssueFactory.create(project=project1)
+ issue = attach_issue_extra_info(Issue.objects.all()).get(id=issue.id)
url = reverse('issues-detail', kwargs={"pk": issue.pk})
diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py
index a1a06172..1a3ab5cb 100644
--- a/tests/integration/resources_permissions/test_milestones_resources.py
+++ b/tests/integration/resources_permissions/test_milestones_resources.py
@@ -22,8 +22,11 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
+from taiga.projects.models import Project
+from taiga.projects.utils import attach_extra_info as attach_project_extra_info
from taiga.projects.milestones.serializers import MilestoneSerializer
from taiga.projects.milestones.models import Milestone
+from taiga.projects.milestones.utils import attach_extra_info as attach_milestone_extra_info
from taiga.projects.notifications.services import add_watcher
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
@@ -56,44 +59,55 @@ def data():
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
+ m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id)
+
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner)
+ m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
+
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id)
+
m.blocked_project = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner,
blocked_code=project_choices.BLOCKED_BY_STAFF)
+ m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id)
- m.public_membership = f.MembershipFactory(project=m.public_project,
- user=m.project_member_with_perms,
- 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,
- role__project=m.private_project1,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ m.public_membership = f.MembershipFactory(
+ project=m.public_project,
+ user=m.project_member_with_perms,
+ 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,
+ 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,
role__project=m.private_project1,
role__permissions=[])
- m.private_membership2 = f.MembershipFactory(project=m.private_project2,
- user=m.project_member_with_perms,
- role__project=m.private_project2,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ m.private_membership2 = f.MembershipFactory(
+ project=m.private_project2,
+ user=m.project_member_with_perms,
+ 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,
role__project=m.private_project2,
role__permissions=[])
- m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
- user=m.project_member_with_perms,
- role__project=m.blocked_project,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ m.blocked_membership = f.MembershipFactory(
+ project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.blocked_project,
user=m.project_member_without_perms,
role__project=m.blocked_project,
@@ -112,13 +126,17 @@ def data():
is_admin=True)
f.MembershipFactory(project=m.blocked_project,
- user=m.project_owner,
- is_admin=True)
+ user=m.project_owner,
+ is_admin=True)
m.public_milestone = f.MilestoneFactory(project=m.public_project)
+ m.public_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.public_milestone.id)
m.private_milestone1 = f.MilestoneFactory(project=m.private_project1)
+ m.private_milestone1 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone1.id)
m.private_milestone2 = f.MilestoneFactory(project=m.private_project2)
+ m.private_milestone2 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone2.id)
m.blocked_milestone = f.MilestoneFactory(project=m.blocked_project)
+ m.blocked_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.blocked_milestone.id)
return m
@@ -422,16 +440,16 @@ def test_milestone_watchers_list(client, data):
def test_milestone_watchers_retrieve(client, data):
add_watcher(data.public_milestone, data.project_owner)
public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_watcher(data.private_milestone1, data.project_owner)
private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_watcher(data.private_milestone2, data.project_owner)
private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_watcher(data.blocked_milestone, data.project_owner)
blocked_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.blocked_milestone.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
users = [
None,
diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py
index 949893b6..1410c86d 100644
--- a/tests/integration/resources_permissions/test_projects_resource.py
+++ b/tests/integration/resources_permissions/test_projects_resource.py
@@ -22,8 +22,10 @@ from django.apps import apps
from taiga.base.utils import json
from taiga.projects import choices as project_choices
+from taiga.projects import models as project_models
from taiga.projects.serializers import ProjectSerializer
from taiga.permissions.choices import MEMBERS_PERMISSIONS
+from taiga.projects.utils import attach_extra_info
from tests import factories as f
from tests.utils import helper_test_http_method, helper_test_http_method_and_count
@@ -45,19 +47,26 @@ def data():
m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=['view_project'],
public_permissions=['view_project'])
+ m.public_project = attach_extra_info(project_models.Project.objects.all()).get(id=m.public_project.id)
+
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=['view_project'],
public_permissions=['view_project'],
owner=m.project_owner)
+ m.private_project1 = attach_extra_info(project_models.Project.objects.all()).get(id=m.private_project1.id)
+
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner)
+ m.private_project2 = attach_extra_info(project_models.Project.objects.all()).get(id=m.private_project2.id)
+
m.blocked_project = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner,
blocked_code=project_choices.BLOCKED_BY_STAFF)
+ m.blocked_project = attach_extra_info(project_models.Project.objects.all()).get(id=m.blocked_project.id)
f.RoleFactory(project=m.public_project)
diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py
index 5eaf5243..4d6427dd 100644
--- a/tests/integration/resources_permissions/test_tasks_resources.py
+++ b/tests/integration/resources_permissions/test_tasks_resources.py
@@ -23,12 +23,16 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
+from taiga.projects.models import Project
from taiga.projects.tasks.serializers import TaskSerializer
+from taiga.projects.tasks.models import Task
+from taiga.projects.tasks.utils import attach_extra_info as attach_task_extra_info
+from taiga.projects.utils import attach_extra_info as attach_project_extra_info
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin
from tests import factories as f
-from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals
+from tests.utils import helper_test_http_method, reconnect_signals
from taiga.projects.votes.services import add_vote
from taiga.projects.notifications.services import add_watcher
@@ -38,10 +42,6 @@ import pytest
pytestmark = pytest.mark.django_db
-def setup_function(function):
- disconnect_signals()
-
-
def setup_function(function):
reconnect_signals()
@@ -61,47 +61,61 @@ def data():
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex)
+ m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id)
+
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex)
+ m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
+
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex)
+ m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id)
+
m.blocked_project = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex,
blocked_code=project_choices.BLOCKED_BY_STAFF)
+ m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id)
- m.public_membership = f.MembershipFactory(project=m.public_project,
- user=m.project_member_with_perms,
- 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,
- 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,
- role__project=m.private_project1,
- role__permissions=[])
- m.private_membership2 = f.MembershipFactory(project=m.private_project2,
- user=m.project_member_with_perms,
- 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,
- role__project=m.private_project2,
- role__permissions=[])
- m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
- user=m.project_member_with_perms,
- role__project=m.blocked_project,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ m.public_membership = f.MembershipFactory(
+ project=m.public_project,
+ user=m.project_member_with_perms,
+ 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,
+ 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,
+ role__project=m.private_project1,
+ role__permissions=[])
+ m.private_membership2 = f.MembershipFactory(
+ project=m.private_project2,
+ user=m.project_member_with_perms,
+ 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,
+ role__project=m.private_project2,
+ role__permissions=[])
+ m.blocked_membership = f.MembershipFactory(
+ project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.blocked_project,
user=m.project_member_without_perms,
role__project=m.blocked_project,
@@ -120,8 +134,8 @@ def data():
is_admin=True)
f.MembershipFactory(project=m.blocked_project,
- user=m.project_owner,
- is_admin=True)
+ user=m.project_owner,
+ is_admin=True)
milestone_public_task = f.MilestoneFactory(project=m.public_project)
milestone_private_task1 = f.MilestoneFactory(project=m.private_project1)
@@ -133,21 +147,28 @@ def data():
milestone=milestone_public_task,
user_story__project=m.public_project,
user_story__milestone=milestone_public_task)
+ m.public_task = attach_task_extra_info(Task.objects.all()).get(id=m.public_task.id)
+
m.private_task1 = f.TaskFactory(project=m.private_project1,
status__project=m.private_project1,
milestone=milestone_private_task1,
user_story__project=m.private_project1,
user_story__milestone=milestone_private_task1)
+ m.private_task1 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task1.id)
+
m.private_task2 = f.TaskFactory(project=m.private_project2,
status__project=m.private_project2,
milestone=milestone_private_task2,
user_story__project=m.private_project2,
user_story__milestone=milestone_private_task2)
+ m.private_task2 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task2.id)
+
m.blocked_task = f.TaskFactory(project=m.blocked_project,
- status__project=m.blocked_project,
- milestone=milestone_blocked_task,
- user_story__project=m.blocked_project,
- user_story__milestone=milestone_blocked_task)
+ status__project=m.blocked_project,
+ milestone=milestone_blocked_task,
+ user_story__project=m.blocked_project,
+ user_story__milestone=milestone_blocked_task)
+ m.blocked_task = attach_task_extra_info(Task.objects.all()).get(id=m.blocked_task.id)
m.public_project.default_task_status = m.public_task.status
m.public_project.save()
@@ -404,24 +425,28 @@ def test_task_put_update_with_project_change(client):
project1.save()
project2.save()
- membership1 = f.MembershipFactory(project=project1,
- user=user1,
- role__project=project1,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
- membership2 = f.MembershipFactory(project=project2,
- user=user1,
- role__project=project2,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
- membership3 = f.MembershipFactory(project=project1,
- user=user2,
- role__project=project1,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
- membership4 = f.MembershipFactory(project=project2,
- user=user3,
- role__project=project2,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id)
+ project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id)
+
+ f.MembershipFactory(project=project1,
+ user=user1,
+ role__project=project1,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=project2,
+ user=user1,
+ role__project=project2,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=project1,
+ user=user2,
+ role__project=project1,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=project2,
+ user=user3,
+ role__project=project2,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
task = f.TaskFactory.create(project=project1)
+ task = attach_task_extra_info(Task.objects.all()).get(id=task.id)
url = reverse('tasks-detail', kwargs={"pk": task.pk})
@@ -739,17 +764,17 @@ def test_task_voters_list(client, data):
def test_task_voters_retrieve(client, data):
add_vote(data.public_task, data.project_owner)
public_url = reverse('task-voters-detail', kwargs={"resource_id": data.public_task.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_vote(data.private_task1, data.project_owner)
private_url1 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task1.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_vote(data.private_task2, data.project_owner)
private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_vote(data.blocked_task, data.project_owner)
blocked_url = reverse('task-voters-detail', kwargs={"resource_id": data.blocked_task.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
users = [
None,
@@ -844,17 +869,17 @@ def test_task_watchers_list(client, data):
def test_task_watchers_retrieve(client, data):
add_watcher(data.public_task, data.project_owner)
public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_watcher(data.private_task1, data.project_owner)
private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_watcher(data.private_task2, data.project_owner)
private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_watcher(data.blocked_task, data.project_owner)
blocked_url = reverse('task-watchers-detail', kwargs={"resource_id": data.blocked_task.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
users = [
None,
data.registered_user,
diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py
index c9f95a31..4eb0c416 100644
--- a/tests/integration/resources_permissions/test_userstories_resources.py
+++ b/tests/integration/resources_permissions/test_userstories_resources.py
@@ -23,7 +23,11 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects import choices as project_choices
+from taiga.projects.models import Project
+from taiga.projects.utils import attach_extra_info as attach_project_extra_info
+from taiga.projects.userstories.models import UserStory
from taiga.projects.userstories.serializers import UserStorySerializer
+from taiga.projects.userstories.utils import attach_extra_info as attach_userstory_extra_info
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin
@@ -61,47 +65,58 @@ def data():
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex)
+ m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id)
+
m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex)
+ m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
+
m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[],
public_permissions=[],
owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex)
- m.blocked_project = f.ProjectFactory(is_private=True,
- anon_permissions=[],
- public_permissions=[],
- owner=m.project_owner,
- userstories_csv_uuid=uuid.uuid4().hex,
- blocked_code=project_choices.BLOCKED_BY_STAFF)
+ m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id)
- m.public_membership = f.MembershipFactory(project=m.public_project,
- user=m.project_member_with_perms,
- 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,
- role__project=m.private_project1,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ m.blocked_project = f.ProjectFactory(is_private=True,
+ anon_permissions=[],
+ public_permissions=[],
+ owner=m.project_owner,
+ userstories_csv_uuid=uuid.uuid4().hex,
+ blocked_code=project_choices.BLOCKED_BY_STAFF)
+ m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id)
+
+ m.public_membership = f.MembershipFactory(
+ project=m.public_project,
+ user=m.project_member_with_perms,
+ 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,
+ 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,
role__project=m.private_project1,
role__permissions=[])
- m.private_membership2 = f.MembershipFactory(project=m.private_project2,
- user=m.project_member_with_perms,
- role__project=m.private_project2,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ m.private_membership2 = f.MembershipFactory(
+ project=m.private_project2,
+ user=m.project_member_with_perms,
+ 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,
role__project=m.private_project2,
role__permissions=[])
- m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
- user=m.project_member_with_perms,
- role__project=m.blocked_project,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ m.blocked_membership = f.MembershipFactory(
+ project=m.blocked_project,
+ user=m.project_member_with_perms,
+ role__project=m.blocked_project,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.blocked_project,
user=m.project_member_without_perms,
role__project=m.blocked_project,
@@ -120,8 +135,8 @@ def data():
is_admin=True)
f.MembershipFactory(project=m.blocked_project,
- user=m.project_owner,
- is_admin=True)
+ user=m.project_owner,
+ is_admin=True)
m.public_points = f.PointsFactory(project=m.public_project)
m.private_points1 = f.PointsFactory(project=m.private_project1)
@@ -144,15 +159,19 @@ def data():
user_story__milestone__project=m.private_project2,
user_story__status__project=m.private_project2)
m.blocked_role_points = f.RolePointsFactory(role=m.blocked_project.roles.all()[0],
- points=m.blocked_points,
- user_story__project=m.blocked_project,
- user_story__milestone__project=m.blocked_project,
- user_story__status__project=m.blocked_project)
+ points=m.blocked_points,
+ user_story__project=m.blocked_project,
+ user_story__milestone__project=m.blocked_project,
+ user_story__status__project=m.blocked_project)
m.public_user_story = m.public_role_points.user_story
+ m.public_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.public_user_story.id)
m.private_user_story1 = m.private_role_points1.user_story
+ m.private_user_story1 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story1.id)
m.private_user_story2 = m.private_role_points2.user_story
+ m.private_user_story2 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story2.id)
m.blocked_user_story = m.blocked_role_points.user_story
+ m.blocked_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.blocked_user_story.id)
return m
@@ -380,24 +399,28 @@ def test_user_story_put_update_with_project_change(client):
project1.save()
project2.save()
- membership1 = f.MembershipFactory(project=project1,
- user=user1,
- role__project=project1,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
- membership2 = f.MembershipFactory(project=project2,
- user=user1,
- role__project=project2,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
- membership3 = f.MembershipFactory(project=project1,
- user=user2,
- role__project=project1,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
- membership4 = f.MembershipFactory(project=project2,
- user=user3,
- role__project=project2,
- role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id)
+ project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id)
+
+ f.MembershipFactory(project=project1,
+ user=user1,
+ role__project=project1,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=project2,
+ user=user1,
+ role__project=project2,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=project1,
+ user=user2,
+ role__project=project1,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
+ f.MembershipFactory(project=project2,
+ user=user3,
+ role__project=project2,
+ role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
us = f.UserStoryFactory.create(project=project1)
+ us = attach_userstory_extra_info(UserStory.objects.all()).get(id=us.id)
url = reverse('userstories-detail', kwargs={"pk": us.pk})
@@ -592,7 +615,6 @@ def test_user_story_delete(client, data):
assert results == [401, 403, 403, 451]
-
def test_user_story_action_bulk_create(client, data):
url = reverse('userstories-bulk-create')
@@ -746,7 +768,7 @@ def test_user_story_voters_retrieve(client, data):
add_vote(data.blocked_user_story, data.project_owner)
blocked_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.blocked_user_story.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
users = [
None,
data.registered_user,
@@ -840,16 +862,16 @@ def test_userstory_watchers_list(client, data):
def test_userstory_watchers_retrieve(client, data):
add_watcher(data.public_user_story, data.project_owner)
public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_watcher(data.private_user_story1, data.project_owner)
private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_watcher(data.private_user_story2, data.project_owner)
private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
add_watcher(data.blocked_user_story, data.project_owner)
blocked_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.blocked_user_story.pk,
- "pk": data.project_owner.pk})
+ "pk": data.project_owner.pk})
users = [
None,
diff --git a/tests/integration/resources_permissions/test_webhooks_resources.py b/tests/integration/resources_permissions/test_webhooks_resources.py
index afc3597d..34d4cf00 100644
--- a/tests/integration/resources_permissions/test_webhooks_resources.py
+++ b/tests/integration/resources_permissions/test_webhooks_resources.py
@@ -242,16 +242,19 @@ def test_webhook_action_test(client, data):
]
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
+ _send_request_mock.return_value = data.webhooklog1
results = helper_test_http_method(client, 'post', url1, None, users)
assert results == [404, 404, 200]
assert _send_request_mock.called is True
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
+ _send_request_mock.return_value = data.webhooklog1
results = helper_test_http_method(client, 'post', url2, None, users)
assert results == [404, 404, 404]
assert _send_request_mock.called is False
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
+ _send_request_mock.return_value = data.webhooklog1
results = helper_test_http_method(client, 'post', blocked_url, None, users)
assert results == [404, 404, 451]
assert _send_request_mock.called is False
diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py
index ad28cc16..18562a8e 100644
--- a/tests/integration/test_milestones.py
+++ b/tests/integration/test_milestones.py
@@ -43,7 +43,7 @@ def test_update_milestone_with_userstories_list(client):
form_data = {
"name": "test",
- "user_stories": [UserStorySerializer(us).data]
+ "user_stories": [{"id": us.id}]
}
client.login(user)
diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py
index 661b68ff..d8943e71 100644
--- a/tests/integration/test_notifications.py
+++ b/tests/integration/test_notifications.py
@@ -790,7 +790,7 @@ def test_watchers_assignation_for_issue(client):
assert response.status_code == 400
issue = f.create_issue(project=project1, owner=user1)
- data = dict(IssueSerializer(issue).data)
+ data = {}
data["id"] = None
data["version"] = None
data["watchers"] = [user1.pk, user2.pk]
@@ -802,8 +802,7 @@ def test_watchers_assignation_for_issue(client):
# Test the impossible case when project is not
# exists in create request, and validator works as expected
issue = f.create_issue(project=project1, owner=user1)
- data = dict(IssueSerializer(issue).data)
-
+ data = {}
data["id"] = None
data["watchers"] = [user1.pk, user2.pk]
data["project"] = None
@@ -842,10 +841,11 @@ def test_watchers_assignation_for_task(client):
assert response.status_code == 400
task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1)
- data = dict(TaskSerializer(task).data)
- data["id"] = None
- data["version"] = None
- data["watchers"] = [user1.pk, user2.pk]
+ data = {
+ "id": None,
+ "version": None,
+ "watchers": [user1.pk, user2.pk]
+ }
url = reverse("tasks-list")
response = client.json.post(url, json.dumps(data))
@@ -854,11 +854,11 @@ def test_watchers_assignation_for_task(client):
# Test the impossible case when project is not
# exists in create request, and validator works as expected
task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1)
- data = dict(TaskSerializer(task).data)
-
- data["id"] = None
- data["watchers"] = [user1.pk, user2.pk]
- data["project"] = None
+ data = {
+ "id": None,
+ "watchers": [user1.pk, user2.pk],
+ "project": None
+ }
url = reverse("tasks-list")
response = client.json.post(url, json.dumps(data))
@@ -894,10 +894,11 @@ def test_watchers_assignation_for_us(client):
assert response.status_code == 400
us = f.create_userstory(project=project1, owner=user1, status__project=project1)
- data = dict(UserStorySerializer(us).data)
- data["id"] = None
- data["version"] = None
- data["watchers"] = [user1.pk, user2.pk]
+ data = {
+ "id": None,
+ "version": None,
+ "watchers": [user1.pk, user2.pk]
+ }
url = reverse("userstories-list")
response = client.json.post(url, json.dumps(data))
@@ -906,11 +907,11 @@ def test_watchers_assignation_for_us(client):
# Test the impossible case when project is not
# exists in create request, and validator works as expected
us = f.create_userstory(project=project1, owner=user1, status__project=project1)
- data = dict(UserStorySerializer(us).data)
-
- data["id"] = None
- data["watchers"] = [user1.pk, user2.pk]
- data["project"] = None
+ data = {
+ "id": None,
+ "watchers": [user1.pk, user2.pk],
+ "project": None
+ }
url = reverse("userstories-list")
response = client.json.post(url, json.dumps(data))
diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py
index 7eac9b06..10805f8e 100644
--- a/tests/integration/test_userstories.py
+++ b/tests/integration/test_userstories.py
@@ -17,7 +17,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import copy
import uuid
import csv
@@ -26,7 +25,6 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json
from taiga.projects.userstories import services, models
-from taiga.projects.userstories.serializers import UserStorySerializer
from .. import factories as f
@@ -108,7 +106,7 @@ def test_create_userstory_without_default_values(client):
client.login(user)
response = client.json.post(url, json.dumps(data))
assert response.status_code == 201
- assert response.data['status'] == None
+ assert response.data['status'] is None
def test_api_delete_userstory(client):
@@ -211,7 +209,7 @@ def test_api_update_milestone_in_bulk_invalid_milestone(client):
f.MembershipFactory.create(project=project, user=project.owner, is_admin=True)
us1 = f.create_userstory(project=project)
us2 = f.create_userstory(project=project)
- m1 = f.MilestoneFactory.create(project=project)
+ f.MilestoneFactory.create(project=project)
m2 = f.MilestoneFactory.create()
url = reverse("userstories-bulk-update-milestone")
@@ -262,48 +260,53 @@ def test_update_userstory_points(client):
f.MembershipFactory.create(project=project, user=user1, role=role1, is_admin=True)
f.MembershipFactory.create(project=project, user=user2, role=role2)
- f.PointsFactory.create(project=project, value=None)
- f.PointsFactory.create(project=project, value=1)
+ points1 = f.PointsFactory.create(project=project, value=None)
+ points2 = f.PointsFactory.create(project=project, value=1)
points3 = f.PointsFactory.create(project=project, value=2)
- us = f.UserStoryFactory.create(project=project,owner=user1, status__project=project,
+ us = f.UserStoryFactory.create(project=project, owner=user1, status__project=project,
milestone__project=project)
- usdata = UserStorySerializer(us).data
url = reverse("userstories-detail", args=[us.pk])
client.login(user1)
# invalid role
- data = {}
- data["version"] = usdata["version"]
- data["points"] = copy.copy(usdata["points"])
- data["points"].update({"222222": points3.pk})
+ data = {
+ "version": us.version,
+ "points": {
+ str(role1.pk): points1.pk,
+ str(role2.pk): points2.pk,
+ "222222": points3.pk
+ }
+ }
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
# invalid point
- data = {}
- data["version"] = usdata["version"]
- data["points"] = copy.copy(usdata["points"])
- data["points"].update({str(role1.pk): "999999"})
+ data = {
+ "version": us.version,
+ "points": {
+ str(role1.pk): 999999,
+ str(role2.pk): points2.pk
+ }
+ }
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 400
# Api should save successful
- data = {}
- data["version"] = usdata["version"]
- data["points"] = copy.copy(usdata["points"])
- data["points"].update({str(role1.pk): points3.pk})
+ data = {
+ "version": us.version,
+ "points": {
+ str(role1.pk): points3.pk,
+ str(role2.pk): points2.pk
+ }
+ }
response = client.json.patch(url, json.dumps(data))
- us = models.UserStory.objects.get(pk=us.pk)
- usdatanew = UserStorySerializer(us).data
- assert response.status_code == 200, str(response.content)
- assert response.data["points"] == usdatanew['points']
- assert response.data["points"] != usdata['points']
+ assert response.data["points"][str(role1.pk)] == points3.pk
def test_update_userstory_rolepoints_on_add_new_role(client):
@@ -438,32 +441,32 @@ def test_api_filters_data(client):
# | 9 | user2 | user3 | tag0 |
# ------------------------------------------------------
- user_story0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
- status=status3, tags=[tag1])
- user_story1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None,
- status=status3, tags=[tag2])
- user_story2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None,
- status=status1, tags=[tag1, tag2])
- user_story3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
- status=status0, tags=[tag3])
- user_story4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1,
- status=status0, tags=[tag1, tag2, tag3])
- user_story5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1,
- status=status2, tags=[tag3])
- user_story6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1,
- status=status3, tags=[tag1, tag2])
- user_story7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2,
- status=status0, tags=[tag3])
- user_story8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2,
- status=status3, tags=[tag1])
- user_story9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3,
- status=status1, tags=[tag0])
+ f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
+ status=status3, tags=[tag1])
+ f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None,
+ status=status3, tags=[tag2])
+ f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None,
+ status=status1, tags=[tag1, tag2])
+ f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
+ status=status0, tags=[tag3])
+ f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1,
+ status=status0, tags=[tag1, tag2, tag3])
+ f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1,
+ status=status2, tags=[tag3])
+ f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1,
+ status=status3, tags=[tag1, tag2])
+ f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2,
+ status=status0, tags=[tag3])
+ f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2,
+ status=status3, tags=[tag1])
+ f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3,
+ status=status1, tags=[tag0])
url = reverse("userstories-filters-data") + "?project={}".format(project.id)
client.login(user1)
- ## No filter
+ # No filter
response = client.get(url)
assert response.status_code == 200
@@ -471,7 +474,7 @@ def test_api_filters_data(client):
assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4
assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3
- assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4
+ assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 4
assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3
assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2
assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1
@@ -486,7 +489,7 @@ def test_api_filters_data(client):
assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4
assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4
- ## Filter ((status0 or status3)
+ # Filter ((status0 or status3)
response = client.get(url + "&status={},{}".format(status3.id, status0.id))
assert response.status_code == 200
@@ -494,7 +497,7 @@ def test_api_filters_data(client):
assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 3
assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1
- assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 3
+ assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 3
assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2
assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2
assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0
@@ -509,7 +512,7 @@ def test_api_filters_data(client):
assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3
assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3
- ## Filter ((tag1 and tag2) and (user1 or user2))
+ # Filter ((tag1 and tag2) and (user1 or user2))
response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id))
assert response.status_code == 200
@@ -517,7 +520,7 @@ def test_api_filters_data(client):
assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1
assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1
- assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0
+ assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 0
assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2
assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0
assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0
@@ -556,7 +559,7 @@ def test_custom_fields_csv_generation():
attr = f.UserStoryCustomAttributeFactory.create(project=project, name="attr1", description="desc")
us = f.UserStoryFactory.create(project=project)
attr_values = us.custom_attributes_values
- attr_values.attributes_values = {str(attr.id):"val1"}
+ attr_values.attributes_values = {str(attr.id): "val1"}
attr_values.save()
queryset = project.user_stories.all()
data = services.userstories_to_csv(project, queryset)
@@ -595,7 +598,7 @@ def test_update_userstory_update_watchers(client):
client.login(user=us.owner)
url = reverse("userstories-detail", kwargs={"pk": us.pk})
- data = {"watchers": [watching_user.id], "version":1}
+ data = {"watchers": [watching_user.id], "version": 1}
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200
@@ -614,7 +617,7 @@ def test_update_userstory_remove_watchers(client):
client.login(user=us.owner)
url = reverse("userstories-detail", kwargs={"pk": us.pk})
- data = {"watchers": [], "version":1}
+ data = {"watchers": [], "version": 1}
response = client.json.patch(url, json.dumps(data))
assert response.status_code == 200
@@ -634,7 +637,7 @@ def test_update_userstory_update_tribe_gig(client):
"id": 2,
"title": "This is a gig test title"
},
- "version":1
+ "version": 1
}
client.login(user=us.owner)
diff --git a/tests/integration/test_webhooks_issues.py b/tests/integration/test_webhooks_issues.py
index 491ec5b4..8789408d 100644
--- a/tests/integration/test_webhooks_issues.py
+++ b/tests/integration/test_webhooks_issues.py
@@ -19,7 +19,6 @@
import pytest
from unittest.mock import patch
-from unittest.mock import Mock
from .. import factories as f
@@ -29,8 +28,6 @@ from taiga.projects.history import services
pytestmark = pytest.mark.django_db(transaction=True)
-from taiga.base.utils import json
-
def test_webhooks_when_create_issue(settings):
settings.WEBHOOKS_ENABLED = True
project = f.ProjectFactory()
@@ -79,7 +76,7 @@ def test_webhooks_when_update_issue(settings):
assert data["data"]["subject"] == obj.subject
assert data["change"]["comment"] == "test_comment"
assert data["change"]["diff"]["subject"]["to"] == data["data"]["subject"]
- assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"]
+ assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"]
def test_webhooks_when_delete_issue(settings):
diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py
index 349a912c..cc88552f 100644
--- a/tests/unit/test_serializer_mixins.py
+++ b/tests/unit/test_serializer_mixins.py
@@ -19,46 +19,43 @@
import pytest
-from .. import factories as f
from django.db import models
-from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
-from taiga.projects.models import Project
+from taiga.base.api.validators import ModelValidator
+from taiga.projects.validators import DuplicatedNameInProjectValidator
pytestmark = pytest.mark.django_db(transaction=True)
-import factory
-
class AuxProjectModel(models.Model):
pass
+
class AuxModelWithNameAttribute(models.Model):
name = models.CharField(max_length=255, null=False, blank=False)
project = models.ForeignKey(AuxProjectModel, null=False, blank=False)
-class AuxSerializer(ValidateDuplicatedNameInProjectMixin):
+class AuxValidator(DuplicatedNameInProjectValidator, ModelValidator):
class Meta:
model = AuxModelWithNameAttribute
-
def test_duplicated_name_validation():
project = AuxProjectModel.objects.create()
- instance_1 = AuxModelWithNameAttribute.objects.create(name="1", project=project)
+ AuxModelWithNameAttribute.objects.create(name="1", project=project)
instance_2 = AuxModelWithNameAttribute.objects.create(name="2", project=project)
# No duplicated_name
- serializer = AuxSerializer(data={"name": "3", "project": project.id})
+ validator = AuxValidator(data={"name": "3", "project": project.id})
- assert serializer.is_valid()
+ assert validator.is_valid()
# Create duplicated_name
- serializer = AuxSerializer(data={"name": "1", "project": project.id})
+ validator = AuxValidator(data={"name": "1", "project": project.id})
- assert not serializer.is_valid()
+ assert not validator.is_valid()
# Update name to existing one
- serializer = AuxSerializer(data={"id": instance_2.id, "name": "1","project": project.id})
+ validator = AuxValidator(data={"id": instance_2.id, "name": "1", "project": project.id})
- assert not serializer.is_valid()
+ assert not validator.is_valid()