Merge pull request #777 from taigaio/extra-api-migration
Extra api migrationremotes/origin/issue/4795/notification_even_they_are_disabled
commit
df2f504125
|
@ -22,15 +22,16 @@ from enum import Enum
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from taiga.base.api import validators
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.api import viewsets
|
from taiga.base.api import viewsets
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base.decorators import list_route
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
|
|
||||||
from .serializers import PublicRegisterSerializer
|
from .validators import PublicRegisterValidator
|
||||||
from .serializers import PrivateRegisterForExistingUserSerializer
|
from .validators import PrivateRegisterForExistingUserValidator
|
||||||
from .serializers import PrivateRegisterForNewUserSerializer
|
from .validators import PrivateRegisterForNewUserValidator
|
||||||
|
|
||||||
from .services import private_register_for_existing_user
|
from .services import private_register_for_existing_user
|
||||||
from .services import private_register_for_new_user
|
from .services import private_register_for_new_user
|
||||||
|
@ -44,7 +45,7 @@ from .permissions import AuthPermission
|
||||||
def _parse_data(data:dict, *, cls):
|
def _parse_data(data:dict, *, cls):
|
||||||
"""
|
"""
|
||||||
Generic function for parse user data using
|
Generic function for parse user data using
|
||||||
specified serializer on `cls` keyword parameter.
|
specified validator on `cls` keyword parameter.
|
||||||
|
|
||||||
Raises: RequestValidationError exception if
|
Raises: RequestValidationError exception if
|
||||||
some errors found when data is validated.
|
some errors found when data is validated.
|
||||||
|
@ -52,21 +53,21 @@ def _parse_data(data:dict, *, cls):
|
||||||
Returns the parsed data.
|
Returns the parsed data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer = cls(data=data)
|
validator = cls(data=data)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
raise exc.RequestValidationError(serializer.errors)
|
raise exc.RequestValidationError(validator.errors)
|
||||||
return serializer.data
|
return validator.data
|
||||||
|
|
||||||
# Parse public register 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 data for existing user
|
||||||
parse_private_register_for_existing_user_data = \
|
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 data for new user
|
||||||
parse_private_register_for_new_user_data = \
|
parse_private_register_for_new_user_data = \
|
||||||
partial(_parse_data, cls=PrivateRegisterForNewUserSerializer)
|
partial(_parse_data, cls=PrivateRegisterForNewUserValidator)
|
||||||
|
|
||||||
|
|
||||||
class RegisterTypeEnum(Enum):
|
class RegisterTypeEnum(Enum):
|
||||||
|
@ -81,10 +82,10 @@ def parse_register_type(userdata:dict) -> str:
|
||||||
"""
|
"""
|
||||||
# Create adhoc inner serializer for avoid parse
|
# Create adhoc inner serializer for avoid parse
|
||||||
# manually the user data.
|
# manually the user data.
|
||||||
class _serializer(serializers.Serializer):
|
class _validator(validators.Validator):
|
||||||
existing = serializers.BooleanField()
|
existing = serializers.BooleanField()
|
||||||
|
|
||||||
instance = _serializer(data=userdata)
|
instance = _validator(data=userdata)
|
||||||
if not instance.is_valid():
|
if not instance.is_valid():
|
||||||
raise exc.RequestValidationError(instance.errors)
|
raise exc.RequestValidationError(instance.errors)
|
||||||
|
|
||||||
|
|
|
@ -16,16 +16,17 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.core import validators
|
from django.core import validators as core_validators
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.api import validators
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
class BaseRegisterSerializer(serializers.Serializer):
|
class BaseRegisterValidator(validators.Validator):
|
||||||
full_name = serializers.CharField(max_length=256)
|
full_name = serializers.CharField(max_length=256)
|
||||||
email = serializers.EmailField(max_length=255)
|
email = serializers.EmailField(max_length=255)
|
||||||
username = serializers.CharField(max_length=255)
|
username = serializers.CharField(max_length=255)
|
||||||
|
@ -33,25 +34,25 @@ class BaseRegisterSerializer(serializers.Serializer):
|
||||||
|
|
||||||
def validate_username(self, attrs, source):
|
def validate_username(self, attrs, source):
|
||||||
value = 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:
|
try:
|
||||||
validator(value)
|
validator(value)
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, numbers "
|
raise ValidationError(_("Required. 255 characters or fewer. Letters, numbers "
|
||||||
"and /./-/_ characters'"))
|
"and /./-/_ characters'"))
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class PublicRegisterSerializer(BaseRegisterSerializer):
|
class PublicRegisterValidator(BaseRegisterValidator):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PrivateRegisterForNewUserSerializer(BaseRegisterSerializer):
|
class PrivateRegisterForNewUserValidator(BaseRegisterValidator):
|
||||||
token = serializers.CharField(max_length=255, required=True)
|
token = serializers.CharField(max_length=255, required=True)
|
||||||
|
|
||||||
|
|
||||||
class PrivateRegisterForExistingUserSerializer(serializers.Serializer):
|
class PrivateRegisterForExistingUserValidator(validators.Validator):
|
||||||
username = serializers.CharField(max_length=255)
|
username = serializers.CharField(max_length=255)
|
||||||
password = serializers.CharField(min_length=4)
|
password = serializers.CharField(min_length=4)
|
||||||
token = serializers.CharField(max_length=255, required=True)
|
token = serializers.CharField(max_length=255, required=True)
|
|
@ -50,7 +50,6 @@ They are very similar to Django's form fields.
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.http import QueryDict
|
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
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
|
|
||||||
from . import ISO_8601
|
from . import ISO_8601
|
||||||
from .settings import api_settings
|
from .settings import api_settings
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,7 @@ class GenericAPIView(pagination.PaginationMixin,
|
||||||
# or override `get_queryset()`/`get_serializer_class()`.
|
# or override `get_queryset()`/`get_serializer_class()`.
|
||||||
queryset = None
|
queryset = None
|
||||||
serializer_class = None
|
serializer_class = None
|
||||||
|
validator_class = None
|
||||||
|
|
||||||
# This shortcut may be used instead of setting either or both
|
# This shortcut may be used instead of setting either or both
|
||||||
# of the `queryset`/`serializer_class` attributes, although using
|
# of the `queryset`/`serializer_class` attributes, although using
|
||||||
|
@ -79,6 +80,7 @@ class GenericAPIView(pagination.PaginationMixin,
|
||||||
# The following attributes may be subject to change,
|
# The following attributes may be subject to change,
|
||||||
# and should be considered private API.
|
# and should be considered private API.
|
||||||
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
|
||||||
|
model_validator_class = api_settings.DEFAULT_MODEL_VALIDATOR_CLASS
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
# These are pending deprecation...
|
# These are pending deprecation...
|
||||||
|
@ -88,7 +90,7 @@ class GenericAPIView(pagination.PaginationMixin,
|
||||||
slug_field = 'slug'
|
slug_field = 'slug'
|
||||||
allow_empty = True
|
allow_empty = True
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_extra_context(self):
|
||||||
"""
|
"""
|
||||||
Extra context provided to the serializer class.
|
Extra context provided to the serializer class.
|
||||||
"""
|
"""
|
||||||
|
@ -101,14 +103,24 @@ class GenericAPIView(pagination.PaginationMixin,
|
||||||
def get_serializer(self, instance=None, data=None,
|
def get_serializer(self, instance=None, data=None,
|
||||||
files=None, many=False, partial=False):
|
files=None, many=False, partial=False):
|
||||||
"""
|
"""
|
||||||
Return the serializer instance that should be used for validating and
|
Return the serializer instance that should be used for deserializing
|
||||||
deserializing input, and for serializing output.
|
input, and for serializing output.
|
||||||
"""
|
"""
|
||||||
serializer_class = self.get_serializer_class()
|
serializer_class = self.get_serializer_class()
|
||||||
context = self.get_serializer_context()
|
context = self.get_extra_context()
|
||||||
return serializer_class(instance, data=data, files=files,
|
return serializer_class(instance, data=data, files=files,
|
||||||
many=many, partial=partial, context=context)
|
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):
|
def filter_queryset(self, queryset, filter_backends=None):
|
||||||
"""
|
"""
|
||||||
|
@ -160,6 +172,22 @@ class GenericAPIView(pagination.PaginationMixin,
|
||||||
model = self.model
|
model = self.model
|
||||||
return DefaultSerializer
|
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):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
Get the list of items for this view.
|
Get the list of items for this view.
|
||||||
|
|
|
@ -44,12 +44,12 @@
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.db import transaction as tx
|
from django.db import transaction as tx
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
|
|
||||||
from .settings import api_settings
|
from .settings import api_settings
|
||||||
from .utils import get_object_or_404
|
from .utils import get_object_or_404
|
||||||
|
@ -57,6 +57,7 @@ from .utils import get_object_or_404
|
||||||
from .. import exceptions as exc
|
from .. import exceptions as exc
|
||||||
from ..decorators import model_pk_lock
|
from ..decorators import model_pk_lock
|
||||||
|
|
||||||
|
|
||||||
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
|
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
|
||||||
"""
|
"""
|
||||||
Given a model instance, and an optional pk and slug field,
|
Given a model instance, and an optional pk and slug field,
|
||||||
|
@ -89,19 +90,21 @@ class CreateModelMixin:
|
||||||
Create a model instance.
|
Create a model instance.
|
||||||
"""
|
"""
|
||||||
def create(self, request, *args, **kwargs):
|
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():
|
if validator.is_valid():
|
||||||
self.check_permissions(request, 'create', serializer.object)
|
self.check_permissions(request, 'create', validator.object)
|
||||||
|
|
||||||
self.pre_save(serializer.object)
|
self.pre_save(validator.object)
|
||||||
self.pre_conditions_on_save(serializer.object)
|
self.pre_conditions_on_save(validator.object)
|
||||||
self.object = serializer.save(force_insert=True)
|
self.object = validator.save(force_insert=True)
|
||||||
self.post_save(self.object, created=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)
|
headers = self.get_success_headers(serializer.data)
|
||||||
return response.Created(serializer.data, headers=headers)
|
return response.Created(serializer.data, headers=headers)
|
||||||
|
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
def get_success_headers(self, data):
|
def get_success_headers(self, data):
|
||||||
try:
|
try:
|
||||||
|
@ -171,28 +174,32 @@ class UpdateModelMixin:
|
||||||
if self.object is None:
|
if self.object is None:
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
serializer = self.get_serializer(self.object, data=request.DATA,
|
validator = self.get_validator(self.object, data=request.DATA,
|
||||||
files=request.FILES, partial=partial)
|
files=request.FILES, partial=partial)
|
||||||
|
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
# Hooks
|
# Hooks
|
||||||
try:
|
try:
|
||||||
self.pre_save(serializer.object)
|
self.pre_save(validator.object)
|
||||||
self.pre_conditions_on_save(serializer.object)
|
self.pre_conditions_on_save(validator.object)
|
||||||
except ValidationError as err:
|
except ValidationError as err:
|
||||||
# full_clean on model instance may be called in pre_save,
|
# full_clean on model instance may be called in pre_save,
|
||||||
# so we have to handle eventual errors.
|
# so we have to handle eventual errors.
|
||||||
return response.BadRequest(err.message_dict)
|
return response.BadRequest(err.message_dict)
|
||||||
|
|
||||||
if self.object is None:
|
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)
|
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)
|
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)
|
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)
|
return response.Ok(serializer.data)
|
||||||
|
|
||||||
def partial_update(self, request, *args, **kwargs):
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
|
|
@ -48,7 +48,7 @@ Serializer fields that deal with relationships.
|
||||||
These fields allow you to specify the style that should be used to represent
|
These fields allow you to specify the style that should be used to represent
|
||||||
model relationships, including hyperlinks, primary keys, or slugs.
|
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.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
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 .fields import Field, WritableField, get_component, is_simple_callable
|
||||||
from .reverse import reverse
|
from .reverse import reverse
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from urllib import parse as urlparse
|
from urllib import parse as urlparse
|
||||||
|
|
|
@ -78,6 +78,8 @@ import serpy
|
||||||
# This helps keep the separation between model fields, form fields, and
|
# This helps keep the separation between model fields, form fields, and
|
||||||
# serializer fields more explicit.
|
# serializer fields more explicit.
|
||||||
|
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
|
|
||||||
from .relations import *
|
from .relations import *
|
||||||
from .fields import *
|
from .fields import *
|
||||||
|
|
||||||
|
@ -1228,4 +1230,8 @@ class LightSerializer(serpy.Serializer):
|
||||||
kwargs.pop("read_only", None)
|
kwargs.pop("read_only", None)
|
||||||
kwargs.pop("partial", None)
|
kwargs.pop("partial", None)
|
||||||
kwargs.pop("files", None)
|
kwargs.pop("files", None)
|
||||||
|
context = kwargs.pop("context", {})
|
||||||
|
view = kwargs.pop("view", {})
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.context = context
|
||||||
|
self.view = view
|
||||||
|
|
|
@ -98,6 +98,8 @@ DEFAULTS = {
|
||||||
# Genric view behavior
|
# Genric view behavior
|
||||||
"DEFAULT_MODEL_SERIALIZER_CLASS":
|
"DEFAULT_MODEL_SERIALIZER_CLASS":
|
||||||
"taiga.base.api.serializers.ModelSerializer",
|
"taiga.base.api.serializers.ModelSerializer",
|
||||||
|
"DEFAULT_MODEL_VALIDATOR_CLASS":
|
||||||
|
"taiga.base.api.validators.ModelValidator",
|
||||||
"DEFAULT_FILTER_BACKENDS": (),
|
"DEFAULT_FILTER_BACKENDS": (),
|
||||||
|
|
||||||
# Throttling
|
# Throttling
|
||||||
|
|
|
@ -16,15 +16,12 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from . import serializers
|
||||||
|
|
||||||
|
|
||||||
class FanResourceSerializerMixin(serializers.ModelSerializer):
|
class Validator(serializers.Serializer):
|
||||||
is_fan = serializers.SerializerMethodField("get_is_fan")
|
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
|
|
@ -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 PermissionDenied as DjangoPermissionDenied
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
@ -224,6 +225,7 @@ class NotEnoughSlotsForProject(BaseException):
|
||||||
"total_memberships": total_memberships
|
"total_memberships": total_memberships
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def format_exception(exc):
|
def format_exception(exc):
|
||||||
if isinstance(exc.detail, (dict, list, tuple,)):
|
if isinstance(exc.detail, (dict, list, tuple,)):
|
||||||
detail = exc.detail
|
detail = exc.detail
|
||||||
|
@ -270,3 +272,6 @@ def exception_handler(exc):
|
||||||
|
|
||||||
# Note: Unhandled exceptions will raise a 500 error.
|
# Note: Unhandled exceptions will raise a 500 error.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
ValidationError = DjangoValidationError
|
||||||
|
|
|
@ -20,10 +20,14 @@ from django.forms import widgets
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from taiga.base.api import serializers
|
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):
|
class JsonField(serializers.WritableField):
|
||||||
"""
|
"""
|
||||||
|
@ -38,40 +42,6 @@ class JsonField(serializers.WritableField):
|
||||||
return data
|
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):
|
class PgArrayField(serializers.WritableField):
|
||||||
"""
|
"""
|
||||||
PgArray objects serializer.
|
PgArray objects serializer.
|
||||||
|
@ -104,3 +74,58 @@ class WatchersField(serializers.WritableField):
|
||||||
|
|
||||||
def from_native(self, data):
|
def from_native(self, data):
|
||||||
return 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
|
||||||
|
|
|
@ -23,6 +23,7 @@ from django.db import connection
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models.sql.datastructures import EmptyResultSet
|
from django.db.models.sql.datastructures import EmptyResultSet
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import Field, MethodField
|
||||||
|
|
||||||
Neighbor = namedtuple("Neighbor", "left right")
|
Neighbor = namedtuple("Neighbor", "left right")
|
||||||
|
|
||||||
|
@ -71,7 +72,6 @@ def get_neighbors(obj, results_set=None):
|
||||||
if row is None:
|
if row is None:
|
||||||
return Neighbor(None, None)
|
return Neighbor(None, None)
|
||||||
|
|
||||||
obj_position = row[1] - 1
|
|
||||||
left_object_id = row[2]
|
left_object_id = row[2]
|
||||||
right_object_id = row[3]
|
right_object_id = row[3]
|
||||||
|
|
||||||
|
@ -88,13 +88,19 @@ def get_neighbors(obj, results_set=None):
|
||||||
return Neighbor(left, right)
|
return Neighbor(left, right)
|
||||||
|
|
||||||
|
|
||||||
class NeighborsSerializerMixin:
|
class NeighborSerializer(serializers.LightSerializer):
|
||||||
def __init__(self, *args, **kwargs):
|
id = Field()
|
||||||
super().__init__(*args, **kwargs)
|
ref = Field()
|
||||||
self.fields["neighbors"] = serializers.SerializerMethodField("get_neighbors")
|
subject = Field()
|
||||||
|
|
||||||
|
|
||||||
|
class NeighborsSerializerMixin(serializers.LightSerializer):
|
||||||
|
neighbors = MethodField()
|
||||||
|
|
||||||
def serialize_neighbor(self, neighbor):
|
def serialize_neighbor(self, neighbor):
|
||||||
raise NotImplementedError
|
if neighbor:
|
||||||
|
return NeighborSerializer(neighbor).data
|
||||||
|
return None
|
||||||
|
|
||||||
def get_neighbors(self, obj):
|
def get_neighbors(self, obj):
|
||||||
view, request = self.context.get("view", None), self.context.get("request", None)
|
view, request = self.context.get("view", None), self.context.get("request", None)
|
||||||
|
|
|
@ -25,3 +25,7 @@ def dict_sum(*args):
|
||||||
assert isinstance(arg, dict)
|
assert isinstance(arg, dict)
|
||||||
result += collections.Counter(arg)
|
result += collections.Counter(arg)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def into_namedtuple(dictionary):
|
||||||
|
return collections.namedtuple('GenericDict', dictionary.keys())(**dictionary)
|
||||||
|
|
|
@ -34,6 +34,7 @@ from taiga.base import exceptions as exc
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
from taiga.base.api.mixins import CreateModelMixin
|
from taiga.base.api.mixins import CreateModelMixin
|
||||||
from taiga.base.api.viewsets import GenericViewSet
|
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.models import Project, Membership
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.tasks.models import Task
|
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})
|
return response.BadRequest({"error": e.message, "details": e.errors})
|
||||||
else:
|
else:
|
||||||
# On Success
|
# 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)
|
return response.Created(response_data)
|
||||||
|
|
|
@ -23,11 +23,11 @@ from collections import OrderedDict
|
||||||
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
from taiga.base.fields import JsonField
|
from taiga.base.fields import JsonField
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
from taiga.users import models as users_models
|
from taiga.users import models as users_models
|
||||||
|
|
|
@ -16,13 +16,11 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.fields import JsonField, PgArrayField
|
from taiga.base.fields import JsonField, PgArrayField
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
|
|
||||||
from taiga.projects import models as projects_models
|
from taiga.projects import models as projects_models
|
||||||
from taiga.projects.custom_attributes import models as custom_attributes_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.issues import models as issues_models
|
||||||
from taiga.projects.milestones import models as milestones_models
|
from taiga.projects.milestones import models as milestones_models
|
||||||
from taiga.projects.wiki import models as wiki_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.timeline import models as timeline_models
|
||||||
from taiga.users import models as users_models
|
from taiga.users import models as users_models
|
||||||
from taiga.projects.votes import services as votes_service
|
from taiga.projects.votes import services as votes_service
|
||||||
|
|
||||||
from .fields import (FileField, RelatedNoneSafeField, UserRelatedField,
|
from .fields import (FileField, UserRelatedField,
|
||||||
UserPkField, CommentField, ProjectRelatedField,
|
ProjectRelatedField,
|
||||||
HistoryUserField, HistoryValuesField, HistoryDiffField,
|
|
||||||
TimelineDataField, ContentTypeField)
|
TimelineDataField, ContentTypeField)
|
||||||
from .mixins import (HistoryExportSerializerMixin,
|
from .mixins import (HistoryExportSerializerMixin,
|
||||||
AttachmentExportSerializerMixin,
|
AttachmentExportSerializerMixin,
|
||||||
|
@ -158,6 +153,7 @@ class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
|
class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
|
||||||
_custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute
|
_custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute
|
||||||
_container_model = "userstories.UserStory"
|
_container_model = "userstories.UserStory"
|
||||||
|
@ -224,7 +220,7 @@ class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin):
|
||||||
name = attrs[source]
|
name = attrs[source]
|
||||||
qs = self.project.milestones.filter(name=name)
|
qs = self.project.milestones.filter(name=name)
|
||||||
if qs.exists():
|
if qs.exists():
|
||||||
raise serializers.ValidationError(_("Name duplicated for the project"))
|
raise ValidationError(_("Name duplicated for the project"))
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
@ -268,7 +264,9 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His
|
||||||
|
|
||||||
def custom_attributes_queryset(self, project):
|
def custom_attributes_queryset(self, project):
|
||||||
if project.id not in _custom_userstories_attributes_cache:
|
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]
|
return _custom_userstories_attributes_cache[project.id]
|
||||||
|
|
||||||
|
|
||||||
|
@ -314,10 +312,10 @@ class WikiLinkExportSerializer(serializers.ModelSerializer):
|
||||||
exclude = ('id', 'project')
|
exclude = ('id', 'project')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TimelineExportSerializer(serializers.ModelSerializer):
|
class TimelineExportSerializer(serializers.ModelSerializer):
|
||||||
data = TimelineDataField()
|
data = TimelineDataField()
|
||||||
data_content_type = ContentTypeField()
|
data_content_type = ContentTypeField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = timeline_models.Timeline
|
model = timeline_models.Timeline
|
||||||
exclude = ('id', 'project', 'namespace', 'object_id', 'content_type')
|
exclude = ('id', 'project', 'namespace', 'object_id', 'content_type')
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from . import validators
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import services
|
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.api.utils import get_object_or_404
|
||||||
from taiga.base.decorators import list_route, detail_route
|
from taiga.base.decorators import list_route, detail_route
|
||||||
|
|
||||||
from django.db import transaction
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
class Application(ModelRetrieveViewSet):
|
class Application(ModelRetrieveViewSet):
|
||||||
serializer_class = serializers.ApplicationSerializer
|
serializer_class = serializers.ApplicationSerializer
|
||||||
|
validator_class = validators.ApplicationValidator
|
||||||
permission_classes = (permissions.ApplicationPermission,)
|
permission_classes = (permissions.ApplicationPermission,)
|
||||||
model = models.Application
|
model = models.Application
|
||||||
|
|
||||||
|
@ -61,6 +62,7 @@ class Application(ModelRetrieveViewSet):
|
||||||
|
|
||||||
class ApplicationToken(ModelCrudViewSet):
|
class ApplicationToken(ModelCrudViewSet):
|
||||||
serializer_class = serializers.ApplicationTokenSerializer
|
serializer_class = serializers.ApplicationTokenSerializer
|
||||||
|
validator_class = validators.ApplicationTokenValidator
|
||||||
permission_classes = (permissions.ApplicationTokenPermission,)
|
permission_classes = (permissions.ApplicationTokenPermission,)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
|
@ -16,9 +16,8 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import Field
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import services
|
from . import services
|
||||||
|
@ -26,33 +25,27 @@ from . import services
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
|
||||||
class ApplicationSerializer(serializers.ModelSerializer):
|
class ApplicationSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = Field()
|
||||||
model = models.Application
|
name = Field()
|
||||||
fields = ("id", "name", "web", "description", "icon_url")
|
web = Field()
|
||||||
|
description = Field()
|
||||||
|
icon_url = Field()
|
||||||
|
|
||||||
|
|
||||||
class ApplicationTokenSerializer(serializers.ModelSerializer):
|
class ApplicationTokenSerializer(serializers.LightSerializer):
|
||||||
cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
|
id = Field()
|
||||||
next_url = serializers.CharField(source="next_url", read_only=True)
|
user = Field(attr="user_id")
|
||||||
application = ApplicationSerializer(read_only=True)
|
application = ApplicationSerializer()
|
||||||
|
auth_code = Field()
|
||||||
class Meta:
|
next_url = Field()
|
||||||
model = models.ApplicationToken
|
|
||||||
fields = ("user", "id", "application", "auth_code", "next_url")
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationCodeSerializer(serializers.ModelSerializer):
|
class AuthorizationCodeSerializer(serializers.LightSerializer):
|
||||||
next_url = serializers.CharField(source="next_url", read_only=True)
|
state = Field()
|
||||||
class Meta:
|
auth_code = Field()
|
||||||
model = models.ApplicationToken
|
next_url = Field()
|
||||||
fields = ("auth_code", "state", "next_url")
|
|
||||||
|
|
||||||
|
|
||||||
class AccessTokenSerializer(serializers.ModelSerializer):
|
class AccessTokenSerializer(serializers.LightSerializer):
|
||||||
cyphered_token = serializers.CharField(source="cyphered_token", read_only=True)
|
cyphered_token = Field()
|
||||||
next_url = serializers.CharField(source="next_url", read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = models.ApplicationToken
|
|
||||||
fields = ("cyphered_token", )
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base.api 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", )
|
|
@ -20,7 +20,7 @@ from taiga.base import response
|
||||||
from taiga.base.api import viewsets
|
from taiga.base.api import viewsets
|
||||||
|
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import validators
|
||||||
from . import services
|
from . import services
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
@ -28,7 +28,7 @@ import copy
|
||||||
|
|
||||||
class FeedbackViewSet(viewsets.ViewSet):
|
class FeedbackViewSet(viewsets.ViewSet):
|
||||||
permission_classes = (permissions.FeedbackPermission,)
|
permission_classes = (permissions.FeedbackPermission,)
|
||||||
serializer_class = serializers.FeedbackEntrySerializer
|
validator_class = validators.FeedbackEntryValidator
|
||||||
|
|
||||||
def create(self, request, **kwargs):
|
def create(self, request, **kwargs):
|
||||||
self.check_permissions(request, "create", None)
|
self.check_permissions(request, "create", None)
|
||||||
|
@ -37,11 +37,11 @@ class FeedbackViewSet(viewsets.ViewSet):
|
||||||
data.update({"full_name": request.user.get_full_name(),
|
data.update({"full_name": request.user.get_full_name(),
|
||||||
"email": request.user.email})
|
"email": request.user.email})
|
||||||
|
|
||||||
serializer = self.serializer_class(data=data)
|
validator = self.validator_class(data=data)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
self.object = serializer.save(force_insert=True)
|
self.object = validator.save(force_insert=True)
|
||||||
|
|
||||||
extra = {
|
extra = {
|
||||||
"HTTP_HOST": request.META.get("HTTP_HOST", None),
|
"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])
|
services.send_feedback(self.object, extra, reply_to=[request.user.email])
|
||||||
|
|
||||||
return response.Ok(serializer.data)
|
return response.Ok(validator.data)
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import validators
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class FeedbackEntrySerializer(serializers.ModelSerializer):
|
class FeedbackEntryValidator(validators.ModelValidator):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.FeedbackEntry
|
model = models.FeedbackEntry
|
|
@ -91,39 +91,55 @@ def _get_membership_permissions(membership):
|
||||||
return []
|
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"):
|
def get_user_project_permissions(user, project, cache="user"):
|
||||||
"""
|
"""
|
||||||
cache param determines how memberships are calculated trying to reuse the existing data
|
cache param determines how memberships are calculated trying to reuse the existing data
|
||||||
in cache
|
in cache
|
||||||
"""
|
"""
|
||||||
membership = _get_user_project_membership(user, project, cache=cache)
|
membership = _get_user_project_membership(user, project, cache=cache)
|
||||||
if user.is_superuser:
|
is_member = membership is not None
|
||||||
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
|
is_admin = is_member and membership.is_admin
|
||||||
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
|
return calculate_permissions(
|
||||||
public_permissions = []
|
is_authenticated = user.is_authenticated(),
|
||||||
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
|
is_superuser = user.is_superuser,
|
||||||
elif membership:
|
is_member = is_member,
|
||||||
if membership.is_admin:
|
is_admin = is_admin,
|
||||||
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
|
role_permissions = _get_membership_permissions(membership),
|
||||||
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
|
anon_permissions = project.anon_permissions,
|
||||||
else:
|
public_permissions = project.public_permissions
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def set_base_permissions_for_project(project):
|
def set_base_permissions_for_project(project):
|
||||||
|
|
|
@ -22,10 +22,6 @@ from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
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.http import Http404
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils import timezone
|
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.history.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
|
||||||
from taiga.projects.notifications.models import NotifyPolicy
|
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
|
||||||
from taiga.projects.notifications.choices import NotifyLevel
|
from taiga.projects.notifications.choices import NotifyLevel
|
||||||
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
|
||||||
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
|
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.tagging.api import TagsColorsResourceMixin
|
||||||
|
|
||||||
from taiga.projects.userstories.models import UserStory, RolePoints
|
from taiga.projects.userstories.models import UserStory, RolePoints
|
||||||
from taiga.users import services as users_services
|
|
||||||
|
|
||||||
from . import filters as project_filters
|
from . import filters as project_filters
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from . import validators
|
||||||
from . import services
|
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):
|
TagsColorsResourceMixin, ModelCrudViewSet):
|
||||||
|
validator_class = validators.ProjectValidator
|
||||||
queryset = models.Project.objects.all()
|
queryset = models.Project.objects.all()
|
||||||
serializer_class = serializers.ProjectDetailSerializer
|
|
||||||
admin_serializer_class = serializers.ProjectDetailAdminSerializer
|
|
||||||
list_serializer_class = serializers.ProjectSerializer
|
|
||||||
permission_classes = (permissions.ProjectPermission, )
|
permission_classes = (permissions.ProjectPermission, )
|
||||||
filter_backends = (project_filters.QFilterBackend,
|
filter_backends = (project_filters.UserOrderFilterBackend,
|
||||||
|
project_filters.QFilterBackend,
|
||||||
project_filters.CanViewProjectObjFilterBackend,
|
project_filters.CanViewProjectObjFilterBackend,
|
||||||
project_filters.DiscoverModeFilterBackend)
|
project_filters.DiscoverModeFilterBackend)
|
||||||
|
|
||||||
|
@ -85,8 +81,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
|
||||||
"is_kanban_activated")
|
"is_kanban_activated")
|
||||||
|
|
||||||
ordering = ("name", "id")
|
ordering = ("name", "id")
|
||||||
order_by_fields = ("memberships__user_order",
|
order_by_fields = ("total_fans",
|
||||||
"total_fans",
|
|
||||||
"total_fans_last_week",
|
"total_fans_last_week",
|
||||||
"total_fans_last_month",
|
"total_fans_last_month",
|
||||||
"total_fans_last_year",
|
"total_fans_last_year",
|
||||||
|
@ -106,18 +101,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
|
|
||||||
qs = qs.select_related("owner")
|
qs = qs.select_related("owner")
|
||||||
# Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries)
|
qs = project_utils.attach_extra_info(qs, user=self.request.user)
|
||||||
# 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"))
|
|
||||||
|
|
||||||
# If filtering an activity period we must exclude the activities not updated recently enough
|
# If filtering an activity period we must exclude the activities not updated recently enough
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
@ -137,22 +122,17 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
serializer_class = self.serializer_class
|
|
||||||
|
|
||||||
if self.action == "list":
|
|
||||||
serializer_class = self.list_serializer_class
|
|
||||||
elif self.action != "create":
|
|
||||||
if self.action == "by_slug":
|
if self.action == "by_slug":
|
||||||
slug = self.request.QUERY_PARAMS.get("slug", None)
|
self.lookup_field = "slug"
|
||||||
project = get_object_or_404(models.Project, slug=slug)
|
|
||||||
else:
|
|
||||||
project = self.get_object()
|
|
||||||
|
|
||||||
if permissions_services.is_project_admin(self.request.user, project):
|
return super().retrieve(request, *args, **kwargs)
|
||||||
serializer_class = self.admin_serializer_class
|
|
||||||
|
|
||||||
return serializer_class
|
def get_serializer_class(self):
|
||||||
|
if self.action == "list":
|
||||||
|
return serializers.ProjectSerializer
|
||||||
|
|
||||||
|
return serializers.ProjectDetailSerializer
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def change_logo(self, request, *args, **kwargs):
|
def change_logo(self, request, *args, **kwargs):
|
||||||
|
@ -215,11 +195,11 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
|
||||||
if self.request.user.is_anonymous():
|
if self.request.user.is_anonymous():
|
||||||
return response.Unauthorized()
|
return response.Unauthorized()
|
||||||
|
|
||||||
serializer = serializers.UpdateProjectOrderBulkSerializer(data=request.DATA, many=True)
|
validator = validators.UpdateProjectOrderBulkValidator(data=request.DATA, many=True)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
services.update_projects_order_in_bulk(data, "user_order", request.user)
|
services.update_projects_order_in_bulk(data, "user_order", request.user)
|
||||||
return response.NoContent(data=None)
|
return response.NoContent(data=None)
|
||||||
|
|
||||||
|
@ -283,10 +263,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
|
||||||
return response.Ok(data)
|
return response.Ok(data)
|
||||||
|
|
||||||
@list_route(methods=["GET"])
|
@list_route(methods=["GET"])
|
||||||
def by_slug(self, request):
|
def by_slug(self, request, *args, **kwargs):
|
||||||
slug = request.QUERY_PARAMS.get("slug", None)
|
slug = request.QUERY_PARAMS.get("slug", None)
|
||||||
project = get_object_or_404(models.Project, slug=slug)
|
return self.retrieve(request, slug=slug)
|
||||||
return self.retrieve(request, pk=project.pk)
|
|
||||||
|
|
||||||
@detail_route(methods=["GET", "PATCH"])
|
@detail_route(methods=["GET", "PATCH"])
|
||||||
def modules(self, request, pk=None):
|
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"))
|
return response.BadRequest(_("The user must be already a project member"))
|
||||||
|
|
||||||
reason = request.DATA.get('reason', None)
|
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()
|
return response.Ok()
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
|
@ -471,6 +450,7 @@ class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
|
||||||
model = models.Points
|
model = models.Points
|
||||||
serializer_class = serializers.PointsSerializer
|
serializer_class = serializers.PointsSerializer
|
||||||
|
validator_class = validators.PointsValidator
|
||||||
permission_classes = (permissions.PointsPermission,)
|
permission_classes = (permissions.PointsPermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
@ -487,6 +467,7 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
|
||||||
model = models.UserStoryStatus
|
model = models.UserStoryStatus
|
||||||
serializer_class = serializers.UserStoryStatusSerializer
|
serializer_class = serializers.UserStoryStatusSerializer
|
||||||
|
validator_class = validators.UserStoryStatusValidator
|
||||||
permission_classes = (permissions.UserStoryStatusPermission,)
|
permission_classes = (permissions.UserStoryStatusPermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
@ -503,6 +484,7 @@ class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
|
||||||
model = models.TaskStatus
|
model = models.TaskStatus
|
||||||
serializer_class = serializers.TaskStatusSerializer
|
serializer_class = serializers.TaskStatusSerializer
|
||||||
|
validator_class = validators.TaskStatusValidator
|
||||||
permission_classes = (permissions.TaskStatusPermission,)
|
permission_classes = (permissions.TaskStatusPermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project",)
|
||||||
|
@ -519,6 +501,7 @@ class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
|
|
||||||
model = models.Severity
|
model = models.Severity
|
||||||
serializer_class = serializers.SeveritySerializer
|
serializer_class = serializers.SeveritySerializer
|
||||||
|
validator_class = validators.SeverityValidator
|
||||||
permission_classes = (permissions.SeverityPermission,)
|
permission_classes = (permissions.SeverityPermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project",)
|
||||||
|
@ -534,6 +517,7 @@ class PriorityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
model = models.Priority
|
model = models.Priority
|
||||||
serializer_class = serializers.PrioritySerializer
|
serializer_class = serializers.PrioritySerializer
|
||||||
|
validator_class = validators.PriorityValidator
|
||||||
permission_classes = (permissions.PriorityPermission,)
|
permission_classes = (permissions.PriorityPermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project",)
|
||||||
|
@ -549,6 +533,7 @@ class IssueTypeViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
model = models.IssueType
|
model = models.IssueType
|
||||||
serializer_class = serializers.IssueTypeSerializer
|
serializer_class = serializers.IssueTypeSerializer
|
||||||
|
validator_class = validators.IssueTypeValidator
|
||||||
permission_classes = (permissions.IssueTypePermission,)
|
permission_classes = (permissions.IssueTypePermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project",)
|
||||||
|
@ -564,6 +549,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
ModelCrudViewSet, BulkUpdateOrderMixin):
|
ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
model = models.IssueStatus
|
model = models.IssueStatus
|
||||||
serializer_class = serializers.IssueStatusSerializer
|
serializer_class = serializers.IssueStatusSerializer
|
||||||
|
validator_class = validators.IssueStatusValidator
|
||||||
permission_classes = (permissions.IssueStatusPermission,)
|
permission_classes = (permissions.IssueStatusPermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project",)
|
||||||
|
@ -582,6 +568,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
|
||||||
class ProjectTemplateViewSet(ModelCrudViewSet):
|
class ProjectTemplateViewSet(ModelCrudViewSet):
|
||||||
model = models.ProjectTemplate
|
model = models.ProjectTemplate
|
||||||
serializer_class = serializers.ProjectTemplateSerializer
|
serializer_class = serializers.ProjectTemplateSerializer
|
||||||
|
validator_class = validators.ProjectTemplateValidator
|
||||||
permission_classes = (permissions.ProjectTemplatePermission,)
|
permission_classes = (permissions.ProjectTemplatePermission,)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -595,7 +582,9 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
|
||||||
class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.Membership
|
model = models.Membership
|
||||||
admin_serializer_class = serializers.MembershipAdminSerializer
|
admin_serializer_class = serializers.MembershipAdminSerializer
|
||||||
|
admin_validator_class = validators.MembershipAdminValidator
|
||||||
serializer_class = serializers.MembershipSerializer
|
serializer_class = serializers.MembershipSerializer
|
||||||
|
validator_class = validators.MembershipValidator
|
||||||
permission_classes = (permissions.MembershipPermission,)
|
permission_classes = (permissions.MembershipPermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project", "role")
|
filter_fields = ("project", "role")
|
||||||
|
@ -620,6 +609,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
else:
|
else:
|
||||||
return self.serializer_class
|
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):
|
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(
|
(can_add_memberships, error_type) = services.check_if_project_can_have_more_memberships(
|
||||||
project,
|
project,
|
||||||
|
@ -634,11 +629,11 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_create(self, request, **kwargs):
|
def bulk_create(self, request, **kwargs):
|
||||||
serializer = serializers.MembersBulkSerializer(data=request.DATA)
|
validator = validators.MembersBulkValidator(data=request.DATA)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
project = models.Project.objects.get(id=data["project_id"])
|
project = models.Project.objects.get(id=data["project_id"])
|
||||||
invitation_extra_text = data.get("invitation_extra_text", None)
|
invitation_extra_text = data.get("invitation_extra_text", None)
|
||||||
self.check_permissions(request, 'bulk_create', project)
|
self.check_permissions(request, 'bulk_create', project)
|
||||||
|
@ -655,7 +650,7 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
invitation_extra_text=invitation_extra_text,
|
invitation_extra_text=invitation_extra_text,
|
||||||
callback=self.post_save,
|
callback=self.post_save,
|
||||||
precall=self.pre_save)
|
precall=self.pre_save)
|
||||||
except ValidationError as err:
|
except exc.ValidationError as err:
|
||||||
return response.BadRequest(err.message_dict)
|
return response.BadRequest(err.message_dict)
|
||||||
|
|
||||||
members_serialized = self.admin_serializer_class(members, many=True)
|
members_serialized = self.admin_serializer_class(members, many=True)
|
||||||
|
|
|
@ -34,6 +34,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin
|
||||||
|
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from . import validators
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,6 +43,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||||
|
|
||||||
model = models.Attachment
|
model = models.Attachment
|
||||||
serializer_class = serializers.AttachmentSerializer
|
serializer_class = serializers.AttachmentSerializer
|
||||||
|
validator_class = validators.AttachmentValidator
|
||||||
filter_fields = ["project", "object_id"]
|
filter_fields = ["project", "object_id"]
|
||||||
|
|
||||||
content_type = None
|
content_type = None
|
||||||
|
|
|
@ -19,36 +19,38 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import MethodField, Field, FileField
|
||||||
from taiga.base.utils.thumbnails import get_thumbnail_url
|
from taiga.base.utils.thumbnails import get_thumbnail_url
|
||||||
|
|
||||||
from . import services
|
from . import services
|
||||||
from . import models
|
|
||||||
|
|
||||||
import json
|
|
||||||
import serpy
|
|
||||||
|
|
||||||
|
|
||||||
class AttachmentSerializer(serializers.ModelSerializer):
|
class AttachmentSerializer(serializers.LightSerializer):
|
||||||
url = serializers.SerializerMethodField("get_url")
|
id = Field()
|
||||||
thumbnail_card_url = serializers.SerializerMethodField("get_thumbnail_card_url")
|
project = Field(attr="project_id")
|
||||||
attached_file = serializers.FileField(required=True)
|
owner = Field(attr="owner_id")
|
||||||
|
name = Field()
|
||||||
class Meta:
|
attached_file = FileField()
|
||||||
model = models.Attachment
|
size = Field()
|
||||||
fields = ("id", "project", "owner", "name", "attached_file", "size",
|
url = Field()
|
||||||
"url", "thumbnail_card_url", "description", "is_deprecated",
|
description = Field()
|
||||||
"created_date", "modified_date", "object_id", "order", "sha1")
|
is_deprecated = Field()
|
||||||
read_only_fields = ("owner", "created_date", "modified_date", "sha1")
|
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):
|
def get_url(self, obj):
|
||||||
return obj.attached_file.url
|
return obj.attached_file.url
|
||||||
|
|
||||||
|
|
||||||
def get_thumbnail_card_url(self, obj):
|
def get_thumbnail_card_url(self, obj):
|
||||||
return services.get_card_image_thumbnail_url(obj)
|
return services.get_card_image_thumbnail_url(obj)
|
||||||
|
|
||||||
|
|
||||||
class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer):
|
class BasicAttachmentsInfoSerializerMixin(serializers.LightSerializer):
|
||||||
"""
|
"""
|
||||||
Assumptions:
|
Assumptions:
|
||||||
- The queryset has an attribute called "include_attachments" indicating if the attachments array should contain information
|
- 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
|
- The method attach_basic_attachments has been used to include the necessary
|
||||||
json data about the attachments in the "attachments_attr" column
|
json data about the attachments in the "attachments_attr" column
|
||||||
"""
|
"""
|
||||||
attachments = serpy.MethodField()
|
attachments = MethodField()
|
||||||
|
|
||||||
def get_attachments(self, obj):
|
def get_attachments(self, obj):
|
||||||
include_attachments = getattr(obj, "include_attachments", False)
|
include_attachments = getattr(obj, "include_attachments", False)
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base.api 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")
|
|
@ -32,6 +32,7 @@ from taiga.projects.occ.mixins import OCCResourceMixin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from . import validators
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import services
|
from . import services
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ from . import services
|
||||||
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.UserStoryCustomAttribute
|
model = models.UserStoryCustomAttribute
|
||||||
serializer_class = serializers.UserStoryCustomAttributeSerializer
|
serializer_class = serializers.UserStoryCustomAttributeSerializer
|
||||||
|
validator_class = validators.UserStoryCustomAttributeValidator
|
||||||
permission_classes = (permissions.UserStoryCustomAttributePermission,)
|
permission_classes = (permissions.UserStoryCustomAttributePermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project",)
|
||||||
|
@ -54,6 +56,7 @@ class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixi
|
||||||
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.TaskCustomAttribute
|
model = models.TaskCustomAttribute
|
||||||
serializer_class = serializers.TaskCustomAttributeSerializer
|
serializer_class = serializers.TaskCustomAttributeSerializer
|
||||||
|
validator_class = validators.TaskCustomAttributeValidator
|
||||||
permission_classes = (permissions.TaskCustomAttributePermission,)
|
permission_classes = (permissions.TaskCustomAttributePermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project",)
|
||||||
|
@ -65,6 +68,7 @@ class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, Mo
|
||||||
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.IssueCustomAttribute
|
model = models.IssueCustomAttribute
|
||||||
serializer_class = serializers.IssueCustomAttributeSerializer
|
serializer_class = serializers.IssueCustomAttributeSerializer
|
||||||
|
validator_class = validators.IssueCustomAttributeValidator
|
||||||
permission_classes = (permissions.IssueCustomAttributePermission,)
|
permission_classes = (permissions.IssueCustomAttributePermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project",)
|
||||||
|
@ -86,6 +90,7 @@ class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin,
|
||||||
class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
|
class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
|
||||||
model = models.UserStoryCustomAttributesValues
|
model = models.UserStoryCustomAttributesValues
|
||||||
serializer_class = serializers.UserStoryCustomAttributesValuesSerializer
|
serializer_class = serializers.UserStoryCustomAttributesValuesSerializer
|
||||||
|
validator_class = validators.UserStoryCustomAttributesValuesValidator
|
||||||
permission_classes = (permissions.UserStoryCustomAttributesValuesPermission,)
|
permission_classes = (permissions.UserStoryCustomAttributesValuesPermission,)
|
||||||
lookup_field = "user_story_id"
|
lookup_field = "user_story_id"
|
||||||
content_object = "user_story"
|
content_object = "user_story"
|
||||||
|
@ -99,6 +104,7 @@ class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
|
||||||
class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
|
class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
|
||||||
model = models.TaskCustomAttributesValues
|
model = models.TaskCustomAttributesValues
|
||||||
serializer_class = serializers.TaskCustomAttributesValuesSerializer
|
serializer_class = serializers.TaskCustomAttributesValuesSerializer
|
||||||
|
validator_class = validators.TaskCustomAttributesValuesValidator
|
||||||
permission_classes = (permissions.TaskCustomAttributesValuesPermission,)
|
permission_classes = (permissions.TaskCustomAttributesValuesPermission,)
|
||||||
lookup_field = "task_id"
|
lookup_field = "task_id"
|
||||||
content_object = "task"
|
content_object = "task"
|
||||||
|
@ -112,6 +118,7 @@ class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
|
||||||
class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
|
class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
|
||||||
model = models.IssueCustomAttributesValues
|
model = models.IssueCustomAttributesValues
|
||||||
serializer_class = serializers.IssueCustomAttributesValuesSerializer
|
serializer_class = serializers.IssueCustomAttributesValuesSerializer
|
||||||
|
validator_class = validators.IssueCustomAttributesValuesValidator
|
||||||
permission_classes = (permissions.IssueCustomAttributesValuesPermission,)
|
permission_classes = (permissions.IssueCustomAttributesValuesPermission,)
|
||||||
lookup_field = "issue_id"
|
lookup_field = "issue_id"
|
||||||
content_object = "issue"
|
content_object = "issue"
|
||||||
|
|
|
@ -17,131 +17,51 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
from django.apps import apps
|
from taiga.base.fields import JsonField, Field
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from taiga.base.api import serializers
|
||||||
|
|
||||||
from taiga.base.fields import JsonField
|
|
||||||
from taiga.base.api.serializers import ValidationError
|
|
||||||
from taiga.base.api.serializers import ModelSerializer
|
|
||||||
|
|
||||||
from . import models
|
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
# Custom Attribute Serializer
|
# Custom Attribute Serializer
|
||||||
#######################################################
|
#######################################################
|
||||||
|
|
||||||
class BaseCustomAttributeSerializer(ModelSerializer):
|
class BaseCustomAttributeSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
name = Field()
|
||||||
read_only_fields = ('id',)
|
description = Field()
|
||||||
exclude = ('created_date', 'modified_date')
|
type = Field()
|
||||||
|
order = Field()
|
||||||
def _validate_integrity_between_project_and_name(self, attrs, source):
|
project = Field(attr="project_id")
|
||||||
"""
|
created_date = Field()
|
||||||
Check the name is not duplicated in the project. Check when:
|
modified_date = Field()
|
||||||
- create a new one
|
|
||||||
- update the name
|
|
||||||
- update the project (move to another project)
|
|
||||||
"""
|
|
||||||
data_id = attrs.get("id", None)
|
|
||||||
data_name = attrs.get("name", None)
|
|
||||||
data_project = attrs.get("project", None)
|
|
||||||
|
|
||||||
if self.object:
|
|
||||||
data_id = data_id or self.object.id
|
|
||||||
data_name = data_name or self.object.name
|
|
||||||
data_project = data_project or self.object.project
|
|
||||||
|
|
||||||
model = self.Meta.model
|
|
||||||
qs = (model.objects.filter(project=data_project, name=data_name)
|
|
||||||
.exclude(id=data_id))
|
|
||||||
if qs.exists():
|
|
||||||
raise ValidationError(_("Already exists one with the same name."))
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def validate_name(self, attrs, source):
|
|
||||||
return self._validate_integrity_between_project_and_name(attrs, source)
|
|
||||||
|
|
||||||
def validate_project(self, attrs, source):
|
|
||||||
return self._validate_integrity_between_project_and_name(attrs, source)
|
|
||||||
|
|
||||||
|
|
||||||
class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer):
|
class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer):
|
||||||
class Meta(BaseCustomAttributeSerializer.Meta):
|
pass
|
||||||
model = models.UserStoryCustomAttribute
|
|
||||||
|
|
||||||
|
|
||||||
class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer):
|
class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer):
|
||||||
class Meta(BaseCustomAttributeSerializer.Meta):
|
pass
|
||||||
model = models.TaskCustomAttribute
|
|
||||||
|
|
||||||
|
|
||||||
class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer):
|
class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer):
|
||||||
class Meta(BaseCustomAttributeSerializer.Meta):
|
pass
|
||||||
model = models.IssueCustomAttribute
|
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
# Custom Attribute Serializer
|
# Custom Attribute Serializer
|
||||||
#######################################################
|
#######################################################
|
||||||
|
class BaseCustomAttributesValuesSerializer(serializers.LightSerializer):
|
||||||
|
attributes_values = Field()
|
||||||
class BaseCustomAttributesValuesSerializer(ModelSerializer):
|
version = Field()
|
||||||
attributes_values = JsonField(source="attributes_values", label="attributes values")
|
|
||||||
_custom_attribute_model = None
|
|
||||||
_container_field = None
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
exclude = ("id",)
|
|
||||||
|
|
||||||
def validate_attributes_values(self, attrs, source):
|
|
||||||
# values must be a dict
|
|
||||||
data_values = attrs.get("attributes_values", None)
|
|
||||||
if self.object:
|
|
||||||
data_values = (data_values or self.object.attributes_values)
|
|
||||||
|
|
||||||
if type(data_values) is not dict:
|
|
||||||
raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
|
|
||||||
|
|
||||||
# Values keys must be in the container object project
|
|
||||||
data_container = attrs.get(self._container_field, None)
|
|
||||||
if data_container:
|
|
||||||
project_id = data_container.project_id
|
|
||||||
elif self.object:
|
|
||||||
project_id = getattr(self.object, self._container_field).project_id
|
|
||||||
else:
|
|
||||||
project_id = None
|
|
||||||
|
|
||||||
values_ids = list(data_values.keys())
|
|
||||||
qs = self._custom_attribute_model.objects.filter(project=project_id,
|
|
||||||
id__in=values_ids)
|
|
||||||
if qs.count() != len(values_ids):
|
|
||||||
raise ValidationError(_("It contain invalid custom fields."))
|
|
||||||
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
|
class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
|
||||||
_custom_attribute_model = models.UserStoryCustomAttribute
|
user_story = Field(attr="user_story.id")
|
||||||
_container_model = "userstories.UserStory"
|
|
||||||
_container_field = "user_story"
|
|
||||||
|
|
||||||
class Meta(BaseCustomAttributesValuesSerializer.Meta):
|
|
||||||
model = models.UserStoryCustomAttributesValues
|
|
||||||
|
|
||||||
|
|
||||||
class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer):
|
class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
|
||||||
_custom_attribute_model = models.TaskCustomAttribute
|
task = Field(attr="task.id")
|
||||||
_container_field = "task"
|
|
||||||
|
|
||||||
class Meta(BaseCustomAttributesValuesSerializer.Meta):
|
|
||||||
model = models.TaskCustomAttributesValues
|
|
||||||
|
|
||||||
|
|
||||||
class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer):
|
class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
|
||||||
_custom_attribute_model = models.IssueCustomAttribute
|
issue = Field(attr="issue.id")
|
||||||
_container_field = "issue"
|
|
||||||
|
|
||||||
class Meta(BaseCustomAttributesValuesSerializer.Meta):
|
|
||||||
model = models.IssueCustomAttributesValues
|
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from taiga.base.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
|
|
@ -45,7 +45,7 @@ class DiscoverModeFilterBackend(FilterBackend):
|
||||||
if request.QUERY_PARAMS.get("is_featured", None) == 'true':
|
if request.QUERY_PARAMS.get("is_featured", None) == 'true':
|
||||||
qs = qs.order_by("?")
|
qs = qs.order_by("?")
|
||||||
|
|
||||||
return super().filter_queryset(request, qs.distinct(), view)
|
return super().filter_queryset(request, qs, view)
|
||||||
|
|
||||||
|
|
||||||
class CanViewProjectObjFilterBackend(FilterBackend):
|
class CanViewProjectObjFilterBackend(FilterBackend):
|
||||||
|
@ -86,7 +86,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
|
||||||
# external users / anonymous
|
# external users / anonymous
|
||||||
qs = qs.filter(anon_permissions__contains=["view_project"])
|
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):
|
class QFilterBackend(FilterBackend):
|
||||||
|
@ -121,3 +121,34 @@ class QFilterBackend(FilterBackend):
|
||||||
params=params,
|
params=params,
|
||||||
order_by=order_by)
|
order_by=order_by)
|
||||||
return queryset
|
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
|
||||||
|
|
|
@ -23,7 +23,6 @@ from django.utils import timezone
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
from taiga.base.decorators import detail_route
|
from taiga.base.decorators import detail_route
|
||||||
from taiga.base.api import ReadOnlyListViewSet
|
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 taiga.mdrender.service import render as mdrender
|
||||||
|
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
@ -38,7 +37,7 @@ class HistoryViewSet(ReadOnlyListViewSet):
|
||||||
|
|
||||||
def get_content_type(self):
|
def get_content_type(self):
|
||||||
app_name, model = self.content_type.split(".", 1)
|
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):
|
def get_queryset(self):
|
||||||
ct = self.get_content_type()
|
ct = self.get_content_type()
|
||||||
|
|
|
@ -35,6 +35,7 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts
|
||||||
# previous diff has value for the attribute and we want to prevent their propagation
|
# 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():
|
def _generate_uuid():
|
||||||
return str(uuid.uuid1())
|
return str(uuid.uuid1())
|
||||||
|
|
||||||
|
|
|
@ -17,28 +17,31 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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 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):
|
class HistoryEntrySerializer(serializers.LightSerializer):
|
||||||
diff = JsonField()
|
id = Field()
|
||||||
snapshot = JsonField()
|
user = MethodField()
|
||||||
values = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
|
created_at = Field()
|
||||||
values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS)
|
type = Field()
|
||||||
user = serializers.SerializerMethodField("get_user")
|
key = Field()
|
||||||
delete_comment_user = JsonField()
|
diff = Field()
|
||||||
comment_versions = JsonField()
|
snapshot = Field()
|
||||||
|
values = Field()
|
||||||
class Meta:
|
values_diff = I18NJsonField()
|
||||||
model = models.HistoryEntry
|
comment = I18NJsonField()
|
||||||
exclude = ("comment_versions",)
|
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):
|
def get_user(self, entry):
|
||||||
user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False}
|
user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False}
|
||||||
|
|
|
@ -34,12 +34,9 @@ from collections import namedtuple
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from functools import lru_cache
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
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.apps import apps
|
||||||
from django.db import transaction as tx
|
from django.db import transaction as tx
|
||||||
from django_pglocks import advisory_lock
|
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
|
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
|
# Type that represents a freezed object
|
||||||
FrozenObj = namedtuple("FrozenObj", ["key", "snapshot"])
|
FrozenObj = namedtuple("FrozenObj", ["key", "snapshot"])
|
||||||
|
@ -280,8 +292,7 @@ def get_modified_fields(obj:object, last_modifications):
|
||||||
history_entries = (entry_model.objects
|
history_entries = (entry_model.objects
|
||||||
.filter(key=key)
|
.filter(key=key)
|
||||||
.order_by("-created_at")
|
.order_by("-created_at")
|
||||||
.values_list("diff", flat=True)
|
.values_list("diff", flat=True)[0:last_modifications])
|
||||||
[0:last_modifications])
|
|
||||||
|
|
||||||
modified_fields = []
|
modified_fields = []
|
||||||
for history_entry in history_entries:
|
for history_entry in history_entries:
|
||||||
|
@ -301,7 +312,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = make_key_from_model_object(obj)
|
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__)
|
typename = get_typename_for_model_class(obj.__class__)
|
||||||
|
|
||||||
new_fobj = freeze_model_instance(obj)
|
new_fobj = freeze_model_instance(obj)
|
||||||
|
@ -383,14 +394,6 @@ def prefetch_owners_in_history_queryset(qs):
|
||||||
return qs
|
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("projects.project", project_freezer)
|
||||||
register_freeze_implementation("milestones.milestone", milestone_freezer,)
|
register_freeze_implementation("milestones.milestone", milestone_freezer,)
|
||||||
register_freeze_implementation("userstories.userstory", userstory_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("tasks.task", task_freezer)
|
||||||
register_freeze_implementation("wiki.wikipage", wikipage_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("projects.project", project_values)
|
||||||
register_values_implementation("milestones.milestone", milestone_values)
|
register_values_implementation("milestones.milestone", milestone_values)
|
||||||
register_values_implementation("userstories.userstory", userstory_values)
|
register_values_implementation("userstories.userstory", userstory_values)
|
||||||
|
|
|
@ -34,14 +34,18 @@ from taiga.projects.occ import OCCResourceMixin
|
||||||
from taiga.projects.tagging.api import TaggedResourceMixin
|
from taiga.projects.tagging.api import TaggedResourceMixin
|
||||||
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
|
||||||
|
|
||||||
|
from .utils import attach_extra_info
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import services
|
from . import services
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from . import validators
|
||||||
|
|
||||||
|
|
||||||
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
|
validator_class = validators.IssueValidator
|
||||||
queryset = models.Issue.objects.all()
|
queryset = models.Issue.objects.all()
|
||||||
permission_classes = (permissions.IssuePermission, )
|
permission_classes = (permissions.IssuePermission, )
|
||||||
filter_backends = (filters.CanViewIssuesFilterBackend,
|
filter_backends = (filters.CanViewIssuesFilterBackend,
|
||||||
|
@ -144,10 +148,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
qs = qs.prefetch_related("attachments", "generated_user_stories")
|
|
||||||
qs = qs.select_related("owner", "assigned_to", "status", "project")
|
qs = qs.select_related("owner", "assigned_to", "status", "project")
|
||||||
qs = self.attach_votes_attrs_to_queryset(qs)
|
qs = attach_extra_info(qs, user=self.request.user)
|
||||||
return self.attach_watchers_attrs_to_queryset(qs)
|
return qs
|
||||||
|
|
||||||
def pre_save(self, obj):
|
def pre_save(self, obj):
|
||||||
if not obj.id:
|
if not obj.id:
|
||||||
|
@ -180,10 +183,18 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
|
|
||||||
@list_route(methods=["GET"])
|
@list_route(methods=["GET"])
|
||||||
def by_ref(self, request):
|
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)
|
project_id = request.QUERY_PARAMS.get("project", None)
|
||||||
issue = get_object_or_404(models.Issue, ref=ref, project_id=project_id)
|
if project_id is not None:
|
||||||
return self.retrieve(request, pk=issue.pk)
|
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"])
|
@list_route(methods=["GET"])
|
||||||
def filters_data(self, request, *args, **kwargs):
|
def filters_data(self, request, *args, **kwargs):
|
||||||
|
@ -225,9 +236,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_create(self, request, **kwargs):
|
def bulk_create(self, request, **kwargs):
|
||||||
serializer = serializers.IssuesBulkSerializer(data=request.DATA)
|
validator = validators.IssuesBulkValidator(data=request.DATA)
|
||||||
if serializer.is_valid():
|
if validator.is_valid():
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
project = Project.objects.get(pk=data["project_id"])
|
project = Project.objects.get(pk=data["project_id"])
|
||||||
self.check_permissions(request, 'bulk_create', project)
|
self.check_permissions(request, 'bulk_create', project)
|
||||||
if project.blocked_code is not None:
|
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,
|
status=project.default_issue_status, severity=project.default_severity,
|
||||||
priority=project.default_priority, type=project.default_issue_type,
|
priority=project.default_priority, type=project.default_issue_type,
|
||||||
callback=self.post_save, precall=self.pre_save)
|
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)
|
issues_serialized = self.get_serializer_class()(issues, many=True)
|
||||||
|
|
||||||
return response.Ok(data=issues_serialized.data)
|
return response.Ok(data=issues_serialized.data)
|
||||||
|
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
|
|
||||||
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
|
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):
|
||||||
|
|
|
@ -17,56 +17,52 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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.base.neighbors import NeighborsSerializerMixin
|
||||||
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
|
from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
|
||||||
from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
|
from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
|
||||||
from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
|
from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
|
||||||
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
|
from taiga.projects.notifications.mixins import WatchedResourceSerializer
|
||||||
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.votes.mixins.serializers import VoteResourceSerializerMixin
|
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,
|
class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
|
||||||
serializers.ModelSerializer):
|
OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
|
||||||
tags = TagsAndTagsColorsField(default=[], required=False)
|
StatusExtraInfoSerializerMixin, serializers.LightSerializer):
|
||||||
external_reference = PgArrayField(required=False)
|
id = Field()
|
||||||
is_closed = serializers.Field(source="is_closed")
|
ref = Field()
|
||||||
comment = serializers.SerializerMethodField("get_comment")
|
severity = Field(attr="severity_id")
|
||||||
generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories")
|
priority = Field(attr="priority_id")
|
||||||
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
|
type = Field(attr="type_id")
|
||||||
description_html = serializers.SerializerMethodField("get_description_html")
|
milestone = Field(attr="milestone_id")
|
||||||
status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True)
|
project = Field(attr="project_id")
|
||||||
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
|
created_date = Field()
|
||||||
owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
|
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
|
class IssueSerializer(IssueListSerializer):
|
||||||
read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
|
comment = MethodField()
|
||||||
|
generated_user_stories = MethodField()
|
||||||
|
blocked_note_html = MethodField()
|
||||||
|
description = Field()
|
||||||
|
description_html = MethodField()
|
||||||
|
|
||||||
def get_comment(self, obj):
|
def get_comment(self, obj):
|
||||||
# NOTE: This method and field is necessary to historical comments work
|
# NOTE: This method and field is necessary to historical comments work
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_generated_user_stories(self, obj):
|
def get_generated_user_stories(self, obj):
|
||||||
return [{
|
assert hasattr(obj, "generated_user_stories_attr"), "instance must have a generated_user_stories_attr attribute"
|
||||||
"id": us.id,
|
return obj.generated_user_stories_attr
|
||||||
"ref": us.ref,
|
|
||||||
"subject": us.subject,
|
|
||||||
} for us in obj.generated_user_stories.all()]
|
|
||||||
|
|
||||||
def get_blocked_note_html(self, obj):
|
def get_blocked_note_html(self, obj):
|
||||||
return mdrender(obj.project, obj.blocked_note)
|
return mdrender(obj.project, obj.blocked_note)
|
||||||
|
@ -75,39 +71,5 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa
|
||||||
return mdrender(obj.project, obj.description)
|
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):
|
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
|
||||||
def serialize_neighbor(self, neighbor):
|
pass
|
||||||
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()
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.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
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base.api 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()
|
|
@ -17,14 +17,14 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import Field, MethodField
|
||||||
|
|
||||||
|
|
||||||
class FanSerializer(serializers.ModelSerializer):
|
class FanSerializer(serializers.LightSerializer):
|
||||||
full_name = serializers.CharField(source='get_full_name', required=False)
|
id = Field()
|
||||||
|
username = Field()
|
||||||
|
full_name = MethodField()
|
||||||
|
|
||||||
class Meta:
|
def get_full_name(self, obj):
|
||||||
model = get_user_model()
|
return obj.get_full_name()
|
||||||
fields = ('id', 'username', 'full_name')
|
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db.models import Prefetch
|
|
||||||
|
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import response
|
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 WatchedResourceMixin
|
||||||
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchersViewSetMixin
|
||||||
from taiga.projects.history.mixins import HistoryResourceMixin
|
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 serializers
|
||||||
|
from . import validators
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import utils as milestones_utils
|
from . import utils as milestones_utils
|
||||||
|
@ -47,6 +42,8 @@ import datetime
|
||||||
|
|
||||||
class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||||
BlockedByProjectMixin, ModelCrudViewSet):
|
BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
|
serializer_class = serializers.MilestoneSerializer
|
||||||
|
validator_class = validators.MilestoneValidator
|
||||||
permission_classes = (permissions.MilestonePermission,)
|
permission_classes = (permissions.MilestonePermission,)
|
||||||
filter_backends = (filters.CanViewMilestonesFilterBackend,)
|
filter_backends = (filters.CanViewMilestonesFilterBackend,)
|
||||||
filter_fields = (
|
filter_fields = (
|
||||||
|
@ -56,12 +53,6 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||||
)
|
)
|
||||||
queryset = models.Milestone.objects.all()
|
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):
|
def list(self, request, *args, **kwargs):
|
||||||
res = super().list(request, *args, **kwargs)
|
res = super().list(request, *args, **kwargs)
|
||||||
self._add_taiga_info_headers()
|
self._add_taiga_info_headers()
|
||||||
|
@ -84,33 +75,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
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 = qs.select_related("project", "owner")
|
||||||
qs = self.attach_watchers_attrs_to_queryset(qs)
|
qs = milestones_utils.attach_extra_info(qs, user=self.request.user)
|
||||||
qs = milestones_utils.attach_total_points(qs)
|
|
||||||
qs = milestones_utils.attach_closed_points(qs)
|
|
||||||
qs = qs.order_by("-estimated_start")
|
qs = qs.order_by("-estimated_start")
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
|
@ -16,58 +16,29 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.utils import json
|
from taiga.base.fields import Field, MethodField
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
from taiga.projects.notifications.mixins import WatchedResourceSerializer
|
||||||
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
|
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
|
||||||
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
|
|
||||||
from taiga.projects.userstories.serializers import UserStoryListSerializer
|
from taiga.projects.userstories.serializers import UserStoryListSerializer
|
||||||
|
|
||||||
from . import models
|
|
||||||
|
|
||||||
import serpy
|
class MilestoneSerializer(WatchedResourceSerializer, serializers.LightSerializer):
|
||||||
|
id = Field()
|
||||||
|
name = Field()
|
||||||
class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer,
|
slug = Field()
|
||||||
ValidateDuplicatedNameInProjectMixin):
|
owner = Field(attr="owner_id")
|
||||||
total_points = serializers.SerializerMethodField("get_total_points")
|
project = Field(attr="project_id")
|
||||||
closed_points = serializers.SerializerMethodField("get_closed_points")
|
estimated_start = Field()
|
||||||
user_stories = serializers.SerializerMethodField("get_user_stories")
|
estimated_finish = Field()
|
||||||
|
created_date = Field()
|
||||||
class Meta:
|
modified_date = Field()
|
||||||
model = models.Milestone
|
closed = Field()
|
||||||
read_only_fields = ("id", "created_date", "modified_date")
|
disponibility = Field()
|
||||||
|
order = Field()
|
||||||
def get_total_points(self, obj):
|
watchers = Field()
|
||||||
return sum(obj.total_points.values())
|
user_stories = MethodField()
|
||||||
|
total_points = MethodField()
|
||||||
def get_closed_points(self, obj):
|
closed_points = MethodField()
|
||||||
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()
|
|
||||||
|
|
||||||
def get_user_stories(self, obj):
|
def get_user_stories(self, obj):
|
||||||
return UserStoryListSerializer(obj.user_stories.all(), many=True).data
|
return UserStoryListSerializer(obj.user_stories.all(), many=True).data
|
||||||
|
|
|
@ -17,6 +17,16 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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"):
|
def attach_total_points(queryset, as_field="total_points_attr"):
|
||||||
"""Attach total of point values to each object of the queryset.
|
"""Attach total of point values to each object of the queryset.
|
||||||
|
@ -56,3 +66,33 @@ def attach_closed_points(queryset, as_field="closed_points_attr"):
|
||||||
sql = sql.format(tbl=model._meta.db_table)
|
sql = sql.format(tbl=model._meta.db_table)
|
||||||
queryset = queryset.extra(select={as_field: sql})
|
queryset = queryset.extra(select={as_field: sql})
|
||||||
return queryset
|
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
|
||||||
|
|
|
@ -18,15 +18,24 @@
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
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
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
class SprintExistsValidator:
|
class MilestoneExistsValidator:
|
||||||
def validate_sprint_id(self, attrs, source):
|
def validate_sprint_id(self, attrs, source):
|
||||||
value = attrs[source]
|
value = attrs[source]
|
||||||
if not models.Milestone.objects.filter(pk=value).exists():
|
if not models.Milestone.objects.filter(pk=value).exists():
|
||||||
msg = _("There's no sprint with that id")
|
msg = _("There's no milestone with that id")
|
||||||
raise serializers.ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, validators.ModelValidator):
|
||||||
|
class Meta:
|
||||||
|
model = models.Milestone
|
||||||
|
read_only_fields = ("id", "created_date", "modified_date")
|
||||||
|
|
|
@ -17,34 +17,13 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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 _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
import serpy
|
|
||||||
|
|
||||||
class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer):
|
class CachedUsersSerializerMixin(serializers.LightSerializer):
|
||||||
|
|
||||||
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):
|
|
||||||
def to_value(self, instance):
|
def to_value(self, instance):
|
||||||
self._serialized_users = {}
|
self._serialized_users = {}
|
||||||
return super().to_value(instance)
|
return super().to_value(instance)
|
||||||
|
@ -55,37 +34,40 @@ class ListCachedUsersSerializerMixin(serpy.Serializer):
|
||||||
|
|
||||||
serialized_user = self._serialized_users.get(user.id, None)
|
serialized_user = self._serialized_users.get(user.id, None)
|
||||||
if serialized_user is None:
|
if serialized_user is None:
|
||||||
serializer_user = ListUserBasicInfoSerializer(user).data
|
serialized_user = UserBasicInfoSerializer(user).data
|
||||||
self._serialized_users[user.id] = serializer_user
|
self._serialized_users[user.id] = serialized_user
|
||||||
|
|
||||||
return serialized_user
|
return serialized_user
|
||||||
|
|
||||||
|
|
||||||
class ListOwnerExtraInfoSerializerMixin(ListCachedUsersSerializerMixin):
|
class OwnerExtraInfoSerializerMixin(CachedUsersSerializerMixin):
|
||||||
owner = serpy.Field(attr="owner_id")
|
owner = Field(attr="owner_id")
|
||||||
owner_extra_info = serpy.MethodField()
|
owner_extra_info = MethodField()
|
||||||
|
|
||||||
def get_owner_extra_info(self, obj):
|
def get_owner_extra_info(self, obj):
|
||||||
return self.get_user_extra_info(obj.owner)
|
return self.get_user_extra_info(obj.owner)
|
||||||
|
|
||||||
|
|
||||||
class ListAssignedToExtraInfoSerializerMixin(ListCachedUsersSerializerMixin):
|
class AssignedToExtraInfoSerializerMixin(CachedUsersSerializerMixin):
|
||||||
assigned_to = serpy.Field(attr="assigned_to_id")
|
assigned_to = Field(attr="assigned_to_id")
|
||||||
assigned_to_extra_info = serpy.MethodField()
|
assigned_to_extra_info = MethodField()
|
||||||
|
|
||||||
def get_assigned_to_extra_info(self, obj):
|
def get_assigned_to_extra_info(self, obj):
|
||||||
return self.get_user_extra_info(obj.assigned_to)
|
return self.get_user_extra_info(obj.assigned_to)
|
||||||
|
|
||||||
|
|
||||||
class ListStatusExtraInfoSerializerMixin(serpy.Serializer):
|
class StatusExtraInfoSerializerMixin(serializers.LightSerializer):
|
||||||
status = serpy.Field(attr="status_id")
|
status = Field(attr="status_id")
|
||||||
status_extra_info = serpy.MethodField()
|
status_extra_info = MethodField()
|
||||||
|
|
||||||
def to_value(self, instance):
|
def to_value(self, instance):
|
||||||
self._serialized_status = {}
|
self._serialized_status = {}
|
||||||
return super().to_value(instance)
|
return super().to_value(instance)
|
||||||
|
|
||||||
def get_status_extra_info(self, obj):
|
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)
|
serialized_status = self._serialized_status.get(obj.status_id, None)
|
||||||
if serialized_status is None:
|
if serialized_status is None:
|
||||||
serialized_status = {
|
serialized_status = {
|
||||||
|
|
|
@ -16,8 +16,6 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import serpy
|
|
||||||
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from operator import is_not
|
from operator import is_not
|
||||||
|
|
||||||
|
@ -28,16 +26,12 @@ from taiga.base import response
|
||||||
from taiga.base.decorators import detail_route
|
from taiga.base.decorators import detail_route
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.api.utils import get_object_or_404
|
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 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
|
from . serializers import WatcherSerializer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class WatchedResourceMixin:
|
class WatchedResourceMixin:
|
||||||
"""
|
"""
|
||||||
Rest Framework resource mixin for resources susceptible
|
Rest Framework resource mixin for resources susceptible
|
||||||
|
@ -54,14 +48,6 @@ class WatchedResourceMixin:
|
||||||
|
|
||||||
_not_notify = False
|
_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"])
|
@detail_route(methods=["POST"])
|
||||||
def watch(self, request, pk=None):
|
def watch(self, request, pk=None):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
@ -186,11 +172,15 @@ class WatchedModelMixin(object):
|
||||||
return frozenset(filter(is_not_none, participants))
|
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):
|
def get_is_watcher(self, obj):
|
||||||
|
# The "is_watcher" attribute is attached in the get_queryset of the viewset.
|
||||||
if "request" in self.context:
|
if "request" in self.context:
|
||||||
user = self.context["request"].user
|
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
|
return False
|
||||||
|
|
||||||
|
@ -199,26 +189,16 @@ class BaseWatchedResourceModelSerializer(object):
|
||||||
return getattr(obj, "total_watchers", 0) or 0
|
return getattr(obj, "total_watchers", 0) or 0
|
||||||
|
|
||||||
|
|
||||||
class WatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serializers.ModelSerializer):
|
class EditableWatchedResourceSerializer(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):
|
|
||||||
watchers = WatchersField(required=False)
|
watchers = WatchersField(required=False)
|
||||||
|
|
||||||
def restore_object(self, attrs, instance=None):
|
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.
|
# 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
|
# If that's the case we need to remove it before calling the super method
|
||||||
watcher_field = self.fields.pop("watchers", None)
|
self.fields.pop("watchers", None)
|
||||||
self.validate_watchers(attrs, "watchers")
|
self.validate_watchers(attrs, "watchers")
|
||||||
new_watcher_ids = attrs.pop("watchers", None)
|
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:
|
if instance is None or new_watcher_ids is None:
|
||||||
|
@ -229,7 +209,6 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
|
||||||
adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
|
adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
|
||||||
removing_watcher_ids = list(old_watcher_ids.difference(new_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)
|
adding_users = get_user_model().objects.filter(id__in=adding_watcher_ids)
|
||||||
removing_users = get_user_model().objects.filter(id__in=removing_watcher_ids)
|
removing_users = get_user_model().objects.filter(id__in=removing_watcher_ids)
|
||||||
for user in adding_users:
|
for user in adding_users:
|
||||||
|
@ -253,10 +232,10 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
|
||||||
if user and user.is_authenticated():
|
if user and user.is_authenticated():
|
||||||
obj.is_watcher = user.id in obj.watchers
|
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):
|
def save(self, **kwargs):
|
||||||
obj = super(EditableWatchedResourceModelSerializer, self).save(**kwargs)
|
obj = super(EditableWatchedResourceSerializer, self).save(**kwargs)
|
||||||
self.fields["watchers"] = WatchersField(required=False)
|
self.fields["watchers"] = WatchersField(required=False)
|
||||||
obj.watchers = [user.id for user in obj.get_watchers()]
|
obj.watchers = [user.id for user in obj.get_watchers()]
|
||||||
return obj
|
return obj
|
||||||
|
|
|
@ -53,6 +53,9 @@ def attach_is_watcher_to_queryset(queryset, user, as_field="is_watcher"):
|
||||||
"""
|
"""
|
||||||
model = queryset.model
|
model = queryset.model
|
||||||
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
||||||
|
if user is None or user.is_anonymous():
|
||||||
|
sql = """SELECT false"""
|
||||||
|
else:
|
||||||
sql = ("""SELECT CASE WHEN (SELECT count(*)
|
sql = ("""SELECT CASE WHEN (SELECT count(*)
|
||||||
FROM notifications_watched
|
FROM notifications_watched
|
||||||
WHERE notifications_watched.content_type_id = {type_id}
|
WHERE notifications_watched.content_type_id = {type_id}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
class WatchersValidator:
|
class WatchersValidator:
|
||||||
|
@ -45,6 +45,6 @@ class WatchersValidator:
|
||||||
existing_watcher_ids = project.get_watchers().values_list("id", flat=True)
|
existing_watcher_ids = project.get_watchers().values_list("id", flat=True)
|
||||||
result = set(users).difference(member_ids).difference(existing_watcher_ids)
|
result = set(users).difference(member_ids).difference(existing_watcher_ids)
|
||||||
if result:
|
if result:
|
||||||
raise serializers.ValidationError(_("Watchers contains invalid users"))
|
raise ValidationError(_("Watchers contains invalid users"))
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
|
@ -24,7 +24,7 @@ from taiga.base.api import viewsets
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.api.utils import get_object_or_404
|
||||||
from taiga.permissions.services import user_has_perm
|
from taiga.permissions.services import user_has_perm
|
||||||
|
|
||||||
from .serializers import ResolverSerializer
|
from .validators import ResolverValidator
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,11 +32,11 @@ class ResolverViewSet(viewsets.ViewSet):
|
||||||
permission_classes = (permissions.ResolverPermission,)
|
permission_classes = (permissions.ResolverPermission,)
|
||||||
|
|
||||||
def list(self, request, **kwargs):
|
def list(self, request, **kwargs):
|
||||||
serializer = ResolverSerializer(data=request.QUERY_PARAMS)
|
validator = ResolverValidator(data=request.QUERY_PARAMS)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
raise exc.BadRequest(serializer.errors)
|
raise exc.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
|
|
||||||
project_model = apps.get_model("projects", "Project")
|
project_model = apps.get_model("projects", "Project")
|
||||||
project = get_object_or_404(project_model, slug=data["project"])
|
project = get_object_or_404(project_model, slug=data["project"])
|
||||||
|
|
|
@ -17,9 +17,11 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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)
|
project = serializers.CharField(max_length=512, required=True)
|
||||||
milestone = serializers.CharField(max_length=512, required=False)
|
milestone = serializers.CharField(max_length=512, required=False)
|
||||||
us = serializers.IntegerField(required=False)
|
us = serializers.IntegerField(required=False)
|
||||||
|
@ -31,10 +33,10 @@ class ResolverSerializer(serializers.Serializer):
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
if "ref" in attrs:
|
if "ref" in attrs:
|
||||||
if "us" 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:
|
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:
|
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
|
return attrs
|
|
@ -16,131 +16,121 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import serpy
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.fields import JsonField
|
from taiga.base.fields import Field, MethodField, I18NField
|
||||||
from taiga.base.fields import PgArrayField
|
|
||||||
|
|
||||||
|
from taiga.permissions import services as permissions_services
|
||||||
from taiga.users.services import get_photo_or_gravatar_url
|
from taiga.users.services import get_photo_or_gravatar_url
|
||||||
from taiga.users.serializers import UserBasicInfoSerializer
|
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 taiga.permissions.services import is_project_admin, is_project_owner
|
||||||
|
|
||||||
from . import models
|
|
||||||
from . import services
|
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.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 PointsSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
name = I18NField()
|
||||||
model = models.Points
|
order = Field()
|
||||||
i18n_fields = ("name",)
|
value = Field()
|
||||||
|
project = Field(attr="project_id")
|
||||||
|
|
||||||
|
|
||||||
class UserStoryStatusSerializer(ValidateDuplicatedNameInProjectMixin):
|
class UserStoryStatusSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
name = I18NField()
|
||||||
model = models.UserStoryStatus
|
slug = Field()
|
||||||
i18n_fields = ("name",)
|
order = Field()
|
||||||
|
is_closed = Field()
|
||||||
|
is_archived = Field()
|
||||||
|
color = Field()
|
||||||
|
wip_limit = Field()
|
||||||
|
project = Field(attr="project_id")
|
||||||
|
|
||||||
|
|
||||||
class BasicUserStoryStatusSerializer(serializers.ModelSerializer):
|
class TaskStatusSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
name = I18NField()
|
||||||
model = models.UserStoryStatus
|
slug = Field()
|
||||||
i18n_fields = ("name",)
|
order = Field()
|
||||||
fields = ("name", "color")
|
is_closed = Field()
|
||||||
|
color = Field()
|
||||||
|
project = Field(attr="project_id")
|
||||||
|
|
||||||
|
|
||||||
class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin):
|
class SeveritySerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
name = I18NField()
|
||||||
model = models.TaskStatus
|
order = Field()
|
||||||
i18n_fields = ("name",)
|
color = Field()
|
||||||
|
project = Field(attr="project_id")
|
||||||
|
|
||||||
|
|
||||||
class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer):
|
class PrioritySerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
name = I18NField()
|
||||||
model = models.TaskStatus
|
order = Field()
|
||||||
i18n_fields = ("name",)
|
color = Field()
|
||||||
fields = ("name", "color")
|
project = Field(attr="project_id")
|
||||||
|
|
||||||
|
|
||||||
class SeveritySerializer(ValidateDuplicatedNameInProjectMixin):
|
class IssueStatusSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
name = I18NField()
|
||||||
model = models.Severity
|
slug = Field()
|
||||||
i18n_fields = ("name",)
|
order = Field()
|
||||||
|
is_closed = Field()
|
||||||
|
color = Field()
|
||||||
|
project = Field(attr="project_id")
|
||||||
|
|
||||||
|
|
||||||
class PrioritySerializer(ValidateDuplicatedNameInProjectMixin):
|
class IssueTypeSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
name = I18NField()
|
||||||
model = models.Priority
|
order = Field()
|
||||||
i18n_fields = ("name",)
|
color = Field()
|
||||||
|
project = Field(attr="project_id")
|
||||||
|
|
||||||
class IssueStatusSerializer(ValidateDuplicatedNameInProjectMixin):
|
|
||||||
class Meta:
|
|
||||||
model = models.IssueStatus
|
|
||||||
i18n_fields = ("name",)
|
|
||||||
|
|
||||||
|
|
||||||
class BasicIssueStatusSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = models.IssueStatus
|
|
||||||
i18n_fields = ("name",)
|
|
||||||
fields = ("name", "color")
|
|
||||||
|
|
||||||
|
|
||||||
class IssueTypeSerializer(ValidateDuplicatedNameInProjectMixin):
|
|
||||||
class Meta:
|
|
||||||
model = models.IssueType
|
|
||||||
i18n_fields = ("name",)
|
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
## Members
|
# Members
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class MembershipSerializer(serializers.ModelSerializer):
|
class MembershipSerializer(serializers.LightSerializer):
|
||||||
role_name = serializers.CharField(source='role.name', required=False, read_only=True, i18n=True)
|
id = Field()
|
||||||
full_name = serializers.CharField(source='user.get_full_name', required=False, read_only=True)
|
user = Field(attr="user_id")
|
||||||
user_email = serializers.EmailField(source='user.email', required=False, read_only=True)
|
project = Field(attr="project_id")
|
||||||
is_user_active = serializers.BooleanField(source='user.is_active', required=False,
|
role = Field(attr="role_id")
|
||||||
read_only=True)
|
is_admin = Field()
|
||||||
email = serializers.EmailField(required=True)
|
created_at = Field()
|
||||||
color = serializers.CharField(source='user.color', required=False, read_only=True)
|
invited_by = Field(attr="invited_by_id")
|
||||||
photo = serializers.SerializerMethodField("get_photo")
|
invitation_extra_text = Field()
|
||||||
project_name = serializers.SerializerMethodField("get_project_name")
|
user_order = Field()
|
||||||
project_slug = serializers.SerializerMethodField("get_project_slug")
|
role_name = MethodField()
|
||||||
invited_by = UserBasicInfoSerializer(read_only=True)
|
full_name = MethodField()
|
||||||
is_owner = serializers.SerializerMethodField("get_is_owner")
|
is_user_active = MethodField()
|
||||||
|
color = MethodField()
|
||||||
|
photo = MethodField()
|
||||||
|
project_name = MethodField()
|
||||||
|
project_slug = MethodField()
|
||||||
|
invited_by = UserBasicInfoSerializer()
|
||||||
|
is_owner = MethodField()
|
||||||
|
|
||||||
class Meta:
|
def get_role_name(self, obj):
|
||||||
model = models.Membership
|
return obj.role.name if obj.role else None
|
||||||
# IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date
|
|
||||||
# with this info (excluding here user_email and email)
|
|
||||||
read_only_fields = ("user",)
|
|
||||||
exclude = ("token", "user_email", "email")
|
|
||||||
|
|
||||||
def get_photo(self, project):
|
def get_full_name(self, obj):
|
||||||
return get_photo_or_gravatar_url(project.user)
|
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):
|
def get_project_name(self, obj):
|
||||||
return obj.project.name if obj and obj.project else ""
|
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
|
return (obj and obj.user_id and obj.project_id and obj.project.owner_id and
|
||||||
obj.user_id == obj.project.owner_id)
|
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 MembershipAdminSerializer(MembershipSerializer):
|
||||||
class Meta:
|
email = Field()
|
||||||
model = models.Membership
|
user_email = MethodField()
|
||||||
|
|
||||||
|
def get_user_email(self, obj):
|
||||||
|
return obj.user.email if obj.user else None
|
||||||
|
|
||||||
# IMPORTANT: Maintain the MembershipSerializer Meta up to date
|
# IMPORTANT: Maintain the MembershipSerializer Meta up to date
|
||||||
# with this info (excluding there user_email and email)
|
# with this info (excluding there user_email and email)
|
||||||
read_only_fields = ("user",)
|
|
||||||
exclude = ("token",)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
## Projects
|
# Projects
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializer,
|
class ProjectSerializer(serializers.LightSerializer):
|
||||||
serializers.ModelSerializer):
|
id = Field()
|
||||||
anon_permissions = PgArrayField(required=False)
|
name = Field()
|
||||||
public_permissions = PgArrayField(required=False)
|
slug = Field()
|
||||||
my_permissions = serializers.SerializerMethodField("get_my_permissions")
|
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)
|
tags = Field()
|
||||||
i_am_owner = serializers.SerializerMethodField("get_i_am_owner")
|
tags_colors = MethodField()
|
||||||
i_am_admin = serializers.SerializerMethodField("get_i_am_admin")
|
|
||||||
i_am_member = serializers.SerializerMethodField("get_i_am_member")
|
|
||||||
|
|
||||||
tags = TagsField(default=[], required=False)
|
default_points = Field(attr="default_points_id")
|
||||||
tags_colors = TagsColorsField(required=False, read_only=True)
|
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")
|
my_permissions = MethodField()
|
||||||
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
|
|
||||||
total_watchers = serializers.SerializerMethodField("get_total_watchers")
|
|
||||||
|
|
||||||
logo_small_url = serializers.SerializerMethodField("get_logo_small_url")
|
i_am_owner = MethodField()
|
||||||
logo_big_url = serializers.SerializerMethodField("get_logo_big_url")
|
i_am_admin = MethodField()
|
||||||
|
i_am_member = MethodField()
|
||||||
|
|
||||||
class Meta:
|
notify_level = MethodField()
|
||||||
model = models.Project
|
total_closed_milestones = MethodField()
|
||||||
read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
|
|
||||||
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref",
|
is_watcher = MethodField()
|
||||||
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid",
|
total_watchers = MethodField()
|
||||||
"transfer_token")
|
|
||||||
|
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):
|
def get_my_permissions(self, obj):
|
||||||
if "request" in self.context:
|
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 []
|
return []
|
||||||
|
|
||||||
|
def get_owner(self, obj):
|
||||||
|
return UserBasicInfoSerializer(obj.owner).data
|
||||||
|
|
||||||
def get_i_am_owner(self, obj):
|
def get_i_am_owner(self, obj):
|
||||||
if "request" in self.context:
|
if "request" in self.context:
|
||||||
return is_project_owner(self.context["request"].user, obj)
|
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 is_project_admin(self.context["request"].user, obj)
|
||||||
return False
|
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):
|
def get_total_closed_milestones(self, obj):
|
||||||
# The "closed_milestone" attribute can be attached in the get_queryset method of the viewset.
|
assert hasattr(obj, "closed_milestones_attr"), "instance must have a closed_milestones_attr attribute"
|
||||||
qs_closed_milestones = getattr(obj, "closed_milestones", None)
|
return obj.closed_milestones_attr
|
||||||
if qs_closed_milestones is not None:
|
|
||||||
return len(qs_closed_milestones)
|
|
||||||
|
|
||||||
return obj.milestones.filter(closed=True).count()
|
def get_is_watcher(self, obj):
|
||||||
|
assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
|
||||||
def get_notify_level(self, obj):
|
np = self.get_notify_level(obj)
|
||||||
if "request" in self.context:
|
return np is not None and np != NotifyLevel.none
|
||||||
user = self.context["request"].user
|
|
||||||
return user.is_authenticated() and user.get_notify_level(obj)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_total_watchers(self, obj):
|
def get_total_watchers(self, obj):
|
||||||
# The "valid_notify_policies" attribute can be attached in the get_queryset method of the viewset.
|
assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
|
||||||
qs_valid_notify_policies = getattr(obj, "valid_notify_policies", None)
|
if obj.notify_policies_attr is None:
|
||||||
if qs_valid_notify_policies is not None:
|
return 0
|
||||||
return len(qs_valid_notify_policies)
|
|
||||||
|
|
||||||
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):
|
def get_logo_small_url(self, obj):
|
||||||
return services.get_logo_small_thumbnail_url(obj)
|
return services.get_logo_small_thumbnail_url(obj)
|
||||||
|
@ -325,94 +308,132 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
|
||||||
|
|
||||||
|
|
||||||
class ProjectDetailSerializer(ProjectSerializer):
|
class ProjectDetailSerializer(ProjectSerializer):
|
||||||
us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories
|
us_statuses = Field(attr="userstory_statuses_attr")
|
||||||
points = PointsSerializer(many=True, required=False)
|
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)
|
def to_value(self, instance):
|
||||||
issue_types = IssueTypeSerializer(many=True, required=False)
|
# Name attributes must be translated
|
||||||
priorities = PrioritySerializer(many=True, required=False) # Issues
|
for attr in ["userstory_statuses_attr", "points_attr", "task_statuses_attr",
|
||||||
severities = SeveritySerializer(many=True, required=False)
|
"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",
|
assert hasattr(instance, attr), "instance must have a {} attribute".format(attr)
|
||||||
many=True, required=False)
|
val = getattr(instance, attr)
|
||||||
task_custom_attributes = TaskCustomAttributeSerializer(source="taskcustomattributes",
|
if val is None:
|
||||||
many=True, required=False)
|
continue
|
||||||
issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes",
|
|
||||||
many=True, required=False)
|
|
||||||
|
|
||||||
roles = ProjectRoleSerializer(source="roles", many=True, read_only=True)
|
for elem in val:
|
||||||
members = serializers.SerializerMethodField(method_name="get_members")
|
elem["name"] = _(elem["name"])
|
||||||
total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships")
|
|
||||||
is_out_of_owner_limits = serializers.SerializerMethodField(method_name="get_is_out_of_owner_limits")
|
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):
|
def get_members(self, obj):
|
||||||
qs = obj.memberships.filter(user__isnull=False)
|
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
|
||||||
qs = qs.extra(select={"complete_user_name":"concat(full_name, username)"})
|
if obj.members_attr is None:
|
||||||
qs = qs.order_by("complete_user_name")
|
return []
|
||||||
qs = qs.select_related("role", "user")
|
|
||||||
serializer = ProjectMemberSerializer(qs, many=True)
|
ret = []
|
||||||
return serializer.data
|
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):
|
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):
|
def get_is_out_of_owner_limits(self, obj):
|
||||||
return services.check_if_project_is_out_of_owner_limits(obj)
|
assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
|
||||||
|
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
|
||||||
|
return services.check_if_project_is_out_of_owner_limits(
|
||||||
class ProjectDetailAdminSerializer(ProjectDetailSerializer):
|
obj,
|
||||||
is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info")
|
current_memberships=self.get_total_memberships(obj),
|
||||||
max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships")
|
current_private_projects=obj.private_projects_same_owner_attr,
|
||||||
|
current_public_projects=obj.public_projects_same_owner_attr
|
||||||
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")
|
|
||||||
|
|
||||||
def get_is_private_extra_info(self, obj):
|
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):
|
def get_max_memberships(self, obj):
|
||||||
return services.get_max_memberships_for_project(obj)
|
return services.get_max_memberships_for_project(obj)
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
## Liked
|
# Project Templates
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class LikedSerializer(serializers.ModelSerializer):
|
class ProjectTemplateSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = Field()
|
||||||
model = models.Project
|
name = I18NField()
|
||||||
fields = ['id', 'name', 'slug']
|
slug = Field()
|
||||||
|
description = I18NField()
|
||||||
|
order = Field()
|
||||||
|
created_date = Field()
|
||||||
######################################################
|
modified_date = Field()
|
||||||
## Project Templates
|
default_owner_role = Field()
|
||||||
######################################################
|
is_backlog_activated = Field()
|
||||||
|
is_kanban_activated = Field()
|
||||||
class ProjectTemplateSerializer(serializers.ModelSerializer):
|
is_wiki_activated = Field()
|
||||||
default_options = JsonField(required=False, label=_("Default options"))
|
is_issues_activated = Field()
|
||||||
us_statuses = JsonField(required=False, label=_("User story's statuses"))
|
videoconferences = Field()
|
||||||
points = JsonField(required=False, label=_("Points"))
|
videoconferences_extra_data = Field()
|
||||||
task_statuses = JsonField(required=False, label=_("Task's statuses"))
|
default_options = Field()
|
||||||
issue_statuses = JsonField(required=False, label=_("Issue's statuses"))
|
us_statuses = Field()
|
||||||
issue_types = JsonField(required=False, label=_("Issue's types"))
|
points = Field()
|
||||||
priorities = JsonField(required=False, label=_("Priorities"))
|
task_statuses = Field()
|
||||||
severities = JsonField(required=False, label=_("Severities"))
|
issue_statuses = Field()
|
||||||
roles = JsonField(required=False, label=_("Roles"))
|
issue_types = Field()
|
||||||
|
priorities = Field()
|
||||||
class Meta:
|
severities = Field()
|
||||||
model = models.ProjectTemplate
|
roles = Field()
|
||||||
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()
|
|
||||||
|
|
|
@ -27,30 +27,45 @@ ERROR_MAX_PUBLIC_PROJECTS = 'max_public_projects'
|
||||||
ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects'
|
ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects'
|
||||||
ERROR_PROJECT_WITHOUT_OWNER = 'project_without_owner'
|
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.
|
"""Return if the project privacity can be changed from private to public or viceversa.
|
||||||
|
|
||||||
:param project: A project object.
|
: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}.
|
:return: A dict like this {'can_be_updated': bool, 'reason': error message}.
|
||||||
"""
|
"""
|
||||||
if project.owner is None:
|
if project.owner is None:
|
||||||
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
|
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
|
||||||
|
|
||||||
if project.is_private:
|
if current_memberships is None:
|
||||||
current_memberships = project.memberships.count()
|
current_memberships = project.memberships.count()
|
||||||
|
|
||||||
|
if project.is_private:
|
||||||
max_memberships = project.owner.max_memberships_public_projects
|
max_memberships = project.owner.max_memberships_public_projects
|
||||||
error_memberships_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS
|
error_memberships_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS
|
||||||
|
|
||||||
|
if current_public_projects is None:
|
||||||
current_projects = project.owner.owned_projects.filter(is_private=False).count()
|
current_projects = project.owner.owned_projects.filter(is_private=False).count()
|
||||||
|
else:
|
||||||
|
current_projects = current_public_projects
|
||||||
|
|
||||||
max_projects = project.owner.max_public_projects
|
max_projects = project.owner.max_public_projects
|
||||||
error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS
|
error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS
|
||||||
else:
|
else:
|
||||||
current_memberships = project.memberships.count()
|
|
||||||
max_memberships = project.owner.max_memberships_private_projects
|
max_memberships = project.owner.max_memberships_private_projects
|
||||||
error_memberships_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
|
error_memberships_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
|
||||||
|
|
||||||
|
if current_private_projects is None:
|
||||||
current_projects = project.owner.owned_projects.filter(is_private=True).count()
|
current_projects = project.owner.owned_projects.filter(is_private=True).count()
|
||||||
|
else:
|
||||||
|
current_projects = current_private_projects
|
||||||
|
|
||||||
max_projects = project.owner.max_private_projects
|
max_projects = project.owner.max_private_projects
|
||||||
error_project_exceeded = ERROR_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)
|
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.
|
"""Return if the project fits on its owner limits.
|
||||||
|
|
||||||
:param project: A project object.
|
: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
|
:return: bool
|
||||||
"""
|
"""
|
||||||
if project.owner is None:
|
if project.owner is None:
|
||||||
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
|
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
|
||||||
|
|
||||||
if project.is_private:
|
if current_memberships is None:
|
||||||
current_memberships = project.memberships.count()
|
current_memberships = project.memberships.count()
|
||||||
|
|
||||||
|
if project.is_private:
|
||||||
max_memberships = project.owner.max_memberships_private_projects
|
max_memberships = project.owner.max_memberships_private_projects
|
||||||
|
|
||||||
|
if current_private_projects is None:
|
||||||
current_projects = project.owner.owned_projects.filter(is_private=True).count()
|
current_projects = project.owner.owned_projects.filter(is_private=True).count()
|
||||||
|
else:
|
||||||
|
current_projects = current_private_projects
|
||||||
|
|
||||||
max_projects = project.owner.max_private_projects
|
max_projects = project.owner.max_private_projects
|
||||||
else:
|
else:
|
||||||
current_memberships = project.memberships.count()
|
|
||||||
max_memberships = project.owner.max_memberships_public_projects
|
max_memberships = project.owner.max_memberships_public_projects
|
||||||
|
|
||||||
|
if current_public_projects is None:
|
||||||
current_projects = project.owner.owned_projects.filter(is_private=False).count()
|
current_projects = project.owner.owned_projects.filter(is_private=False).count()
|
||||||
|
else:
|
||||||
|
current_projects = current_public_projects
|
||||||
|
|
||||||
max_projects = project.owner.max_public_projects
|
max_projects = project.owner.max_public_projects
|
||||||
|
|
||||||
if max_memberships is not None and current_memberships > max_memberships:
|
if max_memberships is not None and current_memberships > max_memberships:
|
||||||
|
|
|
@ -21,7 +21,7 @@ from taiga.base.decorators import detail_route
|
||||||
from taiga.base.utils.collections import OrderedSet
|
from taiga.base.utils.collections import OrderedSet
|
||||||
|
|
||||||
from . import services
|
from . import services
|
||||||
from . import serializers
|
from . import validators
|
||||||
|
|
||||||
|
|
||||||
class TagsColorsResourceMixin:
|
class TagsColorsResourceMixin:
|
||||||
|
@ -38,27 +38,26 @@ class TagsColorsResourceMixin:
|
||||||
self.check_permissions(request, "create_tag", project)
|
self.check_permissions(request, "create_tag", project)
|
||||||
self._raise_if_blocked(project)
|
self._raise_if_blocked(project)
|
||||||
|
|
||||||
serializer = serializers.CreateTagSerializer(data=request.DATA, project=project)
|
validator = validators.CreateTagValidator(data=request.DATA, project=project)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
services.create_tag(project, data.get("tag"), data.get("color"))
|
services.create_tag(project, data.get("tag"), data.get("color"))
|
||||||
|
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def edit_tag(self, request, pk=None):
|
def edit_tag(self, request, pk=None):
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
self.check_permissions(request, "edit_tag", project)
|
self.check_permissions(request, "edit_tag", project)
|
||||||
self._raise_if_blocked(project)
|
self._raise_if_blocked(project)
|
||||||
|
|
||||||
serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project)
|
validator = validators.EditTagTagValidator(data=request.DATA, project=project)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
services.edit_tag(project,
|
services.edit_tag(project,
|
||||||
data.get("from_tag"),
|
data.get("from_tag"),
|
||||||
to_tag=data.get("to_tag", None),
|
to_tag=data.get("to_tag", None),
|
||||||
|
@ -66,18 +65,17 @@ class TagsColorsResourceMixin:
|
||||||
|
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
|
||||||
|
|
||||||
@detail_route(methods=["POST"])
|
@detail_route(methods=["POST"])
|
||||||
def delete_tag(self, request, pk=None):
|
def delete_tag(self, request, pk=None):
|
||||||
project = self.get_object()
|
project = self.get_object()
|
||||||
self.check_permissions(request, "delete_tag", project)
|
self.check_permissions(request, "delete_tag", project)
|
||||||
self._raise_if_blocked(project)
|
self._raise_if_blocked(project)
|
||||||
|
|
||||||
serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project)
|
validator = validators.DeleteTagValidator(data=request.DATA, project=project)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
services.delete_tag(project, data.get("tag"))
|
services.delete_tag(project, data.get("tag"))
|
||||||
|
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
@ -88,11 +86,11 @@ class TagsColorsResourceMixin:
|
||||||
self.check_permissions(request, "mix_tags", project)
|
self.check_permissions(request, "mix_tags", project)
|
||||||
self._raise_if_blocked(project)
|
self._raise_if_blocked(project)
|
||||||
|
|
||||||
serializer = serializers.MixTagsSerializer(data=request.DATA, project=project)
|
validator = validators.MixTagsValidator(data=request.DATA, project=project)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
services.mix_tags(project, data.get("from_tags"), data.get("to_tag"))
|
services.mix_tags(project, data.get("from_tags"), data.get("to_tag"))
|
||||||
|
|
||||||
return response.Ok()
|
return response.Ok()
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.forms import widgets
|
from django.forms import widgets
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.exceptions import ValidationError
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.api import validators
|
||||||
|
|
||||||
from . import services
|
from . import services
|
||||||
from . import fields
|
from . import fields
|
||||||
|
@ -26,7 +27,7 @@ from . import fields
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
class ProjectTagSerializer(serializers.Serializer):
|
class ProjectTagValidator(validators.Validator):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# Don't pass the extra project arg
|
# Don't pass the extra project arg
|
||||||
self.project = kwargs.pop("project")
|
self.project = kwargs.pop("project")
|
||||||
|
@ -35,26 +36,26 @@ class ProjectTagSerializer(serializers.Serializer):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class CreateTagSerializer(ProjectTagSerializer):
|
class CreateTagValidator(ProjectTagValidator):
|
||||||
tag = serializers.CharField()
|
tag = serializers.CharField()
|
||||||
color = serializers.CharField(required=False)
|
color = serializers.CharField(required=False)
|
||||||
|
|
||||||
def validate_tag(self, attrs, source):
|
def validate_tag(self, attrs, source):
|
||||||
tag = attrs.get(source, None)
|
tag = attrs.get(source, None)
|
||||||
if services.tag_exist_for_project_elements(self.project, tag):
|
if services.tag_exist_for_project_elements(self.project, tag):
|
||||||
raise serializers.ValidationError(_("The tag exists."))
|
raise validators.ValidationError(_("The tag exists."))
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
def validate_color(self, attrs, source):
|
def validate_color(self, attrs, source):
|
||||||
color = attrs.get(source, None)
|
color = attrs.get(source, None)
|
||||||
if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
|
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
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class EditTagTagSerializer(ProjectTagSerializer):
|
class EditTagTagValidator(ProjectTagValidator):
|
||||||
from_tag = serializers.CharField()
|
from_tag = serializers.CharField()
|
||||||
to_tag = serializers.CharField(required=False)
|
to_tag = serializers.CharField(required=False)
|
||||||
color = serializers.CharField(required=False)
|
color = serializers.CharField(required=False)
|
||||||
|
@ -62,37 +63,37 @@ class EditTagTagSerializer(ProjectTagSerializer):
|
||||||
def validate_from_tag(self, attrs, source):
|
def validate_from_tag(self, attrs, source):
|
||||||
tag = attrs.get(source, None)
|
tag = attrs.get(source, None)
|
||||||
if not services.tag_exist_for_project_elements(self.project, tag):
|
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
|
return attrs
|
||||||
|
|
||||||
def validate_to_tag(self, attrs, source):
|
def validate_to_tag(self, attrs, source):
|
||||||
tag = attrs.get(source, None)
|
tag = attrs.get(source, None)
|
||||||
if services.tag_exist_for_project_elements(self.project, tag):
|
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
|
return attrs
|
||||||
|
|
||||||
def validate_color(self, attrs, source):
|
def validate_color(self, attrs, source):
|
||||||
color = attrs.get(source, None)
|
color = attrs.get(source, None)
|
||||||
if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
|
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
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class DeleteTagSerializer(ProjectTagSerializer):
|
class DeleteTagValidator(ProjectTagValidator):
|
||||||
tag = serializers.CharField()
|
tag = serializers.CharField()
|
||||||
|
|
||||||
def validate_tag(self, attrs, source):
|
def validate_tag(self, attrs, source):
|
||||||
tag = attrs.get(source, None)
|
tag = attrs.get(source, None)
|
||||||
if not services.tag_exist_for_project_elements(self.project, tag):
|
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
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class MixTagsSerializer(ProjectTagSerializer):
|
class MixTagsValidator(ProjectTagValidator):
|
||||||
from_tags = fields.TagsField()
|
from_tags = fields.TagsField()
|
||||||
to_tag = serializers.CharField()
|
to_tag = serializers.CharField()
|
||||||
|
|
||||||
|
@ -100,13 +101,13 @@ class MixTagsSerializer(ProjectTagSerializer):
|
||||||
tags = attrs.get(source, None)
|
tags = attrs.get(source, None)
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if not services.tag_exist_for_project_elements(self.project, tag):
|
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
|
return attrs
|
||||||
|
|
||||||
def validate_to_tag(self, attrs, source):
|
def validate_to_tag(self, attrs, source):
|
||||||
tag = attrs.get(source, None)
|
tag = attrs.get(source, None)
|
||||||
if not services.tag_exist_for_project_elements(self.project, tag):
|
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
|
return attrs
|
|
@ -26,7 +26,6 @@ from taiga.base.decorators import list_route
|
||||||
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
|
||||||
from taiga.base.api.mixins import BlockedByProjectMixin
|
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.history.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.models import Project, TaskStatus
|
from taiga.projects.models import Project, TaskStatus
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
|
||||||
|
@ -38,10 +37,14 @@ from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import services
|
from . import services
|
||||||
|
from . import validators
|
||||||
|
from . import utils as tasks_utils
|
||||||
|
|
||||||
|
|
||||||
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin,
|
||||||
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin,
|
||||||
|
ModelCrudViewSet):
|
||||||
|
validator_class = validators.TaskValidator
|
||||||
queryset = models.Task.objects.all()
|
queryset = models.Task.objects.all()
|
||||||
permission_classes = (permissions.TaskPermission,)
|
permission_classes = (permissions.TaskPermission,)
|
||||||
filter_backends = (filters.CanViewTasksFilterBackend,
|
filter_backends = (filters.CanViewTasksFilterBackend,
|
||||||
|
@ -74,17 +77,15 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
qs = self.attach_votes_attrs_to_queryset(qs)
|
|
||||||
qs = qs.select_related("milestone",
|
qs = qs.select_related("milestone",
|
||||||
"project",
|
"project",
|
||||||
"status",
|
"status",
|
||||||
"owner",
|
"owner",
|
||||||
"assigned_to")
|
"assigned_to")
|
||||||
|
|
||||||
qs = self.attach_watchers_attrs_to_queryset(qs)
|
include_attachments = "include_attachments" in self.request.QUERY_PARAMS
|
||||||
if "include_attachments" in self.request.QUERY_PARAMS:
|
qs = tasks_utils.attach_extra_info(qs, user=self.request.user,
|
||||||
qs = attach_basic_attachments(qs)
|
include_attachments=include_attachments)
|
||||||
qs = qs.extra(select={"include_attachments": "True"})
|
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
@ -162,10 +163,18 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
||||||
|
|
||||||
@list_route(methods=["GET"])
|
@list_route(methods=["GET"])
|
||||||
def by_ref(self, request):
|
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)
|
project_id = request.QUERY_PARAMS.get("project", None)
|
||||||
task = get_object_or_404(models.Task, ref=ref, project_id=project_id)
|
if project_id is not None:
|
||||||
return self.retrieve(request, pk=task.pk)
|
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"])
|
@list_route(methods=["GET"])
|
||||||
def csv(self, request):
|
def csv(self, request):
|
||||||
|
@ -182,9 +191,9 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_create(self, request, **kwargs):
|
def bulk_create(self, request, **kwargs):
|
||||||
serializer = serializers.TasksBulkSerializer(data=request.DATA)
|
validator = validators.TasksBulkValidator(data=request.DATA)
|
||||||
if serializer.is_valid():
|
if validator.is_valid():
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
project = Project.objects.get(id=data["project_id"])
|
project = Project.objects.get(id=data["project_id"])
|
||||||
self.check_permissions(request, 'bulk_create', project)
|
self.check_permissions(request, 'bulk_create', project)
|
||||||
if project.blocked_code is not None:
|
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"],
|
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,
|
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)
|
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)
|
tasks_serialized = self.get_serializer_class()(tasks, many=True)
|
||||||
|
|
||||||
return response.Ok(tasks_serialized.data)
|
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):
|
def _bulk_update_order(self, order_field, request, **kwargs):
|
||||||
serializer = serializers.UpdateTasksOrderBulkSerializer(data=request.DATA)
|
validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
project = get_object_or_404(Project, pk=data["project_id"])
|
project = get_object_or_404(Project, pk=data["project_id"])
|
||||||
|
|
||||||
self.check_permissions(request, "bulk_update_order", project)
|
self.check_permissions(request, "bulk_update_order", project)
|
||||||
|
|
|
@ -16,101 +16,44 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
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 import serializers
|
||||||
from taiga.base.fields import PgArrayField
|
from taiga.base.fields import Field, MethodField
|
||||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin
|
from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
|
||||||
from taiga.projects.milestones.validators import SprintExistsValidator
|
from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
|
||||||
from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
|
from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
|
||||||
from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
|
from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
|
||||||
from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
|
from taiga.projects.notifications.mixins import WatchedResourceSerializer
|
||||||
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.votes.mixins.serializers import VoteResourceSerializerMixin
|
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,
|
class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
|
||||||
serializers.ModelSerializer):
|
OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
|
||||||
|
StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
|
||||||
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,
|
|
||||||
serializers.LightSerializer):
|
serializers.LightSerializer):
|
||||||
|
|
||||||
id = serpy.Field()
|
id = Field()
|
||||||
user_story = serpy.Field(attr="user_story_id")
|
user_story = Field(attr="user_story_id")
|
||||||
ref = serpy.Field()
|
ref = Field()
|
||||||
project = serpy.Field(attr="project_id")
|
project = Field(attr="project_id")
|
||||||
milestone = serpy.Field(attr="milestone_id")
|
milestone = Field(attr="milestone_id")
|
||||||
milestone_slug = serpy.MethodField("get_milestone_slug")
|
milestone_slug = MethodField()
|
||||||
created_date = serpy.Field()
|
created_date = Field()
|
||||||
modified_date = serpy.Field()
|
modified_date = Field()
|
||||||
finished_date = serpy.Field()
|
finished_date = Field()
|
||||||
subject = serpy.Field()
|
subject = Field()
|
||||||
us_order = serpy.Field()
|
us_order = Field()
|
||||||
taskboard_order = serpy.Field()
|
taskboard_order = Field()
|
||||||
is_iocaine = serpy.Field()
|
is_iocaine = Field()
|
||||||
external_reference = serpy.Field()
|
external_reference = Field()
|
||||||
version = serpy.Field()
|
version = Field()
|
||||||
watchers = serpy.Field()
|
watchers = Field()
|
||||||
is_blocked = serpy.Field()
|
is_blocked = Field()
|
||||||
blocked_note = serpy.Field()
|
blocked_note = Field()
|
||||||
tags = serpy.Field()
|
tags = Field()
|
||||||
is_closed = serpy.MethodField()
|
is_closed = MethodField()
|
||||||
|
|
||||||
def get_milestone_slug(self, obj):
|
def get_milestone_slug(self, obj):
|
||||||
return obj.milestone.slug if obj.milestone else None
|
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
|
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):
|
class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
|
||||||
def serialize_neighbor(self, neighbor):
|
pass
|
||||||
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)
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.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
|
|
@ -19,7 +19,14 @@
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,5 +35,35 @@ class TaskExistsValidator:
|
||||||
value = attrs[source]
|
value = attrs[source]
|
||||||
if not models.Task.objects.filter(pk=value).exists():
|
if not models.Task.objects.filter(pk=value).exists():
|
||||||
msg = _("There's no task with that id")
|
msg = _("There's no task with that id")
|
||||||
raise serializers.ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
return attrs
|
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)
|
||||||
|
|
|
@ -16,12 +16,8 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from contextlib import closing
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db import transaction, connection
|
from django.db import transaction
|
||||||
from django.db.models.sql import datastructures
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.http import HttpResponse
|
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 import ModelListViewSet
|
||||||
from taiga.base.api.utils import get_object_or_404
|
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.mixins import HistoryResourceMixin
|
||||||
from taiga.projects.history.services import take_snapshot
|
from taiga.projects.history.services import take_snapshot
|
||||||
from taiga.projects.milestones.models import Milestone
|
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.notifications.mixins import WatchersViewSetMixin
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
from taiga.projects.occ import OCCResourceMixin
|
||||||
from taiga.projects.tagging.api import TaggedResourceMixin
|
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 VotedResourceMixin
|
||||||
from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
|
from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
|
||||||
from taiga.projects.userstories.utils import attach_total_points
|
from taiga.projects.userstories.utils import attach_extra_info
|
||||||
from taiga.projects.userstories.utils import attach_role_points
|
|
||||||
from taiga.projects.userstories.utils import attach_tasks
|
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
from . import services
|
from . import services
|
||||||
|
from . import validators
|
||||||
|
|
||||||
|
|
||||||
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
|
validator_class = validators.UserStoryValidator
|
||||||
queryset = models.UserStory.objects.all()
|
queryset = models.UserStory.objects.all()
|
||||||
permission_classes = (permissions.UserStoryPermission,)
|
permission_classes = (permissions.UserStoryPermission,)
|
||||||
filter_backends = (filters.CanViewUsFilterBackend,
|
filter_backends = (filters.CanViewUsFilterBackend,
|
||||||
|
@ -105,18 +99,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
"assigned_to",
|
"assigned_to",
|
||||||
"generated_from_issue")
|
"generated_from_issue")
|
||||||
|
|
||||||
qs = self.attach_votes_attrs_to_queryset(qs)
|
include_attachments = "include_attachments" in self.request.QUERY_PARAMS
|
||||||
qs = self.attach_watchers_attrs_to_queryset(qs)
|
include_tasks = "include_tasks" in self.request.QUERY_PARAMS
|
||||||
qs = attach_total_points(qs)
|
qs = attach_extra_info(qs, user=self.request.user,
|
||||||
qs = attach_role_points(qs)
|
include_attachments=include_attachments,
|
||||||
|
include_tasks=include_tasks)
|
||||||
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"})
|
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
@ -237,10 +224,18 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
|
|
||||||
@list_route(methods=["GET"])
|
@list_route(methods=["GET"])
|
||||||
def by_ref(self, request):
|
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)
|
project_id = request.QUERY_PARAMS.get("project", None)
|
||||||
userstory = get_object_or_404(models.UserStory, ref=ref, project_id=project_id)
|
if project_id is not None:
|
||||||
return self.retrieve(request, pk=userstory.pk)
|
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"])
|
@list_route(methods=["GET"])
|
||||||
def csv(self, request):
|
def csv(self, request):
|
||||||
|
@ -257,9 +252,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_create(self, request, **kwargs):
|
def bulk_create(self, request, **kwargs):
|
||||||
serializer = serializers.UserStoriesBulkSerializer(data=request.DATA)
|
validator = validators.UserStoriesBulkValidator(data=request.DATA)
|
||||||
if serializer.is_valid():
|
if validator.is_valid():
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
project = Project.objects.get(id=data["project_id"])
|
project = Project.objects.get(id=data["project_id"])
|
||||||
self.check_permissions(request, 'bulk_create', project)
|
self.check_permissions(request, 'bulk_create', project)
|
||||||
if project.blocked_code is not None:
|
if project.blocked_code is not None:
|
||||||
|
@ -269,17 +264,20 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
data["bulk_stories"], project=project, owner=request.user,
|
data["bulk_stories"], project=project, owner=request.user,
|
||||||
status_id=data.get("status_id") or project.default_us_status_id,
|
status_id=data.get("status_id") or project.default_us_status_id,
|
||||||
callback=self.post_save, precall=self.pre_save)
|
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)
|
user_stories_serialized = self.get_serializer_class()(user_stories, many=True)
|
||||||
|
|
||||||
return response.Ok(user_stories_serialized.data)
|
return response.Ok(user_stories_serialized.data)
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
@list_route(methods=["POST"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_update_milestone(self, request, **kwargs):
|
def bulk_update_milestone(self, request, **kwargs):
|
||||||
serializer = serializers.UpdateMilestoneBulkSerializer(data=request.DATA)
|
validator = validators.UpdateMilestoneBulkValidator(data=request.DATA)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
project = get_object_or_404(Project, pk=data["project_id"])
|
project = get_object_or_404(Project, pk=data["project_id"])
|
||||||
milestone = get_object_or_404(Milestone, pk=data["milestone_id"])
|
milestone = get_object_or_404(Milestone, pk=data["milestone_id"])
|
||||||
|
|
||||||
|
@ -291,11 +289,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
return response.NoContent()
|
return response.NoContent()
|
||||||
|
|
||||||
def _bulk_update_order(self, order_field, request, **kwargs):
|
def _bulk_update_order(self, order_field, request, **kwargs):
|
||||||
serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA)
|
validator = validators.UpdateUserStoriesOrderBulkValidator(data=request.DATA)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
return response.BadRequest(serializer.errors)
|
return response.BadRequest(validator.errors)
|
||||||
|
|
||||||
data = serializer.data
|
data = validator.data
|
||||||
project = get_object_or_404(Project, pk=data["project_id"])
|
project = get_object_or_404(Project, pk=data["project_id"])
|
||||||
|
|
||||||
self.check_permissions(request, "bulk_update_order", project)
|
self.check_permissions(request, "bulk_update_order", project)
|
||||||
|
|
|
@ -16,96 +16,111 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
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 import serializers
|
||||||
from taiga.base.api.utils import get_object_or_404
|
from taiga.base.fields import Field, MethodField
|
||||||
from taiga.base.fields import PickledObjectField
|
|
||||||
from taiga.base.fields import PgArrayField
|
|
||||||
from taiga.base.neighbors import NeighborsSerializerMixin
|
from taiga.base.neighbors import NeighborsSerializerMixin
|
||||||
from taiga.base.utils import json
|
|
||||||
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
from taiga.mdrender.service import render as mdrender
|
||||||
from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin
|
from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
|
||||||
from taiga.projects.milestones.validators import SprintExistsValidator
|
from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
|
||||||
from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin
|
from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
|
||||||
from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin
|
from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
|
||||||
from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin
|
from taiga.projects.notifications.mixins import WatchedResourceSerializer
|
||||||
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.votes.mixins.serializers import VoteResourceSerializerMixin
|
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):
|
class OriginIssueSerializer(serializers.LightSerializer):
|
||||||
def to_native(self, obj):
|
id = Field()
|
||||||
return {str(o.role.id): o.points.id for o in obj.all()}
|
ref = Field()
|
||||||
|
subject = Field()
|
||||||
|
|
||||||
def from_native(self, obj):
|
def to_value(self, instance):
|
||||||
if isinstance(obj, dict):
|
if instance is None:
|
||||||
return obj
|
return None
|
||||||
return json.loads(obj)
|
|
||||||
|
return super().to_value(instance)
|
||||||
|
|
||||||
|
|
||||||
class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin,
|
class UserStoryListSerializer(
|
||||||
EditableWatchedResourceModelSerializer, serializers.ModelSerializer):
|
VoteResourceSerializerMixin, WatchedResourceSerializer,
|
||||||
tags = TagsAndTagsColorsField(default=[], required=False)
|
OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
|
||||||
external_reference = PgArrayField(required=False)
|
StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
|
||||||
points = RolePointsField(source="role_points", required=False)
|
serializers.LightSerializer):
|
||||||
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 Meta:
|
id = Field()
|
||||||
model = models.UserStory
|
ref = Field()
|
||||||
depth = 0
|
milestone = Field(attr="milestone_id")
|
||||||
read_only_fields = ('created_date', 'modified_date', 'owner')
|
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):
|
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):
|
def get_comment(self, obj):
|
||||||
# NOTE: This method and field is necessary to historical comments work
|
# NOTE: This method and field is necessary to historical comments work
|
||||||
return ""
|
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):
|
def get_origin_issue(self, obj):
|
||||||
if obj.generated_from_issue:
|
if obj.generated_from_issue:
|
||||||
return {
|
return {
|
||||||
|
@ -122,142 +137,5 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin,
|
||||||
return mdrender(obj.project, obj.description)
|
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):
|
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
|
||||||
def serialize_neighbor(self, neighbor):
|
pass
|
||||||
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
|
|
||||||
|
|
|
@ -17,6 +17,13 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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"):
|
def attach_total_points(queryset, as_field="total_points_attr"):
|
||||||
"""Attach total of point values to each object of the queryset.
|
"""Attach total of point values to each object of the queryset.
|
||||||
|
@ -46,10 +53,15 @@ def attach_role_points(queryset, as_field="role_points_attr"):
|
||||||
:return: Queryset object with the additional `as_field` field.
|
:return: Queryset object with the additional `as_field` field.
|
||||||
"""
|
"""
|
||||||
model = queryset.model
|
model = queryset.model
|
||||||
sql = """SELECT json_agg((userstories_rolepoints.role_id, userstories_rolepoints.points_id))
|
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
|
FROM userstories_rolepoints
|
||||||
WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
|
WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
|
||||||
|
|
||||||
sql = sql.format(tbl=model._meta.db_table)
|
sql = sql.format(tbl=model._meta.db_table)
|
||||||
queryset = queryset.extra(select={as_field: sql})
|
queryset = queryset.extra(select={as_field: sql})
|
||||||
return queryset
|
return queryset
|
||||||
|
@ -82,3 +94,23 @@ def attach_tasks(queryset, as_field="tasks_attr"):
|
||||||
sql = sql.format(tbl=model._meta.db_table)
|
sql = sql.format(tbl=model._meta.db_table)
|
||||||
queryset = queryset.extra(select={as_field: sql})
|
queryset = queryset.extra(select={as_field: sql})
|
||||||
return queryset
|
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
|
||||||
|
|
|
@ -19,14 +19,96 @@
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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
|
from . import models
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class UserStoryExistsValidator:
|
class UserStoryExistsValidator:
|
||||||
def validate_us_id(self, attrs, source):
|
def validate_us_id(self, attrs, source):
|
||||||
value = attrs[source]
|
value = attrs[source]
|
||||||
if not models.UserStory.objects.filter(pk=value).exists():
|
if not models.UserStory.objects.filter(pk=value).exists():
|
||||||
msg = _("There's no user story with that id")
|
msg = _("There's no user story with that id")
|
||||||
raise serializers.ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
return attrs
|
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
|
||||||
|
|
|
@ -0,0 +1,436 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
|
@ -16,11 +16,43 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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 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:
|
class ProjectExistsValidator:
|
||||||
|
@ -28,7 +60,7 @@ class ProjectExistsValidator:
|
||||||
value = attrs[source]
|
value = attrs[source]
|
||||||
if not models.Project.objects.filter(pk=value).exists():
|
if not models.Project.objects.filter(pk=value).exists():
|
||||||
msg = _("There's no project with that id")
|
msg = _("There's no project with that id")
|
||||||
raise serializers.ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,7 +69,7 @@ class UserStoryStatusExistsValidator:
|
||||||
value = attrs[source]
|
value = attrs[source]
|
||||||
if not models.UserStoryStatus.objects.filter(pk=value).exists():
|
if not models.UserStoryStatus.objects.filter(pk=value).exists():
|
||||||
msg = _("There's no user story status with that id")
|
msg = _("There's no user story status with that id")
|
||||||
raise serializers.ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,5 +78,172 @@ class TaskStatusExistsValidator:
|
||||||
value = attrs[source]
|
value = attrs[source]
|
||||||
if not models.TaskStatus.objects.filter(pk=value).exists():
|
if not models.TaskStatus.objects.filter(pk=value).exists():
|
||||||
msg = _("There's no task status with that id")
|
msg = _("There's no task status with that id")
|
||||||
raise serializers.ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
return attrs
|
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()
|
||||||
|
|
|
@ -16,12 +16,14 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import serpy
|
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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):
|
def get_is_voter(self, obj):
|
||||||
# The "is_voted" attribute is attached in the get_queryset of the viewset.
|
# The "is_voted" attribute is attached in the get_queryset of the viewset.
|
||||||
return getattr(obj, "is_voter", False) or False
|
return getattr(obj, "is_voter", False) or False
|
||||||
|
@ -29,13 +31,3 @@ class BaseVoteResourceSerializerMixin(object):
|
||||||
def get_total_voters(self, obj):
|
def get_total_voters(self, obj):
|
||||||
# The "total_voters" attribute is attached in the get_queryset of the viewset.
|
# The "total_voters" attribute is attached in the get_queryset of the viewset.
|
||||||
return getattr(obj, "total_voters", 0) or 0
|
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")
|
|
||||||
|
|
|
@ -39,14 +39,6 @@ class VotedResourceMixin:
|
||||||
def pre_conditions_on_save(self, obj)
|
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"])
|
@detail_route(methods=["POST"])
|
||||||
def upvote(self, request, pk=None):
|
def upvote(self, request, pk=None):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
|
|
@ -17,14 +17,14 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
|
from taiga.base.fields import Field, MethodField
|
||||||
|
|
||||||
|
|
||||||
class VoterSerializer(serializers.ModelSerializer):
|
class VoterSerializer(serializers.LightSerializer):
|
||||||
full_name = serializers.CharField(source='get_full_name', required=False)
|
id = Field()
|
||||||
|
username = Field()
|
||||||
|
full_name = MethodField()
|
||||||
|
|
||||||
class Meta:
|
def get_full_name(self, obj):
|
||||||
model = get_user_model()
|
return obj.get_full_name()
|
||||||
fields = ('id', 'username', 'full_name')
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ def attach_total_voters_to_queryset(queryset, as_field="total_voters"):
|
||||||
return qs
|
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.
|
"""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
|
Because of laziness of vote objects creation, this makes much simpler and more efficient to
|
||||||
|
@ -57,14 +57,17 @@ 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
|
(The other way was to do it in the serializer with some try/except blocks and additional
|
||||||
queries)
|
queries)
|
||||||
|
|
||||||
:param user: A users.User object model
|
|
||||||
:param queryset: A Django queryset object.
|
: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.
|
:param as_field: Attach the boolean as an attribute with this name.
|
||||||
|
|
||||||
:return: Queryset object with the additional `as_field` field.
|
:return: Queryset object with the additional `as_field` field.
|
||||||
"""
|
"""
|
||||||
model = queryset.model
|
model = queryset.model
|
||||||
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
|
||||||
|
if user is None or user.is_anonymous():
|
||||||
|
sql = """SELECT false"""
|
||||||
|
else:
|
||||||
sql = ("""SELECT CASE WHEN (SELECT count(*)
|
sql = ("""SELECT CASE WHEN (SELECT count(*)
|
||||||
FROM votes_vote
|
FROM votes_vote
|
||||||
WHERE votes_vote.content_type_id = {type_id}
|
WHERE votes_vote.content_type_id = {type_id}
|
||||||
|
@ -74,5 +77,6 @@ def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"):
|
||||||
ELSE FALSE
|
ELSE FALSE
|
||||||
END""")
|
END""")
|
||||||
sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
|
sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
|
||||||
|
|
||||||
qs = queryset.extra(select={as_field: sql})
|
qs = queryset.extra(select={as_field: sql})
|
||||||
return qs
|
return qs
|
||||||
|
|
|
@ -24,7 +24,6 @@ from taiga.base import response
|
||||||
from taiga.base.api import ModelCrudViewSet
|
from taiga.base.api import ModelCrudViewSet
|
||||||
from taiga.base.api import ModelListViewSet
|
from taiga.base.api import ModelListViewSet
|
||||||
from taiga.base.api.mixins import BlockedByProjectMixin
|
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.api.utils import get_object_or_404
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base.decorators import list_route
|
||||||
|
|
||||||
|
@ -42,6 +41,8 @@ from taiga.projects.occ import OCCResourceMixin
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from . import validators
|
||||||
|
from . import utils as wiki_utils
|
||||||
|
|
||||||
|
|
||||||
class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
|
@ -49,6 +50,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
|
|
||||||
model = models.WikiPage
|
model = models.WikiPage
|
||||||
serializer_class = serializers.WikiPageSerializer
|
serializer_class = serializers.WikiPageSerializer
|
||||||
|
validator_class = validators.WikiPageValidator
|
||||||
permission_classes = (permissions.WikiPagePermission,)
|
permission_classes = (permissions.WikiPagePermission,)
|
||||||
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
|
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
|
||||||
filter_fields = ("project", "slug")
|
filter_fields = ("project", "slug")
|
||||||
|
@ -56,7 +58,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
qs = super().get_queryset()
|
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
|
return qs
|
||||||
|
|
||||||
@list_route(methods=["GET"])
|
@list_route(methods=["GET"])
|
||||||
|
@ -100,6 +102,7 @@ class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
|
||||||
class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.WikiLink
|
model = models.WikiLink
|
||||||
serializer_class = serializers.WikiLinkSerializer
|
serializer_class = serializers.WikiLinkSerializer
|
||||||
|
validator_class = validators.WikiLinkValidator
|
||||||
permission_classes = (permissions.WikiLinkPermission,)
|
permission_classes = (permissions.WikiLinkPermission,)
|
||||||
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
|
filter_backends = (filters.CanViewWikiPagesFilterBackend,)
|
||||||
filter_fields = ["project"]
|
filter_fields = ["project"]
|
||||||
|
|
|
@ -17,21 +17,26 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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.history import services as history_service
|
||||||
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer
|
from taiga.projects.notifications.mixins import WatchedResourceSerializer
|
||||||
from taiga.projects.notifications.validators import WatchersValidator
|
|
||||||
from taiga.mdrender.service import render as mdrender
|
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 = MethodField()
|
||||||
html = serializers.SerializerMethodField("get_html")
|
editions = MethodField()
|
||||||
editions = serializers.SerializerMethodField("get_editions")
|
|
||||||
|
|
||||||
class Meta:
|
version = Field()
|
||||||
model = models.WikiPage
|
|
||||||
read_only_fields = ('modified_date', 'created_date', 'owner')
|
|
||||||
|
|
||||||
def get_html(self, obj):
|
def get_html(self, obj):
|
||||||
return mdrender(obj.project, obj.content)
|
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
|
return history_service.get_history_queryset_by_model_instance(obj).count() + 1 # +1 for creation
|
||||||
|
|
||||||
|
|
||||||
class WikiLinkSerializer(serializers.ModelSerializer):
|
class WikiLinkSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = Field()
|
||||||
model = models.WikiLink
|
project = Field(attr="project_id")
|
||||||
read_only_fields = ('href',)
|
title = Field()
|
||||||
|
href = Field()
|
||||||
|
order = Field()
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.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
|
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base.api 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',)
|
|
@ -16,37 +16,48 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.projects.issues.serializers import IssueSerializer
|
from taiga.base.api import serializers
|
||||||
from taiga.projects.userstories.serializers import UserStorySerializer
|
from taiga.base.fields import Field, MethodField
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class IssueSearchResultsSerializer(IssueSerializer):
|
class IssueSearchResultsSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = Field()
|
||||||
model = Issue
|
ref = Field()
|
||||||
fields = ('id', 'ref', 'subject', 'status', 'assigned_to')
|
subject = Field()
|
||||||
|
status = Field(attr="status_id")
|
||||||
|
assigned_to = Field(attr="assigned_to_id")
|
||||||
|
|
||||||
|
|
||||||
class TaskSearchResultsSerializer(TaskSerializer):
|
class TaskSearchResultsSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = Field()
|
||||||
model = Task
|
ref = Field()
|
||||||
fields = ('id', 'ref', 'subject', 'status', 'assigned_to')
|
subject = Field()
|
||||||
|
status = Field(attr="status_id")
|
||||||
|
assigned_to = Field(attr="assigned_to_id")
|
||||||
|
|
||||||
|
|
||||||
class UserStorySearchResultsSerializer(UserStorySerializer):
|
class UserStorySearchResultsSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = Field()
|
||||||
model = UserStory
|
ref = Field()
|
||||||
fields = ('id', 'ref', 'subject', 'status', 'total_points',
|
subject = Field()
|
||||||
'milestone_name', 'milestone_slug')
|
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 WikiPageSearchResultsSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = Field()
|
||||||
model = WikiPage
|
slug = Field()
|
||||||
fields = ('id', 'slug')
|
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from taiga.base.utils.db import to_tsquery
|
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)
|
MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150)
|
||||||
|
|
||||||
|
@ -30,11 +31,13 @@ def search_user_stories(project, text):
|
||||||
"coalesce(userstories_userstory.description, '')) "
|
"coalesce(userstories_userstory.description, '')) "
|
||||||
"@@ to_tsquery('english_nostop', %s)")
|
"@@ to_tsquery('english_nostop', %s)")
|
||||||
|
|
||||||
if text:
|
queryset = model_cls.objects.filter(project_id=project.pk)
|
||||||
return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)])
|
|
||||||
.filter(project_id=project.pk)[:MAX_RESULTS])
|
|
||||||
|
|
||||||
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):
|
def search_tasks(project, text):
|
||||||
|
|
|
@ -18,10 +18,8 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.apps import apps
|
|
||||||
|
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
from taiga.base.api.utils import get_object_or_404
|
|
||||||
from taiga.base.api import ReadOnlyListViewSet
|
from taiga.base.api import ReadOnlyListViewSet
|
||||||
|
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
@ -36,7 +34,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
|
||||||
|
|
||||||
def get_content_type(self):
|
def get_content_type(self):
|
||||||
app_name, model = self.content_type.split(".", 1)
|
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):
|
def get_queryset(self):
|
||||||
ct = self.get_content_type()
|
ct = self.get_content_type()
|
||||||
|
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -20,13 +20,12 @@ from django.db import models
|
||||||
from django_pgjson.fields import JsonField
|
from django_pgjson.fields import JsonField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
|
||||||
from taiga.projects.models import Project
|
from taiga.projects.models import Project
|
||||||
|
|
||||||
|
|
||||||
class Timeline(models.Model):
|
class Timeline(models.Model):
|
||||||
content_type = models.ForeignKey(ContentType, related_name="content_type_timelines")
|
content_type = models.ForeignKey(ContentType, related_name="content_type_timelines")
|
||||||
object_id = models.PositiveIntegerField()
|
object_id = models.PositiveIntegerField()
|
||||||
|
@ -36,12 +35,11 @@ class Timeline(models.Model):
|
||||||
project = models.ForeignKey(Project, null=True)
|
project = models.ForeignKey(Project, null=True)
|
||||||
data = JsonField()
|
data = JsonField()
|
||||||
data_content_type = models.ForeignKey(ContentType, related_name="data_timelines")
|
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:
|
class Meta:
|
||||||
index_together = [('content_type', 'object_id', 'namespace'), ]
|
index_together = [('content_type', 'object_id', 'namespace'), ]
|
||||||
|
|
||||||
|
|
||||||
# Register all implementations
|
# Register all implementations
|
||||||
from .timeline_implementations import *
|
from .timeline_implementations import *
|
||||||
|
|
||||||
|
|
|
@ -16,20 +16,26 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.forms import widgets
|
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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 taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import service
|
|
||||||
|
|
||||||
|
|
||||||
class TimelineSerializer(serializers.ModelSerializer):
|
class TimelineSerializer(serializers.LightSerializer):
|
||||||
data = serializers.SerializerMethodField("get_data")
|
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:
|
class Meta:
|
||||||
model = models.Timeline
|
model = models.Timeline
|
||||||
|
|
|
@ -27,7 +27,6 @@ from functools import partial, wraps
|
||||||
|
|
||||||
from taiga.base.utils.db import get_typename_for_model_class
|
from taiga.base.utils.db import get_typename_for_model_class
|
||||||
from taiga.celery import app
|
from taiga.celery import app
|
||||||
from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
|
|
||||||
|
|
||||||
_timeline_impl_map = {}
|
_timeline_impl_map = {}
|
||||||
|
|
||||||
|
@ -111,7 +110,7 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id
|
||||||
except projectModel.DoesNotExist:
|
except projectModel.DoesNotExist:
|
||||||
return
|
return
|
||||||
|
|
||||||
## Project timeline
|
# Project timeline
|
||||||
_push_to_timeline(project, obj, event_type, created_datetime,
|
_push_to_timeline(project, obj, event_type, created_datetime,
|
||||||
namespace=build_project_namespace(project),
|
namespace=build_project_namespace(project),
|
||||||
extra_data=extra_data)
|
extra_data=extra_data)
|
||||||
|
@ -126,7 +125,7 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id
|
||||||
extra_data=extra_data)
|
extra_data=extra_data)
|
||||||
else:
|
else:
|
||||||
# Actions not related with a project
|
# Actions not related with a project
|
||||||
## - Me
|
# - Me
|
||||||
_push_to_timeline(user, obj, event_type, created_datetime,
|
_push_to_timeline(user, obj, event_type, created_datetime,
|
||||||
namespace=build_user_namespace(user),
|
namespace=build_user_namespace(user),
|
||||||
extra_data=extra_data)
|
extra_data=extra_data)
|
||||||
|
@ -141,7 +140,6 @@ def get_timeline(obj, namespace=None):
|
||||||
if namespace is not None:
|
if namespace is not None:
|
||||||
timeline = timeline.filter(namespace=namespace)
|
timeline = timeline.filter(namespace=namespace)
|
||||||
|
|
||||||
timeline = timeline.select_related("project")
|
|
||||||
timeline = timeline.order_by("-created", "-id")
|
timeline = timeline.order_by("-created", "-id")
|
||||||
return timeline
|
return timeline
|
||||||
|
|
||||||
|
@ -156,13 +154,13 @@ def filter_timeline_for_user(timeline, user):
|
||||||
|
|
||||||
# Filtering private project with some public parts
|
# Filtering private project with some public parts
|
||||||
content_types = {
|
content_types = {
|
||||||
"view_project": ContentType.objects.get(app_label="projects", model="project"),
|
"view_project": ContentType.objects.get_by_natural_key("projects", "project"),
|
||||||
"view_milestones": ContentType.objects.get(app_label="milestones", model="milestone"),
|
"view_milestones": ContentType.objects.get_by_natural_key("milestones", "milestone"),
|
||||||
"view_us": ContentType.objects.get(app_label="userstories", model="userstory"),
|
"view_us": ContentType.objects.get_by_natural_key("userstories", "userstory"),
|
||||||
"view_tasks": ContentType.objects.get(app_label="tasks", model="task"),
|
"view_tasks": ContentType.objects.get_by_natural_key("tasks", "task"),
|
||||||
"view_issues": ContentType.objects.get(app_label="issues", model="issue"),
|
"view_issues": ContentType.objects.get_by_natural_key("issues", "issue"),
|
||||||
"view_wiki_pages": ContentType.objects.get(app_label="wiki", model="wikipage"),
|
"view_wiki_pages": ContentType.objects.get_by_natural_key("wiki", "wikipage"),
|
||||||
"view_wiki_links": ContentType.objects.get(app_label="wiki", model="wikilink"),
|
"view_wiki_links": ContentType.objects.get_by_natural_key("wiki", "wikilink"),
|
||||||
}
|
}
|
||||||
|
|
||||||
for content_type_key, content_type in content_types.items():
|
for content_type_key, content_type in content_types.items():
|
||||||
|
@ -171,7 +169,7 @@ def filter_timeline_for_user(timeline, user):
|
||||||
data_content_type=content_type)
|
data_content_type=content_type)
|
||||||
|
|
||||||
# There is no specific permission for seeing new memberships
|
# 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,
|
tl_filter |= Q(project__is_private=True,
|
||||||
project__anon_permissions__contains=["view_project"],
|
project__anon_permissions__contains=["view_project"],
|
||||||
data_content_type=membership_content_type)
|
data_content_type=membership_content_type)
|
||||||
|
@ -231,7 +229,6 @@ def register_timeline_implementation(typename:str, event_type:str, fn=None):
|
||||||
return _wrapper
|
return _wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def extract_project_info(instance):
|
def extract_project_info(instance):
|
||||||
return {
|
return {
|
||||||
"id": instance.pk,
|
"id": instance.pk,
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db.models import Q, F
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
from django.core.exceptions import ValidationError
|
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 exceptions as exc
|
||||||
from taiga.base import filters
|
from taiga.base import filters
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
|
from taiga.base.utils.dicts import into_namedtuple
|
||||||
from taiga.auth.tokens import get_user_for_token
|
from taiga.auth.tokens import get_user_for_token
|
||||||
from taiga.base.decorators import list_route
|
from taiga.base.decorators import list_route
|
||||||
from taiga.base.decorators import detail_route
|
from taiga.base.decorators import detail_route
|
||||||
from taiga.base.api import ModelCrudViewSet
|
from taiga.base.api import ModelCrudViewSet
|
||||||
from taiga.base.api.mixins import BlockedByProjectMixin
|
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.api.utils import get_object_or_404
|
||||||
from taiga.base.filters import MembersFilterBackend
|
from taiga.base.filters import MembersFilterBackend
|
||||||
from taiga.base.mails import mail_builder
|
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 taiga.users.services import get_user_by_username_or_email
|
||||||
from easy_thumbnails.source_generators import pil_image
|
from easy_thumbnails.source_generators import pil_image
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from . import validators
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import filters as user_filters
|
from . import filters as user_filters
|
||||||
from . import services
|
from . import services
|
||||||
|
@ -53,6 +52,8 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
permission_classes = (permissions.UserPermission,)
|
permission_classes = (permissions.UserPermission,)
|
||||||
admin_serializer_class = serializers.UserAdminSerializer
|
admin_serializer_class = serializers.UserAdminSerializer
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
|
admin_validator_class = validators.UserAdminValidator
|
||||||
|
validator_class = validators.UserValidator
|
||||||
queryset = models.User.objects.all().prefetch_related("memberships")
|
queryset = models.User.objects.all().prefetch_related("memberships")
|
||||||
filter_backends = (MembersFilterBackend,)
|
filter_backends = (MembersFilterBackend,)
|
||||||
|
|
||||||
|
@ -64,6 +65,14 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
|
|
||||||
return self.serializer_class
|
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):
|
def create(self, *args, **kwargs):
|
||||||
raise exc.NotSupported()
|
raise exc.NotSupported()
|
||||||
|
|
||||||
|
@ -96,8 +105,6 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
self.check_permissions(request, "update", user)
|
self.check_permissions(request, "update", user)
|
||||||
|
|
||||||
ret = super().partial_update(request, *args, **kwargs)
|
|
||||||
|
|
||||||
new_email = request.DATA.get('email', None)
|
new_email = request.DATA.get('email', None)
|
||||||
if new_email is not None:
|
if new_email is not None:
|
||||||
valid_new_email = True
|
valid_new_email = True
|
||||||
|
@ -119,10 +126,17 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
request.user.email_token = str(uuid.uuid1())
|
request.user.email_token = str(uuid.uuid1())
|
||||||
request.user.new_email = new_email
|
request.user.new_email = new_email
|
||||||
request.user.save(update_fields=["email_token", "new_email"])
|
request.user.save(update_fields=["email_token", "new_email"])
|
||||||
email = mail_builder.change_email(request.user.new_email, {"user": request.user,
|
email = mail_builder.change_email(
|
||||||
"lang": request.user.lang})
|
request.user.new_email,
|
||||||
|
{
|
||||||
|
"user": request.user,
|
||||||
|
"lang": request.user.lang
|
||||||
|
}
|
||||||
|
)
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
|
ret = super().partial_update(request, *args, **kwargs)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def destroy(self, request, pk=None):
|
def destroy(self, request, pk=None):
|
||||||
|
@ -165,16 +179,16 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
|
|
||||||
self.check_permissions(request, "change_password_from_recovery", None)
|
self.check_permissions(request, "change_password_from_recovery", None)
|
||||||
|
|
||||||
serializer = serializers.RecoverySerializer(data=request.DATA, many=False)
|
validator = validators.RecoveryValidator(data=request.DATA, many=False)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
raise exc.WrongArguments(_("Token is invalid"))
|
raise exc.WrongArguments(_("Token is invalid"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = models.User.objects.get(token=serializer.data["token"])
|
user = models.User.objects.get(token=validator.data["token"])
|
||||||
except models.User.DoesNotExist:
|
except models.User.DoesNotExist:
|
||||||
raise exc.WrongArguments(_("Token is invalid"))
|
raise exc.WrongArguments(_("Token is invalid"))
|
||||||
|
|
||||||
user.set_password(serializer.data["password"])
|
user.set_password(validator.data["password"])
|
||||||
user.token = None
|
user.token = None
|
||||||
user.save(update_fields=["password", "token"])
|
user.save(update_fields=["password", "token"])
|
||||||
|
|
||||||
|
@ -247,13 +261,13 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
"""
|
"""
|
||||||
Verify the email change to current logged user.
|
Verify the email change to current logged user.
|
||||||
"""
|
"""
|
||||||
serializer = serializers.ChangeEmailSerializer(data=request.DATA, many=False)
|
validator = validators.ChangeEmailValidator(data=request.DATA, many=False)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
|
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
|
||||||
"didn't use it before?"))
|
"didn't use it before?"))
|
||||||
|
|
||||||
try:
|
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:
|
except models.User.DoesNotExist:
|
||||||
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
|
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
|
||||||
"didn't use it before?"))
|
"didn't use it before?"))
|
||||||
|
@ -280,13 +294,13 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
"""
|
"""
|
||||||
Cancel an account via token
|
Cancel an account via token
|
||||||
"""
|
"""
|
||||||
serializer = serializers.CancelAccountSerializer(data=request.DATA, many=False)
|
validator = validators.CancelAccountValidator(data=request.DATA, many=False)
|
||||||
if not serializer.is_valid():
|
if not validator.is_valid():
|
||||||
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
|
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None)
|
max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None)
|
||||||
user = get_user_for_token(serializer.data["cancel_token"], "cancel_account",
|
user = get_user_for_token(validator.data["cancel_token"], "cancel_account",
|
||||||
max_age=max_age_cancel_account)
|
max_age=max_age_cancel_account)
|
||||||
|
|
||||||
except exc.NotAuthenticated:
|
except exc.NotAuthenticated:
|
||||||
|
@ -349,10 +363,10 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
for elem in elements:
|
for elem in elements:
|
||||||
if elem["type"] == "project":
|
if elem["type"] == "project":
|
||||||
# projects are liked objects
|
# 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:
|
else:
|
||||||
# stories, tasks and issues are voted objects
|
# 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)
|
return response.Ok(response_data)
|
||||||
|
|
||||||
|
@ -374,7 +388,7 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
"user_likes": services.get_liked_content_for_user(request.user),
|
"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)
|
return response.Ok(response_data)
|
||||||
|
|
||||||
|
@ -397,17 +411,18 @@ class UsersViewSet(ModelCrudViewSet):
|
||||||
"user_votes": services.get_voted_content_for_user(request.user),
|
"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)
|
return response.Ok(response_data)
|
||||||
|
|
||||||
######################################################
|
|
||||||
## Role
|
|
||||||
######################################################
|
|
||||||
|
|
||||||
|
######################################################
|
||||||
|
# Role
|
||||||
|
######################################################
|
||||||
class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.Role
|
model = models.Role
|
||||||
serializer_class = serializers.RoleSerializer
|
serializer_class = serializers.RoleSerializer
|
||||||
|
validator_class = validators.RoleValidator
|
||||||
permission_classes = (permissions.RolesPermission, )
|
permission_classes = (permissions.RolesPermission, )
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
|
@ -198,7 +198,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||||
|
|
||||||
def _fill_cached_memberships(self):
|
def _fill_cached_memberships(self):
|
||||||
self._cached_memberships = {}
|
self._cached_memberships = {}
|
||||||
qs = self.memberships.prefetch_related("user", "project", "role")
|
qs = self.memberships.select_related("user", "project", "role")
|
||||||
for membership in qs.all():
|
for membership in qs.all():
|
||||||
self._cached_memberships[membership.project.id] = membership
|
self._cached_memberships[membership.project.id] = membership
|
||||||
|
|
||||||
|
|
|
@ -17,70 +17,45 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.conf import settings
|
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.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.base.utils.thumbnails import get_thumbnail_url
|
||||||
|
|
||||||
from taiga.projects.models import Project
|
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 .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
|
||||||
from .gravatar import get_gravatar_url
|
from .gravatar import get_gravatar_url
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
import re
|
|
||||||
import serpy
|
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
## User
|
# User
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class ContactProjectDetailSerializer(serializers.ModelSerializer):
|
class ContactProjectDetailSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = Field()
|
||||||
model = Project
|
slug = Field()
|
||||||
fields = ("id", "slug", "name")
|
name = Field()
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(serializers.ModelSerializer):
|
class UserSerializer(serializers.LightSerializer):
|
||||||
full_name_display = serializers.SerializerMethodField("get_full_name_display")
|
id = Field()
|
||||||
photo = serializers.SerializerMethodField("get_photo")
|
username = Field()
|
||||||
big_photo = serializers.SerializerMethodField("get_big_photo")
|
full_name = Field()
|
||||||
gravatar_url = serializers.SerializerMethodField("get_gravatar_url")
|
full_name_display = MethodField()
|
||||||
roles = serializers.SerializerMethodField("get_roles")
|
color = Field()
|
||||||
projects_with_me = serializers.SerializerMethodField("get_projects_with_me")
|
bio = Field()
|
||||||
|
lang = Field()
|
||||||
class Meta:
|
theme = Field()
|
||||||
model = User
|
timezone = Field()
|
||||||
# IMPORTANT: Maintain the UserAdminSerializer Meta up to date
|
is_active = Field()
|
||||||
# with this info (including there the email)
|
photo = MethodField()
|
||||||
fields = ("id", "username", "full_name", "full_name_display",
|
big_photo = MethodField()
|
||||||
"color", "bio", "lang", "theme", "timezone", "is_active",
|
gravatar_url = MethodField()
|
||||||
"photo", "big_photo", "roles", "projects_with_me",
|
roles = MethodField()
|
||||||
"gravatar_url")
|
projects_with_me = MethodField()
|
||||||
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
|
|
||||||
|
|
||||||
def get_full_name_display(self, obj):
|
def get_full_name_display(self, obj):
|
||||||
return obj.get_full_name() if obj else ""
|
return obj.get_full_name() if obj else ""
|
||||||
|
@ -113,24 +88,13 @@ class UserSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class UserAdminSerializer(UserSerializer):
|
class UserAdminSerializer(UserSerializer):
|
||||||
total_private_projects = serializers.SerializerMethodField("get_total_private_projects")
|
total_private_projects = MethodField()
|
||||||
total_public_projects = serializers.SerializerMethodField("get_total_public_projects")
|
total_public_projects = MethodField()
|
||||||
|
email = Field()
|
||||||
class Meta:
|
max_private_projects = Field()
|
||||||
model = User
|
max_public_projects = Field()
|
||||||
# IMPORTANT: Maintain the UserSerializer Meta up to date
|
max_memberships_private_projects = Field()
|
||||||
# with this info (including here the email)
|
max_memberships_public_projects = Field()
|
||||||
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")
|
|
||||||
|
|
||||||
def get_total_private_projects(self, user):
|
def get_total_private_projects(self, user):
|
||||||
return user.owned_projects.filter(is_private=True).count()
|
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()
|
return user.owned_projects.filter(is_private=False).count()
|
||||||
|
|
||||||
|
|
||||||
class UserBasicInfoSerializer(UserSerializer):
|
class UserBasicInfoSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
username = Field()
|
||||||
model = User
|
full_name_display = MethodField()
|
||||||
fields = ("username", "full_name_display", "photo", "big_photo", "is_active", "id")
|
photo = MethodField()
|
||||||
|
big_photo = MethodField()
|
||||||
|
is_active = Field()
|
||||||
class ListUserBasicInfoSerializer(serpy.Serializer):
|
id = Field()
|
||||||
username = serpy.Field()
|
|
||||||
full_name_display = serpy.MethodField()
|
|
||||||
photo = serpy.MethodField()
|
|
||||||
big_photo = serpy.MethodField()
|
|
||||||
is_active = serpy.Field()
|
|
||||||
id = serpy.Field()
|
|
||||||
|
|
||||||
def get_full_name_display(self, obj):
|
def get_full_name_display(self, obj):
|
||||||
return obj.get_full_name()
|
return obj.get_full_name()
|
||||||
|
@ -162,76 +120,70 @@ class ListUserBasicInfoSerializer(serpy.Serializer):
|
||||||
def get_big_photo(self, obj):
|
def get_big_photo(self, obj):
|
||||||
return get_big_photo_or_gravatar_url(obj)
|
return get_big_photo_or_gravatar_url(obj)
|
||||||
|
|
||||||
|
def to_value(self, instance):
|
||||||
|
if instance is None:
|
||||||
|
return None
|
||||||
|
|
||||||
class RecoverySerializer(serializers.Serializer):
|
return super().to_value(instance)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
## Role
|
# Role
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class RoleSerializer(serializers.ModelSerializer):
|
class RoleSerializer(serializers.LightSerializer):
|
||||||
members_count = serializers.SerializerMethodField("get_members_count")
|
id = Field()
|
||||||
|
name = Field()
|
||||||
|
computable = Field()
|
||||||
|
project = Field(attr="project_id")
|
||||||
|
order = Field()
|
||||||
|
members_count = MethodField()
|
||||||
permissions = PgArrayField(required=False)
|
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):
|
def get_members_count(self, obj):
|
||||||
return obj.memberships.count()
|
return obj.memberships.count()
|
||||||
|
|
||||||
|
|
||||||
class ProjectRoleSerializer(serializers.ModelSerializer):
|
class ProjectRoleSerializer(serializers.LightSerializer):
|
||||||
class Meta:
|
id = Field()
|
||||||
model = Role
|
name = I18NField()
|
||||||
fields = ('id', 'name', 'slug', 'order', 'computable')
|
slug = Field()
|
||||||
i18n_fields = ("name",)
|
order = Field()
|
||||||
|
computable = Field()
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
## Like
|
# Like
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
class HighLightedContentSerializer(serializers.Serializer):
|
class HighLightedContentSerializer(serializers.LightSerializer):
|
||||||
type = serializers.CharField()
|
type = Field()
|
||||||
id = serializers.IntegerField()
|
id = Field()
|
||||||
ref = serializers.IntegerField()
|
ref = Field()
|
||||||
slug = serializers.CharField()
|
slug = Field()
|
||||||
name = serializers.CharField()
|
name = Field()
|
||||||
subject = serializers.CharField()
|
subject = Field()
|
||||||
description = serializers.SerializerMethodField("get_description")
|
description = MethodField()
|
||||||
assigned_to = serializers.IntegerField()
|
assigned_to = Field()
|
||||||
status = serializers.CharField()
|
status = Field()
|
||||||
status_color = serializers.CharField()
|
status_color = Field()
|
||||||
tags_colors = serializers.SerializerMethodField("get_tags_color")
|
tags_colors = MethodField()
|
||||||
created_date = serializers.DateTimeField()
|
created_date = Field()
|
||||||
is_private = serializers.SerializerMethodField("get_is_private")
|
is_private = MethodField()
|
||||||
logo_small_url = serializers.SerializerMethodField("get_logo_small_url")
|
logo_small_url = MethodField()
|
||||||
|
|
||||||
project = serializers.SerializerMethodField("get_project")
|
project = MethodField()
|
||||||
project_name = serializers.SerializerMethodField("get_project_name")
|
project_name = MethodField()
|
||||||
project_slug = serializers.SerializerMethodField("get_project_slug")
|
project_slug = MethodField()
|
||||||
project_is_private = serializers.SerializerMethodField("get_project_is_private")
|
project_is_private = MethodField()
|
||||||
project_blocked_code = serializers.CharField()
|
project_blocked_code = Field()
|
||||||
|
|
||||||
assigned_to_username = serializers.CharField()
|
assigned_to_username = Field()
|
||||||
assigned_to_full_name = serializers.CharField()
|
assigned_to_full_name = Field()
|
||||||
assigned_to_photo = serializers.SerializerMethodField("get_photo")
|
assigned_to_photo = MethodField()
|
||||||
|
|
||||||
is_watcher = serializers.SerializerMethodField("get_is_watcher")
|
is_watcher = MethodField()
|
||||||
total_watchers = serializers.IntegerField()
|
total_watchers = Field()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# Don't pass the extra ids args up to the superclass
|
# Don't pass the extra ids args up to the superclass
|
||||||
|
@ -241,18 +193,18 @@ class HighLightedContentSerializer(serializers.Serializer):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def _none_if_project(self, obj, property):
|
def _none_if_project(self, obj, property):
|
||||||
type = obj.get("type", "")
|
type = getattr(obj, "type", "")
|
||||||
if type == "project":
|
if type == "project":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return obj.get(property)
|
return getattr(obj, property)
|
||||||
|
|
||||||
def _none_if_not_project(self, obj, property):
|
def _none_if_not_project(self, obj, property):
|
||||||
type = obj.get("type", "")
|
type = getattr(obj, "type", "")
|
||||||
if type != "project":
|
if type != "project":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return obj.get(property)
|
return getattr(obj, property)
|
||||||
|
|
||||||
def get_project(self, obj):
|
def get_project(self, obj):
|
||||||
return self._none_if_project(obj, "project")
|
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 get_thumbnail_url(logo, settings.THN_LOGO_SMALL)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_photo(self, obj):
|
def get_assigned_to_photo(self, obj):
|
||||||
type = obj.get("type", "")
|
type = getattr(obj, "type", "")
|
||||||
if type == "project":
|
if type == "project":
|
||||||
return None
|
return None
|
||||||
|
|
||||||
UserData = namedtuple("UserData", ["photo", "email"])
|
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)
|
return get_photo_or_gravatar_url(user_data)
|
||||||
|
|
||||||
def get_tags_color(self, obj):
|
def get_tags_colors(self, obj):
|
||||||
tags = obj.get("tags", [])
|
tags = getattr(obj, "tags", [])
|
||||||
tags = tags if tags is not None else []
|
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 []
|
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]
|
return [{"name": tc[0], "color": tc[1]} for tc in tags_colors if tc[0] in tags]
|
||||||
|
|
||||||
def get_is_watcher(self, obj):
|
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):
|
class LikedObjectSerializer(HighLightedContentSerializer):
|
||||||
is_fan = serializers.SerializerMethodField("get_is_fan")
|
is_fan = MethodField()
|
||||||
total_fans = serializers.IntegerField()
|
total_fans = Field()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# Don't pass the extra ids args up to the superclass
|
# Don't pass the extra ids args up to the superclass
|
||||||
|
@ -310,12 +262,12 @@ class LikedObjectSerializer(HighLightedContentSerializer):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_is_fan(self, obj):
|
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):
|
class VotedObjectSerializer(HighLightedContentSerializer):
|
||||||
is_voter = serializers.SerializerMethodField("get_is_voter")
|
is_voter = MethodField()
|
||||||
total_voters = serializers.IntegerField()
|
total_voters = Field()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# Don't pass the extra ids args up to the superclass
|
# Don't pass the extra ids args up to the superclass
|
||||||
|
@ -325,4 +277,4 @@ class VotedObjectSerializer(HighLightedContentSerializer):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_is_voter(self, obj):
|
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, [])
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Affero General Public License as
|
# it under the terms of the GNU Affero General Public License as
|
||||||
# published by the Free Software Foundation, either version 3 of the
|
# 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
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
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 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:
|
class RoleExistsValidator:
|
||||||
def validate_role_id(self, attrs, source):
|
def validate_role_id(self, attrs, source):
|
||||||
value = 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")
|
msg = _("There's no role with that id")
|
||||||
raise serializers.ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
return attrs
|
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')
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.db import IntegrityError
|
|
||||||
|
|
||||||
from taiga.base.api import ModelCrudViewSet
|
from taiga.base.api import ModelCrudViewSet
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
|
@ -25,6 +24,7 @@ from taiga.base import exceptions as exc
|
||||||
from . import models
|
from . import models
|
||||||
from . import filters
|
from . import filters
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from . import validators
|
||||||
from . import permissions
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ class StorageEntriesViewSet(ModelCrudViewSet):
|
||||||
model = models.StorageEntry
|
model = models.StorageEntry
|
||||||
filter_backends = (filters.StorageEntriesFilterBackend,)
|
filter_backends = (filters.StorageEntriesFilterBackend,)
|
||||||
serializer_class = serializers.StorageEntrySerializer
|
serializer_class = serializers.StorageEntrySerializer
|
||||||
|
validator_class = validators.StorageEntryValidator
|
||||||
permission_classes = [permissions.StorageEntriesPermission]
|
permission_classes = [permissions.StorageEntriesPermission]
|
||||||
lookup_field = "key"
|
lookup_field = "key"
|
||||||
|
|
||||||
|
@ -45,9 +46,11 @@ class StorageEntriesViewSet(ModelCrudViewSet):
|
||||||
obj.owner = self.request.user
|
obj.owner = self.request.user
|
||||||
|
|
||||||
def create(self, *args, **kwargs):
|
def create(self, *args, **kwargs):
|
||||||
try:
|
|
||||||
return super().create(*args, **kwargs)
|
|
||||||
except IntegrityError:
|
|
||||||
key = self.request.DATA.get("key", None)
|
key = self.request.DATA.get("key", None)
|
||||||
raise exc.IntegrityError(_("Duplicate key value violates unique constraint. "
|
if (key and self.request.user.is_authenticated() and
|
||||||
"Key '{}' already exists.").format(key))
|
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)
|
||||||
|
|
|
@ -17,15 +17,11 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
from taiga.base.api import serializers
|
||||||
from taiga.base.fields import JsonField
|
from taiga.base.fields import Field
|
||||||
|
|
||||||
from . import models
|
|
||||||
|
|
||||||
|
|
||||||
class StorageEntrySerializer(serializers.ModelSerializer):
|
class StorageEntrySerializer(serializers.LightSerializer):
|
||||||
value = JsonField(label="value")
|
key = Field()
|
||||||
|
value = Field()
|
||||||
class Meta:
|
created_date = Field()
|
||||||
model = models.StorageEntry
|
modified_date = Field()
|
||||||
fields = ("key", "value", "created_date", "modified_date")
|
|
||||||
read_only_fields = ("created_date", "modified_date")
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base.api import validators
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class StorageEntryValidator(validators.ModelValidator):
|
||||||
|
class Meta:
|
||||||
|
model = models.StorageEntry
|
||||||
|
fields = ("key", "value")
|
|
@ -30,6 +30,7 @@ from taiga.base.decorators import detail_route
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
from . import validators
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import tasks
|
from . import tasks
|
||||||
|
|
||||||
|
@ -37,6 +38,7 @@ from . import tasks
|
||||||
class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
model = models.Webhook
|
model = models.Webhook
|
||||||
serializer_class = serializers.WebhookSerializer
|
serializer_class = serializers.WebhookSerializer
|
||||||
|
validator_class = validators.WebhookValidator
|
||||||
permission_classes = (permissions.WebhookPermission,)
|
permission_classes = (permissions.WebhookPermission,)
|
||||||
filter_backends = (filters.IsProjectAdminFilterBackend,)
|
filter_backends = (filters.IsProjectAdminFilterBackend,)
|
||||||
filter_fields = ("project",)
|
filter_fields = ("project",)
|
||||||
|
|
|
@ -19,63 +19,55 @@
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from taiga.base.api import serializers
|
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.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.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.gravatar import get_gravatar_url
|
||||||
from taiga.users.services import get_photo_or_gravatar_url
|
from taiga.users.services import get_photo_or_gravatar_url
|
||||||
|
|
||||||
from .models import Webhook, WebhookLog
|
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
## WebHooks
|
# WebHooks
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
class WebhookSerializer(serializers.ModelSerializer):
|
class WebhookSerializer(serializers.LightSerializer):
|
||||||
logs_counter = serializers.SerializerMethodField("get_logs_counter")
|
id = Field()
|
||||||
|
project = Field(attr="project_id")
|
||||||
class Meta:
|
name = Field()
|
||||||
model = Webhook
|
url = Field()
|
||||||
|
key = Field()
|
||||||
|
logs_counter = MethodField()
|
||||||
|
|
||||||
def get_logs_counter(self, obj):
|
def get_logs_counter(self, obj):
|
||||||
return obj.logs.count()
|
return obj.logs.count()
|
||||||
|
|
||||||
|
|
||||||
class WebhookLogSerializer(serializers.ModelSerializer):
|
class WebhookLogSerializer(serializers.LightSerializer):
|
||||||
request_data = JsonField()
|
id = Field()
|
||||||
request_headers = JsonField()
|
webhook = Field(attr="webhook_id")
|
||||||
response_headers = JsonField()
|
url = Field()
|
||||||
|
status = Field()
|
||||||
class Meta:
|
request_data = Field()
|
||||||
model = WebhookLog
|
request_headers = Field()
|
||||||
|
response_data = Field()
|
||||||
|
response_headers = Field()
|
||||||
|
duration = Field()
|
||||||
|
created = Field()
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
## User
|
# User
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
class UserSerializer(serializers.Serializer):
|
class UserSerializer(serializers.LightSerializer):
|
||||||
id = serializers.SerializerMethodField("get_pk")
|
id = Field(attr="pk")
|
||||||
permalink = serializers.SerializerMethodField("get_permalink")
|
permalink = MethodField()
|
||||||
gravatar_url = serializers.SerializerMethodField("get_gravatar_url")
|
gravatar_url = MethodField()
|
||||||
username = serializers.SerializerMethodField("get_username")
|
username = MethodField()
|
||||||
full_name = serializers.SerializerMethodField("get_full_name")
|
full_name = MethodField()
|
||||||
photo = serializers.SerializerMethodField("get_photo")
|
photo = MethodField()
|
||||||
|
|
||||||
def get_pk(self, obj):
|
|
||||||
return obj.pk
|
|
||||||
|
|
||||||
def get_permalink(self, obj):
|
def get_permalink(self, obj):
|
||||||
return resolve_front_url("user", obj.username)
|
return resolve_front_url("user", obj.username)
|
||||||
|
@ -84,7 +76,7 @@ class UserSerializer(serializers.Serializer):
|
||||||
return get_gravatar_url(obj.email)
|
return get_gravatar_url(obj.email)
|
||||||
|
|
||||||
def get_username(self, obj):
|
def get_username(self, obj):
|
||||||
return obj.get_username
|
return obj.get_username()
|
||||||
|
|
||||||
def get_full_name(self, obj):
|
def get_full_name(self, obj):
|
||||||
return obj.get_full_name()
|
return obj.get_full_name()
|
||||||
|
@ -92,18 +84,22 @@ class UserSerializer(serializers.Serializer):
|
||||||
def get_photo(self, obj):
|
def get_photo(self, obj):
|
||||||
return get_photo_or_gravatar_url(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):
|
class ProjectSerializer(serializers.LightSerializer):
|
||||||
id = serializers.SerializerMethodField("get_pk")
|
id = Field(attr="pk")
|
||||||
permalink = serializers.SerializerMethodField("get_permalink")
|
permalink = MethodField()
|
||||||
name = serializers.SerializerMethodField("get_name")
|
name = MethodField()
|
||||||
logo_big_url = serializers.SerializerMethodField("get_logo_big_url")
|
logo_big_url = MethodField()
|
||||||
|
|
||||||
def get_pk(self, obj):
|
|
||||||
return obj.pk
|
|
||||||
|
|
||||||
def get_permalink(self, obj):
|
def get_permalink(self, obj):
|
||||||
return resolve_front_url("project", obj.slug)
|
return resolve_front_url("project", obj.slug)
|
||||||
|
@ -116,11 +112,11 @@ class ProjectSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
## History Serializer
|
# History Serializer
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
class HistoryDiffField(serializers.Field):
|
class HistoryDiffField(Field):
|
||||||
def to_native(self, value):
|
def to_value(self, value):
|
||||||
# Tip: 'value' is the object returned by
|
# Tip: 'value' is the object returned by
|
||||||
# taiga.projects.history.models.HistoryEntry.values_diff()
|
# taiga.projects.history.models.HistoryEntry.values_diff()
|
||||||
|
|
||||||
|
@ -137,21 +133,21 @@ class HistoryDiffField(serializers.Field):
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
class HistoryEntrySerializer(serializers.ModelSerializer):
|
class HistoryEntrySerializer(serializers.LightSerializer):
|
||||||
diff = HistoryDiffField(source="values_diff")
|
comment = Field()
|
||||||
|
comment_html = Field()
|
||||||
class Meta:
|
delete_comment_date = Field()
|
||||||
model = history_models.HistoryEntry
|
comment_versions = Field()
|
||||||
exclude = ("id", "type", "key", "is_hidden", "is_snapshot", "snapshot", "user", "delete_comment_user",
|
edit_comment_date = Field()
|
||||||
"values", "created_at")
|
diff = HistoryDiffField(attr="values_diff")
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
## _Misc_
|
# _Misc_
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
|
class CustomAttributesValuesWebhookSerializerMixin(serializers.LightSerializer):
|
||||||
custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values")
|
custom_attributes_values = MethodField()
|
||||||
|
|
||||||
def custom_attributes_queryset(self, project):
|
def custom_attributes_queryset(self, project):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
@ -175,10 +171,10 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class RolePointsSerializer(serializers.Serializer):
|
class RolePointsSerializer(serializers.LightSerializer):
|
||||||
role = serializers.SerializerMethodField("get_role")
|
role = MethodField()
|
||||||
name = serializers.SerializerMethodField("get_name")
|
name = MethodField()
|
||||||
value = serializers.SerializerMethodField("get_value")
|
value = MethodField()
|
||||||
|
|
||||||
def get_role(self, obj):
|
def get_role(self, obj):
|
||||||
return obj.role.name
|
return obj.role.name
|
||||||
|
@ -190,16 +186,13 @@ class RolePointsSerializer(serializers.Serializer):
|
||||||
return obj.points.value
|
return obj.points.value
|
||||||
|
|
||||||
|
|
||||||
class UserStoryStatusSerializer(serializers.Serializer):
|
class UserStoryStatusSerializer(serializers.LightSerializer):
|
||||||
id = serializers.SerializerMethodField("get_pk")
|
id = Field(attr="pk")
|
||||||
name = serializers.SerializerMethodField("get_name")
|
name = MethodField()
|
||||||
slug = serializers.SerializerMethodField("get_slug")
|
slug = MethodField()
|
||||||
color = serializers.SerializerMethodField("get_color")
|
color = MethodField()
|
||||||
is_closed = serializers.SerializerMethodField("get_is_closed")
|
is_closed = MethodField()
|
||||||
is_archived = serializers.SerializerMethodField("get_is_archived")
|
is_archived = MethodField()
|
||||||
|
|
||||||
def get_pk(self, obj):
|
|
||||||
return obj.pk
|
|
||||||
|
|
||||||
def get_name(self, obj):
|
def get_name(self, obj):
|
||||||
return obj.name
|
return obj.name
|
||||||
|
@ -217,15 +210,12 @@ class UserStoryStatusSerializer(serializers.Serializer):
|
||||||
return obj.is_archived
|
return obj.is_archived
|
||||||
|
|
||||||
|
|
||||||
class TaskStatusSerializer(serializers.Serializer):
|
class TaskStatusSerializer(serializers.LightSerializer):
|
||||||
id = serializers.SerializerMethodField("get_pk")
|
id = Field(attr="pk")
|
||||||
name = serializers.SerializerMethodField("get_name")
|
name = MethodField()
|
||||||
slug = serializers.SerializerMethodField("get_slug")
|
slug = MethodField()
|
||||||
color = serializers.SerializerMethodField("get_color")
|
color = MethodField()
|
||||||
is_closed = serializers.SerializerMethodField("get_is_closed")
|
is_closed = MethodField()
|
||||||
|
|
||||||
def get_pk(self, obj):
|
|
||||||
return obj.pk
|
|
||||||
|
|
||||||
def get_name(self, obj):
|
def get_name(self, obj):
|
||||||
return obj.name
|
return obj.name
|
||||||
|
@ -240,15 +230,12 @@ class TaskStatusSerializer(serializers.Serializer):
|
||||||
return obj.is_closed
|
return obj.is_closed
|
||||||
|
|
||||||
|
|
||||||
class IssueStatusSerializer(serializers.Serializer):
|
class IssueStatusSerializer(serializers.LightSerializer):
|
||||||
id = serializers.SerializerMethodField("get_pk")
|
id = Field(attr="pk")
|
||||||
name = serializers.SerializerMethodField("get_name")
|
name = MethodField()
|
||||||
slug = serializers.SerializerMethodField("get_slug")
|
slug = MethodField()
|
||||||
color = serializers.SerializerMethodField("get_color")
|
color = MethodField()
|
||||||
is_closed = serializers.SerializerMethodField("get_is_closed")
|
is_closed = MethodField()
|
||||||
|
|
||||||
def get_pk(self, obj):
|
|
||||||
return obj.pk
|
|
||||||
|
|
||||||
def get_name(self, obj):
|
def get_name(self, obj):
|
||||||
return obj.name
|
return obj.name
|
||||||
|
@ -263,13 +250,10 @@ class IssueStatusSerializer(serializers.Serializer):
|
||||||
return obj.is_closed
|
return obj.is_closed
|
||||||
|
|
||||||
|
|
||||||
class IssueTypeSerializer(serializers.Serializer):
|
class IssueTypeSerializer(serializers.LightSerializer):
|
||||||
id = serializers.SerializerMethodField("get_pk")
|
id = Field(attr="pk")
|
||||||
name = serializers.SerializerMethodField("get_name")
|
name = MethodField()
|
||||||
color = serializers.SerializerMethodField("get_color")
|
color = MethodField()
|
||||||
|
|
||||||
def get_pk(self, obj):
|
|
||||||
return obj.pk
|
|
||||||
|
|
||||||
def get_name(self, obj):
|
def get_name(self, obj):
|
||||||
return obj.name
|
return obj.name
|
||||||
|
@ -278,13 +262,10 @@ class IssueTypeSerializer(serializers.Serializer):
|
||||||
return obj.color
|
return obj.color
|
||||||
|
|
||||||
|
|
||||||
class PrioritySerializer(serializers.Serializer):
|
class PrioritySerializer(serializers.LightSerializer):
|
||||||
id = serializers.SerializerMethodField("get_pk")
|
id = Field(attr="pk")
|
||||||
name = serializers.SerializerMethodField("get_name")
|
name = MethodField()
|
||||||
color = serializers.SerializerMethodField("get_color")
|
color = MethodField()
|
||||||
|
|
||||||
def get_pk(self, obj):
|
|
||||||
return obj.pk
|
|
||||||
|
|
||||||
def get_name(self, obj):
|
def get_name(self, obj):
|
||||||
return obj.name
|
return obj.name
|
||||||
|
@ -293,13 +274,10 @@ class PrioritySerializer(serializers.Serializer):
|
||||||
return obj.color
|
return obj.color
|
||||||
|
|
||||||
|
|
||||||
class SeveritySerializer(serializers.Serializer):
|
class SeveritySerializer(serializers.LightSerializer):
|
||||||
id = serializers.SerializerMethodField("get_pk")
|
id = Field(attr="pk")
|
||||||
name = serializers.SerializerMethodField("get_name")
|
name = MethodField()
|
||||||
color = serializers.SerializerMethodField("get_color")
|
color = MethodField()
|
||||||
|
|
||||||
def get_pk(self, obj):
|
|
||||||
return obj.pk
|
|
||||||
|
|
||||||
def get_name(self, obj):
|
def get_name(self, obj):
|
||||||
return obj.name
|
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")
|
permalink = serializers.SerializerMethodField("get_permalink")
|
||||||
project = ProjectSerializer()
|
project = ProjectSerializer()
|
||||||
owner = UserSerializer()
|
owner = UserSerializer()
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = milestone_models.Milestone
|
|
||||||
exclude = ("order", "watchers")
|
|
||||||
|
|
||||||
def get_permalink(self, obj):
|
def get_permalink(self, obj):
|
||||||
return resolve_front_url("taskboard", obj.project.slug, obj.slug)
|
return resolve_front_url("taskboard", obj.project.slug, obj.slug)
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
########################################################################
|
||||||
## User Story
|
# User Story
|
||||||
########################################################################
|
########################################################################
|
||||||
|
|
||||||
class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer,
|
class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
|
||||||
serializers.ModelSerializer):
|
id = Field()
|
||||||
permalink = serializers.SerializerMethodField("get_permalink")
|
ref = Field()
|
||||||
tags = TagsField(default=[], required=False)
|
|
||||||
external_reference = PgArrayField(required=False)
|
|
||||||
project = ProjectSerializer()
|
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()
|
owner = UserSerializer()
|
||||||
assigned_to = UserSerializer()
|
assigned_to = UserSerializer()
|
||||||
points = RolePointsSerializer(source="role_points", many=True)
|
points = MethodField()
|
||||||
status = UserStoryStatusSerializer()
|
status = UserStoryStatusSerializer()
|
||||||
milestone = MilestoneSerializer()
|
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):
|
def get_permalink(self, obj):
|
||||||
return resolve_front_url("userstory", obj.project.slug, obj.ref)
|
return resolve_front_url("userstory", obj.project.slug, obj.ref)
|
||||||
|
|
||||||
def custom_attributes_queryset(self, project):
|
def custom_attributes_queryset(self, project):
|
||||||
return project.userstorycustomattributes.all()
|
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,
|
class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
|
||||||
serializers.ModelSerializer):
|
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")
|
permalink = serializers.SerializerMethodField("get_permalink")
|
||||||
tags = TagsField(default=[], required=False)
|
|
||||||
project = ProjectSerializer()
|
project = ProjectSerializer()
|
||||||
owner = UserSerializer()
|
owner = UserSerializer()
|
||||||
assigned_to = UserSerializer()
|
assigned_to = UserSerializer()
|
||||||
|
@ -367,25 +378,32 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatch
|
||||||
user_story = UserStorySerializer()
|
user_story = UserStorySerializer()
|
||||||
milestone = MilestoneSerializer()
|
milestone = MilestoneSerializer()
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = task_models.Task
|
|
||||||
exclude = ("version", "total_watchers", "is_watcher")
|
|
||||||
|
|
||||||
def get_permalink(self, obj):
|
def get_permalink(self, obj):
|
||||||
return resolve_front_url("task", obj.project.slug, obj.ref)
|
return resolve_front_url("task", obj.project.slug, obj.ref)
|
||||||
|
|
||||||
def custom_attributes_queryset(self, project):
|
def custom_attributes_queryset(self, project):
|
||||||
return project.taskcustomattributes.all()
|
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,
|
class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
|
||||||
serializers.ModelSerializer):
|
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")
|
permalink = serializers.SerializerMethodField("get_permalink")
|
||||||
tags = TagsField(default=[], required=False)
|
|
||||||
project = ProjectSerializer()
|
project = ProjectSerializer()
|
||||||
milestone = MilestoneSerializer()
|
milestone = MilestoneSerializer()
|
||||||
owner = UserSerializer()
|
owner = UserSerializer()
|
||||||
|
@ -395,30 +413,30 @@ class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatc
|
||||||
priority = PrioritySerializer()
|
priority = PrioritySerializer()
|
||||||
severity = SeveritySerializer()
|
severity = SeveritySerializer()
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = issue_models.Issue
|
|
||||||
exclude = ("version", "total_watchers", "is_watcher")
|
|
||||||
|
|
||||||
def get_permalink(self, obj):
|
def get_permalink(self, obj):
|
||||||
return resolve_front_url("issue", obj.project.slug, obj.ref)
|
return resolve_front_url("issue", obj.project.slug, obj.ref)
|
||||||
|
|
||||||
def custom_attributes_queryset(self, project):
|
def custom_attributes_queryset(self, project):
|
||||||
return project.issuecustomattributes.all()
|
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")
|
permalink = serializers.SerializerMethodField("get_permalink")
|
||||||
project = ProjectSerializer()
|
project = ProjectSerializer()
|
||||||
owner = UserSerializer()
|
owner = UserSerializer()
|
||||||
last_modifier = UserSerializer()
|
last_modifier = UserSerializer()
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = wiki_models.WikiPage
|
|
||||||
exclude = ("watchers", "total_watchers", "is_watcher", "version")
|
|
||||||
|
|
||||||
def get_permalink(self, obj):
|
def get_permalink(self, obj):
|
||||||
return resolve_front_url("wiki", obj.project.slug, obj.slug)
|
return resolve_front_url("wiki", obj.project.slug, obj.slug)
|
||||||
|
|
|
@ -149,5 +149,4 @@ def test_webhook(webhook_id, url, key, by, date):
|
||||||
data['by'] = UserSerializer(by).data
|
data['by'] = UserSerializer(by).data
|
||||||
data['date'] = date
|
data['date'] = date
|
||||||
data['data'] = {"test": "test"}
|
data['data'] = {"test": "test"}
|
||||||
|
|
||||||
return _send_request(webhook_id, url, key, data)
|
return _send_request(webhook_id, url, key, data)
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base.api import validators
|
||||||
|
|
||||||
|
from .models import Webhook
|
||||||
|
|
||||||
|
|
||||||
|
class WebhookValidator(validators.ModelValidator):
|
||||||
|
class Meta:
|
||||||
|
model = Webhook
|
|
@ -22,7 +22,11 @@ import uuid
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from taiga.projects import choices as project_choices
|
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.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.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
|
|
||||||
|
@ -61,22 +65,29 @@ def data():
|
||||||
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
issues_csv_uuid=uuid.uuid4().hex)
|
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,
|
m.private_project1 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
issues_csv_uuid=uuid.uuid4().hex)
|
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,
|
m.private_project2 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
issues_csv_uuid=uuid.uuid4().hex)
|
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,
|
m.blocked_project = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
issues_csv_uuid=uuid.uuid4().hex,
|
issues_csv_uuid=uuid.uuid4().hex,
|
||||||
blocked_code=project_choices.BLOCKED_BY_STAFF)
|
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,
|
m.public_membership = f.MembershipFactory(project=m.public_project,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
|
@ -129,24 +140,31 @@ def data():
|
||||||
priority__project=m.public_project,
|
priority__project=m.public_project,
|
||||||
type__project=m.public_project,
|
type__project=m.public_project,
|
||||||
milestone__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,
|
m.private_issue1 = f.IssueFactory(project=m.private_project1,
|
||||||
status__project=m.private_project1,
|
status__project=m.private_project1,
|
||||||
severity__project=m.private_project1,
|
severity__project=m.private_project1,
|
||||||
priority__project=m.private_project1,
|
priority__project=m.private_project1,
|
||||||
type__project=m.private_project1,
|
type__project=m.private_project1,
|
||||||
milestone__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,
|
m.private_issue2 = f.IssueFactory(project=m.private_project2,
|
||||||
status__project=m.private_project2,
|
status__project=m.private_project2,
|
||||||
severity__project=m.private_project2,
|
severity__project=m.private_project2,
|
||||||
priority__project=m.private_project2,
|
priority__project=m.private_project2,
|
||||||
type__project=m.private_project2,
|
type__project=m.private_project2,
|
||||||
milestone__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,
|
m.blocked_issue = f.IssueFactory(project=m.blocked_project,
|
||||||
status__project=m.blocked_project,
|
status__project=m.blocked_project,
|
||||||
severity__project=m.blocked_project,
|
severity__project=m.blocked_project,
|
||||||
priority__project=m.blocked_project,
|
priority__project=m.blocked_project,
|
||||||
type__project=m.blocked_project,
|
type__project=m.blocked_project,
|
||||||
milestone__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
|
return m
|
||||||
|
|
||||||
|
@ -443,24 +461,28 @@ def test_issue_put_update_with_project_change(client):
|
||||||
project1.save()
|
project1.save()
|
||||||
project2.save()
|
project2.save()
|
||||||
|
|
||||||
membership1 = f.MembershipFactory(project=project1,
|
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,
|
user=user1,
|
||||||
role__project=project1,
|
role__project=project1,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
membership2 = f.MembershipFactory(project=project2,
|
f.MembershipFactory(project=project2,
|
||||||
user=user1,
|
user=user1,
|
||||||
role__project=project2,
|
role__project=project2,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
membership3 = f.MembershipFactory(project=project1,
|
f.MembershipFactory(project=project1,
|
||||||
user=user2,
|
user=user2,
|
||||||
role__project=project1,
|
role__project=project1,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
membership4 = f.MembershipFactory(project=project2,
|
f.MembershipFactory(project=project2,
|
||||||
user=user3,
|
user=user3,
|
||||||
role__project=project2,
|
role__project=project2,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
|
||||||
issue = f.IssueFactory.create(project=project1)
|
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})
|
url = reverse('issues-detail', kwargs={"pk": issue.pk})
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,11 @@ from django.core.urlresolvers import reverse
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
|
|
||||||
from taiga.projects import choices as project_choices
|
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.serializers import MilestoneSerializer
|
||||||
from taiga.projects.milestones.models import Milestone
|
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.projects.notifications.services import add_watcher
|
||||||
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
|
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
|
||||||
|
|
||||||
|
@ -56,25 +59,34 @@ def data():
|
||||||
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
owner=m.project_owner)
|
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,
|
m.private_project1 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
owner=m.project_owner)
|
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,
|
m.private_project2 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner)
|
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,
|
m.blocked_project = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
blocked_code=project_choices.BLOCKED_BY_STAFF)
|
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,
|
m.public_membership = f.MembershipFactory(
|
||||||
|
project=m.public_project,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.public_project,
|
role__project=m.public_project,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
m.private_membership1 = f.MembershipFactory(project=m.private_project1,
|
m.private_membership1 = f.MembershipFactory(
|
||||||
|
project=m.private_project1,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.private_project1,
|
role__project=m.private_project1,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
@ -82,7 +94,8 @@ def data():
|
||||||
user=m.project_member_without_perms,
|
user=m.project_member_without_perms,
|
||||||
role__project=m.private_project1,
|
role__project=m.private_project1,
|
||||||
role__permissions=[])
|
role__permissions=[])
|
||||||
m.private_membership2 = f.MembershipFactory(project=m.private_project2,
|
m.private_membership2 = f.MembershipFactory(
|
||||||
|
project=m.private_project2,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.private_project2,
|
role__project=m.private_project2,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
@ -90,7 +103,8 @@ def data():
|
||||||
user=m.project_member_without_perms,
|
user=m.project_member_without_perms,
|
||||||
role__project=m.private_project2,
|
role__project=m.private_project2,
|
||||||
role__permissions=[])
|
role__permissions=[])
|
||||||
m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
|
m.blocked_membership = f.MembershipFactory(
|
||||||
|
project=m.blocked_project,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.blocked_project,
|
role__project=m.blocked_project,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
@ -116,9 +130,13 @@ def data():
|
||||||
is_admin=True)
|
is_admin=True)
|
||||||
|
|
||||||
m.public_milestone = f.MilestoneFactory(project=m.public_project)
|
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 = 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 = 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 = f.MilestoneFactory(project=m.blocked_project)
|
||||||
|
m.blocked_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.blocked_milestone.id)
|
||||||
|
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,10 @@ from django.apps import apps
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
from taiga.projects import choices as project_choices
|
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.permissions.choices import MEMBERS_PERMISSIONS
|
||||||
|
from taiga.projects.utils import attach_extra_info
|
||||||
|
|
||||||
from tests import factories as f
|
from tests import factories as f
|
||||||
from tests.utils import helper_test_http_method, helper_test_http_method_and_count
|
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,
|
m.public_project = f.ProjectFactory(is_private=False,
|
||||||
anon_permissions=['view_project'],
|
anon_permissions=['view_project'],
|
||||||
public_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,
|
m.private_project1 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=['view_project'],
|
anon_permissions=['view_project'],
|
||||||
public_permissions=['view_project'],
|
public_permissions=['view_project'],
|
||||||
owner=m.project_owner)
|
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,
|
m.private_project2 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner)
|
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,
|
m.blocked_project = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
blocked_code=project_choices.BLOCKED_BY_STAFF)
|
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)
|
f.RoleFactory(project=m.public_project)
|
||||||
|
|
||||||
|
@ -153,12 +162,12 @@ def test_project_update(client, data):
|
||||||
data.project_owner
|
data.project_owner
|
||||||
]
|
]
|
||||||
|
|
||||||
project_data = ProjectDetailSerializer(data.private_project2).data
|
project_data = ProjectSerializer(data.private_project2).data
|
||||||
project_data["is_private"] = False
|
project_data["is_private"] = False
|
||||||
results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users)
|
results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users)
|
||||||
assert results == [401, 403, 403, 200]
|
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
|
project_data["is_private"] = False
|
||||||
results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users)
|
results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users)
|
||||||
assert results == [401, 403, 403, 451]
|
assert results == [401, 403, 403, 451]
|
||||||
|
|
|
@ -23,12 +23,16 @@ from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
from taiga.projects import choices as project_choices
|
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.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.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
from taiga.projects.occ import OCCResourceMixin
|
||||||
|
|
||||||
from tests import factories as f
|
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.votes.services import add_vote
|
||||||
from taiga.projects.notifications.services import add_watcher
|
from taiga.projects.notifications.services import add_watcher
|
||||||
|
|
||||||
|
@ -38,10 +42,6 @@ import pytest
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def setup_function(function):
|
|
||||||
disconnect_signals()
|
|
||||||
|
|
||||||
|
|
||||||
def setup_function(function):
|
def setup_function(function):
|
||||||
reconnect_signals()
|
reconnect_signals()
|
||||||
|
|
||||||
|
@ -61,44 +61,58 @@ def data():
|
||||||
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
tasks_csv_uuid=uuid.uuid4().hex)
|
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,
|
m.private_project1 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
tasks_csv_uuid=uuid.uuid4().hex)
|
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,
|
m.private_project2 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
tasks_csv_uuid=uuid.uuid4().hex)
|
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,
|
m.blocked_project = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
tasks_csv_uuid=uuid.uuid4().hex,
|
tasks_csv_uuid=uuid.uuid4().hex,
|
||||||
blocked_code=project_choices.BLOCKED_BY_STAFF)
|
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,
|
m.public_membership = f.MembershipFactory(
|
||||||
|
project=m.public_project,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.public_project,
|
role__project=m.public_project,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
m.private_membership1 = f.MembershipFactory(project=m.private_project1,
|
|
||||||
|
m.private_membership1 = f.MembershipFactory(
|
||||||
|
project=m.private_project1,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.private_project1,
|
role__project=m.private_project1,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
f.MembershipFactory(project=m.private_project1,
|
f.MembershipFactory(
|
||||||
|
project=m.private_project1,
|
||||||
user=m.project_member_without_perms,
|
user=m.project_member_without_perms,
|
||||||
role__project=m.private_project1,
|
role__project=m.private_project1,
|
||||||
role__permissions=[])
|
role__permissions=[])
|
||||||
m.private_membership2 = f.MembershipFactory(project=m.private_project2,
|
m.private_membership2 = f.MembershipFactory(
|
||||||
|
project=m.private_project2,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.private_project2,
|
role__project=m.private_project2,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
f.MembershipFactory(project=m.private_project2,
|
f.MembershipFactory(
|
||||||
|
project=m.private_project2,
|
||||||
user=m.project_member_without_perms,
|
user=m.project_member_without_perms,
|
||||||
role__project=m.private_project2,
|
role__project=m.private_project2,
|
||||||
role__permissions=[])
|
role__permissions=[])
|
||||||
m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
|
m.blocked_membership = f.MembershipFactory(
|
||||||
|
project=m.blocked_project,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.blocked_project,
|
role__project=m.blocked_project,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
@ -133,21 +147,28 @@ def data():
|
||||||
milestone=milestone_public_task,
|
milestone=milestone_public_task,
|
||||||
user_story__project=m.public_project,
|
user_story__project=m.public_project,
|
||||||
user_story__milestone=milestone_public_task)
|
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,
|
m.private_task1 = f.TaskFactory(project=m.private_project1,
|
||||||
status__project=m.private_project1,
|
status__project=m.private_project1,
|
||||||
milestone=milestone_private_task1,
|
milestone=milestone_private_task1,
|
||||||
user_story__project=m.private_project1,
|
user_story__project=m.private_project1,
|
||||||
user_story__milestone=milestone_private_task1)
|
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,
|
m.private_task2 = f.TaskFactory(project=m.private_project2,
|
||||||
status__project=m.private_project2,
|
status__project=m.private_project2,
|
||||||
milestone=milestone_private_task2,
|
milestone=milestone_private_task2,
|
||||||
user_story__project=m.private_project2,
|
user_story__project=m.private_project2,
|
||||||
user_story__milestone=milestone_private_task2)
|
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,
|
m.blocked_task = f.TaskFactory(project=m.blocked_project,
|
||||||
status__project=m.blocked_project,
|
status__project=m.blocked_project,
|
||||||
milestone=milestone_blocked_task,
|
milestone=milestone_blocked_task,
|
||||||
user_story__project=m.blocked_project,
|
user_story__project=m.blocked_project,
|
||||||
user_story__milestone=milestone_blocked_task)
|
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.default_task_status = m.public_task.status
|
||||||
m.public_project.save()
|
m.public_project.save()
|
||||||
|
@ -404,24 +425,28 @@ def test_task_put_update_with_project_change(client):
|
||||||
project1.save()
|
project1.save()
|
||||||
project2.save()
|
project2.save()
|
||||||
|
|
||||||
membership1 = f.MembershipFactory(project=project1,
|
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,
|
user=user1,
|
||||||
role__project=project1,
|
role__project=project1,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
membership2 = f.MembershipFactory(project=project2,
|
f.MembershipFactory(project=project2,
|
||||||
user=user1,
|
user=user1,
|
||||||
role__project=project2,
|
role__project=project2,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
membership3 = f.MembershipFactory(project=project1,
|
f.MembershipFactory(project=project1,
|
||||||
user=user2,
|
user=user2,
|
||||||
role__project=project1,
|
role__project=project1,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
membership4 = f.MembershipFactory(project=project2,
|
f.MembershipFactory(project=project2,
|
||||||
user=user3,
|
user=user3,
|
||||||
role__project=project2,
|
role__project=project2,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
|
||||||
task = f.TaskFactory.create(project=project1)
|
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})
|
url = reverse('tasks-detail', kwargs={"pk": task.pk})
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,11 @@ from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
from taiga.projects import choices as project_choices
|
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.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.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
|
||||||
from taiga.projects.occ import OCCResourceMixin
|
from taiga.projects.occ import OCCResourceMixin
|
||||||
|
|
||||||
|
@ -61,28 +65,37 @@ def data():
|
||||||
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
userstories_csv_uuid=uuid.uuid4().hex)
|
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,
|
m.private_project1 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
userstories_csv_uuid=uuid.uuid4().hex)
|
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,
|
m.private_project2 = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
userstories_csv_uuid=uuid.uuid4().hex)
|
userstories_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,
|
m.blocked_project = f.ProjectFactory(is_private=True,
|
||||||
anon_permissions=[],
|
anon_permissions=[],
|
||||||
public_permissions=[],
|
public_permissions=[],
|
||||||
owner=m.project_owner,
|
owner=m.project_owner,
|
||||||
userstories_csv_uuid=uuid.uuid4().hex,
|
userstories_csv_uuid=uuid.uuid4().hex,
|
||||||
blocked_code=project_choices.BLOCKED_BY_STAFF)
|
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,
|
m.public_membership = f.MembershipFactory(
|
||||||
|
project=m.public_project,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.public_project,
|
role__project=m.public_project,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
m.private_membership1 = f.MembershipFactory(project=m.private_project1,
|
m.private_membership1 = f.MembershipFactory(
|
||||||
|
project=m.private_project1,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.private_project1,
|
role__project=m.private_project1,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
@ -90,7 +103,8 @@ def data():
|
||||||
user=m.project_member_without_perms,
|
user=m.project_member_without_perms,
|
||||||
role__project=m.private_project1,
|
role__project=m.private_project1,
|
||||||
role__permissions=[])
|
role__permissions=[])
|
||||||
m.private_membership2 = f.MembershipFactory(project=m.private_project2,
|
m.private_membership2 = f.MembershipFactory(
|
||||||
|
project=m.private_project2,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.private_project2,
|
role__project=m.private_project2,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
@ -98,7 +112,8 @@ def data():
|
||||||
user=m.project_member_without_perms,
|
user=m.project_member_without_perms,
|
||||||
role__project=m.private_project2,
|
role__project=m.private_project2,
|
||||||
role__permissions=[])
|
role__permissions=[])
|
||||||
m.blocked_membership = f.MembershipFactory(project=m.blocked_project,
|
m.blocked_membership = f.MembershipFactory(
|
||||||
|
project=m.blocked_project,
|
||||||
user=m.project_member_with_perms,
|
user=m.project_member_with_perms,
|
||||||
role__project=m.blocked_project,
|
role__project=m.blocked_project,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
@ -150,9 +165,13 @@ def data():
|
||||||
user_story__status__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 = 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 = 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 = 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 = 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
|
return m
|
||||||
|
|
||||||
|
@ -380,24 +399,28 @@ def test_user_story_put_update_with_project_change(client):
|
||||||
project1.save()
|
project1.save()
|
||||||
project2.save()
|
project2.save()
|
||||||
|
|
||||||
membership1 = f.MembershipFactory(project=project1,
|
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,
|
user=user1,
|
||||||
role__project=project1,
|
role__project=project1,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
membership2 = f.MembershipFactory(project=project2,
|
f.MembershipFactory(project=project2,
|
||||||
user=user1,
|
user=user1,
|
||||||
role__project=project2,
|
role__project=project2,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
membership3 = f.MembershipFactory(project=project1,
|
f.MembershipFactory(project=project1,
|
||||||
user=user2,
|
user=user2,
|
||||||
role__project=project1,
|
role__project=project1,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
membership4 = f.MembershipFactory(project=project2,
|
f.MembershipFactory(project=project2,
|
||||||
user=user3,
|
user=user3,
|
||||||
role__project=project2,
|
role__project=project2,
|
||||||
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
|
||||||
|
|
||||||
us = f.UserStoryFactory.create(project=project1)
|
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})
|
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]
|
assert results == [401, 403, 403, 451]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_user_story_action_bulk_create(client, data):
|
def test_user_story_action_bulk_create(client, data):
|
||||||
url = reverse('userstories-bulk-create')
|
url = reverse('userstories-bulk-create')
|
||||||
|
|
||||||
|
|
|
@ -242,16 +242,19 @@ def test_webhook_action_test(client, data):
|
||||||
]
|
]
|
||||||
|
|
||||||
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
|
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)
|
results = helper_test_http_method(client, 'post', url1, None, users)
|
||||||
assert results == [404, 404, 200]
|
assert results == [404, 404, 200]
|
||||||
assert _send_request_mock.called is True
|
assert _send_request_mock.called is True
|
||||||
|
|
||||||
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
|
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)
|
results = helper_test_http_method(client, 'post', url2, None, users)
|
||||||
assert results == [404, 404, 404]
|
assert results == [404, 404, 404]
|
||||||
assert _send_request_mock.called is False
|
assert _send_request_mock.called is False
|
||||||
|
|
||||||
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
|
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)
|
results = helper_test_http_method(client, 'post', blocked_url, None, users)
|
||||||
assert results == [404, 404, 451]
|
assert results == [404, 404, 451]
|
||||||
assert _send_request_mock.called is False
|
assert _send_request_mock.called is False
|
||||||
|
|
|
@ -43,7 +43,7 @@ def test_update_milestone_with_userstories_list(client):
|
||||||
|
|
||||||
form_data = {
|
form_data = {
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"user_stories": [UserStorySerializer(us).data]
|
"user_stories": [{"id": us.id}]
|
||||||
}
|
}
|
||||||
|
|
||||||
client.login(user)
|
client.login(user)
|
||||||
|
|
|
@ -790,7 +790,7 @@ def test_watchers_assignation_for_issue(client):
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
issue = f.create_issue(project=project1, owner=user1)
|
issue = f.create_issue(project=project1, owner=user1)
|
||||||
data = dict(IssueSerializer(issue).data)
|
data = {}
|
||||||
data["id"] = None
|
data["id"] = None
|
||||||
data["version"] = None
|
data["version"] = None
|
||||||
data["watchers"] = [user1.pk, user2.pk]
|
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
|
# Test the impossible case when project is not
|
||||||
# exists in create request, and validator works as expected
|
# exists in create request, and validator works as expected
|
||||||
issue = f.create_issue(project=project1, owner=user1)
|
issue = f.create_issue(project=project1, owner=user1)
|
||||||
data = dict(IssueSerializer(issue).data)
|
data = {}
|
||||||
|
|
||||||
data["id"] = None
|
data["id"] = None
|
||||||
data["watchers"] = [user1.pk, user2.pk]
|
data["watchers"] = [user1.pk, user2.pk]
|
||||||
data["project"] = None
|
data["project"] = None
|
||||||
|
@ -842,10 +841,11 @@ def test_watchers_assignation_for_task(client):
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1)
|
task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1)
|
||||||
data = dict(TaskSerializer(task).data)
|
data = {
|
||||||
data["id"] = None
|
"id": None,
|
||||||
data["version"] = None
|
"version": None,
|
||||||
data["watchers"] = [user1.pk, user2.pk]
|
"watchers": [user1.pk, user2.pk]
|
||||||
|
}
|
||||||
|
|
||||||
url = reverse("tasks-list")
|
url = reverse("tasks-list")
|
||||||
response = client.json.post(url, json.dumps(data))
|
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
|
# Test the impossible case when project is not
|
||||||
# exists in create request, and validator works as expected
|
# exists in create request, and validator works as expected
|
||||||
task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1)
|
task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1)
|
||||||
data = dict(TaskSerializer(task).data)
|
data = {
|
||||||
|
"id": None,
|
||||||
data["id"] = None
|
"watchers": [user1.pk, user2.pk],
|
||||||
data["watchers"] = [user1.pk, user2.pk]
|
"project": None
|
||||||
data["project"] = None
|
}
|
||||||
|
|
||||||
url = reverse("tasks-list")
|
url = reverse("tasks-list")
|
||||||
response = client.json.post(url, json.dumps(data))
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
@ -894,10 +894,11 @@ def test_watchers_assignation_for_us(client):
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
||||||
us = f.create_userstory(project=project1, owner=user1, status__project=project1)
|
us = f.create_userstory(project=project1, owner=user1, status__project=project1)
|
||||||
data = dict(UserStorySerializer(us).data)
|
data = {
|
||||||
data["id"] = None
|
"id": None,
|
||||||
data["version"] = None
|
"version": None,
|
||||||
data["watchers"] = [user1.pk, user2.pk]
|
"watchers": [user1.pk, user2.pk]
|
||||||
|
}
|
||||||
|
|
||||||
url = reverse("userstories-list")
|
url = reverse("userstories-list")
|
||||||
response = client.json.post(url, json.dumps(data))
|
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
|
# Test the impossible case when project is not
|
||||||
# exists in create request, and validator works as expected
|
# exists in create request, and validator works as expected
|
||||||
us = f.create_userstory(project=project1, owner=user1, status__project=project1)
|
us = f.create_userstory(project=project1, owner=user1, status__project=project1)
|
||||||
data = dict(UserStorySerializer(us).data)
|
data = {
|
||||||
|
"id": None,
|
||||||
data["id"] = None
|
"watchers": [user1.pk, user2.pk],
|
||||||
data["watchers"] = [user1.pk, user2.pk]
|
"project": None
|
||||||
data["project"] = None
|
}
|
||||||
|
|
||||||
url = reverse("userstories-list")
|
url = reverse("userstories-list")
|
||||||
response = client.json.post(url, json.dumps(data))
|
response = client.json.post(url, json.dumps(data))
|
||||||
|
|
|
@ -625,7 +625,7 @@ def test_projects_user_order(client):
|
||||||
|
|
||||||
#Testing user order
|
#Testing user order
|
||||||
url = reverse("projects-list")
|
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 = client.json.get(url)
|
||||||
response_content = response.data
|
response_content = response.data
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
|
@ -30,6 +30,7 @@ from ..utils import DUMMY_BMP_DATA
|
||||||
|
|
||||||
from taiga.base.utils import json
|
from taiga.base.utils import json
|
||||||
from taiga.base.utils.thumbnails import get_thumbnail_url
|
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 import models
|
||||||
from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer
|
from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer
|
||||||
from taiga.auth.tokens import get_token_for_user
|
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]
|
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["type"] == "project"
|
||||||
assert project_watch_info["id"] == project.id
|
assert project_watch_info["id"] == project.id
|
||||||
|
@ -559,7 +560,7 @@ def test_get_liked_list_valid_info():
|
||||||
project.refresh_totals()
|
project.refresh_totals()
|
||||||
|
|
||||||
raw_project_like_info = get_liked_list(fan_user, viewer_user)[0]
|
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["type"] == "project"
|
||||||
assert project_like_info["id"] == project.id
|
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)
|
instance.add_watcher(fav_user)
|
||||||
raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0]
|
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["type"] == object_type
|
||||||
assert instance_watch_info["id"] == instance.id
|
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)
|
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]
|
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["type"] == object_type
|
||||||
assert instance_vote_info["id"] == instance.id
|
assert instance_vote_info["id"] == instance.id
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue