diff --git a/taiga/auth/api.py b/taiga/auth/api.py
index 5d14d18f..df077b52 100644
--- a/taiga/auth/api.py
+++ b/taiga/auth/api.py
@@ -22,15 +22,16 @@ from enum import Enum
from django.utils.translation import ugettext as _
from django.conf import settings
+from taiga.base.api import validators
from taiga.base.api import serializers
from taiga.base.api import viewsets
from taiga.base.decorators import list_route
from taiga.base import exceptions as exc
from taiga.base import response
-from .serializers import PublicRegisterSerializer
-from .serializers import PrivateRegisterForExistingUserSerializer
-from .serializers import PrivateRegisterForNewUserSerializer
+from .validators import PublicRegisterValidator
+from .validators import PrivateRegisterForExistingUserValidator
+from .validators import PrivateRegisterForNewUserValidator
from .services import private_register_for_existing_user
from .services import private_register_for_new_user
@@ -44,7 +45,7 @@ from .permissions import AuthPermission
def _parse_data(data:dict, *, cls):
"""
Generic function for parse user data using
- specified serializer on `cls` keyword parameter.
+ specified validator on `cls` keyword parameter.
Raises: RequestValidationError exception if
some errors found when data is validated.
@@ -52,21 +53,21 @@ def _parse_data(data:dict, *, cls):
Returns the parsed data.
"""
- serializer = cls(data=data)
- if not serializer.is_valid():
- raise exc.RequestValidationError(serializer.errors)
- return serializer.data
+ validator = cls(data=data)
+ if not validator.is_valid():
+ raise exc.RequestValidationError(validator.errors)
+ return validator.data
# Parse public register data
-parse_public_register_data = partial(_parse_data, cls=PublicRegisterSerializer)
+parse_public_register_data = partial(_parse_data, cls=PublicRegisterValidator)
# Parse private register data for existing user
parse_private_register_for_existing_user_data = \
- partial(_parse_data, cls=PrivateRegisterForExistingUserSerializer)
+ partial(_parse_data, cls=PrivateRegisterForExistingUserValidator)
# Parse private register data for new user
parse_private_register_for_new_user_data = \
- partial(_parse_data, cls=PrivateRegisterForNewUserSerializer)
+ partial(_parse_data, cls=PrivateRegisterForNewUserValidator)
class RegisterTypeEnum(Enum):
@@ -81,10 +82,10 @@ def parse_register_type(userdata:dict) -> str:
"""
# Create adhoc inner serializer for avoid parse
# manually the user data.
- class _serializer(serializers.Serializer):
+ class _validator(validators.Validator):
existing = serializers.BooleanField()
- instance = _serializer(data=userdata)
+ instance = _validator(data=userdata)
if not instance.is_valid():
raise exc.RequestValidationError(instance.errors)
diff --git a/taiga/auth/serializers.py b/taiga/auth/validators.py
similarity index 72%
rename from taiga/auth/serializers.py
rename to taiga/auth/validators.py
index 8e8df4e2..a18dc4bc 100644
--- a/taiga/auth/serializers.py
+++ b/taiga/auth/validators.py
@@ -16,16 +16,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.core import validators
-from django.core.exceptions import ValidationError
+from django.core import validators as core_validators
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
import re
-class BaseRegisterSerializer(serializers.Serializer):
+class BaseRegisterValidator(validators.Validator):
full_name = serializers.CharField(max_length=256)
email = serializers.EmailField(max_length=255)
username = serializers.CharField(max_length=255)
@@ -33,25 +34,25 @@ class BaseRegisterSerializer(serializers.Serializer):
def validate_username(self, attrs, source):
value = attrs[source]
- validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), "invalid")
+ validator = core_validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), "invalid")
try:
validator(value)
except ValidationError:
- raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, numbers "
- "and /./-/_ characters'"))
+ raise ValidationError(_("Required. 255 characters or fewer. Letters, numbers "
+ "and /./-/_ characters'"))
return attrs
-class PublicRegisterSerializer(BaseRegisterSerializer):
+class PublicRegisterValidator(BaseRegisterValidator):
pass
-class PrivateRegisterForNewUserSerializer(BaseRegisterSerializer):
+class PrivateRegisterForNewUserValidator(BaseRegisterValidator):
token = serializers.CharField(max_length=255, required=True)
-class PrivateRegisterForExistingUserSerializer(serializers.Serializer):
+class PrivateRegisterForExistingUserValidator(validators.Validator):
username = serializers.CharField(max_length=255)
password = serializers.CharField(min_length=4)
token = serializers.CharField(max_length=255, required=True)
diff --git a/taiga/base/api/fields.py b/taiga/base/api/fields.py
index 7dfa2c0a..fc4035c2 100644
--- a/taiga/base/api/fields.py
+++ b/taiga/base/api/fields.py
@@ -50,7 +50,6 @@ They are very similar to Django's form fields.
from django import forms
from django.conf import settings
from django.core import validators
-from django.core.exceptions import ValidationError
from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms import widgets
from django.http import QueryDict
@@ -66,6 +65,8 @@ from django.utils.functional import Promise
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
+from taiga.base.exceptions import ValidationError
+
from . import ISO_8601
from .settings import api_settings
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..c38b5cb7 100644
--- a/taiga/base/api/mixins.py
+++ b/taiga/base/api/mixins.py
@@ -44,12 +44,12 @@
import warnings
-from django.core.exceptions import ValidationError
from django.http import Http404
from django.db import transaction as tx
from django.utils.translation import ugettext as _
from taiga.base import response
+from taiga.base.exceptions import ValidationError
from .settings import api_settings
from .utils import get_object_or_404
@@ -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/relations.py b/taiga/base/api/relations.py
index 60ba9a6e..6fbb98f5 100644
--- a/taiga/base/api/relations.py
+++ b/taiga/base/api/relations.py
@@ -48,7 +48,7 @@ Serializer fields that deal with relationships.
These fields allow you to specify the style that should be used to represent
model relationships, including hyperlinks, primary keys, or slugs.
"""
-from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
@@ -59,6 +59,7 @@ from django.utils.translation import ugettext_lazy as _
from .fields import Field, WritableField, get_component, is_simple_callable
from .reverse import reverse
+from taiga.base.exceptions import ValidationError
import warnings
from urllib import parse as urlparse
diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py
index 7de82458..f2dfd849 100644
--- a/taiga/base/api/serializers.py
+++ b/taiga/base/api/serializers.py
@@ -78,6 +78,8 @@ import serpy
# This helps keep the separation between model fields, form fields, and
# serializer fields more explicit.
+from taiga.base.exceptions import ValidationError
+
from .relations import *
from .fields import *
@@ -1228,4 +1230,8 @@ class LightSerializer(serpy.Serializer):
kwargs.pop("read_only", None)
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/exceptions.py b/taiga/base/exceptions.py
index cc58ee6d..73d277ff 100644
--- a/taiga/base/exceptions.py
+++ b/taiga/base/exceptions.py
@@ -51,6 +51,7 @@ In addition Django's built in 403 and 404 exceptions are handled.
"""
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
+from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.http import Http404
@@ -224,6 +225,7 @@ class NotEnoughSlotsForProject(BaseException):
"total_memberships": total_memberships
}
+
def format_exception(exc):
if isinstance(exc.detail, (dict, list, tuple,)):
detail = exc.detail
@@ -270,3 +272,6 @@ def exception_handler(exc):
# Note: Unhandled exceptions will raise a 500 error.
return None
+
+
+ValidationError = DjangoValidationError
diff --git a/taiga/base/fields.py b/taiga/base/fields.py
index 5e5c4b5a..30be6b60 100644
--- a/taiga/base/fields.py
+++ b/taiga/base/fields.py
@@ -20,10 +20,14 @@ from django.forms import widgets
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+import serpy
+
####################################################################
-## Serializer fields
+# DRF Serializer fields (OLD)
####################################################################
+# NOTE: This should be in other place, for example taiga.base.api.serializers
+
class JsonField(serializers.WritableField):
"""
@@ -38,40 +42,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 +74,58 @@ class WatchersField(serializers.WritableField):
def from_native(self, data):
return data
+
+
+####################################################################
+# Serpy fields (NEW)
+####################################################################
+
+class Field(serpy.Field):
+ pass
+
+
+class MethodField(serpy.MethodField):
+ pass
+
+
+class I18NField(Field):
+ def to_value(self, value):
+ ret = super(I18NField, self).to_value(value)
+ return _(ret)
+
+
+class I18NJsonField(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
+
+
+class FileField(Field):
+ def to_value(self, value):
+ return value.name
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/base/utils/dicts.py b/taiga/base/utils/dicts.py
index 23b90f17..bf3d2c71 100644
--- a/taiga/base/utils/dicts.py
+++ b/taiga/base/utils/dicts.py
@@ -25,3 +25,7 @@ def dict_sum(*args):
assert isinstance(arg, dict)
result += collections.Counter(arg)
return result
+
+
+def into_namedtuple(dictionary):
+ return collections.namedtuple('GenericDict', dictionary.keys())(**dictionary)
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/export_import/serializers/fields.py b/taiga/export_import/serializers/fields.py
index 64c01436..9ed21a19 100644
--- a/taiga/export_import/serializers/fields.py
+++ b/taiga/export_import/serializers/fields.py
@@ -23,11 +23,11 @@ from collections import OrderedDict
from django.core.files.base import ContentFile
from django.core.exceptions import ObjectDoesNotExist
-from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from django.contrib.contenttypes.models import ContentType
from taiga.base.api import serializers
+from taiga.base.exceptions import ValidationError
from taiga.base.fields import JsonField
from taiga.mdrender.service import render as mdrender
from taiga.users import models as users_models
diff --git a/taiga/export_import/serializers/serializers.py b/taiga/export_import/serializers/serializers.py
index 7cf46cba..6a316b68 100644
--- a/taiga/export_import/serializers/serializers.py
+++ b/taiga/export_import/serializers/serializers.py
@@ -16,13 +16,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import copy
-
-from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
from taiga.base.fields import JsonField, PgArrayField
+from taiga.base.exceptions import ValidationError
from taiga.projects import models as projects_models
from taiga.projects.custom_attributes import models as custom_attributes_models
@@ -31,15 +29,12 @@ from taiga.projects.tasks import models as tasks_models
from taiga.projects.issues import models as issues_models
from taiga.projects.milestones import models as milestones_models
from taiga.projects.wiki import models as wiki_models
-from taiga.projects.history import models as history_models
-from taiga.projects.attachments import models as attachments_models
from taiga.timeline import models as timeline_models
from taiga.users import models as users_models
from taiga.projects.votes import services as votes_service
-from .fields import (FileField, RelatedNoneSafeField, UserRelatedField,
- UserPkField, CommentField, ProjectRelatedField,
- HistoryUserField, HistoryValuesField, HistoryDiffField,
+from .fields import (FileField, UserRelatedField,
+ ProjectRelatedField,
TimelineDataField, ContentTypeField)
from .mixins import (HistoryExportSerializerMixin,
AttachmentExportSerializerMixin,
@@ -125,7 +120,7 @@ class IssueCustomAttributeExportSerializer(serializers.ModelSerializer):
class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer):
- attributes_values = JsonField(source="attributes_values",required=True)
+ attributes_values = JsonField(source="attributes_values", required=True)
_custom_attribute_model = None
_container_field = None
@@ -158,6 +153,7 @@ class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer):
return attrs
+
class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
_custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute
_container_model = "userstories.UserStory"
@@ -224,7 +220,7 @@ class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin):
name = attrs[source]
qs = self.project.milestones.filter(name=name)
if qs.exists():
- raise serializers.ValidationError(_("Name duplicated for the project"))
+ raise ValidationError(_("Name duplicated for the project"))
return attrs
@@ -268,7 +264,9 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His
def custom_attributes_queryset(self, project):
if project.id not in _custom_userstories_attributes_cache:
- _custom_userstories_attributes_cache[project.id] = list(project.userstorycustomattributes.all().values('id', 'name'))
+ _custom_userstories_attributes_cache[project.id] = list(
+ project.userstorycustomattributes.all().values('id', 'name')
+ )
return _custom_userstories_attributes_cache[project.id]
@@ -314,10 +312,10 @@ class WikiLinkExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'project')
-
class TimelineExportSerializer(serializers.ModelSerializer):
data = TimelineDataField()
data_content_type = ContentTypeField()
+
class Meta:
model = timeline_models.Timeline
exclude = ('id', 'project', 'namespace', 'object_id', 'content_type')
diff --git a/taiga/external_apps/api.py b/taiga/external_apps/api.py
index 931337a8..8ded55d5 100644
--- a/taiga/external_apps/api.py
+++ b/taiga/external_apps/api.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
from . import serializers
+from . import validators
from . import models
from . import permissions
from . import services
@@ -27,12 +28,12 @@ from taiga.base.api import ModelCrudViewSet, ModelRetrieveViewSet
from taiga.base.api.utils import get_object_or_404
from taiga.base.decorators import list_route, detail_route
-from django.db import transaction
from django.utils.translation import ugettext_lazy as _
class Application(ModelRetrieveViewSet):
serializer_class = serializers.ApplicationSerializer
+ validator_class = validators.ApplicationValidator
permission_classes = (permissions.ApplicationPermission,)
model = models.Application
@@ -61,6 +62,7 @@ class Application(ModelRetrieveViewSet):
class ApplicationToken(ModelCrudViewSet):
serializer_class = serializers.ApplicationTokenSerializer
+ validator_class = validators.ApplicationTokenValidator
permission_classes = (permissions.ApplicationTokenPermission,)
def get_queryset(self):
@@ -87,9 +89,9 @@ class ApplicationToken(ModelCrudViewSet):
auth_code = request.DATA.get("auth_code", None)
state = request.DATA.get("state", None)
application_token = get_object_or_404(models.ApplicationToken,
- application__id=application_id,
- auth_code=auth_code,
- state=state)
+ application__id=application_id,
+ auth_code=auth_code,
+ state=state)
application_token.generate_token()
application_token.save()
diff --git a/taiga/external_apps/serializers.py b/taiga/external_apps/serializers.py
index 095465fd..12ed3bab 100644
--- a/taiga/external_apps/serializers.py
+++ b/taiga/external_apps/serializers.py
@@ -16,9 +16,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import json
-
from taiga.base.api import serializers
+from taiga.base.fields import Field
from . import models
from . import services
@@ -26,33 +25,27 @@ from . import services
from django.utils.translation import ugettext as _
-class ApplicationSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.Application
- fields = ("id", "name", "web", "description", "icon_url")
+class ApplicationSerializer(serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ web = Field()
+ description = Field()
+ icon_url = Field()
-class ApplicationTokenSerializer(serializers.ModelSerializer):
- cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
- next_url = serializers.CharField(source="next_url", read_only=True)
- application = ApplicationSerializer(read_only=True)
-
- class Meta:
- model = models.ApplicationToken
- fields = ("user", "id", "application", "auth_code", "next_url")
+class ApplicationTokenSerializer(serializers.LightSerializer):
+ id = Field()
+ user = Field(attr="user_id")
+ application = ApplicationSerializer()
+ auth_code = Field()
+ next_url = Field()
-class AuthorizationCodeSerializer(serializers.ModelSerializer):
- next_url = serializers.CharField(source="next_url", read_only=True)
- class Meta:
- model = models.ApplicationToken
- fields = ("auth_code", "state", "next_url")
+class AuthorizationCodeSerializer(serializers.LightSerializer):
+ state = Field()
+ auth_code = Field()
+ next_url = Field()
-class AccessTokenSerializer(serializers.ModelSerializer):
- cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
- next_url = serializers.CharField(source="next_url", read_only=True)
-
- class Meta:
- model = models.ApplicationToken
- fields = ("cyphered_token", )
+class AccessTokenSerializer(serializers.LightSerializer):
+ cyphered_token = Field()
diff --git a/taiga/external_apps/validators.py b/taiga/external_apps/validators.py
new file mode 100644
index 00000000..b2f2354d
--- /dev/null
+++ b/taiga/external_apps/validators.py
@@ -0,0 +1,54 @@
+# -*- 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 . import models
+from taiga.base.api import validators
+
+
+class ApplicationValidator(validators.ModelValidator):
+ class Meta:
+ model = models.Application
+ fields = ("id", "name", "web", "description", "icon_url")
+
+
+class ApplicationTokenValidator(validators.ModelValidator):
+ cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
+ next_url = serializers.CharField(source="next_url", read_only=True)
+ application = ApplicationValidator(read_only=True)
+
+ class Meta:
+ model = models.ApplicationToken
+ fields = ("user", "id", "application", "auth_code", "next_url")
+
+
+class AuthorizationCodeValidator(validators.ModelValidator):
+ next_url = serializers.CharField(source="next_url", read_only=True)
+ class Meta:
+ model = models.ApplicationToken
+ fields = ("auth_code", "state", "next_url")
+
+
+class AccessTokenValidator(validators.ModelValidator):
+ cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
+ next_url = serializers.CharField(source="next_url", read_only=True)
+
+ class Meta:
+ model = models.ApplicationToken
+ fields = ("cyphered_token", )
diff --git a/taiga/feedback/api.py b/taiga/feedback/api.py
index c477b5eb..0f573b87 100644
--- a/taiga/feedback/api.py
+++ b/taiga/feedback/api.py
@@ -20,7 +20,7 @@ from taiga.base import response
from taiga.base.api import viewsets
from . import permissions
-from . import serializers
+from . import validators
from . import services
import copy
@@ -28,7 +28,7 @@ import copy
class FeedbackViewSet(viewsets.ViewSet):
permission_classes = (permissions.FeedbackPermission,)
- serializer_class = serializers.FeedbackEntrySerializer
+ validator_class = validators.FeedbackEntryValidator
def create(self, request, **kwargs):
self.check_permissions(request, "create", None)
@@ -37,11 +37,11 @@ class FeedbackViewSet(viewsets.ViewSet):
data.update({"full_name": request.user.get_full_name(),
"email": request.user.email})
- serializer = self.serializer_class(data=data)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = self.validator_class(data=data)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- self.object = serializer.save(force_insert=True)
+ self.object = validator.save(force_insert=True)
extra = {
"HTTP_HOST": request.META.get("HTTP_HOST", None),
@@ -50,4 +50,4 @@ class FeedbackViewSet(viewsets.ViewSet):
}
services.send_feedback(self.object, extra, reply_to=[request.user.email])
- return response.Ok(serializer.data)
+ return response.Ok(validator.data)
diff --git a/taiga/feedback/serializers.py b/taiga/feedback/validators.py
similarity index 91%
rename from taiga/feedback/serializers.py
rename to taiga/feedback/validators.py
index 1b5f1a3e..7b31ec88 100644
--- a/taiga/feedback/serializers.py
+++ b/taiga/feedback/validators.py
@@ -16,11 +16,11 @@
# 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 . import models
-class FeedbackEntrySerializer(serializers.ModelSerializer):
+class FeedbackEntryValidator(validators.ModelValidator):
class Meta:
model = models.FeedbackEntry
diff --git a/taiga/permissions/services.py b/taiga/permissions/services.py
index 32357926..6d89c168 100644
--- a/taiga/permissions/services.py
+++ b/taiga/permissions/services.py
@@ -91,39 +91,55 @@ def _get_membership_permissions(membership):
return []
+def calculate_permissions(is_authenticated=False, is_superuser=False, is_member=False,
+ is_admin=False, role_permissions=[], anon_permissions=[],
+ public_permissions=[]):
+ if is_superuser:
+ admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
+ members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
+ public_permissions = []
+ anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
+ elif is_member:
+ if is_admin:
+ admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
+ members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
+ else:
+ admins_permissions = []
+ members_permissions = []
+ members_permissions = members_permissions + role_permissions
+ public_permissions = public_permissions if public_permissions is not None else []
+ anon_permissions = anon_permissions if anon_permissions is not None else []
+ elif is_authenticated:
+ admins_permissions = []
+ members_permissions = []
+ public_permissions = public_permissions if public_permissions is not None else []
+ anon_permissions = anon_permissions if anon_permissions is not None else []
+ else:
+ admins_permissions = []
+ members_permissions = []
+ public_permissions = []
+ anon_permissions = anon_permissions if anon_permissions is not None else []
+
+ return set(admins_permissions + members_permissions + public_permissions + anon_permissions)
+
+
def get_user_project_permissions(user, project, cache="user"):
"""
cache param determines how memberships are calculated trying to reuse the existing data
in cache
"""
membership = _get_user_project_membership(user, project, cache=cache)
- if user.is_superuser:
- admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
- members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
- public_permissions = []
- anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
- elif membership:
- if membership.is_admin:
- admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
- members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
- else:
- admins_permissions = []
- members_permissions = []
- members_permissions = members_permissions + _get_membership_permissions(membership)
- public_permissions = project.public_permissions if project.public_permissions is not None else []
- anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
- elif user.is_authenticated():
- admins_permissions = []
- members_permissions = []
- public_permissions = project.public_permissions if project.public_permissions is not None else []
- anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
- else:
- admins_permissions = []
- members_permissions = []
- public_permissions = []
- anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
-
- return set(admins_permissions + members_permissions + public_permissions + anon_permissions)
+ is_member = membership is not None
+ is_admin = is_member and membership.is_admin
+ return calculate_permissions(
+ is_authenticated = user.is_authenticated(),
+ is_superuser = user.is_superuser,
+ is_member = is_member,
+ is_admin = is_admin,
+ role_permissions = _get_membership_permissions(membership),
+ anon_permissions = project.anon_permissions,
+ public_permissions = project.public_permissions
+ )
def set_base_permissions_for_project(project):
diff --git a/taiga/projects/api.py b/taiga/projects/api.py
index 6c7a4ec9..6445c17f 100644
--- a/taiga/projects/api.py
+++ b/taiga/projects/api.py
@@ -22,10 +22,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 +41,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,27 +49,28 @@ 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()
- serializer_class = serializers.ProjectDetailSerializer
- admin_serializer_class = serializers.ProjectDetailAdminSerializer
- list_serializer_class = serializers.ProjectSerializer
permission_classes = (permissions.ProjectPermission, )
- filter_backends = (project_filters.QFilterBackend,
+ filter_backends = (project_filters.UserOrderFilterBackend,
+ project_filters.QFilterBackend,
project_filters.CanViewProjectObjFilterBackend,
project_filters.DiscoverModeFilterBackend)
@@ -85,8 +81,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
"is_kanban_activated")
ordering = ("name", "id")
- order_by_fields = ("memberships__user_order",
- "total_fans",
+ order_by_fields = ("total_fans",
"total_fans_last_week",
"total_fans_last_month",
"total_fans_last_year",
@@ -106,18 +101,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
def get_queryset(self):
qs = super().get_queryset()
-
qs = qs.select_related("owner")
- # Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries)
- # so we add some custom prefetching
- qs = qs.prefetch_related("members")
- qs = qs.prefetch_related("memberships")
- qs = qs.prefetch_related(Prefetch("notify_policies",
- NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
-
- Milestone = apps.get_model("milestones", "Milestone")
- qs = qs.prefetch_related(Prefetch("milestones",
- Milestone.objects.filter(closed=True), to_attr="closed_milestones"))
+ qs = project_utils.attach_extra_info(qs, user=self.request.user)
# If filtering an activity period we must exclude the activities not updated recently enough
now = timezone.now()
@@ -137,22 +122,17 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
return qs
+ def retrieve(self, request, *args, **kwargs):
+ if self.action == "by_slug":
+ self.lookup_field = "slug"
+
+ return super().retrieve(request, *args, **kwargs)
+
def get_serializer_class(self):
- serializer_class = self.serializer_class
-
if self.action == "list":
- serializer_class = self.list_serializer_class
- elif self.action != "create":
- if self.action == "by_slug":
- slug = self.request.QUERY_PARAMS.get("slug", None)
- project = get_object_or_404(models.Project, slug=slug)
- else:
- project = self.get_object()
+ return serializers.ProjectSerializer
- if permissions_services.is_project_admin(self.request.user, project):
- serializer_class = self.admin_serializer_class
-
- return serializer_class
+ return serializers.ProjectDetailSerializer
@detail_route(methods=["POST"])
def change_logo(self, request, *args, **kwargs):
@@ -215,11 +195,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)
@@ -283,10 +263,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
return response.Ok(data)
@list_route(methods=["GET"])
- def by_slug(self, request):
+ def by_slug(self, request, *args, **kwargs):
slug = request.QUERY_PARAMS.get("slug", None)
- project = get_object_or_404(models.Project, slug=slug)
- return self.retrieve(request, pk=project.pk)
+ return self.retrieve(request, slug=slug)
@detail_route(methods=["GET", "PATCH"])
def modules(self, request, pk=None):
@@ -362,7 +341,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"])
@@ -471,6 +450,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',)
@@ -487,6 +467,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',)
@@ -503,6 +484,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",)
@@ -519,6 +501,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",)
@@ -534,6 +517,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",)
@@ -549,6 +533,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",)
@@ -564,6 +549,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",)
@@ -582,6 +568,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):
@@ -595,7 +582,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")
@@ -620,6 +609,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,
@@ -634,11 +629,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)
@@ -655,7 +650,7 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
invitation_extra_text=invitation_extra_text,
callback=self.post_save,
precall=self.pre_save)
- except ValidationError as err:
+ except exc.ValidationError as err:
return response.BadRequest(err.message_dict)
members_serialized = self.admin_serializer_class(members, many=True)
diff --git a/taiga/projects/attachments/api.py b/taiga/projects/attachments/api.py
index f7b223e2..27f7ebe1 100644
--- a/taiga/projects/attachments/api.py
+++ b/taiga/projects/attachments/api.py
@@ -34,6 +34,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin
from . import permissions
from . import serializers
+from . import validators
from . import models
@@ -42,6 +43,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
model = models.Attachment
serializer_class = serializers.AttachmentSerializer
+ validator_class = validators.AttachmentValidator
filter_fields = ["project", "object_id"]
content_type = None
diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py
index 6c5ee05b..ce8893b7 100644
--- a/taiga/projects/attachments/serializers.py
+++ b/taiga/projects/attachments/serializers.py
@@ -19,36 +19,38 @@
from django.conf import settings
from taiga.base.api import serializers
+from taiga.base.fields import MethodField, Field, FileField
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")
- thumbnail_card_url = serializers.SerializerMethodField("get_thumbnail_card_url")
- attached_file = serializers.FileField(required=True)
-
- class Meta:
- model = models.Attachment
- fields = ("id", "project", "owner", "name", "attached_file", "size",
- "url", "thumbnail_card_url", "description", "is_deprecated",
- "created_date", "modified_date", "object_id", "order", "sha1")
- read_only_fields = ("owner", "created_date", "modified_date", "sha1")
+class AttachmentSerializer(serializers.LightSerializer):
+ id = Field()
+ project = Field(attr="project_id")
+ owner = Field(attr="owner_id")
+ name = Field()
+ attached_file = FileField()
+ size = Field()
+ url = Field()
+ description = Field()
+ is_deprecated = Field()
+ created_date = Field()
+ modified_date = Field()
+ object_id = Field()
+ order = Field()
+ sha1 = Field()
+ url = MethodField("get_url")
+ thumbnail_card_url = MethodField("get_thumbnail_card_url")
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 +58,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/attachments/validators.py b/taiga/projects/attachments/validators.py
new file mode 100644
index 00000000..72355ce4
--- /dev/null
+++ b/taiga/projects/attachments/validators.py
@@ -0,0 +1,33 @@
+# -*- 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 . import models
+
+
+class AttachmentValidator(validators.ModelValidator):
+ attached_file = serializers.FileField(required=True)
+
+ class Meta:
+ model = models.Attachment
+ fields = ("id", "project", "owner", "name", "attached_file", "size",
+ "description", "is_deprecated", "created_date",
+ "modified_date", "object_id", "order", "sha1")
+ read_only_fields = ("owner", "created_date", "modified_date", "sha1")
diff --git a/taiga/projects/custom_attributes/api.py b/taiga/projects/custom_attributes/api.py
index 9bfc774f..2d05d186 100644
--- a/taiga/projects/custom_attributes/api.py
+++ b/taiga/projects/custom_attributes/api.py
@@ -32,6 +32,7 @@ from taiga.projects.occ.mixins import OCCResourceMixin
from . import models
from . import serializers
+from . import validators
from . import permissions
from . import services
@@ -43,6 +44,7 @@ from . import services
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.UserStoryCustomAttribute
serializer_class = serializers.UserStoryCustomAttributeSerializer
+ validator_class = validators.UserStoryCustomAttributeValidator
permission_classes = (permissions.UserStoryCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -54,6 +56,7 @@ class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixi
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.TaskCustomAttribute
serializer_class = serializers.TaskCustomAttributeSerializer
+ validator_class = validators.TaskCustomAttributeValidator
permission_classes = (permissions.TaskCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -65,6 +68,7 @@ class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, Mo
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.IssueCustomAttribute
serializer_class = serializers.IssueCustomAttributeSerializer
+ validator_class = validators.IssueCustomAttributeValidator
permission_classes = (permissions.IssueCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",)
@@ -86,6 +90,7 @@ class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin,
class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.UserStoryCustomAttributesValues
serializer_class = serializers.UserStoryCustomAttributesValuesSerializer
+ validator_class = validators.UserStoryCustomAttributesValuesValidator
permission_classes = (permissions.UserStoryCustomAttributesValuesPermission,)
lookup_field = "user_story_id"
content_object = "user_story"
@@ -99,6 +104,7 @@ class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.TaskCustomAttributesValues
serializer_class = serializers.TaskCustomAttributesValuesSerializer
+ validator_class = validators.TaskCustomAttributesValuesValidator
permission_classes = (permissions.TaskCustomAttributesValuesPermission,)
lookup_field = "task_id"
content_object = "task"
@@ -112,6 +118,7 @@ class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.IssueCustomAttributesValues
serializer_class = serializers.IssueCustomAttributesValuesSerializer
+ validator_class = validators.IssueCustomAttributesValuesValidator
permission_classes = (permissions.IssueCustomAttributesValuesPermission,)
lookup_field = "issue_id"
content_object = "issue"
diff --git a/taiga/projects/custom_attributes/serializers.py b/taiga/projects/custom_attributes/serializers.py
index 64a934f5..d4fc084e 100644
--- a/taiga/projects/custom_attributes/serializers.py
+++ b/taiga/projects/custom_attributes/serializers.py
@@ -17,131 +17,51 @@
# along with this program. If not, see .
-from django.apps import apps
-from django.utils.translation import ugettext_lazy as _
-
-from taiga.base.fields import JsonField
-from taiga.base.api.serializers import ValidationError
-from taiga.base.api.serializers import ModelSerializer
-
-from . import models
+from taiga.base.fields import JsonField, Field
+from taiga.base.api import serializers
######################################################
# Custom Attribute Serializer
#######################################################
-class BaseCustomAttributeSerializer(ModelSerializer):
- class Meta:
- read_only_fields = ('id',)
- exclude = ('created_date', 'modified_date')
-
- def _validate_integrity_between_project_and_name(self, attrs, source):
- """
- Check the name is not duplicated in the project. Check when:
- - create a new one
- - update the name
- - update the project (move to another project)
- """
- data_id = attrs.get("id", None)
- data_name = attrs.get("name", None)
- data_project = attrs.get("project", None)
-
- if self.object:
- data_id = data_id or self.object.id
- data_name = data_name or self.object.name
- data_project = data_project or self.object.project
-
- model = self.Meta.model
- qs = (model.objects.filter(project=data_project, name=data_name)
- .exclude(id=data_id))
- if qs.exists():
- raise ValidationError(_("Already exists one with the same name."))
-
- return attrs
-
- def validate_name(self, attrs, source):
- return self._validate_integrity_between_project_and_name(attrs, source)
-
- def validate_project(self, attrs, source):
- return self._validate_integrity_between_project_and_name(attrs, source)
+class BaseCustomAttributeSerializer(serializers.LightSerializer):
+ name = Field()
+ description = Field()
+ type = Field()
+ order = Field()
+ project = Field(attr="project_id")
+ created_date = Field()
+ modified_date = Field()
class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer):
- class Meta(BaseCustomAttributeSerializer.Meta):
- model = models.UserStoryCustomAttribute
+ pass
class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer):
- class Meta(BaseCustomAttributeSerializer.Meta):
- model = models.TaskCustomAttribute
+ pass
class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer):
- class Meta(BaseCustomAttributeSerializer.Meta):
- model = models.IssueCustomAttribute
+ pass
######################################################
# Custom Attribute Serializer
#######################################################
-
-
-class BaseCustomAttributesValuesSerializer(ModelSerializer):
- attributes_values = JsonField(source="attributes_values", label="attributes values")
- _custom_attribute_model = None
- _container_field = None
-
- class Meta:
- exclude = ("id",)
-
- def validate_attributes_values(self, attrs, source):
- # values must be a dict
- data_values = attrs.get("attributes_values", None)
- if self.object:
- data_values = (data_values or self.object.attributes_values)
-
- if type(data_values) is not dict:
- raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
-
- # Values keys must be in the container object project
- data_container = attrs.get(self._container_field, None)
- if data_container:
- project_id = data_container.project_id
- elif self.object:
- project_id = getattr(self.object, self._container_field).project_id
- else:
- project_id = None
-
- values_ids = list(data_values.keys())
- qs = self._custom_attribute_model.objects.filter(project=project_id,
- id__in=values_ids)
- if qs.count() != len(values_ids):
- raise ValidationError(_("It contain invalid custom fields."))
-
- return attrs
+class BaseCustomAttributesValuesSerializer(serializers.LightSerializer):
+ attributes_values = Field()
+ version = Field()
class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
- _custom_attribute_model = models.UserStoryCustomAttribute
- _container_model = "userstories.UserStory"
- _container_field = "user_story"
-
- class Meta(BaseCustomAttributesValuesSerializer.Meta):
- model = models.UserStoryCustomAttributesValues
+ user_story = Field(attr="user_story.id")
-class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer):
- _custom_attribute_model = models.TaskCustomAttribute
- _container_field = "task"
-
- class Meta(BaseCustomAttributesValuesSerializer.Meta):
- model = models.TaskCustomAttributesValues
+class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
+ task = Field(attr="task.id")
-class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer):
- _custom_attribute_model = models.IssueCustomAttribute
- _container_field = "issue"
-
- class Meta(BaseCustomAttributesValuesSerializer.Meta):
- model = models.IssueCustomAttributesValues
+class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
+ issue = Field(attr="issue.id")
diff --git a/taiga/projects/custom_attributes/validators.py b/taiga/projects/custom_attributes/validators.py
new file mode 100644
index 00000000..6663de5d
--- /dev/null
+++ b/taiga/projects/custom_attributes/validators.py
@@ -0,0 +1,146 @@
+# -*- 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 django.utils.translation import ugettext_lazy as _
+
+from taiga.base.fields import JsonField
+from taiga.base.exceptions import ValidationError
+from taiga.base.api.validators import ModelValidator
+
+from . import models
+
+
+######################################################
+# Custom Attribute Validator
+#######################################################
+
+class BaseCustomAttributeValidator(ModelValidator):
+ class Meta:
+ read_only_fields = ('id',)
+ exclude = ('created_date', 'modified_date')
+
+ def _validate_integrity_between_project_and_name(self, attrs, source):
+ """
+ Check the name is not duplicated in the project. Check when:
+ - create a new one
+ - update the name
+ - update the project (move to another project)
+ """
+ data_id = attrs.get("id", None)
+ data_name = attrs.get("name", None)
+ data_project = attrs.get("project", None)
+
+ if self.object:
+ data_id = data_id or self.object.id
+ data_name = data_name or self.object.name
+ data_project = data_project or self.object.project
+
+ model = self.Meta.model
+ qs = (model.objects.filter(project=data_project, name=data_name)
+ .exclude(id=data_id))
+ if qs.exists():
+ raise ValidationError(_("Already exists one with the same name."))
+
+ return attrs
+
+ def validate_name(self, attrs, source):
+ return self._validate_integrity_between_project_and_name(attrs, source)
+
+ def validate_project(self, attrs, source):
+ return self._validate_integrity_between_project_and_name(attrs, source)
+
+
+class UserStoryCustomAttributeValidator(BaseCustomAttributeValidator):
+ class Meta(BaseCustomAttributeValidator.Meta):
+ model = models.UserStoryCustomAttribute
+
+
+class TaskCustomAttributeValidator(BaseCustomAttributeValidator):
+ class Meta(BaseCustomAttributeValidator.Meta):
+ model = models.TaskCustomAttribute
+
+
+class IssueCustomAttributeValidator(BaseCustomAttributeValidator):
+ class Meta(BaseCustomAttributeValidator.Meta):
+ model = models.IssueCustomAttribute
+
+
+######################################################
+# Custom Attribute Validator
+#######################################################
+
+
+class BaseCustomAttributesValuesValidator(ModelValidator):
+ attributes_values = JsonField(source="attributes_values", label="attributes values")
+ _custom_attribute_model = None
+ _container_field = None
+
+ class Meta:
+ exclude = ("id",)
+
+ def validate_attributes_values(self, attrs, source):
+ # values must be a dict
+ data_values = attrs.get("attributes_values", None)
+ if self.object:
+ data_values = (data_values or self.object.attributes_values)
+
+ if type(data_values) is not dict:
+ raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
+
+ # Values keys must be in the container object project
+ data_container = attrs.get(self._container_field, None)
+ if data_container:
+ project_id = data_container.project_id
+ elif self.object:
+ project_id = getattr(self.object, self._container_field).project_id
+ else:
+ project_id = None
+
+ values_ids = list(data_values.keys())
+ qs = self._custom_attribute_model.objects.filter(project=project_id,
+ id__in=values_ids)
+ if qs.count() != len(values_ids):
+ raise ValidationError(_("It contain invalid custom fields."))
+
+ return attrs
+
+
+class UserStoryCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator):
+ _custom_attribute_model = models.UserStoryCustomAttribute
+ _container_model = "userstories.UserStory"
+ _container_field = "user_story"
+
+ class Meta(BaseCustomAttributesValuesValidator.Meta):
+ model = models.UserStoryCustomAttributesValues
+
+
+class TaskCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator, ModelValidator):
+ _custom_attribute_model = models.TaskCustomAttribute
+ _container_field = "task"
+
+ class Meta(BaseCustomAttributesValuesValidator.Meta):
+ model = models.TaskCustomAttributesValues
+
+
+class IssueCustomAttributesValuesValidator(BaseCustomAttributesValuesValidator, ModelValidator):
+ _custom_attribute_model = models.IssueCustomAttribute
+ _container_field = "issue"
+
+ class Meta(BaseCustomAttributesValuesValidator.Meta):
+ model = models.IssueCustomAttributesValues
diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py
index b3be1a0a..fe720f97 100644
--- a/taiga/projects/filters.py
+++ b/taiga/projects/filters.py
@@ -45,7 +45,7 @@ class DiscoverModeFilterBackend(FilterBackend):
if request.QUERY_PARAMS.get("is_featured", None) == 'true':
qs = qs.order_by("?")
- return super().filter_queryset(request, qs.distinct(), view)
+ return super().filter_queryset(request, qs, view)
class CanViewProjectObjFilterBackend(FilterBackend):
@@ -86,7 +86,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
# external users / anonymous
qs = qs.filter(anon_permissions__contains=["view_project"])
- return super().filter_queryset(request, qs.distinct(), view)
+ return super().filter_queryset(request, qs, view)
class QFilterBackend(FilterBackend):
@@ -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", ]
@@ -121,3 +121,34 @@ class QFilterBackend(FilterBackend):
params=params,
order_by=order_by)
return queryset
+
+
+class UserOrderFilterBackend(FilterBackend):
+ def filter_queryset(self, request, queryset, view):
+ if request.user.is_anonymous():
+ return queryset
+
+ raw_fieldname = request.QUERY_PARAMS.get(self.order_by_query_param, None)
+ if not raw_fieldname:
+ return queryset
+
+ if raw_fieldname.startswith("-"):
+ field_name = raw_fieldname[1:]
+ else:
+ field_name = raw_fieldname
+
+ if field_name != "user_order":
+ return queryset
+
+ 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}
+ """
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=request.user.id)
+ queryset = queryset.extra(select={"user_order": sql})
+ queryset = queryset.order_by(raw_fieldname)
+ return queryset
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 57acfca8..093b3ad1 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,
@@ -144,10 +148,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
def get_queryset(self):
qs = super().get_queryset()
- qs = qs.prefetch_related("attachments", "generated_user_stories")
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:
@@ -180,10 +183,18 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
@list_route(methods=["GET"])
def by_ref(self, request):
- ref = request.QUERY_PARAMS.get("ref", None)
+ retrieve_kwargs = {
+ "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)
+ if project_id is not None:
+ retrieve_kwargs["project_id"] = project_id
+
+ project_slug = request.QUERY_PARAMS.get("project__slug", None)
+ if project_slug is not None:
+ retrieve_kwargs["project__slug"] = project_slug
+
+ return self.retrieve(request, **retrieve_kwargs)
@list_route(methods=["GET"])
def filters_data(self, request, *args, **kwargs):
@@ -225,9 +236,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:
@@ -238,11 +249,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/likes/serializers.py b/taiga/projects/likes/serializers.py
index 6a654705..ef058e70 100644
--- a/taiga/projects/likes/serializers.py
+++ b/taiga/projects/likes/serializers.py
@@ -17,14 +17,14 @@
# 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 taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
-class FanSerializer(serializers.ModelSerializer):
- full_name = serializers.CharField(source='get_full_name', required=False)
+class FanSerializer(serializers.LightSerializer):
+ id = Field()
+ username = Field()
+ full_name = MethodField()
- class Meta:
- model = get_user_model()
- fields = ('id', 'username', 'full_name')
+ def get_full_name(self, obj):
+ return obj.get_full_name()
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..b7d4d484 100644
--- a/taiga/projects/milestones/validators.py
+++ b/taiga/projects/milestones/validators.py
@@ -18,15 +18,24 @@
from django.utils.translation import ugettext as _
-from taiga.base.api import serializers
+from taiga.base.exceptions import ValidationError
+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")
- raise serializers.ValidationError(msg)
+ msg = _("There's no milestone with that id")
+ raise 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 62db374e..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,11 +172,15 @@ 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:
user = self.context["request"].user
- return user.is_authenticated() and user.is_watcher(obj)
+ return user.is_authenticated() and getattr(obj, "is_watcher", False)
return False
@@ -199,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 = serializers.SerializerMethodField("get_is_watcher")
- total_watchers = serializers.SerializerMethodField("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
@@ -229,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:
@@ -243,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()]
@@ -253,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/notifications/validators.py b/taiga/projects/notifications/validators.py
index 851cc309..40e02083 100644
--- a/taiga/projects/notifications/validators.py
+++ b/taiga/projects/notifications/validators.py
@@ -18,7 +18,7 @@
from django.utils.translation import ugettext as _
-from taiga.base.api import serializers
+from taiga.base.exceptions import ValidationError
class WatchersValidator:
@@ -45,6 +45,6 @@ class WatchersValidator:
existing_watcher_ids = project.get_watchers().values_list("id", flat=True)
result = set(users).difference(member_ids).difference(existing_watcher_ids)
if result:
- raise serializers.ValidationError(_("Watchers contains invalid users"))
+ raise ValidationError(_("Watchers contains invalid users"))
return attrs
diff --git a/taiga/projects/references/api.py b/taiga/projects/references/api.py
index 42d7f5a6..ff114ac6 100644
--- a/taiga/projects/references/api.py
+++ b/taiga/projects/references/api.py
@@ -24,7 +24,7 @@ from taiga.base.api import viewsets
from taiga.base.api.utils import get_object_or_404
from taiga.permissions.services import user_has_perm
-from .serializers import ResolverSerializer
+from .validators import ResolverValidator
from . import permissions
@@ -32,11 +32,11 @@ class ResolverViewSet(viewsets.ViewSet):
permission_classes = (permissions.ResolverPermission,)
def list(self, request, **kwargs):
- serializer = ResolverSerializer(data=request.QUERY_PARAMS)
- if not serializer.is_valid():
- raise exc.BadRequest(serializer.errors)
+ validator = ResolverValidator(data=request.QUERY_PARAMS)
+ if not validator.is_valid():
+ raise exc.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
project_model = apps.get_model("projects", "Project")
project = get_object_or_404(project_model, slug=data["project"])
diff --git a/taiga/projects/references/serializers.py b/taiga/projects/references/validators.py
similarity index 78%
rename from taiga/projects/references/serializers.py
rename to taiga/projects/references/validators.py
index fb9ad177..85456c4c 100644
--- a/taiga/projects/references/serializers.py
+++ b/taiga/projects/references/validators.py
@@ -17,9 +17,11 @@
# along with this program. If not, see .
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
-class ResolverSerializer(serializers.Serializer):
+class ResolverValidator(validators.Validator):
project = serializers.CharField(max_length=512, required=True)
milestone = serializers.CharField(max_length=512, required=False)
us = serializers.IntegerField(required=False)
@@ -31,10 +33,10 @@ class ResolverSerializer(serializers.Serializer):
def validate(self, attrs):
if "ref" in attrs:
if "us" in attrs:
- raise serializers.ValidationError("'us' param is incompatible with 'ref' in the same request")
+ raise ValidationError("'us' param is incompatible with 'ref' in the same request")
if "task" in attrs:
- raise serializers.ValidationError("'task' param is incompatible with 'ref' in the same request")
+ raise ValidationError("'task' param is incompatible with 'ref' in the same request")
if "issue" in attrs:
- raise serializers.ValidationError("'issue' param is incompatible with 'ref' in the same request")
+ raise ValidationError("'issue' param is incompatible with 'ref' in the same request")
return attrs
diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py
index 9c185a97..96b8d0ba 100644
--- a/taiga/projects/serializers.py
+++ b/taiga/projects/serializers.py
@@ -16,131 +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.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
######################################################
-## Custom values for selectors
+# Custom values for selectors
######################################################
-class PointsSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.Points
- i18n_fields = ("name",)
+class PointsSerializer(serializers.LightSerializer):
+ name = I18NField()
+ order = Field()
+ value = Field()
+ project = Field(attr="project_id")
-class UserStoryStatusSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.UserStoryStatus
- i18n_fields = ("name",)
+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 BasicUserStoryStatusSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.UserStoryStatus
- i18n_fields = ("name",)
- fields = ("name", "color")
+class TaskStatusSerializer(serializers.LightSerializer):
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
+ project = Field(attr="project_id")
-class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.TaskStatus
- i18n_fields = ("name",)
+class SeveritySerializer(serializers.LightSerializer):
+ name = I18NField()
+ order = Field()
+ color = Field()
+ project = Field(attr="project_id")
-class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.TaskStatus
- i18n_fields = ("name",)
- fields = ("name", "color")
+class PrioritySerializer(serializers.LightSerializer):
+ name = I18NField()
+ order = Field()
+ color = Field()
+ project = Field(attr="project_id")
-class SeveritySerializer(ValidateDuplicatedNameInProjectMixin):
- class Meta:
- model = models.Severity
- i18n_fields = ("name",)
+class IssueStatusSerializer(serializers.LightSerializer):
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ is_closed = Field()
+ color = Field()
+ project = Field(attr="project_id")
-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",)
+class IssueTypeSerializer(serializers.LightSerializer):
+ name = I18NField()
+ order = Field()
+ color = Field()
+ project = Field(attr="project_id")
######################################################
-## Members
+# Members
######################################################
-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 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()
- 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_role_name(self, obj):
+ return obj.role.name if obj.role else None
- def get_photo(self, project):
- return get_photo_or_gravatar_url(project.user)
+ 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 ""
@@ -152,131 +142,124 @@ 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()
+
+ is_watcher = MethodField()
+ total_watchers = MethodField()
+
+ logo_small_url = MethodField()
+ logo_big_url = MethodField()
+
+ is_fan = Field(attr="is_fan_attr")
+
+ def get_members(self, obj):
+ assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
+ if obj.members_attr is None:
+ return []
+
+ return [m.get("id") for m in obj.members_attr if m["id"] is not None]
+
+ def get_i_am_member(self, obj):
+ assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
+ if obj.members_attr is None:
+ return False
+
+ if "request" in self.context:
+ user = self.context["request"].user
+ 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
+
+ def get_tags_colors(self, obj):
+ return dict(obj.tags_colors)
def get_my_permissions(self, obj):
if "request" in self.context:
- return get_user_project_permissions(self.context["request"].user, obj)
+ 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)
return []
+ def get_owner(self, obj):
+ return UserBasicInfoSerializer(obj.owner).data
+
def get_i_am_owner(self, obj):
if "request" in self.context:
return is_project_owner(self.context["request"].user, obj)
@@ -287,35 +270,35 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
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):
- # The "closed_milestone" attribute can be attached in the get_queryset method of the viewset.
- qs_closed_milestones = getattr(obj, "closed_milestones", None)
- if qs_closed_milestones is not None:
- return len(qs_closed_milestones)
+ assert hasattr(obj, "closed_milestones_attr"), "instance must have a closed_milestones_attr attribute"
+ return obj.closed_milestones_attr
- 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_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 is not None and np != NotifyLevel.none
def get_total_watchers(self, obj):
- # The "valid_notify_policies" attribute can be attached in the get_queryset method of the viewset.
- qs_valid_notify_policies = getattr(obj, "valid_notify_policies", None)
- if qs_valid_notify_policies is not None:
- return len(qs_valid_notify_policies)
+ assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
+ if obj.notify_policies_attr is None:
+ return 0
- return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count()
+ valid_notify_policies = [np for np in obj.notify_policies_attr if np["notify_level"] != NotifyLevel.none]
+ return len(valid_notify_policies)
+
+ def get_notify_level(self, obj):
+ assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
+ if obj.notify_policies_attr is None:
+ return None
+
+ if "request" in self.context:
+ user = self.context["request"].user
+ for np in obj.notify_policies_attr:
+ if np["user_id"] == user.id:
+ return np["notify_level"]
+
+ return None
def get_logo_small_url(self, obj):
return services.get_logo_small_thumbnail_url(obj)
@@ -325,94 +308,132 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
class ProjectDetailSerializer(ProjectSerializer):
- us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories
- points = PointsSerializer(many=True, required=False)
+ 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()
- task_statuses = TaskStatusSerializer(many=True, required=False) # Tasks
+ # 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()
- issue_statuses = IssueStatusSerializer(many=True, required=False)
- issue_types = IssueTypeSerializer(many=True, required=False)
- priorities = PrioritySerializer(many=True, required=False) # Issues
- severities = SeveritySerializer(many=True, required=False)
+ def to_value(self, instance):
+ # Name attributes must be translated
+ 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"]:
- userstory_custom_attributes = UserStoryCustomAttributeSerializer(source="userstorycustomattributes",
- many=True, required=False)
- task_custom_attributes = TaskCustomAttributeSerializer(source="taskcustomattributes",
- many=True, required=False)
- issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes",
- many=True, required=False)
+ assert hasattr(instance, attr), "instance must have a {} attribute".format(attr)
+ val = getattr(instance, attr)
+ if val is None:
+ continue
- roles = ProjectRoleSerializer(source="roles", many=True, read_only=True)
- members = serializers.SerializerMethodField(method_name="get_members")
- total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships")
- is_out_of_owner_limits = serializers.SerializerMethodField(method_name="get_is_out_of_owner_limits")
+ for elem in val:
+ elem["name"] = _(elem["name"])
+
+ ret = super().to_value(instance)
+
+ admin_fields = [
+ "is_private_extra_info", "max_memberships", "issues_csv_uuid",
+ "tasks_csv_uuid", "userstories_csv_uuid", "transfer_token"
+ ]
+
+ is_admin_user = False
+ if "request" in self.context:
+ user = self.context["request"].user
+ is_admin_user = permissions_services.is_project_admin(user, instance)
+
+ if not is_admin_user:
+ for admin_field in admin_fields:
+ del(ret[admin_field])
+
+ return ret
def get_members(self, obj):
- qs = obj.memberships.filter(user__isnull=False)
- qs = qs.extra(select={"complete_user_name":"concat(full_name, username)"})
- qs = qs.order_by("complete_user_name")
- qs = qs.select_related("role", "user")
- serializer = ProjectMemberSerializer(qs, many=True)
- return serializer.data
+ assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
+ if obj.members_attr is None:
+ return []
+
+ ret = []
+ for m in obj.members_attr:
+ m["full_name_display"] = m["full_name"] or m["username"] or m["email"]
+ del(m["email"])
+ del(m["complete_user_name"])
+ if not m["id"] is None:
+ ret.append(m)
+
+ return ret
def get_total_memberships(self, obj):
- return services.get_total_project_memberships(obj)
+ if obj.members_attr is None:
+ return 0
+
+ return len(obj.members_attr)
def get_is_out_of_owner_limits(self, obj):
- return services.check_if_project_is_out_of_owner_limits(obj)
-
-
-class ProjectDetailAdminSerializer(ProjectDetailSerializer):
- is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info")
- max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships")
-
- 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")
+ 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),
+ current_private_projects=obj.private_projects_same_owner_attr,
+ current_public_projects=obj.public_projects_same_owner_attr
+ )
def get_is_private_extra_info(self, obj):
- return services.check_if_project_privacity_can_be_changed(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),
+ current_private_projects=obj.private_projects_same_owner_attr,
+ current_public_projects=obj.public_projects_same_owner_attr
+ )
def get_max_memberships(self, obj):
return services.get_max_memberships_for_project(obj)
######################################################
-## Liked
+# Project Templates
######################################################
-class LikedSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.Project
- fields = ['id', 'name', 'slug']
-
-
-
-######################################################
-## 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/services/projects.py b/taiga/projects/services/projects.py
index f56a9941..2bd31d94 100644
--- a/taiga/projects/services/projects.py
+++ b/taiga/projects/services/projects.py
@@ -27,30 +27,45 @@ ERROR_MAX_PUBLIC_PROJECTS = 'max_public_projects'
ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects'
ERROR_PROJECT_WITHOUT_OWNER = 'project_without_owner'
-def check_if_project_privacity_can_be_changed(project):
+def check_if_project_privacity_can_be_changed(project,
+ current_memberships=None,
+ current_private_projects=None,
+ current_public_projects=None):
"""Return if the project privacity can be changed from private to public or viceversa.
:param project: A project object.
+ :param current_memberships: Project total memberships, If None it will be calculated.
+ :param current_private_projects: total private projects owned by the project owner, If None it will be calculated.
+ :param current_public_projects: total public projects owned by the project owner, If None it will be calculated.
:return: A dict like this {'can_be_updated': bool, 'reason': error message}.
"""
if project.owner is None:
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
- if project.is_private:
+ if current_memberships is None:
current_memberships = project.memberships.count()
+
+ if project.is_private:
max_memberships = project.owner.max_memberships_public_projects
error_memberships_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS
- current_projects = project.owner.owned_projects.filter(is_private=False).count()
+ if current_public_projects is None:
+ current_projects = project.owner.owned_projects.filter(is_private=False).count()
+ else:
+ current_projects = current_public_projects
+
max_projects = project.owner.max_public_projects
error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS
else:
- current_memberships = project.memberships.count()
max_memberships = project.owner.max_memberships_private_projects
error_memberships_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
- current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ if current_private_projects is None:
+ current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ else:
+ current_projects = current_private_projects
+
max_projects = project.owner.max_private_projects
error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS
@@ -139,25 +154,43 @@ def check_if_project_can_be_transfered(project, new_owner):
return (True, None)
-def check_if_project_is_out_of_owner_limits(project):
+def check_if_project_is_out_of_owner_limits(project,
+ current_memberships=None,
+ current_private_projects=None,
+ current_public_projects=None):
+
"""Return if the project fits on its owner limits.
:param project: A project object.
+ :param current_memberships: Project total memberships, If None it will be calculated.
+ :param current_private_projects: total private projects owned by the project owner, If None it will be calculated.
+ :param current_public_projects: total public projects owned by the project owner, If None it will be calculated.
:return: bool
"""
if project.owner is None:
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
- if project.is_private:
+ if current_memberships is None:
current_memberships = project.memberships.count()
+
+ if project.is_private:
max_memberships = project.owner.max_memberships_private_projects
- current_projects = project.owner.owned_projects.filter(is_private=True).count()
+
+ if current_private_projects is None:
+ current_projects = project.owner.owned_projects.filter(is_private=True).count()
+ else:
+ current_projects = current_private_projects
+
max_projects = project.owner.max_private_projects
else:
- current_memberships = project.memberships.count()
max_memberships = project.owner.max_memberships_public_projects
- current_projects = project.owner.owned_projects.filter(is_private=False).count()
+
+ if current_public_projects is None:
+ current_projects = project.owner.owned_projects.filter(is_private=False).count()
+ else:
+ current_projects = current_public_projects
+
max_projects = project.owner.max_public_projects
if max_memberships is not None and current_memberships > max_memberships:
diff --git a/taiga/projects/tagging/api.py b/taiga/projects/tagging/api.py
index c2dbd38a..db57b946 100644
--- a/taiga/projects/tagging/api.py
+++ b/taiga/projects/tagging/api.py
@@ -21,7 +21,7 @@ from taiga.base.decorators import detail_route
from taiga.base.utils.collections import OrderedSet
from . import services
-from . import serializers
+from . import validators
class TagsColorsResourceMixin:
@@ -38,27 +38,26 @@ class TagsColorsResourceMixin:
self.check_permissions(request, "create_tag", project)
self._raise_if_blocked(project)
- serializer = serializers.CreateTagSerializer(data=request.DATA, project=project)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.CreateTagValidator(data=request.DATA, project=project)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
services.create_tag(project, data.get("tag"), data.get("color"))
return response.Ok()
-
@detail_route(methods=["POST"])
def edit_tag(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, "edit_tag", project)
self._raise_if_blocked(project)
- serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.EditTagTagValidator(data=request.DATA, project=project)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
services.edit_tag(project,
data.get("from_tag"),
to_tag=data.get("to_tag", None),
@@ -66,18 +65,17 @@ class TagsColorsResourceMixin:
return response.Ok()
-
@detail_route(methods=["POST"])
def delete_tag(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, "delete_tag", project)
self._raise_if_blocked(project)
- serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.DeleteTagValidator(data=request.DATA, project=project)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
services.delete_tag(project, data.get("tag"))
return response.Ok()
@@ -88,11 +86,11 @@ class TagsColorsResourceMixin:
self.check_permissions(request, "mix_tags", project)
self._raise_if_blocked(project)
- serializer = serializers.MixTagsSerializer(data=request.DATA, project=project)
- if not serializer.is_valid():
- return response.BadRequest(serializer.errors)
+ validator = validators.MixTagsValidator(data=request.DATA, project=project)
+ if not validator.is_valid():
+ return response.BadRequest(validator.errors)
- data = serializer.data
+ data = validator.data
services.mix_tags(project, data.get("from_tags"), data.get("to_tag"))
return response.Ok()
diff --git a/taiga/projects/tagging/fields.py b/taiga/projects/tagging/fields.py
index 24f92f23..47553d8c 100644
--- a/taiga/projects/tagging/fields.py
+++ b/taiga/projects/tagging/fields.py
@@ -16,11 +16,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers
+from taiga.base.exceptions import ValidationError
import re
diff --git a/taiga/projects/tagging/serializers.py b/taiga/projects/tagging/validators.py
similarity index 77%
rename from taiga/projects/tagging/serializers.py
rename to taiga/projects/tagging/validators.py
index dc25b73a..595a5a3f 100644
--- a/taiga/projects/tagging/serializers.py
+++ b/taiga/projects/tagging/validators.py
@@ -19,6 +19,7 @@
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
+from taiga.base.api import validators
from . import services
from . import fields
@@ -26,7 +27,7 @@ from . import fields
import re
-class ProjectTagSerializer(serializers.Serializer):
+class ProjectTagValidator(validators.Validator):
def __init__(self, *args, **kwargs):
# Don't pass the extra project arg
self.project = kwargs.pop("project")
@@ -35,26 +36,26 @@ class ProjectTagSerializer(serializers.Serializer):
super().__init__(*args, **kwargs)
-class CreateTagSerializer(ProjectTagSerializer):
+class CreateTagValidator(ProjectTagValidator):
tag = serializers.CharField()
color = serializers.CharField(required=False)
def validate_tag(self, attrs, source):
tag = attrs.get(source, None)
if services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag exists."))
+ raise validators.ValidationError(_("The tag exists."))
return attrs
def validate_color(self, attrs, source):
color = attrs.get(source, None)
if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
- raise serializers.ValidationError(_("The color is not a valid HEX color."))
+ raise validators.ValidationError(_("The color is not a valid HEX color."))
return attrs
-class EditTagTagSerializer(ProjectTagSerializer):
+class EditTagTagValidator(ProjectTagValidator):
from_tag = serializers.CharField()
to_tag = serializers.CharField(required=False)
color = serializers.CharField(required=False)
@@ -62,37 +63,37 @@ class EditTagTagSerializer(ProjectTagSerializer):
def validate_from_tag(self, attrs, source):
tag = attrs.get(source, None)
if not services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag doesn't exist."))
+ raise validators.ValidationError(_("The tag doesn't exist."))
return attrs
def validate_to_tag(self, attrs, source):
tag = attrs.get(source, None)
if services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag exists yet"))
+ raise validators.ValidationError(_("The tag exists yet"))
return attrs
def validate_color(self, attrs, source):
color = attrs.get(source, None)
if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
- raise serializers.ValidationError(_("The color is not a valid HEX color."))
+ raise validators.ValidationError(_("The color is not a valid HEX color."))
return attrs
-class DeleteTagSerializer(ProjectTagSerializer):
+class DeleteTagValidator(ProjectTagValidator):
tag = serializers.CharField()
def validate_tag(self, attrs, source):
tag = attrs.get(source, None)
if not services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag doesn't exist."))
+ raise validators.ValidationError(_("The tag doesn't exist."))
return attrs
-class MixTagsSerializer(ProjectTagSerializer):
+class MixTagsValidator(ProjectTagValidator):
from_tags = fields.TagsField()
to_tag = serializers.CharField()
@@ -100,13 +101,13 @@ class MixTagsSerializer(ProjectTagSerializer):
tags = attrs.get(source, None)
for tag in tags:
if not services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag doesn't exist."))
+ raise validators.ValidationError(_("The tag doesn't exist."))
return attrs
def validate_to_tag(self, attrs, source):
tag = attrs.get(source, None)
if not services.tag_exist_for_project_elements(self.project, tag):
- raise serializers.ValidationError(_("The tag doesn't exist."))
+ raise validators.ValidationError(_("The tag doesn't exist."))
return attrs
diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py
index 01ae057e..3dc2bd32 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
@@ -162,10 +163,18 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
@list_route(methods=["GET"])
def by_ref(self, request):
- ref = request.QUERY_PARAMS.get("ref", None)
+ retrieve_kwargs = {
+ "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)
+ if project_id is not None:
+ retrieve_kwargs["project_id"] = project_id
+
+ project_slug = request.QUERY_PARAMS.get("project__slug", None)
+ if project_slug is not None:
+ retrieve_kwargs["project__slug"] = project_slug
+
+ return self.retrieve(request, **retrieve_kwargs)
@list_route(methods=["GET"])
def csv(self, request):
@@ -182,9 +191,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 +203,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..ddb3f33b 100644
--- a/taiga/projects/tasks/validators.py
+++ b/taiga/projects/tasks/validators.py
@@ -19,7 +19,14 @@
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
-
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
+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
@@ -28,5 +35,35 @@ class TaskExistsValidator:
value = attrs[source]
if not models.Task.objects.filter(pk=value).exists():
msg = _("There's no task with that id")
- raise serializers.ValidationError(msg)
+ raise 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..0e718d10 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
@@ -237,10 +224,18 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
@list_route(methods=["GET"])
def by_ref(self, request):
- ref = request.QUERY_PARAMS.get("ref", None)
+ retrieve_kwargs = {
+ "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)
+ if project_id is not None:
+ retrieve_kwargs["project_id"] = project_id
+
+ project_slug = request.QUERY_PARAMS.get("project__slug", None)
+ if project_slug is not None:
+ retrieve_kwargs["project__slug"] = project_slug
+
+ return self.retrieve(request, **retrieve_kwargs)
@list_route(methods=["GET"])
def csv(self, request):
@@ -257,9 +252,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 +264,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 +289,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..2d61934f 100644
--- a/taiga/projects/userstories/validators.py
+++ b/taiga/projects/userstories/validators.py
@@ -19,14 +19,96 @@
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.exceptions import ValidationError
+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):
value = attrs[source]
if not models.UserStory.objects.filter(pk=value).exists():
msg = _("There's no user story with that id")
- raise serializers.ValidationError(msg)
+ raise 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 ValidationError("all the user stories must be from the same project")
+
+ if project.milestones.filter(id=data["milestone_id"]).count() != 1:
+ raise ValidationError("the milestone isn't valid for the project")
+
+ return data
diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py
new file mode 100644
index 00000000..ee552136
--- /dev/null
+++ b/taiga/projects/utils.py
@@ -0,0 +1,436 @@
+# -*- 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 .
+
+def attach_members(queryset, as_field="members_attr"):
+ """Attach a json members representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the members 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
+ users_user.id,
+ users_user.username,
+ users_user.full_name,
+ users_user.email,
+ concat(full_name, username) complete_user_name,
+ users_user.color,
+ users_user.photo,
+ users_user.is_active,
+ users_role.name role_name
+
+ FROM projects_membership
+ LEFT JOIN users_user ON projects_membership.user_id = users_user.id
+ LEFT JOIN users_role ON users_role.id = projects_membership.role_id
+ WHERE projects_membership.project_id = {tbl}.id
+ ORDER BY complete_user_name) t"""
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_closed_milestones(queryset, as_field="closed_milestones_attr"):
+ """Attach a closed milestones counter to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the counter as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ sql = """SELECT COUNT(milestones_milestone.id)
+ FROM milestones_milestone
+ WHERE
+ milestones_milestone.project_id = {tbl}.id AND
+ milestones_milestone.closed = True
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_notify_policies(queryset, as_field="notify_policies_attr"):
+ """Attach a json notification policies representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the notification policies 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(notifications_notifypolicy))
+ FROM notifications_notifypolicy
+ WHERE
+ notifications_notifypolicy.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_userstory_statuses(queryset, as_field="userstory_statuses_attr"):
+ """Attach a json userstory statuses representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the userstory statuses 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(projects_userstorystatus))
+ FROM projects_userstorystatus
+ WHERE
+ projects_userstorystatus.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_points(queryset, as_field="points_attr"):
+ """Attach a json points representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the points 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(projects_points))
+ FROM projects_points
+ WHERE
+ projects_points.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_task_statuses(queryset, as_field="task_statuses_attr"):
+ """Attach a json task statuses representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the task statuses 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(projects_taskstatus))
+ FROM projects_taskstatus
+ WHERE
+ projects_taskstatus.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_issue_statuses(queryset, as_field="issue_statuses_attr"):
+ """Attach a json issue statuses representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the statuses 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(projects_issuestatus))
+ FROM projects_issuestatus
+ WHERE
+ projects_issuestatus.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_issue_types(queryset, as_field="issue_types_attr"):
+ """Attach a json issue types representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the types 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(projects_issuetype))
+ FROM projects_issuetype
+ WHERE
+ projects_issuetype.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_priorities(queryset, as_field="priorities_attr"):
+ """Attach a json priorities representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the priorities 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(projects_priority))
+ FROM projects_priority
+ WHERE
+ projects_priority.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_severities(queryset, as_field="severities_attr"):
+ """Attach a json severities representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the severities 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(projects_severity))
+ FROM projects_severity
+ WHERE
+ projects_severity.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_userstory_custom_attributes(queryset, as_field="userstory_custom_attributes_attr"):
+ """Attach a json userstory custom attributes representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the userstory custom attributes 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(custom_attributes_userstorycustomattribute))
+ FROM custom_attributes_userstorycustomattribute
+ WHERE
+ custom_attributes_userstorycustomattribute.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_task_custom_attributes(queryset, as_field="task_custom_attributes_attr"):
+ """Attach a json task custom attributes representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the task custom attributes 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(custom_attributes_taskcustomattribute))
+ FROM custom_attributes_taskcustomattribute
+ WHERE
+ custom_attributes_taskcustomattribute.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_issue_custom_attributes(queryset, as_field="issue_custom_attributes_attr"):
+ """Attach a json issue custom attributes representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the issue custom attributes 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(custom_attributes_issuecustomattribute))
+ FROM custom_attributes_issuecustomattribute
+ WHERE
+ custom_attributes_issuecustomattribute.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_roles(queryset, as_field="roles_attr"):
+ """Attach a json roles representation to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the roles 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(users_role))
+ FROM users_role
+ WHERE
+ users_role.project_id = {tbl}.id
+ """
+
+ sql = sql.format(tbl=model._meta.db_table)
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_is_fan(queryset, user, as_field="is_fan_attr"):
+ """Attach a is fan boolean to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the boolean as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ if user is None or user.is_anonymous():
+ sql = """SELECT false"""
+ else:
+ sql = """SELECT COUNT(likes_like.id) > 0
+ FROM likes_like
+ INNER JOIN django_content_type
+ ON likes_like.content_type_id = django_content_type.id
+ WHERE
+ django_content_type.model = 'project' AND
+ django_content_type.app_label = 'projects' AND
+ likes_like.user_id = {user_id} AND
+ likes_like.object_id = {tbl}.id"""
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
+
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_my_role_permissions(queryset, user, as_field="my_role_permissions_attr"):
+ """Attach a permission array to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the permissions as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ if user is None or user.is_anonymous():
+ sql = """SELECT '{}'"""
+ else:
+ sql = """SELECT users_role.permissions
+ FROM projects_membership
+ LEFT JOIN users_user ON projects_membership.user_id = users_user.id
+ LEFT JOIN users_role ON users_role.id = projects_membership.role_id
+ WHERE
+ projects_membership.project_id = {tbl}.id AND
+ users_user.id = {user_id}"""
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
+
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_private_projects_same_owner(queryset, user, as_field="private_projects_same_owner_attr"):
+ """Attach a private projects counter to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the counter as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ if user is None or user.is_anonymous():
+ sql = """SELECT 0"""
+ else:
+ sql = """SELECT COUNT(id)
+ FROM projects_project p_aux
+ WHERE
+ p_aux.is_private = True AND
+ p_aux.owner_id = {tbl}.owner_id"""
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
+
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_public_projects_same_owner(queryset, user, as_field="public_projects_same_owner_attr"):
+ """Attach a public projects counter to each object of the queryset.
+
+ :param queryset: A Django projects queryset object.
+ :param as_field: Attach the counter as an attribute with this name.
+
+ :return: Queryset object with the additional `as_field` field.
+ """
+ model = queryset.model
+ if user is None or user.is_anonymous():
+ sql = """SELECT 0"""
+ else:
+ sql = """SELECT COUNT(id)
+ FROM projects_project p_aux
+ WHERE
+ p_aux.is_private = False AND
+ p_aux.owner_id = {tbl}.owner_id"""
+
+ sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
+
+ queryset = queryset.extra(select={as_field: sql})
+ return queryset
+
+
+def attach_extra_info(queryset, user=None):
+ queryset = attach_members(queryset)
+ queryset = attach_closed_milestones(queryset)
+ queryset = attach_notify_policies(queryset)
+ queryset = attach_userstory_statuses(queryset)
+ queryset = attach_points(queryset)
+ queryset = attach_task_statuses(queryset)
+ queryset = attach_issue_statuses(queryset)
+ queryset = attach_issue_types(queryset)
+ queryset = attach_priorities(queryset)
+ queryset = attach_severities(queryset)
+ queryset = attach_userstory_custom_attributes(queryset)
+ queryset = attach_task_custom_attributes(queryset)
+ queryset = attach_issue_custom_attributes(queryset)
+ queryset = attach_roles(queryset)
+ queryset = attach_is_fan(queryset, user)
+ queryset = attach_my_role_permissions(queryset, user)
+ queryset = attach_private_projects_same_owner(queryset, user)
+ queryset = attach_public_projects_same_owner(queryset, user)
+
+ return queryset
diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py
index 05866b66..de06c05c 100644
--- a/taiga/projects/validators.py
+++ b/taiga/projects/validators.py
@@ -16,11 +16,43 @@
# 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.exceptions import ValidationError
+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 ValidationError(_("Name duplicated for the project"))
+
+ return attrs
class ProjectExistsValidator:
@@ -28,7 +60,7 @@ class ProjectExistsValidator:
value = attrs[source]
if not models.Project.objects.filter(pk=value).exists():
msg = _("There's no project with that id")
- raise serializers.ValidationError(msg)
+ raise ValidationError(msg)
return attrs
@@ -37,7 +69,7 @@ class UserStoryStatusExistsValidator:
value = attrs[source]
if not models.UserStoryStatus.objects.filter(pk=value).exists():
msg = _("There's no user story status with that id")
- raise serializers.ValidationError(msg)
+ raise ValidationError(msg)
return attrs
@@ -46,5 +78,172 @@ class TaskStatusExistsValidator:
value = attrs[source]
if not models.TaskStatus.objects.filter(pk=value).exists():
msg = _("There's no task status with that id")
- raise serializers.ValidationError(msg)
+ raise 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 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 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 ValidationError(_("The project owner must be admin."))
+
+ if not services.project_has_valid_admins(project, exclude_user=self.object.user):
+ raise 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/serializers.py b/taiga/projects/votes/serializers.py
index eb47c9ef..b97bd3bf 100644
--- a/taiga/projects/votes/serializers.py
+++ b/taiga/projects/votes/serializers.py
@@ -17,14 +17,14 @@
# 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 taiga.base.api import serializers
+from taiga.base.fields import Field, MethodField
-class VoterSerializer(serializers.ModelSerializer):
- full_name = serializers.CharField(source='get_full_name', required=False)
+class VoterSerializer(serializers.LightSerializer):
+ id = Field()
+ username = Field()
+ full_name = MethodField()
- class Meta:
- model = get_user_model()
- fields = ('id', 'username', 'full_name')
+ def get_full_name(self, obj):
+ return obj.get_full_name()
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/api.py b/taiga/users/api.py
index a02e1576..00d5d279 100644
--- a/taiga/users/api.py
+++ b/taiga/users/api.py
@@ -19,7 +19,6 @@
import uuid
from django.apps import apps
-from django.db.models import Q, F
from django.utils.translation import ugettext as _
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
@@ -28,21 +27,21 @@ from django.conf import settings
from taiga.base import exceptions as exc
from taiga.base import filters
from taiga.base import response
+from taiga.base.utils.dicts import into_namedtuple
from taiga.auth.tokens import get_user_for_token
from taiga.base.decorators import list_route
from taiga.base.decorators import detail_route
from taiga.base.api import ModelCrudViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
-from taiga.base.filters import PermissionBasedFilterBackend
from taiga.base.api.utils import get_object_or_404
from taiga.base.filters import MembersFilterBackend
from taiga.base.mails import mail_builder
-from taiga.projects.votes import services as votes_service
from taiga.users.services import get_user_by_username_or_email
from easy_thumbnails.source_generators import pil_image
from . import models
from . import serializers
+from . import validators
from . import permissions
from . import filters as user_filters
from . import services
@@ -53,6 +52,8 @@ class UsersViewSet(ModelCrudViewSet):
permission_classes = (permissions.UserPermission,)
admin_serializer_class = serializers.UserAdminSerializer
serializer_class = serializers.UserSerializer
+ admin_validator_class = validators.UserAdminValidator
+ validator_class = validators.UserValidator
queryset = models.User.objects.all().prefetch_related("memberships")
filter_backends = (MembersFilterBackend,)
@@ -64,6 +65,14 @@ class UsersViewSet(ModelCrudViewSet):
return self.serializer_class
+ def get_validator_class(self):
+ if self.action in ["partial_update", "update", "retrieve", "by_username"]:
+ user = self.object
+ if self.request.user == user or self.request.user.is_superuser:
+ return self.admin_validator_class
+
+ return self.validator_class
+
def create(self, *args, **kwargs):
raise exc.NotSupported()
@@ -86,7 +95,7 @@ class UsersViewSet(ModelCrudViewSet):
serializer = self.get_serializer(self.object)
return response.Ok(serializer.data)
- #TODO: commit_on_success
+ # TODO: commit_on_success
def partial_update(self, request, *args, **kwargs):
"""
We must detect if the user is trying to change his email so we can
@@ -96,12 +105,10 @@ class UsersViewSet(ModelCrudViewSet):
user = self.get_object()
self.check_permissions(request, "update", user)
- ret = super().partial_update(request, *args, **kwargs)
-
new_email = request.DATA.get('email', None)
if new_email is not None:
valid_new_email = True
- duplicated_email = models.User.objects.filter(email = new_email).exists()
+ duplicated_email = models.User.objects.filter(email=new_email).exists()
try:
validate_email(new_email)
@@ -115,14 +122,21 @@ class UsersViewSet(ModelCrudViewSet):
elif not valid_new_email:
raise exc.WrongArguments(_("Not valid email"))
- #We need to generate a token for the email
+ # We need to generate a token for the email
request.user.email_token = str(uuid.uuid1())
request.user.new_email = new_email
request.user.save(update_fields=["email_token", "new_email"])
- email = mail_builder.change_email(request.user.new_email, {"user": request.user,
- "lang": request.user.lang})
+ email = mail_builder.change_email(
+ request.user.new_email,
+ {
+ "user": request.user,
+ "lang": request.user.lang
+ }
+ )
email.send()
+ ret = super().partial_update(request, *args, **kwargs)
+
return ret
def destroy(self, request, pk=None):
@@ -165,16 +179,16 @@ class UsersViewSet(ModelCrudViewSet):
self.check_permissions(request, "change_password_from_recovery", None)
- serializer = serializers.RecoverySerializer(data=request.DATA, many=False)
- if not serializer.is_valid():
+ validator = validators.RecoveryValidator(data=request.DATA, many=False)
+ if not validator.is_valid():
raise exc.WrongArguments(_("Token is invalid"))
try:
- user = models.User.objects.get(token=serializer.data["token"])
+ user = models.User.objects.get(token=validator.data["token"])
except models.User.DoesNotExist:
raise exc.WrongArguments(_("Token is invalid"))
- user.set_password(serializer.data["password"])
+ user.set_password(validator.data["password"])
user.token = None
user.save(update_fields=["password", "token"])
@@ -247,13 +261,13 @@ class UsersViewSet(ModelCrudViewSet):
"""
Verify the email change to current logged user.
"""
- serializer = serializers.ChangeEmailSerializer(data=request.DATA, many=False)
- if not serializer.is_valid():
+ validator = validators.ChangeEmailValidator(data=request.DATA, many=False)
+ if not validator.is_valid():
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
"didn't use it before?"))
try:
- user = models.User.objects.get(email_token=serializer.data["email_token"])
+ user = models.User.objects.get(email_token=validator.data["email_token"])
except models.User.DoesNotExist:
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
"didn't use it before?"))
@@ -280,14 +294,14 @@ class UsersViewSet(ModelCrudViewSet):
"""
Cancel an account via token
"""
- serializer = serializers.CancelAccountSerializer(data=request.DATA, many=False)
- if not serializer.is_valid():
+ validator = validators.CancelAccountValidator(data=request.DATA, many=False)
+ if not validator.is_valid():
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
try:
max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None)
- user = get_user_for_token(serializer.data["cancel_token"], "cancel_account",
- max_age=max_age_cancel_account)
+ user = get_user_for_token(validator.data["cancel_token"], "cancel_account",
+ max_age=max_age_cancel_account)
except exc.NotAuthenticated:
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
@@ -305,7 +319,7 @@ class UsersViewSet(ModelCrudViewSet):
self.object_list = user_filters.ContactsFilterBackend().filter_queryset(
user, request, self.get_queryset(), self).extra(
- select={"complete_user_name":"concat(full_name, username)"}).order_by("complete_user_name")
+ select={"complete_user_name": "concat(full_name, username)"}).order_by("complete_user_name")
page = self.paginate_queryset(self.object_list)
if page is not None:
@@ -349,10 +363,10 @@ class UsersViewSet(ModelCrudViewSet):
for elem in elements:
if elem["type"] == "project":
# projects are liked objects
- response_data.append(serializers.LikedObjectSerializer(elem, **extra_args_liked).data )
+ response_data.append(serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args_liked).data)
else:
# stories, tasks and issues are voted objects
- response_data.append(serializers.VotedObjectSerializer(elem, **extra_args_voted).data )
+ response_data.append(serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args_voted).data)
return response.Ok(response_data)
@@ -374,7 +388,7 @@ class UsersViewSet(ModelCrudViewSet):
"user_likes": services.get_liked_content_for_user(request.user),
}
- response_data = [serializers.LikedObjectSerializer(elem, **extra_args).data for elem in elements]
+ response_data = [serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements]
return response.Ok(response_data)
@@ -397,17 +411,18 @@ class UsersViewSet(ModelCrudViewSet):
"user_votes": services.get_voted_content_for_user(request.user),
}
- response_data = [serializers.VotedObjectSerializer(elem, **extra_args).data for elem in elements]
+ response_data = [serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements]
return response.Ok(response_data)
-######################################################
-## Role
-######################################################
+######################################################
+# Role
+######################################################
class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Role
serializer_class = serializers.RoleSerializer
+ validator_class = validators.RoleValidator
permission_classes = (permissions.RolesPermission, )
filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',)
diff --git a/taiga/users/models.py b/taiga/users/models.py
index 264d1539..b9c60e4b 100644
--- a/taiga/users/models.py
+++ b/taiga/users/models.py
@@ -198,7 +198,7 @@ class User(AbstractBaseUser, PermissionsMixin):
def _fill_cached_memberships(self):
self._cached_memberships = {}
- qs = self.memberships.prefetch_related("user", "project", "role")
+ qs = self.memberships.select_related("user", "project", "role")
for membership in qs.all():
self._cached_memberships[membership.project.id] = membership
diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py
index 98903ec1..a28f5ce3 100644
--- a/taiga/users/serializers.py
+++ b/taiga/users/serializers.py
@@ -17,70 +17,45 @@
# along with this program. If not, see .
from django.conf import settings
-from django.core import validators
-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, I18NField
+
from taiga.base.utils.thumbnails import get_thumbnail_url
from taiga.projects.models import Project
-from .models import User, Role
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
from .gravatar import get_gravatar_url
from collections import namedtuple
-import re
-import serpy
-
######################################################
-## User
+# User
######################################################
-class ContactProjectDetailSerializer(serializers.ModelSerializer):
- class Meta:
- model = Project
- fields = ("id", "slug", "name")
+class ContactProjectDetailSerializer(serializers.LightSerializer):
+ id = Field()
+ slug = Field()
+ name = Field()
-class UserSerializer(serializers.ModelSerializer):
- full_name_display = serializers.SerializerMethodField("get_full_name_display")
- photo = serializers.SerializerMethodField("get_photo")
- big_photo = serializers.SerializerMethodField("get_big_photo")
- gravatar_url = serializers.SerializerMethodField("get_gravatar_url")
- roles = serializers.SerializerMethodField("get_roles")
- projects_with_me = serializers.SerializerMethodField("get_projects_with_me")
-
- class Meta:
- model = User
- # IMPORTANT: Maintain the UserAdminSerializer Meta up to date
- # with this info (including there the email)
- fields = ("id", "username", "full_name", "full_name_display",
- "color", "bio", "lang", "theme", "timezone", "is_active",
- "photo", "big_photo", "roles", "projects_with_me",
- "gravatar_url")
- read_only_fields = ("id",)
-
- def validate_username(self, attrs, source):
- value = attrs[source]
- validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"),
- _("invalid"))
-
- try:
- validator(value)
- except ValidationError:
- raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, "
- "numbers and /./-/_ characters'"))
-
- if (self.object and
- self.object.username != value and
- User.objects.filter(username=value).exists()):
- raise serializers.ValidationError(_("Invalid username. Try with a different one."))
-
- return attrs
+class UserSerializer(serializers.LightSerializer):
+ id = Field()
+ username = Field()
+ full_name = Field()
+ full_name_display = MethodField()
+ color = Field()
+ bio = Field()
+ lang = Field()
+ theme = Field()
+ timezone = Field()
+ is_active = Field()
+ photo = MethodField()
+ big_photo = MethodField()
+ gravatar_url = MethodField()
+ roles = MethodField()
+ projects_with_me = MethodField()
def get_full_name_display(self, obj):
return obj.get_full_name() if obj else ""
@@ -113,24 +88,13 @@ class UserSerializer(serializers.ModelSerializer):
class UserAdminSerializer(UserSerializer):
- total_private_projects = serializers.SerializerMethodField("get_total_private_projects")
- total_public_projects = serializers.SerializerMethodField("get_total_public_projects")
-
- class Meta:
- model = User
- # IMPORTANT: Maintain the UserSerializer Meta up to date
- # with this info (including here the email)
- fields = ("id", "username", "full_name", "full_name_display", "email",
- "color", "bio", "lang", "theme", "timezone", "is_active", "photo",
- "big_photo", "gravatar_url",
- "max_private_projects", "max_public_projects",
- "max_memberships_private_projects", "max_memberships_public_projects",
- "total_private_projects", "total_public_projects")
-
- read_only_fields = ("id", "email",
- "max_private_projects", "max_public_projects",
- "max_memberships_private_projects",
- "max_memberships_public_projects")
+ total_private_projects = MethodField()
+ total_public_projects = MethodField()
+ email = Field()
+ max_private_projects = Field()
+ max_public_projects = Field()
+ max_memberships_private_projects = Field()
+ max_memberships_public_projects = Field()
def get_total_private_projects(self, user):
return user.owned_projects.filter(is_private=True).count()
@@ -139,19 +103,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,76 +120,70 @@ 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
-class RecoverySerializer(serializers.Serializer):
- token = serializers.CharField(max_length=200)
- password = serializers.CharField(min_length=6)
-
-
-class ChangeEmailSerializer(serializers.Serializer):
- email_token = serializers.CharField(max_length=200)
-
-
-class CancelAccountSerializer(serializers.Serializer):
- cancel_token = serializers.CharField(max_length=200)
+ return super().to_value(instance)
######################################################
-## Role
+# Role
######################################################
-class RoleSerializer(serializers.ModelSerializer):
- members_count = serializers.SerializerMethodField("get_members_count")
+class RoleSerializer(serializers.LightSerializer):
+ id = Field()
+ name = Field()
+ computable = Field()
+ project = Field(attr="project_id")
+ order = Field()
+ members_count = MethodField()
permissions = PgArrayField(required=False)
- class Meta:
- model = Role
- fields = ('id', 'name', 'permissions', 'computable', 'project', 'order', 'members_count')
- i18n_fields = ("name",)
-
def get_members_count(self, obj):
return obj.memberships.count()
-class ProjectRoleSerializer(serializers.ModelSerializer):
- class Meta:
- model = Role
- fields = ('id', 'name', 'slug', 'order', 'computable')
- i18n_fields = ("name",)
+class ProjectRoleSerializer(serializers.LightSerializer):
+ id = Field()
+ name = I18NField()
+ slug = Field()
+ order = Field()
+ computable = Field()
######################################################
-## Like
+# Like
######################################################
-class HighLightedContentSerializer(serializers.Serializer):
- type = serializers.CharField()
- id = serializers.IntegerField()
- ref = serializers.IntegerField()
- slug = serializers.CharField()
- name = serializers.CharField()
- subject = serializers.CharField()
- description = serializers.SerializerMethodField("get_description")
- assigned_to = serializers.IntegerField()
- status = serializers.CharField()
- status_color = serializers.CharField()
- tags_colors = serializers.SerializerMethodField("get_tags_color")
- created_date = serializers.DateTimeField()
- is_private = serializers.SerializerMethodField("get_is_private")
- logo_small_url = serializers.SerializerMethodField("get_logo_small_url")
+class HighLightedContentSerializer(serializers.LightSerializer):
+ type = Field()
+ id = Field()
+ ref = Field()
+ slug = Field()
+ name = Field()
+ subject = Field()
+ description = MethodField()
+ assigned_to = Field()
+ status = Field()
+ status_color = Field()
+ tags_colors = MethodField()
+ created_date = Field()
+ is_private = MethodField()
+ logo_small_url = MethodField()
- project = serializers.SerializerMethodField("get_project")
- project_name = serializers.SerializerMethodField("get_project_name")
- project_slug = serializers.SerializerMethodField("get_project_slug")
- project_is_private = serializers.SerializerMethodField("get_project_is_private")
- project_blocked_code = serializers.CharField()
+ project = MethodField()
+ project_name = MethodField()
+ project_slug = MethodField()
+ project_is_private = MethodField()
+ project_blocked_code = Field()
- assigned_to_username = serializers.CharField()
- assigned_to_full_name = serializers.CharField()
- assigned_to_photo = serializers.SerializerMethodField("get_photo")
+ assigned_to_username = Field()
+ assigned_to_full_name = Field()
+ assigned_to_photo = MethodField()
- is_watcher = serializers.SerializerMethodField("get_is_watcher")
- total_watchers = serializers.IntegerField()
+ is_watcher = MethodField()
+ total_watchers = Field()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
@@ -241,18 +193,18 @@ class HighLightedContentSerializer(serializers.Serializer):
super().__init__(*args, **kwargs)
def _none_if_project(self, obj, property):
- type = obj.get("type", "")
+ type = getattr(obj, "type", "")
if type == "project":
return None
- return obj.get(property)
+ return getattr(obj, property)
def _none_if_not_project(self, obj, property):
- type = obj.get("type", "")
+ type = getattr(obj, "type", "")
if type != "project":
return None
- return obj.get(property)
+ return getattr(obj, property)
def get_project(self, obj):
return self._none_if_project(obj, "project")
@@ -278,29 +230,29 @@ class HighLightedContentSerializer(serializers.Serializer):
return get_thumbnail_url(logo, settings.THN_LOGO_SMALL)
return None
- def get_photo(self, obj):
- type = obj.get("type", "")
+ def get_assigned_to_photo(self, obj):
+ type = getattr(obj, "type", "")
if type == "project":
return None
UserData = namedtuple("UserData", ["photo", "email"])
- user_data = UserData(photo=obj["assigned_to_photo"], email=obj.get("assigned_to_email") or "")
+ user_data = UserData(photo=obj.assigned_to_photo, email=obj.assigned_to_email or "")
return get_photo_or_gravatar_url(user_data)
- def get_tags_color(self, obj):
- tags = obj.get("tags", [])
+ def get_tags_colors(self, obj):
+ tags = getattr(obj, "tags", [])
tags = tags if tags is not None else []
- tags_colors = obj.get("tags_colors", [])
+ tags_colors = getattr(obj, "tags_colors", [])
tags_colors = tags_colors if tags_colors is not None else []
return [{"name": tc[0], "color": tc[1]} for tc in tags_colors if tc[0] in tags]
def get_is_watcher(self, obj):
- return obj["id"] in self.user_watching.get(obj["type"], [])
+ return obj.id in self.user_watching.get(obj.type, [])
class LikedObjectSerializer(HighLightedContentSerializer):
- is_fan = serializers.SerializerMethodField("get_is_fan")
- total_fans = serializers.IntegerField()
+ is_fan = MethodField()
+ total_fans = Field()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
@@ -310,12 +262,12 @@ class LikedObjectSerializer(HighLightedContentSerializer):
super().__init__(*args, **kwargs)
def get_is_fan(self, obj):
- return obj["id"] in self.user_likes.get(obj["type"], [])
+ return obj.id in self.user_likes.get(obj.type, [])
class VotedObjectSerializer(HighLightedContentSerializer):
- is_voter = serializers.SerializerMethodField("get_is_voter")
- total_voters = serializers.IntegerField()
+ is_voter = MethodField()
+ total_voters = Field()
def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass
@@ -325,4 +277,4 @@ class VotedObjectSerializer(HighLightedContentSerializer):
super().__init__(*args, **kwargs)
def get_is_voter(self, obj):
- return obj["id"] in self.user_votes.get(obj["type"], [])
+ return obj.id in self.user_votes.get(obj.type, [])
diff --git a/taiga/users/validators.py b/taiga/users/validators.py
index 477342de..f23da47a 100644
--- a/taiga/users/validators.py
+++ b/taiga/users/validators.py
@@ -3,7 +3,6 @@
# 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
@@ -17,17 +16,92 @@
# 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 django.core import validators as core_validators
+from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers
+from taiga.base.api import validators
+from taiga.base.exceptions import ValidationError
+from taiga.base.fields import PgArrayField
-from . import models
+from .models import User, Role
+
+import re
class RoleExistsValidator:
def validate_role_id(self, attrs, source):
value = attrs[source]
- if not models.Role.objects.filter(pk=value).exists():
+ if not Role.objects.filter(pk=value).exists():
msg = _("There's no role with that id")
- raise serializers.ValidationError(msg)
+ raise ValidationError(msg)
return attrs
+
+
+######################################################
+# User
+######################################################
+class UserValidator(validators.ModelValidator):
+ class Meta:
+ model = User
+ fields = ("username", "full_name", "color", "bio", "lang",
+ "theme", "timezone", "is_active")
+
+ def validate_username(self, attrs, source):
+ value = attrs[source]
+ validator = core_validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"),
+ _("invalid"))
+
+ try:
+ validator(value)
+ except ValidationError:
+ raise ValidationError(_("Required. 255 characters or fewer. Letters, "
+ "numbers and /./-/_ characters'"))
+
+ if (self.object and
+ self.object.username != value and
+ User.objects.filter(username=value).exists()):
+ raise ValidationError(_("Invalid username. Try with a different one."))
+
+ return attrs
+
+
+class UserAdminValidator(UserValidator):
+ class Meta:
+ model = User
+ # IMPORTANT: Maintain the UserSerializer Meta up to date
+ # with this info (including here the email)
+ fields = ("username", "full_name", "color", "bio", "lang",
+ "theme", "timezone", "is_active", "email")
+
+
+class RecoveryValidator(validators.Validator):
+ token = serializers.CharField(max_length=200)
+ password = serializers.CharField(min_length=6)
+
+
+class ChangeEmailValidator(validators.Validator):
+ email_token = serializers.CharField(max_length=200)
+
+
+class CancelAccountValidator(validators.Validator):
+ cancel_token = serializers.CharField(max_length=200)
+
+
+######################################################
+# Role
+######################################################
+
+class RoleValidator(validators.ModelValidator):
+ permissions = PgArrayField(required=False)
+
+ class Meta:
+ model = Role
+ fields = ('id', 'name', 'permissions', 'computable', 'project', 'order')
+ i18n_fields = ("name",)
+
+
+class ProjectRoleValidator(validators.ModelValidator):
+ class Meta:
+ model = Role
+ fields = ('id', 'name', 'slug', 'order', 'computable')
diff --git a/taiga/userstorage/api.py b/taiga/userstorage/api.py
index 62575d2b..94c5ea00 100644
--- a/taiga/userstorage/api.py
+++ b/taiga/userstorage/api.py
@@ -17,7 +17,6 @@
# along with this program. If not, see .
from django.utils.translation import ugettext as _
-from django.db import IntegrityError
from taiga.base.api import ModelCrudViewSet
from taiga.base import exceptions as exc
@@ -25,6 +24,7 @@ from taiga.base import exceptions as exc
from . import models
from . import filters
from . import serializers
+from . import validators
from . import permissions
@@ -32,6 +32,7 @@ class StorageEntriesViewSet(ModelCrudViewSet):
model = models.StorageEntry
filter_backends = (filters.StorageEntriesFilterBackend,)
serializer_class = serializers.StorageEntrySerializer
+ validator_class = validators.StorageEntryValidator
permission_classes = [permissions.StorageEntriesPermission]
lookup_field = "key"
@@ -45,9 +46,11 @@ class StorageEntriesViewSet(ModelCrudViewSet):
obj.owner = self.request.user
def create(self, *args, **kwargs):
- try:
- return super().create(*args, **kwargs)
- except IntegrityError:
- key = self.request.DATA.get("key", None)
- raise exc.IntegrityError(_("Duplicate key value violates unique constraint. "
- "Key '{}' already exists.").format(key))
+ key = self.request.DATA.get("key", None)
+ if (key and self.request.user.is_authenticated() and
+ self.request.user.storage_entries.filter(key=key).exists()):
+ raise exc.BadRequest(
+ _("Duplicate key value violates unique constraint. "
+ "Key '{}' already exists.").format(key)
+ )
+ return super().create(*args, **kwargs)
diff --git a/taiga/userstorage/serializers.py b/taiga/userstorage/serializers.py
index 5fd97692..38765f19 100644
--- a/taiga/userstorage/serializers.py
+++ b/taiga/userstorage/serializers.py
@@ -17,15 +17,11 @@
# along with this program. If not, see .
from taiga.base.api import serializers
-from taiga.base.fields import JsonField
-
-from . import models
+from taiga.base.fields import Field
-class StorageEntrySerializer(serializers.ModelSerializer):
- value = JsonField(label="value")
-
- class Meta:
- model = models.StorageEntry
- fields = ("key", "value", "created_date", "modified_date")
- read_only_fields = ("created_date", "modified_date")
+class StorageEntrySerializer(serializers.LightSerializer):
+ key = Field()
+ value = Field()
+ created_date = Field()
+ modified_date = Field()
diff --git a/taiga/userstorage/validators.py b/taiga/userstorage/validators.py
new file mode 100644
index 00000000..615b88d7
--- /dev/null
+++ b/taiga/userstorage/validators.py
@@ -0,0 +1,27 @@
+# -*- 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 . import models
+
+
+class StorageEntryValidator(validators.ModelValidator):
+ class Meta:
+ model = models.StorageEntry
+ fields = ("key", "value")
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 77e79542..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.serializers import ProjectDetailSerializer
+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)
@@ -153,12 +162,12 @@ def test_project_update(client, data):
data.project_owner
]
- project_data = ProjectDetailSerializer(data.private_project2).data
+ project_data = ProjectSerializer(data.private_project2).data
project_data["is_private"] = False
results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users)
assert results == [401, 403, 403, 200]
- project_data = ProjectDetailSerializer(data.blocked_project).data
+ project_data = ProjectSerializer(data.blocked_project).data
project_data["is_private"] = False
results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users)
assert results == [401, 403, 403, 451]
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_projects.py b/tests/integration/test_projects.py
index 5cadc245..cc3b3cb1 100644
--- a/tests/integration/test_projects.py
+++ b/tests/integration/test_projects.py
@@ -625,7 +625,7 @@ def test_projects_user_order(client):
#Testing user order
url = reverse("projects-list")
- url = "%s?member=%s&order_by=memberships__user_order" % (url, user.id)
+ url = "%s?member=%s&order_by=user_order" % (url, user.id)
response = client.json.get(url)
response_content = response.data
assert response.status_code == 200
diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py
index 90ed5599..f4bf2b51 100644
--- a/tests/integration/test_users.py
+++ b/tests/integration/test_users.py
@@ -30,6 +30,7 @@ from ..utils import DUMMY_BMP_DATA
from taiga.base.utils import json
from taiga.base.utils.thumbnails import get_thumbnail_url
+from taiga.base.utils.dicts import into_namedtuple
from taiga.users import models
from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer
from taiga.auth.tokens import get_token_for_user
@@ -505,7 +506,7 @@ def test_get_watched_list_valid_info_for_project():
raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0]
- project_watch_info = LikedObjectSerializer(raw_project_watch_info).data
+ project_watch_info = LikedObjectSerializer(into_namedtuple(raw_project_watch_info)).data
assert project_watch_info["type"] == "project"
assert project_watch_info["id"] == project.id
@@ -559,7 +560,7 @@ def test_get_liked_list_valid_info():
project.refresh_totals()
raw_project_like_info = get_liked_list(fan_user, viewer_user)[0]
- project_like_info = LikedObjectSerializer(raw_project_like_info).data
+ project_like_info = LikedObjectSerializer(into_namedtuple(raw_project_like_info)).data
assert project_like_info["type"] == "project"
assert project_like_info["id"] == project.id
@@ -609,7 +610,7 @@ def test_get_watched_list_valid_info_for_not_project_types():
instance.add_watcher(fav_user)
raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0]
- instance_watch_info = VotedObjectSerializer(raw_instance_watch_info).data
+ instance_watch_info = VotedObjectSerializer(into_namedtuple(raw_instance_watch_info)).data
assert instance_watch_info["type"] == object_type
assert instance_watch_info["id"] == instance.id
@@ -666,7 +667,7 @@ def test_get_voted_list_valid_info():
f.VotesFactory(content_type=content_type, object_id=instance.id, count=3)
raw_instance_vote_info = get_voted_list(fav_user, viewer_user, type=object_type)[0]
- instance_vote_info = VotedObjectSerializer(raw_instance_vote_info).data
+ instance_vote_info = VotedObjectSerializer(into_namedtuple(raw_instance_vote_info)).data
assert instance_vote_info["type"] == object_type
assert instance_vote_info["id"] == instance.id
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()