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