Merge pull request #777 from taigaio/extra-api-migration

Extra api migration
remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-07-06 20:28:27 +02:00 committed by GitHub
commit df2f504125
103 changed files with 3464 additions and 2154 deletions

View File

@ -22,15 +22,16 @@ from enum import Enum
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.conf import settings from django.conf import settings
from taiga.base.api import validators
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api import viewsets from taiga.base.api import viewsets
from taiga.base.decorators import list_route from taiga.base.decorators import list_route
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
from taiga.base import response from taiga.base import response
from .serializers import PublicRegisterSerializer from .validators import PublicRegisterValidator
from .serializers import PrivateRegisterForExistingUserSerializer from .validators import PrivateRegisterForExistingUserValidator
from .serializers import PrivateRegisterForNewUserSerializer from .validators import PrivateRegisterForNewUserValidator
from .services import private_register_for_existing_user from .services import private_register_for_existing_user
from .services import private_register_for_new_user from .services import private_register_for_new_user
@ -44,7 +45,7 @@ from .permissions import AuthPermission
def _parse_data(data:dict, *, cls): def _parse_data(data:dict, *, cls):
""" """
Generic function for parse user data using Generic function for parse user data using
specified serializer on `cls` keyword parameter. specified validator on `cls` keyword parameter.
Raises: RequestValidationError exception if Raises: RequestValidationError exception if
some errors found when data is validated. some errors found when data is validated.
@ -52,21 +53,21 @@ def _parse_data(data:dict, *, cls):
Returns the parsed data. Returns the parsed data.
""" """
serializer = cls(data=data) validator = cls(data=data)
if not serializer.is_valid(): if not validator.is_valid():
raise exc.RequestValidationError(serializer.errors) raise exc.RequestValidationError(validator.errors)
return serializer.data return validator.data
# Parse public register data # Parse public register data
parse_public_register_data = partial(_parse_data, cls=PublicRegisterSerializer) parse_public_register_data = partial(_parse_data, cls=PublicRegisterValidator)
# Parse private register data for existing user # Parse private register data for existing user
parse_private_register_for_existing_user_data = \ parse_private_register_for_existing_user_data = \
partial(_parse_data, cls=PrivateRegisterForExistingUserSerializer) partial(_parse_data, cls=PrivateRegisterForExistingUserValidator)
# Parse private register data for new user # Parse private register data for new user
parse_private_register_for_new_user_data = \ parse_private_register_for_new_user_data = \
partial(_parse_data, cls=PrivateRegisterForNewUserSerializer) partial(_parse_data, cls=PrivateRegisterForNewUserValidator)
class RegisterTypeEnum(Enum): class RegisterTypeEnum(Enum):
@ -81,10 +82,10 @@ def parse_register_type(userdata:dict) -> str:
""" """
# Create adhoc inner serializer for avoid parse # Create adhoc inner serializer for avoid parse
# manually the user data. # manually the user data.
class _serializer(serializers.Serializer): class _validator(validators.Validator):
existing = serializers.BooleanField() existing = serializers.BooleanField()
instance = _serializer(data=userdata) instance = _validator(data=userdata)
if not instance.is_valid(): if not instance.is_valid():
raise exc.RequestValidationError(instance.errors) raise exc.RequestValidationError(instance.errors)

View File

@ -16,16 +16,17 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core import validators from django.core import validators as core_validators
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.exceptions import ValidationError
import re import re
class BaseRegisterSerializer(serializers.Serializer): class BaseRegisterValidator(validators.Validator):
full_name = serializers.CharField(max_length=256) full_name = serializers.CharField(max_length=256)
email = serializers.EmailField(max_length=255) email = serializers.EmailField(max_length=255)
username = serializers.CharField(max_length=255) username = serializers.CharField(max_length=255)
@ -33,25 +34,25 @@ class BaseRegisterSerializer(serializers.Serializer):
def validate_username(self, attrs, source): def validate_username(self, attrs, source):
value = attrs[source] value = attrs[source]
validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), "invalid") validator = core_validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"), "invalid")
try: try:
validator(value) validator(value)
except ValidationError: except ValidationError:
raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, numbers " raise ValidationError(_("Required. 255 characters or fewer. Letters, numbers "
"and /./-/_ characters'")) "and /./-/_ characters'"))
return attrs return attrs
class PublicRegisterSerializer(BaseRegisterSerializer): class PublicRegisterValidator(BaseRegisterValidator):
pass pass
class PrivateRegisterForNewUserSerializer(BaseRegisterSerializer): class PrivateRegisterForNewUserValidator(BaseRegisterValidator):
token = serializers.CharField(max_length=255, required=True) token = serializers.CharField(max_length=255, required=True)
class PrivateRegisterForExistingUserSerializer(serializers.Serializer): class PrivateRegisterForExistingUserValidator(validators.Validator):
username = serializers.CharField(max_length=255) username = serializers.CharField(max_length=255)
password = serializers.CharField(min_length=4) password = serializers.CharField(min_length=4)
token = serializers.CharField(max_length=255, required=True) token = serializers.CharField(max_length=255, required=True)

View File

@ -50,7 +50,6 @@ They are very similar to Django's form fields.
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError
from django.db.models.fields import BLANK_CHOICE_DASH from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms import widgets from django.forms import widgets
from django.http import QueryDict from django.http import QueryDict
@ -66,6 +65,8 @@ from django.utils.functional import Promise
from django.utils.translation import ugettext from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from taiga.base.exceptions import ValidationError
from . import ISO_8601 from . import ISO_8601
from .settings import api_settings from .settings import api_settings

View File

@ -62,6 +62,7 @@ class GenericAPIView(pagination.PaginationMixin,
# or override `get_queryset()`/`get_serializer_class()`. # or override `get_queryset()`/`get_serializer_class()`.
queryset = None queryset = None
serializer_class = None serializer_class = None
validator_class = None
# This shortcut may be used instead of setting either or both # This shortcut may be used instead of setting either or both
# of the `queryset`/`serializer_class` attributes, although using # of the `queryset`/`serializer_class` attributes, although using
@ -79,6 +80,7 @@ class GenericAPIView(pagination.PaginationMixin,
# The following attributes may be subject to change, # The following attributes may be subject to change,
# and should be considered private API. # and should be considered private API.
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
model_validator_class = api_settings.DEFAULT_MODEL_VALIDATOR_CLASS
###################################### ######################################
# These are pending deprecation... # These are pending deprecation...
@ -88,7 +90,7 @@ class GenericAPIView(pagination.PaginationMixin,
slug_field = 'slug' slug_field = 'slug'
allow_empty = True allow_empty = True
def get_serializer_context(self): def get_extra_context(self):
""" """
Extra context provided to the serializer class. Extra context provided to the serializer class.
""" """
@ -101,14 +103,24 @@ class GenericAPIView(pagination.PaginationMixin,
def get_serializer(self, instance=None, data=None, def get_serializer(self, instance=None, data=None,
files=None, many=False, partial=False): files=None, many=False, partial=False):
""" """
Return the serializer instance that should be used for validating and Return the serializer instance that should be used for deserializing
deserializing input, and for serializing output. input, and for serializing output.
""" """
serializer_class = self.get_serializer_class() serializer_class = self.get_serializer_class()
context = self.get_serializer_context() context = self.get_extra_context()
return serializer_class(instance, data=data, files=files, return serializer_class(instance, data=data, files=files,
many=many, partial=partial, context=context) many=many, partial=partial, context=context)
def get_validator(self, instance=None, data=None,
files=None, many=False, partial=False):
"""
Return the validator instance that should be used for validating the
input, and for serializing output.
"""
validator_class = self.get_validator_class()
context = self.get_extra_context()
return validator_class(instance, data=data, files=files,
many=many, partial=partial, context=context)
def filter_queryset(self, queryset, filter_backends=None): def filter_queryset(self, queryset, filter_backends=None):
""" """
@ -119,7 +131,7 @@ class GenericAPIView(pagination.PaginationMixin,
method if you want to apply the configured filtering backend to the method if you want to apply the configured filtering backend to the
default queryset. default queryset.
""" """
#NOTE TAIGA: Added filter_backends to overwrite the default behavior. # NOTE TAIGA: Added filter_backends to overwrite the default behavior.
backends = filter_backends or self.get_filter_backends() backends = filter_backends or self.get_filter_backends()
for backend in backends: for backend in backends:
@ -160,6 +172,22 @@ class GenericAPIView(pagination.PaginationMixin,
model = self.model model = self.model
return DefaultSerializer return DefaultSerializer
def get_validator_class(self):
validator_class = self.validator_class
serializer_class = self.get_serializer_class()
# Situations where the validator is the rest framework serializer
if validator_class is None and serializer_class is not None:
return serializer_class
if validator_class is not None:
return validator_class
class DefaultValidator(self.model_validator_class):
class Meta:
model = self.model
return DefaultValidator
def get_queryset(self): def get_queryset(self):
""" """
Get the list of items for this view. Get the list of items for this view.

View File

@ -44,12 +44,12 @@
import warnings import warnings
from django.core.exceptions import ValidationError
from django.http import Http404 from django.http import Http404
from django.db import transaction as tx from django.db import transaction as tx
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base import response from taiga.base import response
from taiga.base.exceptions import ValidationError
from .settings import api_settings from .settings import api_settings
from .utils import get_object_or_404 from .utils import get_object_or_404
@ -57,6 +57,7 @@ from .utils import get_object_or_404
from .. import exceptions as exc from .. import exceptions as exc
from ..decorators import model_pk_lock from ..decorators import model_pk_lock
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None): def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
""" """
Given a model instance, and an optional pk and slug field, Given a model instance, and an optional pk and slug field,
@ -89,19 +90,21 @@ class CreateModelMixin:
Create a model instance. Create a model instance.
""" """
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES) validator = self.get_validator(data=request.DATA, files=request.FILES)
if serializer.is_valid(): if validator.is_valid():
self.check_permissions(request, 'create', serializer.object) self.check_permissions(request, 'create', validator.object)
self.pre_save(serializer.object) self.pre_save(validator.object)
self.pre_conditions_on_save(serializer.object) self.pre_conditions_on_save(validator.object)
self.object = serializer.save(force_insert=True) self.object = validator.save(force_insert=True)
self.post_save(self.object, created=True) self.post_save(self.object, created=True)
instance = self.get_queryset().get(id=self.object.id)
serializer = self.get_serializer(instance)
headers = self.get_success_headers(serializer.data) headers = self.get_success_headers(serializer.data)
return response.Created(serializer.data, headers=headers) return response.Created(serializer.data, headers=headers)
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
def get_success_headers(self, data): def get_success_headers(self, data):
try: try:
@ -171,28 +174,32 @@ class UpdateModelMixin:
if self.object is None: if self.object is None:
raise Http404 raise Http404
serializer = self.get_serializer(self.object, data=request.DATA, validator = self.get_validator(self.object, data=request.DATA,
files=request.FILES, partial=partial) files=request.FILES, partial=partial)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
# Hooks # Hooks
try: try:
self.pre_save(serializer.object) self.pre_save(validator.object)
self.pre_conditions_on_save(serializer.object) self.pre_conditions_on_save(validator.object)
except ValidationError as err: except ValidationError as err:
# full_clean on model instance may be called in pre_save, # full_clean on model instance may be called in pre_save,
# so we have to handle eventual errors. # so we have to handle eventual errors.
return response.BadRequest(err.message_dict) return response.BadRequest(err.message_dict)
if self.object is None: if self.object is None:
self.object = serializer.save(force_insert=True) self.object = validator.save(force_insert=True)
self.post_save(self.object, created=True) self.post_save(self.object, created=True)
instance = self.get_queryset().get(id=self.object.id)
serializer = self.get_serializer(instance)
return response.Created(serializer.data) return response.Created(serializer.data)
self.object = serializer.save(force_update=True) self.object = validator.save(force_update=True)
self.post_save(self.object, created=False) self.post_save(self.object, created=False)
instance = self.get_queryset().get(id=self.object.id)
serializer = self.get_serializer(instance)
return response.Ok(serializer.data) return response.Ok(serializer.data)
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
@ -251,7 +258,7 @@ class BlockeableModelMixin:
raise NotImplementedError("is_blocked must be overridden") raise NotImplementedError("is_blocked must be overridden")
def pre_conditions_blocked(self, obj): def pre_conditions_blocked(self, obj):
#Raises permission exception # Raises permission exception
if obj is not None and self.is_blocked(obj): if obj is not None and self.is_blocked(obj):
raise exc.Blocked(_("Blocked element")) raise exc.Blocked(_("Blocked element"))

View File

@ -48,7 +48,7 @@ Serializer fields that deal with relationships.
These fields allow you to specify the style that should be used to represent These fields allow you to specify the style that should be used to represent
model relationships, including hyperlinks, primary keys, or slugs. model relationships, including hyperlinks, primary keys, or slugs.
""" """
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django import forms from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH from django.db.models.fields import BLANK_CHOICE_DASH
@ -59,6 +59,7 @@ from django.utils.translation import ugettext_lazy as _
from .fields import Field, WritableField, get_component, is_simple_callable from .fields import Field, WritableField, get_component, is_simple_callable
from .reverse import reverse from .reverse import reverse
from taiga.base.exceptions import ValidationError
import warnings import warnings
from urllib import parse as urlparse from urllib import parse as urlparse

View File

@ -78,6 +78,8 @@ import serpy
# This helps keep the separation between model fields, form fields, and # This helps keep the separation between model fields, form fields, and
# serializer fields more explicit. # serializer fields more explicit.
from taiga.base.exceptions import ValidationError
from .relations import * from .relations import *
from .fields import * from .fields import *
@ -1228,4 +1230,8 @@ class LightSerializer(serpy.Serializer):
kwargs.pop("read_only", None) kwargs.pop("read_only", None)
kwargs.pop("partial", None) kwargs.pop("partial", None)
kwargs.pop("files", None) kwargs.pop("files", None)
context = kwargs.pop("context", {})
view = kwargs.pop("view", {})
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.context = context
self.view = view

View File

@ -98,6 +98,8 @@ DEFAULTS = {
# Genric view behavior # Genric view behavior
"DEFAULT_MODEL_SERIALIZER_CLASS": "DEFAULT_MODEL_SERIALIZER_CLASS":
"taiga.base.api.serializers.ModelSerializer", "taiga.base.api.serializers.ModelSerializer",
"DEFAULT_MODEL_VALIDATOR_CLASS":
"taiga.base.api.validators.ModelValidator",
"DEFAULT_FILTER_BACKENDS": (), "DEFAULT_FILTER_BACKENDS": (),
# Throttling # Throttling

View File

@ -16,15 +16,12 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from . import serializers
class FanResourceSerializerMixin(serializers.ModelSerializer): class Validator(serializers.Serializer):
is_fan = serializers.SerializerMethodField("get_is_fan") pass
def get_is_fan(self, obj):
if "request" in self.context:
user = self.context["request"].user
return user.is_authenticated() and user.is_fan(obj)
return False class ModelValidator(serializers.ModelSerializer):
pass

View File

@ -51,6 +51,7 @@ In addition Django's built in 403 and 404 exceptions are handled.
""" """
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.http import Http404 from django.http import Http404
@ -224,6 +225,7 @@ class NotEnoughSlotsForProject(BaseException):
"total_memberships": total_memberships "total_memberships": total_memberships
} }
def format_exception(exc): def format_exception(exc):
if isinstance(exc.detail, (dict, list, tuple,)): if isinstance(exc.detail, (dict, list, tuple,)):
detail = exc.detail detail = exc.detail
@ -270,3 +272,6 @@ def exception_handler(exc):
# Note: Unhandled exceptions will raise a 500 error. # Note: Unhandled exceptions will raise a 500 error.
return None return None
ValidationError = DjangoValidationError

View File

@ -20,10 +20,14 @@ from django.forms import widgets
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
import serpy
#################################################################### ####################################################################
## Serializer fields # DRF Serializer fields (OLD)
#################################################################### ####################################################################
# NOTE: This should be in other place, for example taiga.base.api.serializers
class JsonField(serializers.WritableField): class JsonField(serializers.WritableField):
""" """
@ -38,40 +42,6 @@ class JsonField(serializers.WritableField):
return data return data
class I18NJsonField(JsonField):
"""
Json objects serializer.
"""
widget = widgets.Textarea
def __init__(self, i18n_fields=(), *args, **kwargs):
super(I18NJsonField, self).__init__(*args, **kwargs)
self.i18n_fields = i18n_fields
def translate_values(self, d):
i18n_d = {}
if d is None:
return d
for key, value in d.items():
if isinstance(value, dict):
i18n_d[key] = self.translate_values(value)
if key in self.i18n_fields:
if isinstance(value, list):
i18n_d[key] = [e is not None and _(str(e)) or e for e in value]
if isinstance(value, str):
i18n_d[key] = value is not None and _(value) or value
else:
i18n_d[key] = value
return i18n_d
def to_native(self, obj):
i18n_obj = self.translate_values(obj)
return i18n_obj
class PgArrayField(serializers.WritableField): class PgArrayField(serializers.WritableField):
""" """
PgArray objects serializer. PgArray objects serializer.
@ -104,3 +74,58 @@ class WatchersField(serializers.WritableField):
def from_native(self, data): def from_native(self, data):
return data return data
####################################################################
# Serpy fields (NEW)
####################################################################
class Field(serpy.Field):
pass
class MethodField(serpy.MethodField):
pass
class I18NField(Field):
def to_value(self, value):
ret = super(I18NField, self).to_value(value)
return _(ret)
class I18NJsonField(Field):
"""
Json objects serializer.
"""
def __init__(self, i18n_fields=(), *args, **kwargs):
super(I18NJsonField, self).__init__(*args, **kwargs)
self.i18n_fields = i18n_fields
def translate_values(self, d):
i18n_d = {}
if d is None:
return d
for key, value in d.items():
if isinstance(value, dict):
i18n_d[key] = self.translate_values(value)
if key in self.i18n_fields:
if isinstance(value, list):
i18n_d[key] = [e is not None and _(str(e)) or e for e in value]
if isinstance(value, str):
i18n_d[key] = value is not None and _(value) or value
else:
i18n_d[key] = value
return i18n_d
def to_native(self, obj):
i18n_obj = self.translate_values(obj)
return i18n_obj
class FileField(Field):
def to_value(self, value):
return value.name

View File

@ -23,6 +23,7 @@ from django.db import connection
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models.sql.datastructures import EmptyResultSet from django.db.models.sql.datastructures import EmptyResultSet
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import Field, MethodField
Neighbor = namedtuple("Neighbor", "left right") Neighbor = namedtuple("Neighbor", "left right")
@ -71,7 +72,6 @@ def get_neighbors(obj, results_set=None):
if row is None: if row is None:
return Neighbor(None, None) return Neighbor(None, None)
obj_position = row[1] - 1
left_object_id = row[2] left_object_id = row[2]
right_object_id = row[3] right_object_id = row[3]
@ -88,13 +88,19 @@ def get_neighbors(obj, results_set=None):
return Neighbor(left, right) return Neighbor(left, right)
class NeighborsSerializerMixin: class NeighborSerializer(serializers.LightSerializer):
def __init__(self, *args, **kwargs): id = Field()
super().__init__(*args, **kwargs) ref = Field()
self.fields["neighbors"] = serializers.SerializerMethodField("get_neighbors") subject = Field()
class NeighborsSerializerMixin(serializers.LightSerializer):
neighbors = MethodField()
def serialize_neighbor(self, neighbor): def serialize_neighbor(self, neighbor):
raise NotImplementedError if neighbor:
return NeighborSerializer(neighbor).data
return None
def get_neighbors(self, obj): def get_neighbors(self, obj):
view, request = self.context.get("view", None), self.context.get("request", None) view, request = self.context.get("view", None), self.context.get("request", None)

View File

@ -25,3 +25,7 @@ def dict_sum(*args):
assert isinstance(arg, dict) assert isinstance(arg, dict)
result += collections.Counter(arg) result += collections.Counter(arg)
return result return result
def into_namedtuple(dictionary):
return collections.namedtuple('GenericDict', dictionary.keys())(**dictionary)

View File

@ -34,6 +34,7 @@ from taiga.base import exceptions as exc
from taiga.base import response from taiga.base import response
from taiga.base.api.mixins import CreateModelMixin from taiga.base.api.mixins import CreateModelMixin
from taiga.base.api.viewsets import GenericViewSet from taiga.base.api.viewsets import GenericViewSet
from taiga.projects import utils as project_utils
from taiga.projects.models import Project, Membership from taiga.projects.models import Project, Membership
from taiga.projects.issues.models import Issue from taiga.projects.issues.models import Issue
from taiga.projects.tasks.models import Task from taiga.projects.tasks.models import Task
@ -366,5 +367,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
return response.BadRequest({"error": e.message, "details": e.errors}) return response.BadRequest({"error": e.message, "details": e.errors})
else: else:
# On Success # On Success
response_data = ProjectSerializer(project).data project_from_qs = project_utils.attach_extra_info(Project.objects.all()).get(id=project.id)
response_data = ProjectSerializer(project_from_qs).data
return response.Created(response_data) return response.Created(response_data)

View File

@ -23,11 +23,11 @@ from collections import OrderedDict
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.exceptions import ValidationError
from taiga.base.fields import JsonField from taiga.base.fields import JsonField
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.users import models as users_models from taiga.users import models as users_models

View File

@ -16,13 +16,11 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import copy
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import JsonField, PgArrayField from taiga.base.fields import JsonField, PgArrayField
from taiga.base.exceptions import ValidationError
from taiga.projects import models as projects_models from taiga.projects import models as projects_models
from taiga.projects.custom_attributes import models as custom_attributes_models from taiga.projects.custom_attributes import models as custom_attributes_models
@ -31,15 +29,12 @@ from taiga.projects.tasks import models as tasks_models
from taiga.projects.issues import models as issues_models from taiga.projects.issues import models as issues_models
from taiga.projects.milestones import models as milestones_models from taiga.projects.milestones import models as milestones_models
from taiga.projects.wiki import models as wiki_models from taiga.projects.wiki import models as wiki_models
from taiga.projects.history import models as history_models
from taiga.projects.attachments import models as attachments_models
from taiga.timeline import models as timeline_models from taiga.timeline import models as timeline_models
from taiga.users import models as users_models from taiga.users import models as users_models
from taiga.projects.votes import services as votes_service from taiga.projects.votes import services as votes_service
from .fields import (FileField, RelatedNoneSafeField, UserRelatedField, from .fields import (FileField, UserRelatedField,
UserPkField, CommentField, ProjectRelatedField, ProjectRelatedField,
HistoryUserField, HistoryValuesField, HistoryDiffField,
TimelineDataField, ContentTypeField) TimelineDataField, ContentTypeField)
from .mixins import (HistoryExportSerializerMixin, from .mixins import (HistoryExportSerializerMixin,
AttachmentExportSerializerMixin, AttachmentExportSerializerMixin,
@ -125,7 +120,7 @@ class IssueCustomAttributeExportSerializer(serializers.ModelSerializer):
class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer): class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer):
attributes_values = JsonField(source="attributes_values",required=True) attributes_values = JsonField(source="attributes_values", required=True)
_custom_attribute_model = None _custom_attribute_model = None
_container_field = None _container_field = None
@ -158,6 +153,7 @@ class BaseCustomAttributesValuesExportSerializer(serializers.ModelSerializer):
return attrs return attrs
class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer): class UserStoryCustomAttributesValuesExportSerializer(BaseCustomAttributesValuesExportSerializer):
_custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute _custom_attribute_model = custom_attributes_models.UserStoryCustomAttribute
_container_model = "userstories.UserStory" _container_model = "userstories.UserStory"
@ -224,7 +220,7 @@ class MilestoneExportSerializer(WatcheableObjectModelSerializerMixin):
name = attrs[source] name = attrs[source]
qs = self.project.milestones.filter(name=name) qs = self.project.milestones.filter(name=name)
if qs.exists(): if qs.exists():
raise serializers.ValidationError(_("Name duplicated for the project")) raise ValidationError(_("Name duplicated for the project"))
return attrs return attrs
@ -268,7 +264,9 @@ class UserStoryExportSerializer(CustomAttributesValuesExportSerializerMixin, His
def custom_attributes_queryset(self, project): def custom_attributes_queryset(self, project):
if project.id not in _custom_userstories_attributes_cache: if project.id not in _custom_userstories_attributes_cache:
_custom_userstories_attributes_cache[project.id] = list(project.userstorycustomattributes.all().values('id', 'name')) _custom_userstories_attributes_cache[project.id] = list(
project.userstorycustomattributes.all().values('id', 'name')
)
return _custom_userstories_attributes_cache[project.id] return _custom_userstories_attributes_cache[project.id]
@ -314,10 +312,10 @@ class WikiLinkExportSerializer(serializers.ModelSerializer):
exclude = ('id', 'project') exclude = ('id', 'project')
class TimelineExportSerializer(serializers.ModelSerializer): class TimelineExportSerializer(serializers.ModelSerializer):
data = TimelineDataField() data = TimelineDataField()
data_content_type = ContentTypeField() data_content_type = ContentTypeField()
class Meta: class Meta:
model = timeline_models.Timeline model = timeline_models.Timeline
exclude = ('id', 'project', 'namespace', 'object_id', 'content_type') exclude = ('id', 'project', 'namespace', 'object_id', 'content_type')

View File

@ -17,6 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from . import serializers from . import serializers
from . import validators
from . import models from . import models
from . import permissions from . import permissions
from . import services from . import services
@ -27,12 +28,12 @@ from taiga.base.api import ModelCrudViewSet, ModelRetrieveViewSet
from taiga.base.api.utils import get_object_or_404 from taiga.base.api.utils import get_object_or_404
from taiga.base.decorators import list_route, detail_route from taiga.base.decorators import list_route, detail_route
from django.db import transaction
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class Application(ModelRetrieveViewSet): class Application(ModelRetrieveViewSet):
serializer_class = serializers.ApplicationSerializer serializer_class = serializers.ApplicationSerializer
validator_class = validators.ApplicationValidator
permission_classes = (permissions.ApplicationPermission,) permission_classes = (permissions.ApplicationPermission,)
model = models.Application model = models.Application
@ -61,6 +62,7 @@ class Application(ModelRetrieveViewSet):
class ApplicationToken(ModelCrudViewSet): class ApplicationToken(ModelCrudViewSet):
serializer_class = serializers.ApplicationTokenSerializer serializer_class = serializers.ApplicationTokenSerializer
validator_class = validators.ApplicationTokenValidator
permission_classes = (permissions.ApplicationTokenPermission,) permission_classes = (permissions.ApplicationTokenPermission,)
def get_queryset(self): def get_queryset(self):
@ -87,9 +89,9 @@ class ApplicationToken(ModelCrudViewSet):
auth_code = request.DATA.get("auth_code", None) auth_code = request.DATA.get("auth_code", None)
state = request.DATA.get("state", None) state = request.DATA.get("state", None)
application_token = get_object_or_404(models.ApplicationToken, application_token = get_object_or_404(models.ApplicationToken,
application__id=application_id, application__id=application_id,
auth_code=auth_code, auth_code=auth_code,
state=state) state=state)
application_token.generate_token() application_token.generate_token()
application_token.save() application_token.save()

View File

@ -16,9 +16,8 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import Field
from . import models from . import models
from . import services from . import services
@ -26,33 +25,27 @@ from . import services
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
class ApplicationSerializer(serializers.ModelSerializer): class ApplicationSerializer(serializers.LightSerializer):
class Meta: id = Field()
model = models.Application name = Field()
fields = ("id", "name", "web", "description", "icon_url") web = Field()
description = Field()
icon_url = Field()
class ApplicationTokenSerializer(serializers.ModelSerializer): class ApplicationTokenSerializer(serializers.LightSerializer):
cyphered_token = serializers.CharField(source="cyphered_token", read_only=True) id = Field()
next_url = serializers.CharField(source="next_url", read_only=True) user = Field(attr="user_id")
application = ApplicationSerializer(read_only=True) application = ApplicationSerializer()
auth_code = Field()
class Meta: next_url = Field()
model = models.ApplicationToken
fields = ("user", "id", "application", "auth_code", "next_url")
class AuthorizationCodeSerializer(serializers.ModelSerializer): class AuthorizationCodeSerializer(serializers.LightSerializer):
next_url = serializers.CharField(source="next_url", read_only=True) state = Field()
class Meta: auth_code = Field()
model = models.ApplicationToken next_url = Field()
fields = ("auth_code", "state", "next_url")
class AccessTokenSerializer(serializers.ModelSerializer): class AccessTokenSerializer(serializers.LightSerializer):
cyphered_token = serializers.CharField(source="cyphered_token", read_only=True) cyphered_token = Field()
next_url = serializers.CharField(source="next_url", read_only=True)
class Meta:
model = models.ApplicationToken
fields = ("cyphered_token", )

View File

@ -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", )

View File

@ -20,7 +20,7 @@ from taiga.base import response
from taiga.base.api import viewsets from taiga.base.api import viewsets
from . import permissions from . import permissions
from . import serializers from . import validators
from . import services from . import services
import copy import copy
@ -28,7 +28,7 @@ import copy
class FeedbackViewSet(viewsets.ViewSet): class FeedbackViewSet(viewsets.ViewSet):
permission_classes = (permissions.FeedbackPermission,) permission_classes = (permissions.FeedbackPermission,)
serializer_class = serializers.FeedbackEntrySerializer validator_class = validators.FeedbackEntryValidator
def create(self, request, **kwargs): def create(self, request, **kwargs):
self.check_permissions(request, "create", None) self.check_permissions(request, "create", None)
@ -37,11 +37,11 @@ class FeedbackViewSet(viewsets.ViewSet):
data.update({"full_name": request.user.get_full_name(), data.update({"full_name": request.user.get_full_name(),
"email": request.user.email}) "email": request.user.email})
serializer = self.serializer_class(data=data) validator = self.validator_class(data=data)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
self.object = serializer.save(force_insert=True) self.object = validator.save(force_insert=True)
extra = { extra = {
"HTTP_HOST": request.META.get("HTTP_HOST", None), "HTTP_HOST": request.META.get("HTTP_HOST", None),
@ -50,4 +50,4 @@ class FeedbackViewSet(viewsets.ViewSet):
} }
services.send_feedback(self.object, extra, reply_to=[request.user.email]) services.send_feedback(self.object, extra, reply_to=[request.user.email])
return response.Ok(serializer.data) return response.Ok(validator.data)

View File

@ -16,11 +16,11 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import validators
from . import models from . import models
class FeedbackEntrySerializer(serializers.ModelSerializer): class FeedbackEntryValidator(validators.ModelValidator):
class Meta: class Meta:
model = models.FeedbackEntry model = models.FeedbackEntry

View File

@ -91,39 +91,55 @@ def _get_membership_permissions(membership):
return [] return []
def calculate_permissions(is_authenticated=False, is_superuser=False, is_member=False,
is_admin=False, role_permissions=[], anon_permissions=[],
public_permissions=[]):
if is_superuser:
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
public_permissions = []
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS))
elif is_member:
if is_admin:
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS))
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS))
else:
admins_permissions = []
members_permissions = []
members_permissions = members_permissions + role_permissions
public_permissions = public_permissions if public_permissions is not None else []
anon_permissions = anon_permissions if anon_permissions is not None else []
elif is_authenticated:
admins_permissions = []
members_permissions = []
public_permissions = public_permissions if public_permissions is not None else []
anon_permissions = anon_permissions if anon_permissions is not None else []
else:
admins_permissions = []
members_permissions = []
public_permissions = []
anon_permissions = anon_permissions if anon_permissions is not None else []
return set(admins_permissions + members_permissions + public_permissions + anon_permissions)
def get_user_project_permissions(user, project, cache="user"): def get_user_project_permissions(user, project, cache="user"):
""" """
cache param determines how memberships are calculated trying to reuse the existing data cache param determines how memberships are calculated trying to reuse the existing data
in cache in cache
""" """
membership = _get_user_project_membership(user, project, cache=cache) membership = _get_user_project_membership(user, project, cache=cache)
if user.is_superuser: is_member = membership is not None
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) is_admin = is_member and membership.is_admin
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) return calculate_permissions(
public_permissions = [] is_authenticated = user.is_authenticated(),
anon_permissions = list(map(lambda perm: perm[0], ANON_PERMISSIONS)) is_superuser = user.is_superuser,
elif membership: is_member = is_member,
if membership.is_admin: is_admin = is_admin,
admins_permissions = list(map(lambda perm: perm[0], ADMINS_PERMISSIONS)) role_permissions = _get_membership_permissions(membership),
members_permissions = list(map(lambda perm: perm[0], MEMBERS_PERMISSIONS)) anon_permissions = project.anon_permissions,
else: public_permissions = project.public_permissions
admins_permissions = [] )
members_permissions = []
members_permissions = members_permissions + _get_membership_permissions(membership)
public_permissions = project.public_permissions if project.public_permissions is not None else []
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
elif user.is_authenticated():
admins_permissions = []
members_permissions = []
public_permissions = project.public_permissions if project.public_permissions is not None else []
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
else:
admins_permissions = []
members_permissions = []
public_permissions = []
anon_permissions = project.anon_permissions if project.anon_permissions is not None else []
return set(admins_permissions + members_permissions + public_permissions + anon_permissions)
def set_base_permissions_for_project(project): def set_base_permissions_for_project(project):

View File

@ -22,10 +22,6 @@ from dateutil.relativedelta import relativedelta
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import signals, Prefetch
from django.db.models import Value as V
from django.db.models.functions import Coalesce
from django.http import Http404 from django.http import Http404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils import timezone from django.utils import timezone
@ -45,8 +41,7 @@ from taiga.permissions import services as permissions_services
from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.issues.models import Issue from taiga.projects.issues.models import Issue
from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin from taiga.projects.likes.mixins.viewsets import LikedResourceMixin, FansViewSetMixin
from taiga.projects.notifications.models import NotifyPolicy from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
from taiga.projects.notifications.choices import NotifyLevel from taiga.projects.notifications.choices import NotifyLevel
from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin from taiga.projects.mixins.on_destroy import MoveOnDestroyMixin
from taiga.projects.mixins.ordering import BulkUpdateOrderMixin from taiga.projects.mixins.ordering import BulkUpdateOrderMixin
@ -54,27 +49,28 @@ from taiga.projects.tasks.models import Task
from taiga.projects.tagging.api import TagsColorsResourceMixin from taiga.projects.tagging.api import TagsColorsResourceMixin
from taiga.projects.userstories.models import UserStory, RolePoints from taiga.projects.userstories.models import UserStory, RolePoints
from taiga.users import services as users_services
from . import filters as project_filters from . import filters as project_filters
from . import models from . import models
from . import permissions from . import permissions
from . import serializers from . import serializers
from . import validators
from . import services from . import services
from . import utils as project_utils
###################################################### ######################################################
## Project # Project
###################################################### ######################################################
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin,
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
BlockeableSaveMixin, BlockeableDeleteMixin,
TagsColorsResourceMixin, ModelCrudViewSet): TagsColorsResourceMixin, ModelCrudViewSet):
validator_class = validators.ProjectValidator
queryset = models.Project.objects.all() queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer
list_serializer_class = serializers.ProjectSerializer
permission_classes = (permissions.ProjectPermission, ) permission_classes = (permissions.ProjectPermission, )
filter_backends = (project_filters.QFilterBackend, filter_backends = (project_filters.UserOrderFilterBackend,
project_filters.QFilterBackend,
project_filters.CanViewProjectObjFilterBackend, project_filters.CanViewProjectObjFilterBackend,
project_filters.DiscoverModeFilterBackend) project_filters.DiscoverModeFilterBackend)
@ -85,8 +81,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
"is_kanban_activated") "is_kanban_activated")
ordering = ("name", "id") ordering = ("name", "id")
order_by_fields = ("memberships__user_order", order_by_fields = ("total_fans",
"total_fans",
"total_fans_last_week", "total_fans_last_week",
"total_fans_last_month", "total_fans_last_month",
"total_fans_last_year", "total_fans_last_year",
@ -106,18 +101,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.select_related("owner") qs = qs.select_related("owner")
# Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries) qs = project_utils.attach_extra_info(qs, user=self.request.user)
# so we add some custom prefetching
qs = qs.prefetch_related("members")
qs = qs.prefetch_related("memberships")
qs = qs.prefetch_related(Prefetch("notify_policies",
NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
Milestone = apps.get_model("milestones", "Milestone")
qs = qs.prefetch_related(Prefetch("milestones",
Milestone.objects.filter(closed=True), to_attr="closed_milestones"))
# If filtering an activity period we must exclude the activities not updated recently enough # If filtering an activity period we must exclude the activities not updated recently enough
now = timezone.now() now = timezone.now()
@ -137,22 +122,17 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
return qs return qs
def retrieve(self, request, *args, **kwargs):
if self.action == "by_slug":
self.lookup_field = "slug"
return super().retrieve(request, *args, **kwargs)
def get_serializer_class(self): def get_serializer_class(self):
serializer_class = self.serializer_class
if self.action == "list": if self.action == "list":
serializer_class = self.list_serializer_class return serializers.ProjectSerializer
elif self.action != "create":
if self.action == "by_slug":
slug = self.request.QUERY_PARAMS.get("slug", None)
project = get_object_or_404(models.Project, slug=slug)
else:
project = self.get_object()
if permissions_services.is_project_admin(self.request.user, project): return serializers.ProjectDetailSerializer
serializer_class = self.admin_serializer_class
return serializer_class
@detail_route(methods=["POST"]) @detail_route(methods=["POST"])
def change_logo(self, request, *args, **kwargs): def change_logo(self, request, *args, **kwargs):
@ -215,11 +195,11 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
if self.request.user.is_anonymous(): if self.request.user.is_anonymous():
return response.Unauthorized() return response.Unauthorized()
serializer = serializers.UpdateProjectOrderBulkSerializer(data=request.DATA, many=True) validator = validators.UpdateProjectOrderBulkValidator(data=request.DATA, many=True)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
data = serializer.data data = validator.data
services.update_projects_order_in_bulk(data, "user_order", request.user) services.update_projects_order_in_bulk(data, "user_order", request.user)
return response.NoContent(data=None) return response.NoContent(data=None)
@ -283,10 +263,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
return response.Ok(data) return response.Ok(data)
@list_route(methods=["GET"]) @list_route(methods=["GET"])
def by_slug(self, request): def by_slug(self, request, *args, **kwargs):
slug = request.QUERY_PARAMS.get("slug", None) slug = request.QUERY_PARAMS.get("slug", None)
project = get_object_or_404(models.Project, slug=slug) return self.retrieve(request, slug=slug)
return self.retrieve(request, pk=project.pk)
@detail_route(methods=["GET", "PATCH"]) @detail_route(methods=["GET", "PATCH"])
def modules(self, request, pk=None): def modules(self, request, pk=None):
@ -362,7 +341,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
return response.BadRequest(_("The user must be already a project member")) return response.BadRequest(_("The user must be already a project member"))
reason = request.DATA.get('reason', None) reason = request.DATA.get('reason', None)
transfer_token = services.start_project_transfer(project, user, reason) services.start_project_transfer(project, user, reason)
return response.Ok() return response.Ok()
@detail_route(methods=["POST"]) @detail_route(methods=["POST"])
@ -471,6 +450,7 @@ class PointsViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.Points model = models.Points
serializer_class = serializers.PointsSerializer serializer_class = serializers.PointsSerializer
validator_class = validators.PointsValidator
permission_classes = (permissions.PointsPermission,) permission_classes = (permissions.PointsPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',) filter_fields = ('project',)
@ -487,6 +467,7 @@ class UserStoryStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.UserStoryStatus model = models.UserStoryStatus
serializer_class = serializers.UserStoryStatusSerializer serializer_class = serializers.UserStoryStatusSerializer
validator_class = validators.UserStoryStatusValidator
permission_classes = (permissions.UserStoryStatusPermission,) permission_classes = (permissions.UserStoryStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',) filter_fields = ('project',)
@ -503,6 +484,7 @@ class TaskStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.TaskStatus model = models.TaskStatus
serializer_class = serializers.TaskStatusSerializer serializer_class = serializers.TaskStatusSerializer
validator_class = validators.TaskStatusValidator
permission_classes = (permissions.TaskStatusPermission,) permission_classes = (permissions.TaskStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",) filter_fields = ("project",)
@ -519,6 +501,7 @@ class SeverityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
model = models.Severity model = models.Severity
serializer_class = serializers.SeveritySerializer serializer_class = serializers.SeveritySerializer
validator_class = validators.SeverityValidator
permission_classes = (permissions.SeverityPermission,) permission_classes = (permissions.SeverityPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",) filter_fields = ("project",)
@ -534,6 +517,7 @@ class PriorityViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin): ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.Priority model = models.Priority
serializer_class = serializers.PrioritySerializer serializer_class = serializers.PrioritySerializer
validator_class = validators.PriorityValidator
permission_classes = (permissions.PriorityPermission,) permission_classes = (permissions.PriorityPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",) filter_fields = ("project",)
@ -549,6 +533,7 @@ class IssueTypeViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin): ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.IssueType model = models.IssueType
serializer_class = serializers.IssueTypeSerializer serializer_class = serializers.IssueTypeSerializer
validator_class = validators.IssueTypeValidator
permission_classes = (permissions.IssueTypePermission,) permission_classes = (permissions.IssueTypePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",) filter_fields = ("project",)
@ -564,6 +549,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
ModelCrudViewSet, BulkUpdateOrderMixin): ModelCrudViewSet, BulkUpdateOrderMixin):
model = models.IssueStatus model = models.IssueStatus
serializer_class = serializers.IssueStatusSerializer serializer_class = serializers.IssueStatusSerializer
validator_class = validators.IssueStatusValidator
permission_classes = (permissions.IssueStatusPermission,) permission_classes = (permissions.IssueStatusPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",) filter_fields = ("project",)
@ -582,6 +568,7 @@ class IssueStatusViewSet(MoveOnDestroyMixin, BlockedByProjectMixin,
class ProjectTemplateViewSet(ModelCrudViewSet): class ProjectTemplateViewSet(ModelCrudViewSet):
model = models.ProjectTemplate model = models.ProjectTemplate
serializer_class = serializers.ProjectTemplateSerializer serializer_class = serializers.ProjectTemplateSerializer
validator_class = validators.ProjectTemplateValidator
permission_classes = (permissions.ProjectTemplatePermission,) permission_classes = (permissions.ProjectTemplatePermission,)
def get_queryset(self): def get_queryset(self):
@ -595,7 +582,9 @@ class ProjectTemplateViewSet(ModelCrudViewSet):
class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Membership model = models.Membership
admin_serializer_class = serializers.MembershipAdminSerializer admin_serializer_class = serializers.MembershipAdminSerializer
admin_validator_class = validators.MembershipAdminValidator
serializer_class = serializers.MembershipSerializer serializer_class = serializers.MembershipSerializer
validator_class = validators.MembershipValidator
permission_classes = (permissions.MembershipPermission,) permission_classes = (permissions.MembershipPermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project", "role") filter_fields = ("project", "role")
@ -620,6 +609,12 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
else: else:
return self.serializer_class return self.serializer_class
def get_validator_class(self):
if self.action == "create":
return self.admin_validator_class
return self.validator_class
def _check_if_project_can_have_more_memberships(self, project, total_new_memberships): def _check_if_project_can_have_more_memberships(self, project, total_new_memberships):
(can_add_memberships, error_type) = services.check_if_project_can_have_more_memberships( (can_add_memberships, error_type) = services.check_if_project_can_have_more_memberships(
project, project,
@ -634,11 +629,11 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
@list_route(methods=["POST"]) @list_route(methods=["POST"])
def bulk_create(self, request, **kwargs): def bulk_create(self, request, **kwargs):
serializer = serializers.MembersBulkSerializer(data=request.DATA) validator = validators.MembersBulkValidator(data=request.DATA)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
data = serializer.data data = validator.data
project = models.Project.objects.get(id=data["project_id"]) project = models.Project.objects.get(id=data["project_id"])
invitation_extra_text = data.get("invitation_extra_text", None) invitation_extra_text = data.get("invitation_extra_text", None)
self.check_permissions(request, 'bulk_create', project) self.check_permissions(request, 'bulk_create', project)
@ -655,7 +650,7 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
invitation_extra_text=invitation_extra_text, invitation_extra_text=invitation_extra_text,
callback=self.post_save, callback=self.post_save,
precall=self.pre_save) precall=self.pre_save)
except ValidationError as err: except exc.ValidationError as err:
return response.BadRequest(err.message_dict) return response.BadRequest(err.message_dict)
members_serialized = self.admin_serializer_class(members, many=True) members_serialized = self.admin_serializer_class(members, many=True)

View File

@ -34,6 +34,7 @@ from taiga.projects.history.mixins import HistoryResourceMixin
from . import permissions from . import permissions
from . import serializers from . import serializers
from . import validators
from . import models from . import models
@ -42,6 +43,7 @@ class BaseAttachmentViewSet(HistoryResourceMixin, WatchedResourceMixin,
model = models.Attachment model = models.Attachment
serializer_class = serializers.AttachmentSerializer serializer_class = serializers.AttachmentSerializer
validator_class = validators.AttachmentValidator
filter_fields = ["project", "object_id"] filter_fields = ["project", "object_id"]
content_type = None content_type = None

View File

@ -19,36 +19,38 @@
from django.conf import settings from django.conf import settings
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import MethodField, Field, FileField
from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.base.utils.thumbnails import get_thumbnail_url
from . import services from . import services
from . import models
import json
import serpy
class AttachmentSerializer(serializers.ModelSerializer): class AttachmentSerializer(serializers.LightSerializer):
url = serializers.SerializerMethodField("get_url") id = Field()
thumbnail_card_url = serializers.SerializerMethodField("get_thumbnail_card_url") project = Field(attr="project_id")
attached_file = serializers.FileField(required=True) owner = Field(attr="owner_id")
name = Field()
class Meta: attached_file = FileField()
model = models.Attachment size = Field()
fields = ("id", "project", "owner", "name", "attached_file", "size", url = Field()
"url", "thumbnail_card_url", "description", "is_deprecated", description = Field()
"created_date", "modified_date", "object_id", "order", "sha1") is_deprecated = Field()
read_only_fields = ("owner", "created_date", "modified_date", "sha1") created_date = Field()
modified_date = Field()
object_id = Field()
order = Field()
sha1 = Field()
url = MethodField("get_url")
thumbnail_card_url = MethodField("get_thumbnail_card_url")
def get_url(self, obj): def get_url(self, obj):
return obj.attached_file.url return obj.attached_file.url
def get_thumbnail_card_url(self, obj): def get_thumbnail_card_url(self, obj):
return services.get_card_image_thumbnail_url(obj) return services.get_card_image_thumbnail_url(obj)
class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer): class BasicAttachmentsInfoSerializerMixin(serializers.LightSerializer):
""" """
Assumptions: Assumptions:
- The queryset has an attribute called "include_attachments" indicating if the attachments array should contain information - The queryset has an attribute called "include_attachments" indicating if the attachments array should contain information
@ -56,7 +58,7 @@ class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer):
- The method attach_basic_attachments has been used to include the necessary - The method attach_basic_attachments has been used to include the necessary
json data about the attachments in the "attachments_attr" column json data about the attachments in the "attachments_attr" column
""" """
attachments = serpy.MethodField() attachments = MethodField()
def get_attachments(self, obj): def get_attachments(self, obj):
include_attachments = getattr(obj, "include_attachments", False) include_attachments = getattr(obj, "include_attachments", False)

View File

@ -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")

View File

@ -32,6 +32,7 @@ from taiga.projects.occ.mixins import OCCResourceMixin
from . import models from . import models
from . import serializers from . import serializers
from . import validators
from . import permissions from . import permissions
from . import services from . import services
@ -43,6 +44,7 @@ from . import services
class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.UserStoryCustomAttribute model = models.UserStoryCustomAttribute
serializer_class = serializers.UserStoryCustomAttributeSerializer serializer_class = serializers.UserStoryCustomAttributeSerializer
validator_class = validators.UserStoryCustomAttributeValidator
permission_classes = (permissions.UserStoryCustomAttributePermission,) permission_classes = (permissions.UserStoryCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",) filter_fields = ("project",)
@ -54,6 +56,7 @@ class UserStoryCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixi
class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.TaskCustomAttribute model = models.TaskCustomAttribute
serializer_class = serializers.TaskCustomAttributeSerializer serializer_class = serializers.TaskCustomAttributeSerializer
validator_class = validators.TaskCustomAttributeValidator
permission_classes = (permissions.TaskCustomAttributePermission,) permission_classes = (permissions.TaskCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",) filter_fields = ("project",)
@ -65,6 +68,7 @@ class TaskCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, Mo
class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet): class IssueCustomAttributeViewSet(BulkUpdateOrderMixin, BlockedByProjectMixin, ModelCrudViewSet):
model = models.IssueCustomAttribute model = models.IssueCustomAttribute
serializer_class = serializers.IssueCustomAttributeSerializer serializer_class = serializers.IssueCustomAttributeSerializer
validator_class = validators.IssueCustomAttributeValidator
permission_classes = (permissions.IssueCustomAttributePermission,) permission_classes = (permissions.IssueCustomAttributePermission,)
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ("project",) filter_fields = ("project",)
@ -86,6 +90,7 @@ class BaseCustomAttributesValuesViewSet(OCCResourceMixin, HistoryResourceMixin,
class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.UserStoryCustomAttributesValues model = models.UserStoryCustomAttributesValues
serializer_class = serializers.UserStoryCustomAttributesValuesSerializer serializer_class = serializers.UserStoryCustomAttributesValuesSerializer
validator_class = validators.UserStoryCustomAttributesValuesValidator
permission_classes = (permissions.UserStoryCustomAttributesValuesPermission,) permission_classes = (permissions.UserStoryCustomAttributesValuesPermission,)
lookup_field = "user_story_id" lookup_field = "user_story_id"
content_object = "user_story" content_object = "user_story"
@ -99,6 +104,7 @@ class UserStoryCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.TaskCustomAttributesValues model = models.TaskCustomAttributesValues
serializer_class = serializers.TaskCustomAttributesValuesSerializer serializer_class = serializers.TaskCustomAttributesValuesSerializer
validator_class = validators.TaskCustomAttributesValuesValidator
permission_classes = (permissions.TaskCustomAttributesValuesPermission,) permission_classes = (permissions.TaskCustomAttributesValuesPermission,)
lookup_field = "task_id" lookup_field = "task_id"
content_object = "task" content_object = "task"
@ -112,6 +118,7 @@ class TaskCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet): class IssueCustomAttributesValuesViewSet(BaseCustomAttributesValuesViewSet):
model = models.IssueCustomAttributesValues model = models.IssueCustomAttributesValues
serializer_class = serializers.IssueCustomAttributesValuesSerializer serializer_class = serializers.IssueCustomAttributesValuesSerializer
validator_class = validators.IssueCustomAttributesValuesValidator
permission_classes = (permissions.IssueCustomAttributesValuesPermission,) permission_classes = (permissions.IssueCustomAttributesValuesPermission,)
lookup_field = "issue_id" lookup_field = "issue_id"
content_object = "issue" content_object = "issue"

View File

@ -17,131 +17,51 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.apps import apps from taiga.base.fields import JsonField, Field
from django.utils.translation import ugettext_lazy as _ from taiga.base.api import serializers
from taiga.base.fields import JsonField
from taiga.base.api.serializers import ValidationError
from taiga.base.api.serializers import ModelSerializer
from . import models
###################################################### ######################################################
# Custom Attribute Serializer # Custom Attribute Serializer
####################################################### #######################################################
class BaseCustomAttributeSerializer(ModelSerializer): class BaseCustomAttributeSerializer(serializers.LightSerializer):
class Meta: name = Field()
read_only_fields = ('id',) description = Field()
exclude = ('created_date', 'modified_date') type = Field()
order = Field()
def _validate_integrity_between_project_and_name(self, attrs, source): project = Field(attr="project_id")
""" created_date = Field()
Check the name is not duplicated in the project. Check when: modified_date = Field()
- create a new one
- update the name
- update the project (move to another project)
"""
data_id = attrs.get("id", None)
data_name = attrs.get("name", None)
data_project = attrs.get("project", None)
if self.object:
data_id = data_id or self.object.id
data_name = data_name or self.object.name
data_project = data_project or self.object.project
model = self.Meta.model
qs = (model.objects.filter(project=data_project, name=data_name)
.exclude(id=data_id))
if qs.exists():
raise ValidationError(_("Already exists one with the same name."))
return attrs
def validate_name(self, attrs, source):
return self._validate_integrity_between_project_and_name(attrs, source)
def validate_project(self, attrs, source):
return self._validate_integrity_between_project_and_name(attrs, source)
class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer): class UserStoryCustomAttributeSerializer(BaseCustomAttributeSerializer):
class Meta(BaseCustomAttributeSerializer.Meta): pass
model = models.UserStoryCustomAttribute
class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer): class TaskCustomAttributeSerializer(BaseCustomAttributeSerializer):
class Meta(BaseCustomAttributeSerializer.Meta): pass
model = models.TaskCustomAttribute
class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer): class IssueCustomAttributeSerializer(BaseCustomAttributeSerializer):
class Meta(BaseCustomAttributeSerializer.Meta): pass
model = models.IssueCustomAttribute
###################################################### ######################################################
# Custom Attribute Serializer # Custom Attribute Serializer
####################################################### #######################################################
class BaseCustomAttributesValuesSerializer(serializers.LightSerializer):
attributes_values = Field()
class BaseCustomAttributesValuesSerializer(ModelSerializer): version = Field()
attributes_values = JsonField(source="attributes_values", label="attributes values")
_custom_attribute_model = None
_container_field = None
class Meta:
exclude = ("id",)
def validate_attributes_values(self, attrs, source):
# values must be a dict
data_values = attrs.get("attributes_values", None)
if self.object:
data_values = (data_values or self.object.attributes_values)
if type(data_values) is not dict:
raise ValidationError(_("Invalid content. It must be {\"key\": \"value\",...}"))
# Values keys must be in the container object project
data_container = attrs.get(self._container_field, None)
if data_container:
project_id = data_container.project_id
elif self.object:
project_id = getattr(self.object, self._container_field).project_id
else:
project_id = None
values_ids = list(data_values.keys())
qs = self._custom_attribute_model.objects.filter(project=project_id,
id__in=values_ids)
if qs.count() != len(values_ids):
raise ValidationError(_("It contain invalid custom fields."))
return attrs
class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer): class UserStoryCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
_custom_attribute_model = models.UserStoryCustomAttribute user_story = Field(attr="user_story.id")
_container_model = "userstories.UserStory"
_container_field = "user_story"
class Meta(BaseCustomAttributesValuesSerializer.Meta):
model = models.UserStoryCustomAttributesValues
class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer): class TaskCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
_custom_attribute_model = models.TaskCustomAttribute task = Field(attr="task.id")
_container_field = "task"
class Meta(BaseCustomAttributesValuesSerializer.Meta):
model = models.TaskCustomAttributesValues
class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer, ModelSerializer): class IssueCustomAttributesValuesSerializer(BaseCustomAttributesValuesSerializer):
_custom_attribute_model = models.IssueCustomAttribute issue = Field(attr="issue.id")
_container_field = "issue"
class Meta(BaseCustomAttributesValuesSerializer.Meta):
model = models.IssueCustomAttributesValues

View File

@ -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

View File

@ -45,7 +45,7 @@ class DiscoverModeFilterBackend(FilterBackend):
if request.QUERY_PARAMS.get("is_featured", None) == 'true': if request.QUERY_PARAMS.get("is_featured", None) == 'true':
qs = qs.order_by("?") qs = qs.order_by("?")
return super().filter_queryset(request, qs.distinct(), view) return super().filter_queryset(request, qs, view)
class CanViewProjectObjFilterBackend(FilterBackend): class CanViewProjectObjFilterBackend(FilterBackend):
@ -86,7 +86,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
# external users / anonymous # external users / anonymous
qs = qs.filter(anon_permissions__contains=["view_project"]) qs = qs.filter(anon_permissions__contains=["view_project"])
return super().filter_queryset(request, qs.distinct(), view) return super().filter_queryset(request, qs, view)
class QFilterBackend(FilterBackend): class QFilterBackend(FilterBackend):
@ -97,12 +97,12 @@ class QFilterBackend(FilterBackend):
tsquery = "to_tsquery('english_nostop', %s)" tsquery = "to_tsquery('english_nostop', %s)"
tsquery_params = [to_tsquery(q)] tsquery_params = [to_tsquery(q)]
tsvector = """ tsvector = """
setweight(to_tsvector('english_nostop', setweight(to_tsvector('english_nostop',
coalesce(projects_project.name, '')), 'A') || coalesce(projects_project.name, '')), 'A') ||
setweight(to_tsvector('english_nostop', setweight(to_tsvector('english_nostop',
coalesce(inmutable_array_to_string(projects_project.tags), '')), 'B') || coalesce(inmutable_array_to_string(projects_project.tags), '')), 'B') ||
setweight(to_tsvector('english_nostop', setweight(to_tsvector('english_nostop',
coalesce(projects_project.description, '')), 'C') coalesce(projects_project.description, '')), 'C')
""" """
select = { select = {
@ -111,7 +111,7 @@ class QFilterBackend(FilterBackend):
} }
select_params = tsquery_params select_params = tsquery_params
where = ["{tsvector} @@ {tsquery}".format(tsquery=tsquery, where = ["{tsvector} @@ {tsquery}".format(tsquery=tsquery,
tsvector=tsvector),] tsvector=tsvector), ]
params = tsquery_params params = tsquery_params
order_by = ["-rank", ] order_by = ["-rank", ]
@ -121,3 +121,34 @@ class QFilterBackend(FilterBackend):
params=params, params=params,
order_by=order_by) order_by=order_by)
return queryset return queryset
class UserOrderFilterBackend(FilterBackend):
def filter_queryset(self, request, queryset, view):
if request.user.is_anonymous():
return queryset
raw_fieldname = request.QUERY_PARAMS.get(self.order_by_query_param, None)
if not raw_fieldname:
return queryset
if raw_fieldname.startswith("-"):
field_name = raw_fieldname[1:]
else:
field_name = raw_fieldname
if field_name != "user_order":
return queryset
model = queryset.model
sql = """SELECT projects_membership.user_order
FROM projects_membership
WHERE
projects_membership.project_id = {tbl}.id AND
projects_membership.user_id = {user_id}
"""
sql = sql.format(tbl=model._meta.db_table, user_id=request.user.id)
queryset = queryset.extra(select={"user_order": sql})
queryset = queryset.order_by(raw_fieldname)
return queryset

View File

@ -23,7 +23,6 @@ from django.utils import timezone
from taiga.base import response from taiga.base import response
from taiga.base.decorators import detail_route from taiga.base.decorators import detail_route
from taiga.base.api import ReadOnlyListViewSet from taiga.base.api import ReadOnlyListViewSet
from taiga.base.api.utils import get_object_or_404
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from . import permissions from . import permissions
@ -38,7 +37,7 @@ class HistoryViewSet(ReadOnlyListViewSet):
def get_content_type(self): def get_content_type(self):
app_name, model = self.content_type.split(".", 1) app_name, model = self.content_type.split(".", 1)
return get_object_or_404(ContentType, app_label=app_name, model=model) return ContentType.objects.get_by_natural_key(app_name, model)
def get_queryset(self): def get_queryset(self):
ct = self.get_content_type() ct = self.get_content_type()

View File

@ -33,7 +33,8 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts
# This keys has been removed from freeze_impl so we can have objects where the # This keys has been removed from freeze_impl so we can have objects where the
# previous diff has value for the attribute and we want to prevent their propagation # previous diff has value for the attribute and we want to prevent their propagation
IGNORE_DIFF_FIELDS = [ "watchers", "description_diff", "content_diff", "blocked_note_diff"] IGNORE_DIFF_FIELDS = ["watchers", "description_diff", "content_diff", "blocked_note_diff"]
def _generate_uuid(): def _generate_uuid():
return str(uuid.uuid1()) return str(uuid.uuid1())
@ -92,15 +93,15 @@ class HistoryEntry(models.Model):
@cached_property @cached_property
def is_change(self): def is_change(self):
return self.type == HistoryType.change return self.type == HistoryType.change
@cached_property @cached_property
def is_create(self): def is_create(self):
return self.type == HistoryType.create return self.type == HistoryType.create
@cached_property @cached_property
def is_delete(self): def is_delete(self):
return self.type == HistoryType.delete return self.type == HistoryType.delete
@property @property
def owner(self): def owner(self):
@ -185,7 +186,7 @@ class HistoryEntry(models.Model):
role_name = resolve_value("roles", role_id) role_name = resolve_value("roles", role_id)
oldpoint_id = pointsold.get(role_id, None) oldpoint_id = pointsold.get(role_id, None)
points[role_name] = [resolve_value("points", oldpoint_id), points[role_name] = [resolve_value("points", oldpoint_id),
resolve_value("points", point_id)] resolve_value("points", point_id)]
# Process that removes points entries with # Process that removes points entries with
# duplicate value. # duplicate value.
@ -204,8 +205,8 @@ class HistoryEntry(models.Model):
"deleted": [], "deleted": [],
} }
oldattachs = {x["id"]:x for x in self.diff["attachments"][0]} oldattachs = {x["id"]: x for x in self.diff["attachments"][0]}
newattachs = {x["id"]:x for x in self.diff["attachments"][1]} newattachs = {x["id"]: x for x in self.diff["attachments"][1]}
for aid in set(tuple(oldattachs.keys()) + tuple(newattachs.keys())): for aid in set(tuple(oldattachs.keys()) + tuple(newattachs.keys())):
if aid in oldattachs and aid in newattachs: if aid in oldattachs and aid in newattachs:
@ -235,8 +236,8 @@ class HistoryEntry(models.Model):
"deleted": [], "deleted": [],
} }
oldcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][0] or []} oldcustattrs = {x["id"]: x for x in self.diff["custom_attributes"][0] or []}
newcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][1] or []} newcustattrs = {x["id"]: x for x in self.diff["custom_attributes"][1] or []}
for aid in set(tuple(oldcustattrs.keys()) + tuple(newcustattrs.keys())): for aid in set(tuple(oldcustattrs.keys()) + tuple(newcustattrs.keys())):
if aid in oldcustattrs and aid in newcustattrs: if aid in oldcustattrs and aid in newcustattrs:

View File

@ -17,28 +17,31 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import JsonField, I18NJsonField from taiga.base.fields import I18NJsonField, Field, MethodField
from taiga.users.services import get_photo_or_gravatar_url from taiga.users.services import get_photo_or_gravatar_url
from . import models
HISTORY_ENTRY_I18N_FIELDS = ("points", "status", "severity", "priority", "type")
HISTORY_ENTRY_I18N_FIELDS=("points", "status", "severity", "priority", "type") class HistoryEntrySerializer(serializers.LightSerializer):
id = Field()
user = MethodField()
class HistoryEntrySerializer(serializers.ModelSerializer): created_at = Field()
diff = JsonField() type = Field()
snapshot = JsonField() key = Field()
values = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) diff = Field()
values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) snapshot = Field()
user = serializers.SerializerMethodField("get_user") values = Field()
delete_comment_user = JsonField() values_diff = I18NJsonField()
comment_versions = JsonField() comment = I18NJsonField()
comment_html = Field()
class Meta: delete_comment_date = Field()
model = models.HistoryEntry delete_comment_user = Field()
exclude = ("comment_versions",) edit_comment_date = Field()
is_hidden = Field()
is_snapshot = Field()
def get_user(self, entry): def get_user(self, entry):
user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False} user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False}

View File

@ -34,12 +34,9 @@ from collections import namedtuple
from copy import deepcopy from copy import deepcopy
from functools import partial from functools import partial
from functools import wraps from functools import wraps
from functools import lru_cache
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.core.paginator import Paginator, InvalidPage
from django.apps import apps from django.apps import apps
from django.db import transaction as tx from django.db import transaction as tx
from django_pglocks import advisory_lock from django_pglocks import advisory_lock
@ -50,6 +47,21 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts
from .models import HistoryType from .models import HistoryType
# Freeze implementatitions
from .freeze_impl import project_freezer
from .freeze_impl import milestone_freezer
from .freeze_impl import userstory_freezer
from .freeze_impl import issue_freezer
from .freeze_impl import task_freezer
from .freeze_impl import wikipage_freezer
from .freeze_impl import project_values
from .freeze_impl import milestone_values
from .freeze_impl import userstory_values
from .freeze_impl import issue_values
from .freeze_impl import task_values
from .freeze_impl import wikipage_values
# Type that represents a freezed object # Type that represents a freezed object
FrozenObj = namedtuple("FrozenObj", ["key", "snapshot"]) FrozenObj = namedtuple("FrozenObj", ["key", "snapshot"])
@ -71,7 +83,7 @@ _not_important_fields = {
log = logging.getLogger("taiga.history") log = logging.getLogger("taiga.history")
def make_key_from_model_object(obj:object) -> str: def make_key_from_model_object(obj: object) -> str:
""" """
Create unique key from model instance. Create unique key from model instance.
""" """
@ -79,7 +91,7 @@ def make_key_from_model_object(obj:object) -> str:
return "{0}:{1}".format(tn, obj.pk) return "{0}:{1}".format(tn, obj.pk)
def get_model_from_key(key:str) -> object: def get_model_from_key(key: str) -> object:
""" """
Get model from key Get model from key
""" """
@ -87,7 +99,7 @@ def get_model_from_key(key:str) -> object:
return apps.get_model(class_name) return apps.get_model(class_name)
def get_pk_from_key(key:str) -> object: def get_pk_from_key(key: str) -> object:
""" """
Get pk from key Get pk from key
""" """
@ -95,7 +107,7 @@ def get_pk_from_key(key:str) -> object:
return pk return pk
def get_instance_from_key(key:str) -> object: def get_instance_from_key(key: str) -> object:
""" """
Get instance from key Get instance from key
""" """
@ -109,7 +121,7 @@ def get_instance_from_key(key:str) -> object:
return None return None
def register_values_implementation(typename:str, fn=None): def register_values_implementation(typename: str, fn=None):
""" """
Register values implementation for specified typename. Register values implementation for specified typename.
This function can be used as decorator. This function can be used as decorator.
@ -128,7 +140,7 @@ def register_values_implementation(typename:str, fn=None):
return _wrapper return _wrapper
def register_freeze_implementation(typename:str, fn=None): def register_freeze_implementation(typename: str, fn=None):
""" """
Register freeze implementation for specified typename. Register freeze implementation for specified typename.
This function can be used as decorator. This function can be used as decorator.
@ -149,7 +161,7 @@ def register_freeze_implementation(typename:str, fn=None):
# Low level api # Low level api
def freeze_model_instance(obj:object) -> FrozenObj: def freeze_model_instance(obj: object) -> FrozenObj:
""" """
Creates a new frozen object from model instance. Creates a new frozen object from model instance.
@ -179,7 +191,7 @@ def freeze_model_instance(obj:object) -> FrozenObj:
return FrozenObj(key, snapshot) return FrozenObj(key, snapshot)
def is_hidden_snapshot(obj:FrozenDiff) -> bool: def is_hidden_snapshot(obj: FrozenDiff) -> bool:
""" """
Check if frozen object is considered Check if frozen object is considered
hidden or not. hidden or not.
@ -199,7 +211,7 @@ def is_hidden_snapshot(obj:FrozenDiff) -> bool:
return False return False
def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff: def make_diff(oldobj: FrozenObj, newobj: FrozenObj) -> FrozenDiff:
""" """
Compute a diff between two frozen objects. Compute a diff between two frozen objects.
""" """
@ -217,7 +229,7 @@ def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff:
return FrozenDiff(newobj.key, diff, newobj.snapshot) return FrozenDiff(newobj.key, diff, newobj.snapshot)
def make_diff_values(typename:str, fdiff:FrozenDiff) -> dict: def make_diff_values(typename: str, fdiff: FrozenDiff) -> dict:
""" """
Given a typename and diff, build a values dict for it. Given a typename and diff, build a values dict for it.
If no implementation found for typename, warnig is raised in If no implementation found for typename, warnig is raised in
@ -242,7 +254,7 @@ def _rebuild_snapshot_from_diffs(keysnapshot, partials):
return result return result
def get_last_snapshot_for_key(key:str) -> FrozenObj: def get_last_snapshot_for_key(key: str) -> FrozenObj:
entry_model = apps.get_model("history", "HistoryEntry") entry_model = apps.get_model("history", "HistoryEntry")
# Search last snapshot # Search last snapshot
@ -271,17 +283,16 @@ def get_last_snapshot_for_key(key:str) -> FrozenObj:
# Public api # Public api
def get_modified_fields(obj:object, last_modifications): def get_modified_fields(obj: object, last_modifications):
""" """
Get the modified fields for an object through his last modifications Get the modified fields for an object through his last modifications
""" """
key = make_key_from_model_object(obj) key = make_key_from_model_object(obj)
entry_model = apps.get_model("history", "HistoryEntry") entry_model = apps.get_model("history", "HistoryEntry")
history_entries = (entry_model.objects history_entries = (entry_model.objects
.filter(key=key) .filter(key=key)
.order_by("-created_at") .order_by("-created_at")
.values_list("diff", flat=True) .values_list("diff", flat=True)[0:last_modifications])
[0:last_modifications])
modified_fields = [] modified_fields = []
for history_entry in history_entries: for history_entry in history_entries:
@ -291,7 +302,7 @@ def get_modified_fields(obj:object, last_modifications):
@tx.atomic @tx.atomic
def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): def take_snapshot(obj: object, *, comment: str="", user=None, delete: bool=False):
""" """
Given any model instance with registred content type, Given any model instance with registred content type,
create new history entry of "change" type. create new history entry of "change" type.
@ -301,7 +312,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
""" """
key = make_key_from_model_object(obj) key = make_key_from_model_object(obj)
with advisory_lock(key) as acquired_key_lock: with advisory_lock(key):
typename = get_typename_for_model_class(obj.__class__) typename = get_typename_for_model_class(obj.__class__)
new_fobj = freeze_model_instance(obj) new_fobj = freeze_model_instance(obj)
@ -327,8 +338,8 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
# If diff and comment are empty, do # If diff and comment are empty, do
# not create empty history entry # not create empty history entry
if (not fdiff.diff and not comment if (not fdiff.diff and not comment
and old_fobj is not None and old_fobj is not None
and entry_type != HistoryType.delete): and entry_type != HistoryType.delete):
return None return None
@ -358,7 +369,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False):
# High level query api # High level query api
def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change,), def get_history_queryset_by_model_instance(obj: object, types=(HistoryType.change,),
include_hidden=False): include_hidden=False):
""" """
Get one page of history for specified object. Get one page of history for specified object.
@ -377,20 +388,12 @@ def prefetch_owners_in_history_queryset(qs):
user_ids = [u["pk"] for u in qs.values_list("user", flat=True)] user_ids = [u["pk"] for u in qs.values_list("user", flat=True)]
users = get_user_model().objects.filter(id__in=user_ids) users = get_user_model().objects.filter(id__in=user_ids)
users_by_id = {u.id: u for u in users} users_by_id = {u.id: u for u in users}
for history_entry in qs: for history_entry in qs:
history_entry.prefetch_owner(users_by_id.get(history_entry.user["pk"], None)) history_entry.prefetch_owner(users_by_id.get(history_entry.user["pk"], None))
return qs return qs
# Freeze implementatitions
from .freeze_impl import project_freezer
from .freeze_impl import milestone_freezer
from .freeze_impl import userstory_freezer
from .freeze_impl import issue_freezer
from .freeze_impl import task_freezer
from .freeze_impl import wikipage_freezer
register_freeze_implementation("projects.project", project_freezer) register_freeze_implementation("projects.project", project_freezer)
register_freeze_implementation("milestones.milestone", milestone_freezer,) register_freeze_implementation("milestones.milestone", milestone_freezer,)
register_freeze_implementation("userstories.userstory", userstory_freezer) register_freeze_implementation("userstories.userstory", userstory_freezer)
@ -398,13 +401,6 @@ register_freeze_implementation("issues.issue", issue_freezer)
register_freeze_implementation("tasks.task", task_freezer) register_freeze_implementation("tasks.task", task_freezer)
register_freeze_implementation("wiki.wikipage", wikipage_freezer) register_freeze_implementation("wiki.wikipage", wikipage_freezer)
from .freeze_impl import project_values
from .freeze_impl import milestone_values
from .freeze_impl import userstory_values
from .freeze_impl import issue_values
from .freeze_impl import task_values
from .freeze_impl import wikipage_values
register_values_implementation("projects.project", project_values) register_values_implementation("projects.project", project_values)
register_values_implementation("milestones.milestone", milestone_values) register_values_implementation("milestones.milestone", milestone_values)
register_values_implementation("userstories.userstory", userstory_values) register_values_implementation("userstories.userstory", userstory_values)

View File

@ -34,14 +34,18 @@ from taiga.projects.occ import OCCResourceMixin
from taiga.projects.tagging.api import TaggedResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from .utils import attach_extra_info
from . import models from . import models
from . import services from . import services
from . import permissions from . import permissions
from . import serializers from . import serializers
from . import validators
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
validator_class = validators.IssueValidator
queryset = models.Issue.objects.all() queryset = models.Issue.objects.all()
permission_classes = (permissions.IssuePermission, ) permission_classes = (permissions.IssuePermission, )
filter_backends = (filters.CanViewIssuesFilterBackend, filter_backends = (filters.CanViewIssuesFilterBackend,
@ -144,10 +148,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.prefetch_related("attachments", "generated_user_stories")
qs = qs.select_related("owner", "assigned_to", "status", "project") qs = qs.select_related("owner", "assigned_to", "status", "project")
qs = self.attach_votes_attrs_to_queryset(qs) qs = attach_extra_info(qs, user=self.request.user)
return self.attach_watchers_attrs_to_queryset(qs) return qs
def pre_save(self, obj): def pre_save(self, obj):
if not obj.id: if not obj.id:
@ -180,10 +183,18 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
@list_route(methods=["GET"]) @list_route(methods=["GET"])
def by_ref(self, request): def by_ref(self, request):
ref = request.QUERY_PARAMS.get("ref", None) retrieve_kwargs = {
"ref": request.QUERY_PARAMS.get("ref", None)
}
project_id = request.QUERY_PARAMS.get("project", None) project_id = request.QUERY_PARAMS.get("project", None)
issue = get_object_or_404(models.Issue, ref=ref, project_id=project_id) if project_id is not None:
return self.retrieve(request, pk=issue.pk) retrieve_kwargs["project_id"] = project_id
project_slug = request.QUERY_PARAMS.get("project__slug", None)
if project_slug is not None:
retrieve_kwargs["project__slug"] = project_slug
return self.retrieve(request, **retrieve_kwargs)
@list_route(methods=["GET"]) @list_route(methods=["GET"])
def filters_data(self, request, *args, **kwargs): def filters_data(self, request, *args, **kwargs):
@ -225,9 +236,9 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
@list_route(methods=["POST"]) @list_route(methods=["POST"])
def bulk_create(self, request, **kwargs): def bulk_create(self, request, **kwargs):
serializer = serializers.IssuesBulkSerializer(data=request.DATA) validator = validators.IssuesBulkValidator(data=request.DATA)
if serializer.is_valid(): if validator.is_valid():
data = serializer.data data = validator.data
project = Project.objects.get(pk=data["project_id"]) project = Project.objects.get(pk=data["project_id"])
self.check_permissions(request, 'bulk_create', project) self.check_permissions(request, 'bulk_create', project)
if project.blocked_code is not None: if project.blocked_code is not None:
@ -238,11 +249,13 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
status=project.default_issue_status, severity=project.default_severity, status=project.default_issue_status, severity=project.default_severity,
priority=project.default_priority, type=project.default_issue_type, priority=project.default_priority, type=project.default_issue_type,
callback=self.post_save, precall=self.pre_save) callback=self.post_save, precall=self.pre_save)
issues = self.get_queryset().filter(id__in=[i.id for i in issues])
issues_serialized = self.get_serializer_class()(issues, many=True) issues_serialized = self.get_serializer_class()(issues, many=True)
return response.Ok(data=issues_serialized.data) return response.Ok(data=issues_serialized.data)
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet): class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet):

View File

@ -17,56 +17,52 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import PgArrayField from taiga.base.fields import Field, MethodField
from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.tagging.fields import TagsAndTagsColorsField
from taiga.projects.serializers import BasicIssueStatusSerializer
from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
from taiga.users.serializers import UserBasicInfoSerializer
from . import models
import serpy
class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
serializers.ModelSerializer): OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
tags = TagsAndTagsColorsField(default=[], required=False) StatusExtraInfoSerializerMixin, serializers.LightSerializer):
external_reference = PgArrayField(required=False) id = Field()
is_closed = serializers.Field(source="is_closed") ref = Field()
comment = serializers.SerializerMethodField("get_comment") severity = Field(attr="severity_id")
generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories") priority = Field(attr="priority_id")
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") type = Field(attr="type_id")
description_html = serializers.SerializerMethodField("get_description_html") milestone = Field(attr="milestone_id")
status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True) project = Field(attr="project_id")
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) created_date = Field()
owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) modified_date = Field()
finished_date = Field()
subject = Field()
external_reference = Field()
version = Field()
watchers = Field()
tags = Field()
is_closed = Field()
class Meta:
model = models.Issue class IssueSerializer(IssueListSerializer):
read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') comment = MethodField()
generated_user_stories = MethodField()
blocked_note_html = MethodField()
description = Field()
description_html = MethodField()
def get_comment(self, obj): def get_comment(self, obj):
# NOTE: This method and field is necessary to historical comments work # NOTE: This method and field is necessary to historical comments work
return "" return ""
def get_generated_user_stories(self, obj): def get_generated_user_stories(self, obj):
return [{ assert hasattr(obj, "generated_user_stories_attr"), "instance must have a generated_user_stories_attr attribute"
"id": us.id, return obj.generated_user_stories_attr
"ref": us.ref,
"subject": us.subject,
} for us in obj.generated_user_stories.all()]
def get_blocked_note_html(self, obj): def get_blocked_note_html(self, obj):
return mdrender(obj.project, obj.blocked_note) return mdrender(obj.project, obj.blocked_note)
@ -75,39 +71,5 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa
return mdrender(obj.project, obj.description) return mdrender(obj.project, obj.description)
class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
ListStatusExtraInfoSerializerMixin, serializers.LightSerializer):
id = serpy.Field()
ref = serpy.Field()
severity = serpy.Field(attr="severity_id")
priority = serpy.Field(attr="priority_id")
type = serpy.Field(attr="type_id")
milestone = serpy.Field(attr="milestone_id")
project = serpy.Field(attr="project_id")
created_date = serpy.Field()
modified_date = serpy.Field()
finished_date = serpy.Field()
subject = serpy.Field()
external_reference = serpy.Field()
version = serpy.Field()
watchers = serpy.Field()
class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer): class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer):
def serialize_neighbor(self, neighbor): pass
if neighbor:
return NeighborIssueSerializer(neighbor).data
return None
class NeighborIssueSerializer(serializers.ModelSerializer):
class Meta:
model = models.Issue
fields = ("id", "ref", "subject")
depth = 0
class IssuesBulkSerializer(ProjectExistsValidator, serializers.Serializer):
project_id = serializers.IntegerField()
bulk_issues = serializers.CharField()

View File

@ -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

View File

@ -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()

View File

@ -17,14 +17,14 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.auth import get_user_model
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import Field, MethodField
class FanSerializer(serializers.ModelSerializer): class FanSerializer(serializers.LightSerializer):
full_name = serializers.CharField(source='get_full_name', required=False) id = Field()
username = Field()
full_name = MethodField()
class Meta: def get_full_name(self, obj):
model = get_user_model() return obj.get_full_name()
fields = ('id', 'username', 'full_name')

View File

@ -17,7 +17,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.apps import apps from django.apps import apps
from django.db.models import Prefetch
from taiga.base import filters from taiga.base import filters
from taiga.base import response from taiga.base import response
@ -31,13 +30,9 @@ from taiga.base.utils.db import get_object_or_none
from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.projects.votes.utils import attach_is_voter_to_queryset
from taiga.projects.notifications.utils import attach_watchers_to_queryset
from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
from taiga.projects.userstories import utils as userstories_utils
from . import serializers from . import serializers
from . import validators
from . import models from . import models
from . import permissions from . import permissions
from . import utils as milestones_utils from . import utils as milestones_utils
@ -47,6 +42,8 @@ import datetime
class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
BlockedByProjectMixin, ModelCrudViewSet): BlockedByProjectMixin, ModelCrudViewSet):
serializer_class = serializers.MilestoneSerializer
validator_class = validators.MilestoneValidator
permission_classes = (permissions.MilestonePermission,) permission_classes = (permissions.MilestonePermission,)
filter_backends = (filters.CanViewMilestonesFilterBackend,) filter_backends = (filters.CanViewMilestonesFilterBackend,)
filter_fields = ( filter_fields = (
@ -56,12 +53,6 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
) )
queryset = models.Milestone.objects.all() queryset = models.Milestone.objects.all()
def get_serializer_class(self, *args, **kwargs):
if self.action == "list":
return serializers.MilestoneListSerializer
return serializers.MilestoneSerializer
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
res = super().list(request, *args, **kwargs) res = super().list(request, *args, **kwargs)
self._add_taiga_info_headers() self._add_taiga_info_headers()
@ -84,33 +75,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin,
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
# Userstories prefetching
UserStory = apps.get_model("userstories", "UserStory")
us_qs = UserStory.objects.select_related("milestone",
"project",
"status",
"owner",
"assigned_to",
"generated_from_issue")
us_qs = userstories_utils.attach_total_points(us_qs)
us_qs = userstories_utils.attach_role_points(us_qs)
us_qs = attach_total_voters_to_queryset(us_qs)
us_qs = self.attach_watchers_attrs_to_queryset(us_qs)
if self.request.user.is_authenticated():
us_qs = attach_is_voter_to_queryset(self.request.user, us_qs)
us_qs = attach_is_watcher_to_queryset(us_qs, self.request.user)
qs = qs.prefetch_related(Prefetch("user_stories", queryset=us_qs))
# Milestones prefetching
qs = qs.select_related("project", "owner") qs = qs.select_related("project", "owner")
qs = self.attach_watchers_attrs_to_queryset(qs) qs = milestones_utils.attach_extra_info(qs, user=self.request.user)
qs = milestones_utils.attach_total_points(qs)
qs = milestones_utils.attach_closed_points(qs)
qs = qs.order_by("-estimated_start") qs = qs.order_by("-estimated_start")
return qs return qs

View File

@ -16,58 +16,29 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.utils import json from taiga.base.fields import Field, MethodField
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin
from taiga.projects.userstories.serializers import UserStoryListSerializer from taiga.projects.userstories.serializers import UserStoryListSerializer
from . import models
import serpy class MilestoneSerializer(WatchedResourceSerializer, serializers.LightSerializer):
id = Field()
name = Field()
class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, slug = Field()
ValidateDuplicatedNameInProjectMixin): owner = Field(attr="owner_id")
total_points = serializers.SerializerMethodField("get_total_points") project = Field(attr="project_id")
closed_points = serializers.SerializerMethodField("get_closed_points") estimated_start = Field()
user_stories = serializers.SerializerMethodField("get_user_stories") estimated_finish = Field()
created_date = Field()
class Meta: modified_date = Field()
model = models.Milestone closed = Field()
read_only_fields = ("id", "created_date", "modified_date") disponibility = Field()
order = Field()
def get_total_points(self, obj): watchers = Field()
return sum(obj.total_points.values()) user_stories = MethodField()
total_points = MethodField()
def get_closed_points(self, obj): closed_points = MethodField()
return sum(obj.closed_points.values())
def get_user_stories(self, obj):
return UserStoryListSerializer(obj.user_stories.all(), many=True).data
class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.LightSerializer):
id = serpy.Field()
name = serpy.Field()
slug = serpy.Field()
owner = serpy.Field(attr="owner_id")
project = serpy.Field(attr="project_id")
estimated_start = serpy.Field()
estimated_finish = serpy.Field()
created_date = serpy.Field()
modified_date = serpy.Field()
closed = serpy.Field()
disponibility = serpy.Field()
order = serpy.Field()
watchers = serpy.Field()
user_stories = serpy.MethodField("get_user_stories")
total_points = serpy.MethodField()
closed_points = serpy.MethodField()
def get_user_stories(self, obj): def get_user_stories(self, obj):
return UserStoryListSerializer(obj.user_stories.all(), many=True).data return UserStoryListSerializer(obj.user_stories.all(), many=True).data

View File

@ -17,6 +17,16 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.apps import apps
from django.db.models import Prefetch
from taiga.projects.notifications.utils import attach_watchers_to_queryset
from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
from taiga.projects.userstories import utils as userstories_utils
from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.projects.votes.utils import attach_is_voter_to_queryset
def attach_total_points(queryset, as_field="total_points_attr"): def attach_total_points(queryset, as_field="total_points_attr"):
"""Attach total of point values to each object of the queryset. """Attach total of point values to each object of the queryset.
@ -28,7 +38,7 @@ def attach_total_points(queryset, as_field="total_points_attr"):
""" """
model = queryset.model model = queryset.model
sql = """SELECT SUM(projects_points.value) sql = """SELECT SUM(projects_points.value)
FROM userstories_rolepoints FROM userstories_rolepoints
INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id
INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
WHERE userstories_userstory.milestone_id = {tbl}.id""" WHERE userstories_userstory.milestone_id = {tbl}.id"""
@ -48,7 +58,7 @@ def attach_closed_points(queryset, as_field="closed_points_attr"):
""" """
model = queryset.model model = queryset.model
sql = """SELECT SUM(projects_points.value) sql = """SELECT SUM(projects_points.value)
FROM userstories_rolepoints FROM userstories_rolepoints
INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id
INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
WHERE userstories_userstory.milestone_id = {tbl}.id AND userstories_userstory.is_closed=True""" WHERE userstories_userstory.milestone_id = {tbl}.id AND userstories_userstory.is_closed=True"""
@ -56,3 +66,33 @@ def attach_closed_points(queryset, as_field="closed_points_attr"):
sql = sql.format(tbl=model._meta.db_table) sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql}) queryset = queryset.extra(select={as_field: sql})
return queryset return queryset
def attach_extra_info(queryset, user=None):
# Userstories prefetching
UserStory = apps.get_model("userstories", "UserStory")
us_queryset = UserStory.objects.select_related("milestone",
"project",
"status",
"owner",
"assigned_to",
"generated_from_issue")
us_queryset = userstories_utils.attach_total_points(us_queryset)
us_queryset = userstories_utils.attach_role_points(us_queryset)
us_queryset = attach_total_voters_to_queryset(us_queryset)
us_queryset = attach_watchers_to_queryset(us_queryset)
us_queryset = attach_total_watchers_to_queryset(us_queryset)
us_queryset = attach_is_voter_to_queryset(us_queryset, user)
us_queryset = attach_is_watcher_to_queryset(us_queryset, user)
queryset = queryset.prefetch_related(Prefetch("user_stories", queryset=us_queryset))
queryset = attach_total_points(queryset)
queryset = attach_closed_points(queryset)
queryset = attach_total_voters_to_queryset(queryset)
queryset = attach_watchers_to_queryset(queryset)
queryset = attach_total_watchers_to_queryset(queryset)
queryset = attach_is_voter_to_queryset(queryset, user)
queryset = attach_is_watcher_to_queryset(queryset, user)
return queryset

View File

@ -18,15 +18,24 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.exceptions import ValidationError
from taiga.base.api import validators
from taiga.projects.validators import DuplicatedNameInProjectValidator
from taiga.projects.notifications.validators import WatchersValidator
from . import models from . import models
class SprintExistsValidator: class MilestoneExistsValidator:
def validate_sprint_id(self, attrs, source): def validate_sprint_id(self, attrs, source):
value = attrs[source] value = attrs[source]
if not models.Milestone.objects.filter(pk=value).exists(): if not models.Milestone.objects.filter(pk=value).exists():
msg = _("There's no sprint with that id") msg = _("There's no milestone with that id")
raise serializers.ValidationError(msg) raise ValidationError(msg)
return attrs return attrs
class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.Milestone
read_only_fields = ("id", "created_date", "modified_date")

View File

@ -17,34 +17,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.users.serializers import ListUserBasicInfoSerializer from taiga.base.fields import Field, MethodField
from taiga.users.serializers import UserBasicInfoSerializer
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import serpy
class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer): class CachedUsersSerializerMixin(serializers.LightSerializer):
def validate_name(self, attrs, source):
"""
Check the points name is not duplicated in the project on creation
"""
model = self.opts.model
qs = None
# If the object exists:
if self.object and attrs.get(source, None):
qs = model.objects.filter(project=self.object.project, name=attrs[source]).exclude(id=self.object.id)
if not self.object and attrs.get("project", None) and attrs.get(source, None):
qs = model.objects.filter(project=attrs["project"], name=attrs[source])
if qs and qs.exists():
raise serializers.ValidationError(_("Name duplicated for the project"))
return attrs
class ListCachedUsersSerializerMixin(serpy.Serializer):
def to_value(self, instance): def to_value(self, instance):
self._serialized_users = {} self._serialized_users = {}
return super().to_value(instance) return super().to_value(instance)
@ -55,37 +34,40 @@ class ListCachedUsersSerializerMixin(serpy.Serializer):
serialized_user = self._serialized_users.get(user.id, None) serialized_user = self._serialized_users.get(user.id, None)
if serialized_user is None: if serialized_user is None:
serializer_user = ListUserBasicInfoSerializer(user).data serialized_user = UserBasicInfoSerializer(user).data
self._serialized_users[user.id] = serializer_user self._serialized_users[user.id] = serialized_user
return serialized_user return serialized_user
class ListOwnerExtraInfoSerializerMixin(ListCachedUsersSerializerMixin): class OwnerExtraInfoSerializerMixin(CachedUsersSerializerMixin):
owner = serpy.Field(attr="owner_id") owner = Field(attr="owner_id")
owner_extra_info = serpy.MethodField() owner_extra_info = MethodField()
def get_owner_extra_info(self, obj): def get_owner_extra_info(self, obj):
return self.get_user_extra_info(obj.owner) return self.get_user_extra_info(obj.owner)
class ListAssignedToExtraInfoSerializerMixin(ListCachedUsersSerializerMixin): class AssignedToExtraInfoSerializerMixin(CachedUsersSerializerMixin):
assigned_to = serpy.Field(attr="assigned_to_id") assigned_to = Field(attr="assigned_to_id")
assigned_to_extra_info = serpy.MethodField() assigned_to_extra_info = MethodField()
def get_assigned_to_extra_info(self, obj): def get_assigned_to_extra_info(self, obj):
return self.get_user_extra_info(obj.assigned_to) return self.get_user_extra_info(obj.assigned_to)
class ListStatusExtraInfoSerializerMixin(serpy.Serializer): class StatusExtraInfoSerializerMixin(serializers.LightSerializer):
status = serpy.Field(attr="status_id") status = Field(attr="status_id")
status_extra_info = serpy.MethodField() status_extra_info = MethodField()
def to_value(self, instance): def to_value(self, instance):
self._serialized_status = {} self._serialized_status = {}
return super().to_value(instance) return super().to_value(instance)
def get_status_extra_info(self, obj): def get_status_extra_info(self, obj):
if obj.status_id is None:
return None
serialized_status = self._serialized_status.get(obj.status_id, None) serialized_status = self._serialized_status.get(obj.status_id, None)
if serialized_status is None: if serialized_status is None:
serialized_status = { serialized_status = {

View File

@ -16,8 +16,6 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import serpy
from functools import partial from functools import partial
from operator import is_not from operator import is_not
@ -28,16 +26,12 @@ from taiga.base import response
from taiga.base.decorators import detail_route from taiga.base.decorators import detail_route
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api.utils import get_object_or_404 from taiga.base.api.utils import get_object_or_404
from taiga.base.fields import WatchersField from taiga.base.fields import WatchersField, MethodField
from taiga.projects.notifications import services from taiga.projects.notifications import services
from taiga.projects.notifications.utils import (attach_watchers_to_queryset,
attach_is_watcher_to_queryset,
attach_total_watchers_to_queryset)
from . serializers import WatcherSerializer from . serializers import WatcherSerializer
class WatchedResourceMixin: class WatchedResourceMixin:
""" """
Rest Framework resource mixin for resources susceptible Rest Framework resource mixin for resources susceptible
@ -54,14 +48,6 @@ class WatchedResourceMixin:
_not_notify = False _not_notify = False
def attach_watchers_attrs_to_queryset(self, queryset):
queryset = attach_watchers_to_queryset(queryset)
queryset = attach_total_watchers_to_queryset(queryset)
if self.request.user.is_authenticated():
queryset = attach_is_watcher_to_queryset(queryset, self.request.user)
return queryset
@detail_route(methods=["POST"]) @detail_route(methods=["POST"])
def watch(self, request, pk=None): def watch(self, request, pk=None):
obj = self.get_object() obj = self.get_object()
@ -186,11 +172,15 @@ class WatchedModelMixin(object):
return frozenset(filter(is_not_none, participants)) return frozenset(filter(is_not_none, participants))
class BaseWatchedResourceModelSerializer(object): class WatchedResourceSerializer(serializers.LightSerializer):
is_watcher = MethodField()
total_watchers = MethodField()
def get_is_watcher(self, obj): def get_is_watcher(self, obj):
# The "is_watcher" attribute is attached in the get_queryset of the viewset.
if "request" in self.context: if "request" in self.context:
user = self.context["request"].user user = self.context["request"].user
return user.is_authenticated() and user.is_watcher(obj) return user.is_authenticated() and getattr(obj, "is_watcher", False)
return False return False
@ -199,28 +189,18 @@ class BaseWatchedResourceModelSerializer(object):
return getattr(obj, "total_watchers", 0) or 0 return getattr(obj, "total_watchers", 0) or 0
class WatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serializers.ModelSerializer): class EditableWatchedResourceSerializer(serializers.ModelSerializer):
is_watcher = serializers.SerializerMethodField("get_is_watcher")
total_watchers = serializers.SerializerMethodField("get_total_watchers")
class ListWatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serpy.Serializer):
is_watcher = serializers.SerializerMethodField("get_is_watcher")
total_watchers = serializers.SerializerMethodField("get_total_watchers")
class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
watchers = WatchersField(required=False) watchers = WatchersField(required=False)
def restore_object(self, attrs, instance=None): def restore_object(self, attrs, instance=None):
#watchers is not a field from the model but can be attached in the get_queryset of the viewset. # watchers is not a field from the model but can be attached in the get_queryset of the viewset.
#If that's the case we need to remove it before calling the super method # If that's the case we need to remove it before calling the super method
watcher_field = self.fields.pop("watchers", None) self.fields.pop("watchers", None)
self.validate_watchers(attrs, "watchers") self.validate_watchers(attrs, "watchers")
new_watcher_ids = attrs.pop("watchers", None) new_watcher_ids = attrs.pop("watchers", None)
obj = super(WatchedResourceModelSerializer, self).restore_object(attrs, instance) obj = super(EditableWatchedResourceSerializer, self).restore_object(attrs, instance)
#A partial update can exclude the watchers field or if the new instance can still not be saved # A partial update can exclude the watchers field or if the new instance can still not be saved
if instance is None or new_watcher_ids is None: if instance is None or new_watcher_ids is None:
return obj return obj
@ -229,7 +209,6 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids)) adding_watcher_ids = list(new_watcher_ids.difference(old_watcher_ids))
removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids)) removing_watcher_ids = list(old_watcher_ids.difference(new_watcher_ids))
User = get_user_model()
adding_users = get_user_model().objects.filter(id__in=adding_watcher_ids) adding_users = get_user_model().objects.filter(id__in=adding_watcher_ids)
removing_users = get_user_model().objects.filter(id__in=removing_watcher_ids) removing_users = get_user_model().objects.filter(id__in=removing_watcher_ids)
for user in adding_users: for user in adding_users:
@ -243,7 +222,7 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
return obj return obj
def to_native(self, obj): def to_native(self, obj):
#if watchers wasn't attached via the get_queryset of the viewset we need to manually add it # if watchers wasn't attached via the get_queryset of the viewset we need to manually add it
if obj is not None: if obj is not None:
if not hasattr(obj, "watchers"): if not hasattr(obj, "watchers"):
obj.watchers = [user.id for user in obj.get_watchers()] obj.watchers = [user.id for user in obj.get_watchers()]
@ -253,10 +232,10 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):
if user and user.is_authenticated(): if user and user.is_authenticated():
obj.is_watcher = user.id in obj.watchers obj.is_watcher = user.id in obj.watchers
return super(WatchedResourceModelSerializer, self).to_native(obj) return super(WatchedResourceSerializer, self).to_native(obj)
def save(self, **kwargs): def save(self, **kwargs):
obj = super(EditableWatchedResourceModelSerializer, self).save(**kwargs) obj = super(EditableWatchedResourceSerializer, self).save(**kwargs)
self.fields["watchers"] = WatchersField(required=False) self.fields["watchers"] = WatchersField(required=False)
obj.watchers = [user.id for user in obj.get_watchers()] obj.watchers = [user.id for user in obj.get_watchers()]
return obj return obj

View File

@ -53,15 +53,18 @@ def attach_is_watcher_to_queryset(queryset, user, as_field="is_watcher"):
""" """
model = queryset.model model = queryset.model
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
sql = ("""SELECT CASE WHEN (SELECT count(*) if user is None or user.is_anonymous():
FROM notifications_watched sql = """SELECT false"""
WHERE notifications_watched.content_type_id = {type_id} else:
AND notifications_watched.object_id = {tbl}.id sql = ("""SELECT CASE WHEN (SELECT count(*)
AND notifications_watched.user_id = {user_id}) > 0 FROM notifications_watched
THEN TRUE WHERE notifications_watched.content_type_id = {type_id}
ELSE FALSE AND notifications_watched.object_id = {tbl}.id
END""") AND notifications_watched.user_id = {user_id}) > 0
sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) THEN TRUE
ELSE FALSE
END""")
sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
qs = queryset.extra(select={as_field: sql}) qs = queryset.extra(select={as_field: sql})
return qs return qs

View File

@ -18,7 +18,7 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.exceptions import ValidationError
class WatchersValidator: class WatchersValidator:
@ -45,6 +45,6 @@ class WatchersValidator:
existing_watcher_ids = project.get_watchers().values_list("id", flat=True) existing_watcher_ids = project.get_watchers().values_list("id", flat=True)
result = set(users).difference(member_ids).difference(existing_watcher_ids) result = set(users).difference(member_ids).difference(existing_watcher_ids)
if result: if result:
raise serializers.ValidationError(_("Watchers contains invalid users")) raise ValidationError(_("Watchers contains invalid users"))
return attrs return attrs

View File

@ -24,7 +24,7 @@ from taiga.base.api import viewsets
from taiga.base.api.utils import get_object_or_404 from taiga.base.api.utils import get_object_or_404
from taiga.permissions.services import user_has_perm from taiga.permissions.services import user_has_perm
from .serializers import ResolverSerializer from .validators import ResolverValidator
from . import permissions from . import permissions
@ -32,11 +32,11 @@ class ResolverViewSet(viewsets.ViewSet):
permission_classes = (permissions.ResolverPermission,) permission_classes = (permissions.ResolverPermission,)
def list(self, request, **kwargs): def list(self, request, **kwargs):
serializer = ResolverSerializer(data=request.QUERY_PARAMS) validator = ResolverValidator(data=request.QUERY_PARAMS)
if not serializer.is_valid(): if not validator.is_valid():
raise exc.BadRequest(serializer.errors) raise exc.BadRequest(validator.errors)
data = serializer.data data = validator.data
project_model = apps.get_model("projects", "Project") project_model = apps.get_model("projects", "Project")
project = get_object_or_404(project_model, slug=data["project"]) project = get_object_or_404(project_model, slug=data["project"])

View File

@ -17,9 +17,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.exceptions import ValidationError
class ResolverSerializer(serializers.Serializer): class ResolverValidator(validators.Validator):
project = serializers.CharField(max_length=512, required=True) project = serializers.CharField(max_length=512, required=True)
milestone = serializers.CharField(max_length=512, required=False) milestone = serializers.CharField(max_length=512, required=False)
us = serializers.IntegerField(required=False) us = serializers.IntegerField(required=False)
@ -31,10 +33,10 @@ class ResolverSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
if "ref" in attrs: if "ref" in attrs:
if "us" in attrs: if "us" in attrs:
raise serializers.ValidationError("'us' param is incompatible with 'ref' in the same request") raise ValidationError("'us' param is incompatible with 'ref' in the same request")
if "task" in attrs: if "task" in attrs:
raise serializers.ValidationError("'task' param is incompatible with 'ref' in the same request") raise ValidationError("'task' param is incompatible with 'ref' in the same request")
if "issue" in attrs: if "issue" in attrs:
raise serializers.ValidationError("'issue' param is incompatible with 'ref' in the same request") raise ValidationError("'issue' param is incompatible with 'ref' in the same request")
return attrs return attrs

View File

@ -16,131 +16,121 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import serpy
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.db.models import Q
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import JsonField from taiga.base.fields import Field, MethodField, I18NField
from taiga.base.fields import PgArrayField
from taiga.permissions import services as permissions_services
from taiga.users.services import get_photo_or_gravatar_url from taiga.users.services import get_photo_or_gravatar_url
from taiga.users.serializers import UserBasicInfoSerializer from taiga.users.serializers import UserBasicInfoSerializer
from taiga.users.serializers import ProjectRoleSerializer
from taiga.users.validators import RoleExistsValidator
from taiga.permissions.services import get_user_project_permissions from taiga.permissions.services import calculate_permissions
from taiga.permissions.services import is_project_admin, is_project_owner from taiga.permissions.services import is_project_admin, is_project_owner
from . import models
from . import services from . import services
from .custom_attributes.serializers import UserStoryCustomAttributeSerializer
from .custom_attributes.serializers import TaskCustomAttributeSerializer
from .custom_attributes.serializers import IssueCustomAttributeSerializer
from .likes.mixins.serializers import FanResourceSerializerMixin
from .mixins.serializers import ValidateDuplicatedNameInProjectMixin
from .notifications.choices import NotifyLevel from .notifications.choices import NotifyLevel
from .notifications.mixins import WatchedResourceModelSerializer
from .tagging.fields import TagsField
from .tagging.fields import TagsColorsField
from .validators import ProjectExistsValidator
###################################################### ######################################################
## Custom values for selectors # Custom values for selectors
###################################################### ######################################################
class PointsSerializer(ValidateDuplicatedNameInProjectMixin): class PointsSerializer(serializers.LightSerializer):
class Meta: name = I18NField()
model = models.Points order = Field()
i18n_fields = ("name",) value = Field()
project = Field(attr="project_id")
class UserStoryStatusSerializer(ValidateDuplicatedNameInProjectMixin): class UserStoryStatusSerializer(serializers.LightSerializer):
class Meta: name = I18NField()
model = models.UserStoryStatus slug = Field()
i18n_fields = ("name",) order = Field()
is_closed = Field()
is_archived = Field()
color = Field()
wip_limit = Field()
project = Field(attr="project_id")
class BasicUserStoryStatusSerializer(serializers.ModelSerializer): class TaskStatusSerializer(serializers.LightSerializer):
class Meta: name = I18NField()
model = models.UserStoryStatus slug = Field()
i18n_fields = ("name",) order = Field()
fields = ("name", "color") is_closed = Field()
color = Field()
project = Field(attr="project_id")
class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin): class SeveritySerializer(serializers.LightSerializer):
class Meta: name = I18NField()
model = models.TaskStatus order = Field()
i18n_fields = ("name",) color = Field()
project = Field(attr="project_id")
class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer): class PrioritySerializer(serializers.LightSerializer):
class Meta: name = I18NField()
model = models.TaskStatus order = Field()
i18n_fields = ("name",) color = Field()
fields = ("name", "color") project = Field(attr="project_id")
class SeveritySerializer(ValidateDuplicatedNameInProjectMixin): class IssueStatusSerializer(serializers.LightSerializer):
class Meta: name = I18NField()
model = models.Severity slug = Field()
i18n_fields = ("name",) order = Field()
is_closed = Field()
color = Field()
project = Field(attr="project_id")
class PrioritySerializer(ValidateDuplicatedNameInProjectMixin): class IssueTypeSerializer(serializers.LightSerializer):
class Meta: name = I18NField()
model = models.Priority order = Field()
i18n_fields = ("name",) color = Field()
project = Field(attr="project_id")
class IssueStatusSerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
model = models.IssueStatus
i18n_fields = ("name",)
class BasicIssueStatusSerializer(serializers.ModelSerializer):
class Meta:
model = models.IssueStatus
i18n_fields = ("name",)
fields = ("name", "color")
class IssueTypeSerializer(ValidateDuplicatedNameInProjectMixin):
class Meta:
model = models.IssueType
i18n_fields = ("name",)
###################################################### ######################################################
## Members # Members
###################################################### ######################################################
class MembershipSerializer(serializers.ModelSerializer): class MembershipSerializer(serializers.LightSerializer):
role_name = serializers.CharField(source='role.name', required=False, read_only=True, i18n=True) id = Field()
full_name = serializers.CharField(source='user.get_full_name', required=False, read_only=True) user = Field(attr="user_id")
user_email = serializers.EmailField(source='user.email', required=False, read_only=True) project = Field(attr="project_id")
is_user_active = serializers.BooleanField(source='user.is_active', required=False, role = Field(attr="role_id")
read_only=True) is_admin = Field()
email = serializers.EmailField(required=True) created_at = Field()
color = serializers.CharField(source='user.color', required=False, read_only=True) invited_by = Field(attr="invited_by_id")
photo = serializers.SerializerMethodField("get_photo") invitation_extra_text = Field()
project_name = serializers.SerializerMethodField("get_project_name") user_order = Field()
project_slug = serializers.SerializerMethodField("get_project_slug") role_name = MethodField()
invited_by = UserBasicInfoSerializer(read_only=True) full_name = MethodField()
is_owner = serializers.SerializerMethodField("get_is_owner") is_user_active = MethodField()
color = MethodField()
photo = MethodField()
project_name = MethodField()
project_slug = MethodField()
invited_by = UserBasicInfoSerializer()
is_owner = MethodField()
class Meta: def get_role_name(self, obj):
model = models.Membership return obj.role.name if obj.role else None
# IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date
# with this info (excluding here user_email and email)
read_only_fields = ("user",)
exclude = ("token", "user_email", "email")
def get_photo(self, project): def get_full_name(self, obj):
return get_photo_or_gravatar_url(project.user) return obj.user.get_full_name() if obj.user else None
def get_is_user_active(self, obj):
return obj.user.is_active if obj.user else False
def get_color(self, obj):
return obj.user.color if obj.user else None
def get_photo(self, obj):
return get_photo_or_gravatar_url(obj.user)
def get_project_name(self, obj): def get_project_name(self, obj):
return obj.project.name if obj and obj.project else "" return obj.project.name if obj and obj.project else ""
@ -152,131 +142,124 @@ class MembershipSerializer(serializers.ModelSerializer):
return (obj and obj.user_id and obj.project_id and obj.project.owner_id and return (obj and obj.user_id and obj.project_id and obj.project.owner_id and
obj.user_id == obj.project.owner_id) obj.user_id == obj.project.owner_id)
def validate_email(self, attrs, source):
project = attrs.get("project", None)
if project is None:
project = self.object.project
email = attrs[source]
qs = models.Membership.objects.all()
# If self.object is not None, the serializer is in update
# mode, and for it, it should exclude self.
if self.object:
qs = qs.exclude(pk=self.object.pk)
qs = qs.filter(Q(project_id=project.id, user__email=email) |
Q(project_id=project.id, email=email))
if qs.count() > 0:
raise serializers.ValidationError(_("Email address is already taken"))
return attrs
def validate_role(self, attrs, source):
project = attrs.get("project", None)
if project is None:
project = self.object.project
role = attrs[source]
if project.roles.filter(id=role.id).count() == 0:
raise serializers.ValidationError(_("Invalid role for the project"))
return attrs
def validate_is_admin(self, attrs, source):
project = attrs.get("project", None)
if project is None:
project = self.object.project
if (self.object and self.object.user):
if self.object.user.id == project.owner_id and attrs[source] != True:
raise serializers.ValidationError(_("The project owner must be admin."))
if not services.project_has_valid_admins(project, exclude_user=self.object.user):
raise serializers.ValidationError(_("At least one user must be an active admin for this project."))
return attrs
class MembershipAdminSerializer(MembershipSerializer): class MembershipAdminSerializer(MembershipSerializer):
class Meta: email = Field()
model = models.Membership user_email = MethodField()
# IMPORTANT: Maintain the MembershipSerializer Meta up to date
# with this info (excluding there user_email and email)
read_only_fields = ("user",)
exclude = ("token",)
def get_user_email(self, obj):
return obj.user.email if obj.user else None
class MemberBulkSerializer(RoleExistsValidator, serializers.Serializer): # IMPORTANT: Maintain the MembershipSerializer Meta up to date
email = serializers.EmailField() # with this info (excluding there user_email and email)
role_id = serializers.IntegerField()
class MembersBulkSerializer(ProjectExistsValidator, serializers.Serializer):
project_id = serializers.IntegerField()
bulk_memberships = MemberBulkSerializer(many=True)
invitation_extra_text = serializers.CharField(required=False, max_length=255)
class ProjectMemberSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source="user.id", read_only=True)
username = serializers.CharField(source='user.username', read_only=True)
full_name = serializers.CharField(source='user.full_name', read_only=True)
full_name_display = serializers.CharField(source='user.get_full_name', read_only=True)
color = serializers.CharField(source='user.color', read_only=True)
photo = serializers.SerializerMethodField("get_photo")
is_active = serializers.BooleanField(source='user.is_active', read_only=True)
role_name = serializers.CharField(source='role.name', read_only=True, i18n=True)
class Meta:
model = models.Membership
exclude = ("project", "email", "created_at", "token", "invited_by", "invitation_extra_text",
"user_order")
def get_photo(self, membership):
return get_photo_or_gravatar_url(membership.user)
###################################################### ######################################################
## Projects # Projects
###################################################### ######################################################
class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializer, class ProjectSerializer(serializers.LightSerializer):
serializers.ModelSerializer): id = Field()
anon_permissions = PgArrayField(required=False) name = Field()
public_permissions = PgArrayField(required=False) slug = Field()
my_permissions = serializers.SerializerMethodField("get_my_permissions") description = Field()
created_date = Field()
modified_date = Field()
owner = MethodField()
members = MethodField()
total_milestones = Field()
total_story_points = Field()
is_backlog_activated = Field()
is_kanban_activated = Field()
is_wiki_activated = Field()
is_issues_activated = Field()
videoconferences = Field()
videoconferences_extra_data = Field()
creation_template = Field(attr="creation_template_id")
is_private = Field()
anon_permissions = Field()
public_permissions = Field()
is_featured = Field()
is_looking_for_people = Field()
looking_for_people_note = Field()
blocked_code = Field()
totals_updated_datetime = Field()
total_fans = Field()
total_fans_last_week = Field()
total_fans_last_month = Field()
total_fans_last_year = Field()
total_activity = Field()
total_activity_last_week = Field()
total_activity_last_month = Field()
total_activity_last_year = Field()
owner = UserBasicInfoSerializer(read_only=True) tags = Field()
i_am_owner = serializers.SerializerMethodField("get_i_am_owner") tags_colors = MethodField()
i_am_admin = serializers.SerializerMethodField("get_i_am_admin")
i_am_member = serializers.SerializerMethodField("get_i_am_member")
tags = TagsField(default=[], required=False) default_points = Field(attr="default_points_id")
tags_colors = TagsColorsField(required=False, read_only=True) default_us_status = Field(attr="default_us_status_id")
default_task_status = Field(attr="default_task_status_id")
default_priority = Field(attr="default_priority_id")
default_severity = Field(attr="default_severity_id")
default_issue_status = Field(attr="default_issue_status_id")
default_issue_type = Field(attr="default_issue_type_id")
notify_level = serializers.SerializerMethodField("get_notify_level") my_permissions = MethodField()
total_closed_milestones = serializers.SerializerMethodField("get_total_closed_milestones")
total_watchers = serializers.SerializerMethodField("get_total_watchers")
logo_small_url = serializers.SerializerMethodField("get_logo_small_url") i_am_owner = MethodField()
logo_big_url = serializers.SerializerMethodField("get_logo_big_url") i_am_admin = MethodField()
i_am_member = MethodField()
class Meta: notify_level = MethodField()
model = models.Project total_closed_milestones = MethodField()
read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref", is_watcher = MethodField()
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid", total_watchers = MethodField()
"transfer_token")
logo_small_url = MethodField()
logo_big_url = MethodField()
is_fan = Field(attr="is_fan_attr")
def get_members(self, obj):
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
if obj.members_attr is None:
return []
return [m.get("id") for m in obj.members_attr if m["id"] is not None]
def get_i_am_member(self, obj):
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
if obj.members_attr is None:
return False
if "request" in self.context:
user = self.context["request"].user
user_ids = [m.get("id") for m in obj.members_attr if m["id"] is not None]
if not user.is_anonymous() and user.id in user_ids:
return True
return False
def get_tags_colors(self, obj):
return dict(obj.tags_colors)
def get_my_permissions(self, obj): def get_my_permissions(self, obj):
if "request" in self.context: if "request" in self.context:
return get_user_project_permissions(self.context["request"].user, obj) user = self.context["request"].user
return calculate_permissions(
is_authenticated=user.is_authenticated(),
is_superuser=user.is_superuser,
is_member=self.get_i_am_member(obj),
is_admin=self.get_i_am_admin(obj),
role_permissions=obj.my_role_permissions_attr,
anon_permissions=obj.anon_permissions,
public_permissions=obj.public_permissions)
return [] return []
def get_owner(self, obj):
return UserBasicInfoSerializer(obj.owner).data
def get_i_am_owner(self, obj): def get_i_am_owner(self, obj):
if "request" in self.context: if "request" in self.context:
return is_project_owner(self.context["request"].user, obj) return is_project_owner(self.context["request"].user, obj)
@ -287,35 +270,35 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return is_project_admin(self.context["request"].user, obj) return is_project_admin(self.context["request"].user, obj)
return False return False
def get_i_am_member(self, obj):
if "request" in self.context:
user = self.context["request"].user
if not user.is_anonymous() and user.cached_membership_for_project(obj):
return True
return False
def get_total_closed_milestones(self, obj): def get_total_closed_milestones(self, obj):
# The "closed_milestone" attribute can be attached in the get_queryset method of the viewset. assert hasattr(obj, "closed_milestones_attr"), "instance must have a closed_milestones_attr attribute"
qs_closed_milestones = getattr(obj, "closed_milestones", None) return obj.closed_milestones_attr
if qs_closed_milestones is not None:
return len(qs_closed_milestones)
return obj.milestones.filter(closed=True).count() def get_is_watcher(self, obj):
assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
def get_notify_level(self, obj): np = self.get_notify_level(obj)
if "request" in self.context: return np is not None and np != NotifyLevel.none
user = self.context["request"].user
return user.is_authenticated() and user.get_notify_level(obj)
return None
def get_total_watchers(self, obj): def get_total_watchers(self, obj):
# The "valid_notify_policies" attribute can be attached in the get_queryset method of the viewset. assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
qs_valid_notify_policies = getattr(obj, "valid_notify_policies", None) if obj.notify_policies_attr is None:
if qs_valid_notify_policies is not None: return 0
return len(qs_valid_notify_policies)
return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count() valid_notify_policies = [np for np in obj.notify_policies_attr if np["notify_level"] != NotifyLevel.none]
return len(valid_notify_policies)
def get_notify_level(self, obj):
assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
if obj.notify_policies_attr is None:
return None
if "request" in self.context:
user = self.context["request"].user
for np in obj.notify_policies_attr:
if np["user_id"] == user.id:
return np["notify_level"]
return None
def get_logo_small_url(self, obj): def get_logo_small_url(self, obj):
return services.get_logo_small_thumbnail_url(obj) return services.get_logo_small_thumbnail_url(obj)
@ -325,94 +308,132 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
class ProjectDetailSerializer(ProjectSerializer): class ProjectDetailSerializer(ProjectSerializer):
us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories us_statuses = Field(attr="userstory_statuses_attr")
points = PointsSerializer(many=True, required=False) points = Field(attr="points_attr")
task_statuses = Field(attr="task_statuses_attr")
issue_statuses = Field(attr="issue_statuses_attr")
issue_types = Field(attr="issue_types_attr")
priorities = Field(attr="priorities_attr")
severities = Field(attr="severities_attr")
userstory_custom_attributes = Field(attr="userstory_custom_attributes_attr")
task_custom_attributes = Field(attr="task_custom_attributes_attr")
issue_custom_attributes = Field(attr="issue_custom_attributes_attr")
roles = Field(attr="roles_attr")
members = MethodField()
total_memberships = MethodField()
is_out_of_owner_limits = MethodField()
task_statuses = TaskStatusSerializer(many=True, required=False) # Tasks # Admin fields
is_private_extra_info = MethodField()
max_memberships = MethodField()
issues_csv_uuid = Field()
tasks_csv_uuid = Field()
userstories_csv_uuid = Field()
transfer_token = Field()
issue_statuses = IssueStatusSerializer(many=True, required=False) def to_value(self, instance):
issue_types = IssueTypeSerializer(many=True, required=False) # Name attributes must be translated
priorities = PrioritySerializer(many=True, required=False) # Issues for attr in ["userstory_statuses_attr", "points_attr", "task_statuses_attr",
severities = SeveritySerializer(many=True, required=False) "issue_statuses_attr", "issue_types_attr", "priorities_attr",
"severities_attr", "userstory_custom_attributes_attr",
"task_custom_attributes_attr", "issue_custom_attributes_attr", "roles_attr"]:
userstory_custom_attributes = UserStoryCustomAttributeSerializer(source="userstorycustomattributes", assert hasattr(instance, attr), "instance must have a {} attribute".format(attr)
many=True, required=False) val = getattr(instance, attr)
task_custom_attributes = TaskCustomAttributeSerializer(source="taskcustomattributes", if val is None:
many=True, required=False) continue
issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes",
many=True, required=False)
roles = ProjectRoleSerializer(source="roles", many=True, read_only=True) for elem in val:
members = serializers.SerializerMethodField(method_name="get_members") elem["name"] = _(elem["name"])
total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships")
is_out_of_owner_limits = serializers.SerializerMethodField(method_name="get_is_out_of_owner_limits") ret = super().to_value(instance)
admin_fields = [
"is_private_extra_info", "max_memberships", "issues_csv_uuid",
"tasks_csv_uuid", "userstories_csv_uuid", "transfer_token"
]
is_admin_user = False
if "request" in self.context:
user = self.context["request"].user
is_admin_user = permissions_services.is_project_admin(user, instance)
if not is_admin_user:
for admin_field in admin_fields:
del(ret[admin_field])
return ret
def get_members(self, obj): def get_members(self, obj):
qs = obj.memberships.filter(user__isnull=False) assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
qs = qs.extra(select={"complete_user_name":"concat(full_name, username)"}) if obj.members_attr is None:
qs = qs.order_by("complete_user_name") return []
qs = qs.select_related("role", "user")
serializer = ProjectMemberSerializer(qs, many=True) ret = []
return serializer.data for m in obj.members_attr:
m["full_name_display"] = m["full_name"] or m["username"] or m["email"]
del(m["email"])
del(m["complete_user_name"])
if not m["id"] is None:
ret.append(m)
return ret
def get_total_memberships(self, obj): def get_total_memberships(self, obj):
return services.get_total_project_memberships(obj) if obj.members_attr is None:
return 0
return len(obj.members_attr)
def get_is_out_of_owner_limits(self, obj): def get_is_out_of_owner_limits(self, obj):
return services.check_if_project_is_out_of_owner_limits(obj) assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_is_out_of_owner_limits(
class ProjectDetailAdminSerializer(ProjectDetailSerializer): obj,
is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info") current_memberships=self.get_total_memberships(obj),
max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships") current_private_projects=obj.private_projects_same_owner_attr,
current_public_projects=obj.public_projects_same_owner_attr
class Meta: )
model = models.Project
read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref")
def get_is_private_extra_info(self, obj): def get_is_private_extra_info(self, obj):
return services.check_if_project_privacity_can_be_changed(obj) assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_privacity_can_be_changed(
obj,
current_memberships=self.get_total_memberships(obj),
current_private_projects=obj.private_projects_same_owner_attr,
current_public_projects=obj.public_projects_same_owner_attr
)
def get_max_memberships(self, obj): def get_max_memberships(self, obj):
return services.get_max_memberships_for_project(obj) return services.get_max_memberships_for_project(obj)
###################################################### ######################################################
## Liked # Project Templates
###################################################### ######################################################
class LikedSerializer(serializers.ModelSerializer): class ProjectTemplateSerializer(serializers.LightSerializer):
class Meta: id = Field()
model = models.Project name = I18NField()
fields = ['id', 'name', 'slug'] slug = Field()
description = I18NField()
order = Field()
created_date = Field()
###################################################### modified_date = Field()
## Project Templates default_owner_role = Field()
###################################################### is_backlog_activated = Field()
is_kanban_activated = Field()
class ProjectTemplateSerializer(serializers.ModelSerializer): is_wiki_activated = Field()
default_options = JsonField(required=False, label=_("Default options")) is_issues_activated = Field()
us_statuses = JsonField(required=False, label=_("User story's statuses")) videoconferences = Field()
points = JsonField(required=False, label=_("Points")) videoconferences_extra_data = Field()
task_statuses = JsonField(required=False, label=_("Task's statuses")) default_options = Field()
issue_statuses = JsonField(required=False, label=_("Issue's statuses")) us_statuses = Field()
issue_types = JsonField(required=False, label=_("Issue's types")) points = Field()
priorities = JsonField(required=False, label=_("Priorities")) task_statuses = Field()
severities = JsonField(required=False, label=_("Severities")) issue_statuses = Field()
roles = JsonField(required=False, label=_("Roles")) issue_types = Field()
priorities = Field()
class Meta: severities = Field()
model = models.ProjectTemplate roles = Field()
read_only_fields = ("created_date", "modified_date")
i18n_fields = ("name", "description")
######################################################
## Project order bulk serializers
######################################################
class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
project_id = serializers.IntegerField()
order = serializers.IntegerField()

View File

@ -27,30 +27,45 @@ ERROR_MAX_PUBLIC_PROJECTS = 'max_public_projects'
ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects' ERROR_MAX_PRIVATE_PROJECTS = 'max_private_projects'
ERROR_PROJECT_WITHOUT_OWNER = 'project_without_owner' ERROR_PROJECT_WITHOUT_OWNER = 'project_without_owner'
def check_if_project_privacity_can_be_changed(project): def check_if_project_privacity_can_be_changed(project,
current_memberships=None,
current_private_projects=None,
current_public_projects=None):
"""Return if the project privacity can be changed from private to public or viceversa. """Return if the project privacity can be changed from private to public or viceversa.
:param project: A project object. :param project: A project object.
:param current_memberships: Project total memberships, If None it will be calculated.
:param current_private_projects: total private projects owned by the project owner, If None it will be calculated.
:param current_public_projects: total public projects owned by the project owner, If None it will be calculated.
:return: A dict like this {'can_be_updated': bool, 'reason': error message}. :return: A dict like this {'can_be_updated': bool, 'reason': error message}.
""" """
if project.owner is None: if project.owner is None:
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER} return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
if project.is_private: if current_memberships is None:
current_memberships = project.memberships.count() current_memberships = project.memberships.count()
if project.is_private:
max_memberships = project.owner.max_memberships_public_projects max_memberships = project.owner.max_memberships_public_projects
error_memberships_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS error_memberships_exceeded = ERROR_MAX_PUBLIC_PROJECTS_MEMBERSHIPS
current_projects = project.owner.owned_projects.filter(is_private=False).count() if current_public_projects is None:
current_projects = project.owner.owned_projects.filter(is_private=False).count()
else:
current_projects = current_public_projects
max_projects = project.owner.max_public_projects max_projects = project.owner.max_public_projects
error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS error_project_exceeded = ERROR_MAX_PUBLIC_PROJECTS
else: else:
current_memberships = project.memberships.count()
max_memberships = project.owner.max_memberships_private_projects max_memberships = project.owner.max_memberships_private_projects
error_memberships_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS error_memberships_exceeded = ERROR_MAX_PRIVATE_PROJECTS_MEMBERSHIPS
current_projects = project.owner.owned_projects.filter(is_private=True).count() if current_private_projects is None:
current_projects = project.owner.owned_projects.filter(is_private=True).count()
else:
current_projects = current_private_projects
max_projects = project.owner.max_private_projects max_projects = project.owner.max_private_projects
error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS error_project_exceeded = ERROR_MAX_PRIVATE_PROJECTS
@ -139,25 +154,43 @@ def check_if_project_can_be_transfered(project, new_owner):
return (True, None) return (True, None)
def check_if_project_is_out_of_owner_limits(project): def check_if_project_is_out_of_owner_limits(project,
current_memberships=None,
current_private_projects=None,
current_public_projects=None):
"""Return if the project fits on its owner limits. """Return if the project fits on its owner limits.
:param project: A project object. :param project: A project object.
:param current_memberships: Project total memberships, If None it will be calculated.
:param current_private_projects: total private projects owned by the project owner, If None it will be calculated.
:param current_public_projects: total public projects owned by the project owner, If None it will be calculated.
:return: bool :return: bool
""" """
if project.owner is None: if project.owner is None:
return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER} return {'can_be_updated': False, 'reason': ERROR_PROJECT_WITHOUT_OWNER}
if project.is_private: if current_memberships is None:
current_memberships = project.memberships.count() current_memberships = project.memberships.count()
if project.is_private:
max_memberships = project.owner.max_memberships_private_projects max_memberships = project.owner.max_memberships_private_projects
current_projects = project.owner.owned_projects.filter(is_private=True).count()
if current_private_projects is None:
current_projects = project.owner.owned_projects.filter(is_private=True).count()
else:
current_projects = current_private_projects
max_projects = project.owner.max_private_projects max_projects = project.owner.max_private_projects
else: else:
current_memberships = project.memberships.count()
max_memberships = project.owner.max_memberships_public_projects max_memberships = project.owner.max_memberships_public_projects
current_projects = project.owner.owned_projects.filter(is_private=False).count()
if current_public_projects is None:
current_projects = project.owner.owned_projects.filter(is_private=False).count()
else:
current_projects = current_public_projects
max_projects = project.owner.max_public_projects max_projects = project.owner.max_public_projects
if max_memberships is not None and current_memberships > max_memberships: if max_memberships is not None and current_memberships > max_memberships:

View File

@ -21,7 +21,7 @@ from taiga.base.decorators import detail_route
from taiga.base.utils.collections import OrderedSet from taiga.base.utils.collections import OrderedSet
from . import services from . import services
from . import serializers from . import validators
class TagsColorsResourceMixin: class TagsColorsResourceMixin:
@ -38,27 +38,26 @@ class TagsColorsResourceMixin:
self.check_permissions(request, "create_tag", project) self.check_permissions(request, "create_tag", project)
self._raise_if_blocked(project) self._raise_if_blocked(project)
serializer = serializers.CreateTagSerializer(data=request.DATA, project=project) validator = validators.CreateTagValidator(data=request.DATA, project=project)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
data = serializer.data data = validator.data
services.create_tag(project, data.get("tag"), data.get("color")) services.create_tag(project, data.get("tag"), data.get("color"))
return response.Ok() return response.Ok()
@detail_route(methods=["POST"]) @detail_route(methods=["POST"])
def edit_tag(self, request, pk=None): def edit_tag(self, request, pk=None):
project = self.get_object() project = self.get_object()
self.check_permissions(request, "edit_tag", project) self.check_permissions(request, "edit_tag", project)
self._raise_if_blocked(project) self._raise_if_blocked(project)
serializer = serializers.EditTagTagSerializer(data=request.DATA, project=project) validator = validators.EditTagTagValidator(data=request.DATA, project=project)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
data = serializer.data data = validator.data
services.edit_tag(project, services.edit_tag(project,
data.get("from_tag"), data.get("from_tag"),
to_tag=data.get("to_tag", None), to_tag=data.get("to_tag", None),
@ -66,18 +65,17 @@ class TagsColorsResourceMixin:
return response.Ok() return response.Ok()
@detail_route(methods=["POST"]) @detail_route(methods=["POST"])
def delete_tag(self, request, pk=None): def delete_tag(self, request, pk=None):
project = self.get_object() project = self.get_object()
self.check_permissions(request, "delete_tag", project) self.check_permissions(request, "delete_tag", project)
self._raise_if_blocked(project) self._raise_if_blocked(project)
serializer = serializers.DeleteTagSerializer(data=request.DATA, project=project) validator = validators.DeleteTagValidator(data=request.DATA, project=project)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
data = serializer.data data = validator.data
services.delete_tag(project, data.get("tag")) services.delete_tag(project, data.get("tag"))
return response.Ok() return response.Ok()
@ -88,11 +86,11 @@ class TagsColorsResourceMixin:
self.check_permissions(request, "mix_tags", project) self.check_permissions(request, "mix_tags", project)
self._raise_if_blocked(project) self._raise_if_blocked(project)
serializer = serializers.MixTagsSerializer(data=request.DATA, project=project) validator = validators.MixTagsValidator(data=request.DATA, project=project)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
data = serializer.data data = validator.data
services.mix_tags(project, data.get("from_tags"), data.get("to_tag")) services.mix_tags(project, data.get("from_tags"), data.get("to_tag"))
return response.Ok() return response.Ok()

View File

@ -16,11 +16,11 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core.exceptions import ValidationError
from django.forms import widgets from django.forms import widgets
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.exceptions import ValidationError
import re import re

View File

@ -19,6 +19,7 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api import validators
from . import services from . import services
from . import fields from . import fields
@ -26,7 +27,7 @@ from . import fields
import re import re
class ProjectTagSerializer(serializers.Serializer): class ProjectTagValidator(validators.Validator):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't pass the extra project arg # Don't pass the extra project arg
self.project = kwargs.pop("project") self.project = kwargs.pop("project")
@ -35,26 +36,26 @@ class ProjectTagSerializer(serializers.Serializer):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
class CreateTagSerializer(ProjectTagSerializer): class CreateTagValidator(ProjectTagValidator):
tag = serializers.CharField() tag = serializers.CharField()
color = serializers.CharField(required=False) color = serializers.CharField(required=False)
def validate_tag(self, attrs, source): def validate_tag(self, attrs, source):
tag = attrs.get(source, None) tag = attrs.get(source, None)
if services.tag_exist_for_project_elements(self.project, tag): if services.tag_exist_for_project_elements(self.project, tag):
raise serializers.ValidationError(_("The tag exists.")) raise validators.ValidationError(_("The tag exists."))
return attrs return attrs
def validate_color(self, attrs, source): def validate_color(self, attrs, source):
color = attrs.get(source, None) color = attrs.get(source, None)
if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
raise serializers.ValidationError(_("The color is not a valid HEX color.")) raise validators.ValidationError(_("The color is not a valid HEX color."))
return attrs return attrs
class EditTagTagSerializer(ProjectTagSerializer): class EditTagTagValidator(ProjectTagValidator):
from_tag = serializers.CharField() from_tag = serializers.CharField()
to_tag = serializers.CharField(required=False) to_tag = serializers.CharField(required=False)
color = serializers.CharField(required=False) color = serializers.CharField(required=False)
@ -62,37 +63,37 @@ class EditTagTagSerializer(ProjectTagSerializer):
def validate_from_tag(self, attrs, source): def validate_from_tag(self, attrs, source):
tag = attrs.get(source, None) tag = attrs.get(source, None)
if not services.tag_exist_for_project_elements(self.project, tag): if not services.tag_exist_for_project_elements(self.project, tag):
raise serializers.ValidationError(_("The tag doesn't exist.")) raise validators.ValidationError(_("The tag doesn't exist."))
return attrs return attrs
def validate_to_tag(self, attrs, source): def validate_to_tag(self, attrs, source):
tag = attrs.get(source, None) tag = attrs.get(source, None)
if services.tag_exist_for_project_elements(self.project, tag): if services.tag_exist_for_project_elements(self.project, tag):
raise serializers.ValidationError(_("The tag exists yet")) raise validators.ValidationError(_("The tag exists yet"))
return attrs return attrs
def validate_color(self, attrs, source): def validate_color(self, attrs, source):
color = attrs.get(source, None) color = attrs.get(source, None)
if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color): if not re.match('^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$', color):
raise serializers.ValidationError(_("The color is not a valid HEX color.")) raise validators.ValidationError(_("The color is not a valid HEX color."))
return attrs return attrs
class DeleteTagSerializer(ProjectTagSerializer): class DeleteTagValidator(ProjectTagValidator):
tag = serializers.CharField() tag = serializers.CharField()
def validate_tag(self, attrs, source): def validate_tag(self, attrs, source):
tag = attrs.get(source, None) tag = attrs.get(source, None)
if not services.tag_exist_for_project_elements(self.project, tag): if not services.tag_exist_for_project_elements(self.project, tag):
raise serializers.ValidationError(_("The tag doesn't exist.")) raise validators.ValidationError(_("The tag doesn't exist."))
return attrs return attrs
class MixTagsSerializer(ProjectTagSerializer): class MixTagsValidator(ProjectTagValidator):
from_tags = fields.TagsField() from_tags = fields.TagsField()
to_tag = serializers.CharField() to_tag = serializers.CharField()
@ -100,13 +101,13 @@ class MixTagsSerializer(ProjectTagSerializer):
tags = attrs.get(source, None) tags = attrs.get(source, None)
for tag in tags: for tag in tags:
if not services.tag_exist_for_project_elements(self.project, tag): if not services.tag_exist_for_project_elements(self.project, tag):
raise serializers.ValidationError(_("The tag doesn't exist.")) raise validators.ValidationError(_("The tag doesn't exist."))
return attrs return attrs
def validate_to_tag(self, attrs, source): def validate_to_tag(self, attrs, source):
tag = attrs.get(source, None) tag = attrs.get(source, None)
if not services.tag_exist_for_project_elements(self.project, tag): if not services.tag_exist_for_project_elements(self.project, tag):
raise serializers.ValidationError(_("The tag doesn't exist.")) raise validators.ValidationError(_("The tag doesn't exist."))
return attrs return attrs

View File

@ -26,7 +26,6 @@ from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, TaskStatus from taiga.projects.models import Project, TaskStatus
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
@ -38,10 +37,14 @@ from . import models
from . import permissions from . import permissions
from . import serializers from . import serializers
from . import services from . import services
from . import validators
from . import utils as tasks_utils
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin,
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin,
ModelCrudViewSet):
validator_class = validators.TaskValidator
queryset = models.Task.objects.all() queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,) permission_classes = (permissions.TaskPermission,)
filter_backends = (filters.CanViewTasksFilterBackend, filter_backends = (filters.CanViewTasksFilterBackend,
@ -74,17 +77,15 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = self.attach_votes_attrs_to_queryset(qs)
qs = qs.select_related("milestone", qs = qs.select_related("milestone",
"project", "project",
"status", "status",
"owner", "owner",
"assigned_to") "assigned_to")
qs = self.attach_watchers_attrs_to_queryset(qs) include_attachments = "include_attachments" in self.request.QUERY_PARAMS
if "include_attachments" in self.request.QUERY_PARAMS: qs = tasks_utils.attach_extra_info(qs, user=self.request.user,
qs = attach_basic_attachments(qs) include_attachments=include_attachments)
qs = qs.extra(select={"include_attachments": "True"})
return qs return qs
@ -162,10 +163,18 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
@list_route(methods=["GET"]) @list_route(methods=["GET"])
def by_ref(self, request): def by_ref(self, request):
ref = request.QUERY_PARAMS.get("ref", None) retrieve_kwargs = {
"ref": request.QUERY_PARAMS.get("ref", None)
}
project_id = request.QUERY_PARAMS.get("project", None) project_id = request.QUERY_PARAMS.get("project", None)
task = get_object_or_404(models.Task, ref=ref, project_id=project_id) if project_id is not None:
return self.retrieve(request, pk=task.pk) retrieve_kwargs["project_id"] = project_id
project_slug = request.QUERY_PARAMS.get("project__slug", None)
if project_slug is not None:
retrieve_kwargs["project__slug"] = project_slug
return self.retrieve(request, **retrieve_kwargs)
@list_route(methods=["GET"]) @list_route(methods=["GET"])
def csv(self, request): def csv(self, request):
@ -182,9 +191,9 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
@list_route(methods=["POST"]) @list_route(methods=["POST"])
def bulk_create(self, request, **kwargs): def bulk_create(self, request, **kwargs):
serializer = serializers.TasksBulkSerializer(data=request.DATA) validator = validators.TasksBulkValidator(data=request.DATA)
if serializer.is_valid(): if validator.is_valid():
data = serializer.data data = validator.data
project = Project.objects.get(id=data["project_id"]) project = Project.objects.get(id=data["project_id"])
self.check_permissions(request, 'bulk_create', project) self.check_permissions(request, 'bulk_create', project)
if project.blocked_code is not None: if project.blocked_code is not None:
@ -194,18 +203,20 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
data["bulk_tasks"], milestone_id=data["sprint_id"], user_story_id=data["us_id"], data["bulk_tasks"], milestone_id=data["sprint_id"], user_story_id=data["us_id"],
status_id=data.get("status_id") or project.default_task_status_id, status_id=data.get("status_id") or project.default_task_status_id,
project=project, owner=request.user, callback=self.post_save, precall=self.pre_save) project=project, owner=request.user, callback=self.post_save, precall=self.pre_save)
tasks = self.get_queryset().filter(id__in=[i.id for i in tasks])
tasks_serialized = self.get_serializer_class()(tasks, many=True) tasks_serialized = self.get_serializer_class()(tasks, many=True)
return response.Ok(tasks_serialized.data) return response.Ok(tasks_serialized.data)
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
def _bulk_update_order(self, order_field, request, **kwargs): def _bulk_update_order(self, order_field, request, **kwargs):
serializer = serializers.UpdateTasksOrderBulkSerializer(data=request.DATA) validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
data = serializer.data data = validator.data
project = get_object_or_404(Project, pk=data["project_id"]) project = get_object_or_404(Project, pk=data["project_id"])
self.check_permissions(request, "bulk_update_order", project) self.check_permissions(request, "bulk_update_order", project)

View File

@ -16,101 +16,44 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import PgArrayField from taiga.base.fields import Field, MethodField
from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
from taiga.projects.milestones.validators import SprintExistsValidator from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicTaskStatusSerializerSerializer
from taiga.mdrender.service import render as mdrender
from taiga.projects.tagging.fields import TagsAndTagsColorsField
from taiga.projects.tasks.validators import TaskExistsValidator
from taiga.projects.validators import ProjectExistsValidator
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
from taiga.users.serializers import UserBasicInfoSerializer
from taiga.users.services import get_photo_or_gravatar_url
from taiga.users.services import get_big_photo_or_gravatar_url
from . import models
import serpy
class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer,
serializers.ModelSerializer): OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
tags = TagsAndTagsColorsField(default=[], required=False)
external_reference = PgArrayField(required=False)
comment = serializers.SerializerMethodField("get_comment")
milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
description_html = serializers.SerializerMethodField("get_description_html")
is_closed = serializers.SerializerMethodField("get_is_closed")
status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True)
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
class Meta:
model = models.Task
read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
def get_comment(self, obj):
return ""
def get_milestone_slug(self, obj):
if obj.milestone:
return obj.milestone.slug
else:
return None
def get_blocked_note_html(self, obj):
return mdrender(obj.project, obj.blocked_note)
def get_description_html(self, obj):
return mdrender(obj.project, obj.description)
def get_is_closed(self, obj):
return obj.status is not None and obj.status.is_closed
class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin,
serializers.LightSerializer): serializers.LightSerializer):
id = serpy.Field() id = Field()
user_story = serpy.Field(attr="user_story_id") user_story = Field(attr="user_story_id")
ref = serpy.Field() ref = Field()
project = serpy.Field(attr="project_id") project = Field(attr="project_id")
milestone = serpy.Field(attr="milestone_id") milestone = Field(attr="milestone_id")
milestone_slug = serpy.MethodField("get_milestone_slug") milestone_slug = MethodField()
created_date = serpy.Field() created_date = Field()
modified_date = serpy.Field() modified_date = Field()
finished_date = serpy.Field() finished_date = Field()
subject = serpy.Field() subject = Field()
us_order = serpy.Field() us_order = Field()
taskboard_order = serpy.Field() taskboard_order = Field()
is_iocaine = serpy.Field() is_iocaine = Field()
external_reference = serpy.Field() external_reference = Field()
version = serpy.Field() version = Field()
watchers = serpy.Field() watchers = Field()
is_blocked = serpy.Field() is_blocked = Field()
blocked_note = serpy.Field() blocked_note = Field()
tags = serpy.Field() tags = Field()
is_closed = serpy.MethodField() is_closed = MethodField()
def get_milestone_slug(self, obj): def get_milestone_slug(self, obj):
return obj.milestone.slug if obj.milestone else None return obj.milestone.slug if obj.milestone else None
@ -119,36 +62,21 @@ class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceMod
return obj.status is not None and obj.status.is_closed return obj.status is not None and obj.status.is_closed
class TaskSerializer(TaskListSerializer):
comment = MethodField()
blocked_note_html = MethodField()
description = Field()
description_html = MethodField()
def get_comment(self, obj):
return ""
def get_blocked_note_html(self, obj):
return mdrender(obj.project, obj.blocked_note)
def get_description_html(self, obj):
return mdrender(obj.project, obj.description)
class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer): class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer):
def serialize_neighbor(self, neighbor): pass
if neighbor:
return NeighborTaskSerializer(neighbor).data
return None
class NeighborTaskSerializer(serializers.ModelSerializer):
class Meta:
model = models.Task
fields = ("id", "ref", "subject")
depth = 0
class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator,
TaskExistsValidator, serializers.Serializer):
project_id = serializers.IntegerField()
sprint_id = serializers.IntegerField()
status_id = serializers.IntegerField(required=False)
us_id = serializers.IntegerField(required=False)
bulk_tasks = serializers.CharField()
## Order bulk serializers
class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer):
task_id = serializers.IntegerField()
order = serializers.IntegerField()
class UpdateTasksOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer):
project_id = serializers.IntegerField()
bulk_tasks = _TaskOrderBulkSerializer(many=True)

View File

@ -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

View File

@ -19,7 +19,14 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.exceptions import ValidationError
from taiga.base.fields import PgArrayField
from taiga.projects.milestones.validators import MilestoneExistsValidator
from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.tagging.fields import TagsAndTagsColorsField
from taiga.projects.validators import ProjectExistsValidator
from . import models from . import models
@ -28,5 +35,35 @@ class TaskExistsValidator:
value = attrs[source] value = attrs[source]
if not models.Task.objects.filter(pk=value).exists(): if not models.Task.objects.filter(pk=value).exists():
msg = _("There's no task with that id") msg = _("There's no task with that id")
raise serializers.ValidationError(msg) raise ValidationError(msg)
return attrs return attrs
class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator):
tags = TagsAndTagsColorsField(default=[], required=False)
external_reference = PgArrayField(required=False)
class Meta:
model = models.Task
read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
class TasksBulkValidator(ProjectExistsValidator, MilestoneExistsValidator,
TaskExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
sprint_id = serializers.IntegerField()
status_id = serializers.IntegerField(required=False)
us_id = serializers.IntegerField(required=False)
bulk_tasks = serializers.CharField()
# Order bulk validators
class _TaskOrderBulkValidator(TaskExistsValidator, validators.Validator):
task_id = serializers.IntegerField()
order = serializers.IntegerField()
class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
bulk_tasks = _TaskOrderBulkValidator(many=True)

View File

@ -16,12 +16,8 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from contextlib import closing
from collections import namedtuple
from django.apps import apps from django.apps import apps
from django.db import transaction, connection from django.db import transaction
from django.db.models.sql import datastructures
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.http import HttpResponse from django.http import HttpResponse
@ -36,7 +32,6 @@ from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet from taiga.base.api import ModelListViewSet
from taiga.base.api.utils import get_object_or_404 from taiga.base.api.utils import get_object_or_404
from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.history.services import take_snapshot from taiga.projects.history.services import take_snapshot
from taiga.projects.milestones.models import Milestone from taiga.projects.milestones.models import Milestone
@ -45,21 +40,20 @@ from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin from taiga.projects.occ import OCCResourceMixin
from taiga.projects.tagging.api import TaggedResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.userstories.models import RolePoints
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
from taiga.projects.userstories.utils import attach_total_points from taiga.projects.userstories.utils import attach_extra_info
from taiga.projects.userstories.utils import attach_role_points
from taiga.projects.userstories.utils import attach_tasks
from . import models from . import models
from . import permissions from . import permissions
from . import serializers from . import serializers
from . import services from . import services
from . import validators
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
validator_class = validators.UserStoryValidator
queryset = models.UserStory.objects.all() queryset = models.UserStory.objects.all()
permission_classes = (permissions.UserStoryPermission,) permission_classes = (permissions.UserStoryPermission,)
filter_backends = (filters.CanViewUsFilterBackend, filter_backends = (filters.CanViewUsFilterBackend,
@ -105,18 +99,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
"assigned_to", "assigned_to",
"generated_from_issue") "generated_from_issue")
qs = self.attach_votes_attrs_to_queryset(qs) include_attachments = "include_attachments" in self.request.QUERY_PARAMS
qs = self.attach_watchers_attrs_to_queryset(qs) include_tasks = "include_tasks" in self.request.QUERY_PARAMS
qs = attach_total_points(qs) qs = attach_extra_info(qs, user=self.request.user,
qs = attach_role_points(qs) include_attachments=include_attachments,
include_tasks=include_tasks)
if "include_attachments" in self.request.QUERY_PARAMS:
qs = attach_basic_attachments(qs)
qs = qs.extra(select={"include_attachments": "True"})
if "include_tasks" in self.request.QUERY_PARAMS:
qs = attach_tasks(qs)
qs = qs.extra(select={"include_tasks": "True"})
return qs return qs
@ -237,10 +224,18 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
@list_route(methods=["GET"]) @list_route(methods=["GET"])
def by_ref(self, request): def by_ref(self, request):
ref = request.QUERY_PARAMS.get("ref", None) retrieve_kwargs = {
"ref": request.QUERY_PARAMS.get("ref", None)
}
project_id = request.QUERY_PARAMS.get("project", None) project_id = request.QUERY_PARAMS.get("project", None)
userstory = get_object_or_404(models.UserStory, ref=ref, project_id=project_id) if project_id is not None:
return self.retrieve(request, pk=userstory.pk) retrieve_kwargs["project_id"] = project_id
project_slug = request.QUERY_PARAMS.get("project__slug", None)
if project_slug is not None:
retrieve_kwargs["project__slug"] = project_slug
return self.retrieve(request, **retrieve_kwargs)
@list_route(methods=["GET"]) @list_route(methods=["GET"])
def csv(self, request): def csv(self, request):
@ -257,9 +252,9 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
@list_route(methods=["POST"]) @list_route(methods=["POST"])
def bulk_create(self, request, **kwargs): def bulk_create(self, request, **kwargs):
serializer = serializers.UserStoriesBulkSerializer(data=request.DATA) validator = validators.UserStoriesBulkValidator(data=request.DATA)
if serializer.is_valid(): if validator.is_valid():
data = serializer.data data = validator.data
project = Project.objects.get(id=data["project_id"]) project = Project.objects.get(id=data["project_id"])
self.check_permissions(request, 'bulk_create', project) self.check_permissions(request, 'bulk_create', project)
if project.blocked_code is not None: if project.blocked_code is not None:
@ -269,17 +264,20 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
data["bulk_stories"], project=project, owner=request.user, data["bulk_stories"], project=project, owner=request.user,
status_id=data.get("status_id") or project.default_us_status_id, status_id=data.get("status_id") or project.default_us_status_id,
callback=self.post_save, precall=self.pre_save) callback=self.post_save, precall=self.pre_save)
user_stories = self.get_queryset().filter(id__in=[i.id for i in user_stories])
user_stories_serialized = self.get_serializer_class()(user_stories, many=True) user_stories_serialized = self.get_serializer_class()(user_stories, many=True)
return response.Ok(user_stories_serialized.data) return response.Ok(user_stories_serialized.data)
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
@list_route(methods=["POST"]) @list_route(methods=["POST"])
def bulk_update_milestone(self, request, **kwargs): def bulk_update_milestone(self, request, **kwargs):
serializer = serializers.UpdateMilestoneBulkSerializer(data=request.DATA) validator = validators.UpdateMilestoneBulkValidator(data=request.DATA)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
data = serializer.data data = validator.data
project = get_object_or_404(Project, pk=data["project_id"]) project = get_object_or_404(Project, pk=data["project_id"])
milestone = get_object_or_404(Milestone, pk=data["milestone_id"]) milestone = get_object_or_404(Milestone, pk=data["milestone_id"])
@ -291,11 +289,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
return response.NoContent() return response.NoContent()
def _bulk_update_order(self, order_field, request, **kwargs): def _bulk_update_order(self, order_field, request, **kwargs):
serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) validator = validators.UpdateUserStoriesOrderBulkValidator(data=request.DATA)
if not serializer.is_valid(): if not validator.is_valid():
return response.BadRequest(serializer.errors) return response.BadRequest(validator.errors)
data = serializer.data data = validator.data
project = get_object_or_404(Project, pk=data["project_id"]) project = get_object_or_404(Project, pk=data["project_id"])
self.check_permissions(request, "bulk_update_order", project) self.check_permissions(request, "bulk_update_order", project)

View File

@ -16,96 +16,111 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from collections import ChainMap
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api.utils import get_object_or_404 from taiga.base.fields import Field, MethodField
from taiga.base.fields import PickledObjectField
from taiga.base.fields import PgArrayField
from taiga.base.neighbors import NeighborsSerializerMixin from taiga.base.neighbors import NeighborsSerializerMixin
from taiga.base.utils import json
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin
from taiga.projects.milestones.validators import SprintExistsValidator from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin
from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.projects.models import Project, UserStoryStatus
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.serializers import BasicUserStoryStatusSerializer
from taiga.projects.tagging.fields import TagsAndTagsColorsField
from taiga.projects.userstories.validators import UserStoryExistsValidator
from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin
from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin
from taiga.users.serializers import UserBasicInfoSerializer
from taiga.users.serializers import ListUserBasicInfoSerializer
from taiga.users.services import get_photo_or_gravatar_url
from taiga.users.services import get_big_photo_or_gravatar_url
from . import models
import serpy
class RolePointsField(serializers.WritableField): class OriginIssueSerializer(serializers.LightSerializer):
def to_native(self, obj): id = Field()
return {str(o.role.id): o.points.id for o in obj.all()} ref = Field()
subject = Field()
def from_native(self, obj): def to_value(self, instance):
if isinstance(obj, dict): if instance is None:
return obj return None
return json.loads(obj)
return super().to_value(instance)
class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, class UserStoryListSerializer(
EditableWatchedResourceModelSerializer, serializers.ModelSerializer): VoteResourceSerializerMixin, WatchedResourceSerializer,
tags = TagsAndTagsColorsField(default=[], required=False) OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin,
external_reference = PgArrayField(required=False) StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin,
points = RolePointsField(source="role_points", required=False) serializers.LightSerializer):
total_points = serializers.SerializerMethodField("get_total_points")
comment = serializers.SerializerMethodField("get_comment")
milestone_slug = serializers.SerializerMethodField("get_milestone_slug")
milestone_name = serializers.SerializerMethodField("get_milestone_name")
origin_issue = serializers.SerializerMethodField("get_origin_issue")
blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html")
description_html = serializers.SerializerMethodField("get_description_html")
status_extra_info = BasicUserStoryStatusSerializer(source="status", required=False, read_only=True)
assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True)
owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True)
tribe_gig = PickledObjectField(required=False)
class Meta: id = Field()
model = models.UserStory ref = Field()
depth = 0 milestone = Field(attr="milestone_id")
read_only_fields = ('created_date', 'modified_date', 'owner') milestone_slug = MethodField()
milestone_name = MethodField()
project = Field(attr="project_id")
is_closed = Field()
points = MethodField()
backlog_order = Field()
sprint_order = Field()
kanban_order = Field()
created_date = Field()
modified_date = Field()
finish_date = Field()
subject = Field()
client_requirement = Field()
team_requirement = Field()
generated_from_issue = Field(attr="generated_from_issue_id")
external_reference = Field()
tribe_gig = Field()
version = Field()
watchers = Field()
is_blocked = Field()
blocked_note = Field()
tags = Field()
total_points = MethodField()
comment = MethodField()
origin_issue = OriginIssueSerializer(attr="generated_from_issue")
tasks = MethodField()
def get_milestone_slug(self, obj):
return obj.milestone.slug if obj.milestone else None
def get_milestone_name(self, obj):
return obj.milestone.name if obj.milestone else None
def get_total_points(self, obj): def get_total_points(self, obj):
return obj.get_total_points() assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute"
return obj.total_points_attr
def get_points(self, obj):
assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute"
if obj.role_points_attr is None:
return {}
return obj.role_points_attr
def get_comment(self, obj):
return ""
def get_tasks(self, obj):
include_tasks = getattr(obj, "include_tasks", False)
if include_tasks:
assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute"
if not include_tasks or obj.tasks_attr is None:
return []
return obj.tasks_attr
class UserStorySerializer(UserStoryListSerializer):
comment = MethodField()
origin_issue = MethodField()
blocked_note_html = MethodField()
description = Field()
description_html = MethodField()
def get_comment(self, obj): def get_comment(self, obj):
# NOTE: This method and field is necessary to historical comments work # NOTE: This method and field is necessary to historical comments work
return "" return ""
def get_milestone_slug(self, obj):
if obj.milestone:
return obj.milestone.slug
else:
return None
def get_milestone_name(self, obj):
if obj.milestone:
return obj.milestone.name
else:
return None
def get_origin_issue(self, obj): def get_origin_issue(self, obj):
if obj.generated_from_issue: if obj.generated_from_issue:
return { return {
@ -122,142 +137,5 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin,
return mdrender(obj.project, obj.description) return mdrender(obj.project, obj.description)
class ListOriginIssueSerializer(serializers.LightSerializer):
id = serpy.Field()
ref = serpy.Field()
subject = serpy.Field()
def to_value(self, instance):
if instance is None:
return None
return super().to_value(instance)
class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer,
ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin,
ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin,
serializers.LightSerializer):
id = serpy.Field()
ref = serpy.Field()
milestone = serpy.Field(attr="milestone_id")
milestone_slug = serpy.MethodField()
milestone_name = serpy.MethodField()
project = serpy.Field(attr="project_id")
is_closed = serpy.Field()
points = serpy.MethodField()
backlog_order = serpy.Field()
sprint_order = serpy.Field()
kanban_order = serpy.Field()
created_date = serpy.Field()
modified_date = serpy.Field()
finish_date = serpy.Field()
subject = serpy.Field()
client_requirement = serpy.Field()
team_requirement = serpy.Field()
generated_from_issue = serpy.Field(attr="generated_from_issue_id")
external_reference = serpy.Field()
tribe_gig = serpy.Field()
version = serpy.Field()
watchers = serpy.Field()
is_blocked = serpy.Field()
blocked_note = serpy.Field()
tags = serpy.Field()
total_points = serpy.MethodField()
comment = serpy.MethodField("get_comment")
origin_issue = ListOriginIssueSerializer(attr="generated_from_issue")
tasks = serpy.MethodField()
def get_milestone_slug(self, obj):
return obj.milestone.slug if obj.milestone else None
def get_milestone_name(self, obj):
return obj.milestone.name if obj.milestone else None
def get_total_points(self, obj):
assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute"
return obj.total_points_attr
def get_points(self, obj):
assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute"
if obj.role_points_attr is None:
return {}
return dict(ChainMap(*obj.role_points_attr))
def get_comment(self, obj):
return ""
def get_tasks(self, obj):
include_tasks = getattr(obj, "include_tasks", False)
if include_tasks:
assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute"
if not include_tasks or obj.tasks_attr is None:
return []
return obj.tasks_attr
class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer): class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer):
def serialize_neighbor(self, neighbor): pass
if neighbor:
return NeighborUserStorySerializer(neighbor).data
return None
class NeighborUserStorySerializer(serializers.ModelSerializer):
class Meta:
model = models.UserStory
fields = ("id", "ref", "subject")
depth = 0
class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator,
serializers.Serializer):
project_id = serializers.IntegerField()
status_id = serializers.IntegerField(required=False)
bulk_stories = serializers.CharField()
## Order bulk serializers
class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serializer):
us_id = serializers.IntegerField()
order = serializers.IntegerField()
class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator,
serializers.Serializer):
project_id = serializers.IntegerField()
bulk_stories = _UserStoryOrderBulkSerializer(many=True)
## Milestone bulk serializers
class _UserStoryMilestoneBulkSerializer(UserStoryExistsValidator, serializers.Serializer):
us_id = serializers.IntegerField()
class UpdateMilestoneBulkSerializer(ProjectExistsValidator, SprintExistsValidator, serializers.Serializer):
project_id = serializers.IntegerField()
milestone_id = serializers.IntegerField()
bulk_stories = _UserStoryMilestoneBulkSerializer(many=True)
def validate(self, data):
"""
All the userstories and the milestone are from the same project
"""
user_story_ids = [us["us_id"] for us in data["bulk_stories"]]
project = get_object_or_404(Project, pk=data["project_id"])
if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids):
raise serializers.ValidationError("all the user stories must be from the same project")
if project.milestones.filter(id=data["milestone_id"]).count() != 1:
raise serializers.ValidationError("the milestone isn't valid for the project")
return data

View File

@ -17,6 +17,13 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.notifications.utils import attach_watchers_to_queryset
from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.projects.votes.utils import attach_is_voter_to_queryset
def attach_total_points(queryset, as_field="total_points_attr"): def attach_total_points(queryset, as_field="total_points_attr"):
"""Attach total of point values to each object of the queryset. """Attach total of point values to each object of the queryset.
@ -28,7 +35,7 @@ def attach_total_points(queryset, as_field="total_points_attr"):
""" """
model = queryset.model model = queryset.model
sql = """SELECT SUM(projects_points.value) sql = """SELECT SUM(projects_points.value)
FROM userstories_rolepoints FROM userstories_rolepoints
INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id
WHERE userstories_rolepoints.user_story_id = {tbl}.id""" WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
@ -46,10 +53,15 @@ def attach_role_points(queryset, as_field="role_points_attr"):
:return: Queryset object with the additional `as_field` field. :return: Queryset object with the additional `as_field` field.
""" """
model = queryset.model model = queryset.model
sql = """SELECT json_agg((userstories_rolepoints.role_id, userstories_rolepoints.points_id)) sql = """SELECT FORMAT('{{%%s}}',
FROM userstories_rolepoints 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""" WHERE userstories_rolepoints.user_story_id = {tbl}.id"""
sql = sql.format(tbl=model._meta.db_table) sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql}) queryset = queryset.extra(select={as_field: sql})
return queryset return queryset
@ -82,3 +94,23 @@ def attach_tasks(queryset, as_field="tasks_attr"):
sql = sql.format(tbl=model._meta.db_table) sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql}) queryset = queryset.extra(select={as_field: sql})
return queryset return queryset
def attach_extra_info(queryset, user=None, include_attachments=False, include_tasks=False):
queryset = attach_total_points(queryset)
queryset = attach_role_points(queryset)
if include_attachments:
queryset = attach_basic_attachments(queryset)
queryset = queryset.extra(select={"include_attachments": "True"})
if include_tasks:
queryset = attach_tasks(queryset)
queryset = queryset.extra(select={"include_tasks": "True"})
queryset = attach_total_voters_to_queryset(queryset)
queryset = attach_watchers_to_queryset(queryset)
queryset = attach_total_watchers_to_queryset(queryset)
queryset = attach_is_voter_to_queryset(queryset, user)
queryset = attach_is_watcher_to_queryset(queryset, user)
return queryset

View File

@ -19,14 +19,96 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.api.utils import get_object_or_404
from taiga.base.exceptions import ValidationError
from taiga.base.fields import PgArrayField
from taiga.base.fields import PickledObjectField
from taiga.projects.milestones.validators import MilestoneExistsValidator
from taiga.projects.models import Project
from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.tagging.fields import TagsAndTagsColorsField
from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator
from . import models from . import models
import json
class UserStoryExistsValidator: class UserStoryExistsValidator:
def validate_us_id(self, attrs, source): def validate_us_id(self, attrs, source):
value = attrs[source] value = attrs[source]
if not models.UserStory.objects.filter(pk=value).exists(): if not models.UserStory.objects.filter(pk=value).exists():
msg = _("There's no user story with that id") msg = _("There's no user story with that id")
raise serializers.ValidationError(msg) raise ValidationError(msg)
return attrs return attrs
class RolePointsField(serializers.WritableField):
def to_native(self, obj):
return {str(o.role.id): o.points.id for o in obj.all()}
def from_native(self, obj):
if isinstance(obj, dict):
return obj
return json.loads(obj)
class UserStoryValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator):
tags = TagsAndTagsColorsField(default=[], required=False)
external_reference = PgArrayField(required=False)
points = RolePointsField(source="role_points", required=False)
tribe_gig = PickledObjectField(required=False)
class Meta:
model = models.UserStory
depth = 0
read_only_fields = ('created_date', 'modified_date', 'owner')
class UserStoriesBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator,
validators.Validator):
project_id = serializers.IntegerField()
status_id = serializers.IntegerField(required=False)
bulk_stories = serializers.CharField()
# Order bulk validators
class _UserStoryOrderBulkValidator(UserStoryExistsValidator, validators.Validator):
us_id = serializers.IntegerField()
order = serializers.IntegerField()
class UpdateUserStoriesOrderBulkValidator(ProjectExistsValidator, UserStoryStatusExistsValidator,
validators.Validator):
project_id = serializers.IntegerField()
bulk_stories = _UserStoryOrderBulkValidator(many=True)
# Milestone bulk validators
class _UserStoryMilestoneBulkValidator(UserStoryExistsValidator, validators.Validator):
us_id = serializers.IntegerField()
class UpdateMilestoneBulkValidator(ProjectExistsValidator, MilestoneExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
milestone_id = serializers.IntegerField()
bulk_stories = _UserStoryMilestoneBulkValidator(many=True)
def validate(self, data):
"""
All the userstories and the milestone are from the same project
"""
user_story_ids = [us["us_id"] for us in data["bulk_stories"]]
project = get_object_or_404(Project, pk=data["project_id"])
if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids):
raise ValidationError("all the user stories must be from the same project")
if project.milestones.filter(id=data["milestone_id"]).count() != 1:
raise ValidationError("the milestone isn't valid for the project")
return data

436
taiga/projects/utils.py Normal file
View File

@ -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

View File

@ -16,11 +16,43 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.db.models import Q
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.exceptions import ValidationError
from taiga.base.fields import JsonField
from taiga.base.fields import PgArrayField
from taiga.users.validators import RoleExistsValidator
from .tagging.fields import TagsField
from . import models from . import models
from . import services
class DuplicatedNameInProjectValidator:
def validate_name(self, attrs, source):
"""
Check the points name is not duplicated in the project on creation
"""
model = self.opts.model
qs = None
# If the object exists:
if self.object and attrs.get(source, None):
qs = model.objects.filter(
project=self.object.project,
name=attrs[source]).exclude(id=self.object.id)
if not self.object and attrs.get("project", None) and attrs.get(source, None):
qs = model.objects.filter(project=attrs["project"], name=attrs[source])
if qs and qs.exists():
raise ValidationError(_("Name duplicated for the project"))
return attrs
class ProjectExistsValidator: class ProjectExistsValidator:
@ -28,7 +60,7 @@ class ProjectExistsValidator:
value = attrs[source] value = attrs[source]
if not models.Project.objects.filter(pk=value).exists(): if not models.Project.objects.filter(pk=value).exists():
msg = _("There's no project with that id") msg = _("There's no project with that id")
raise serializers.ValidationError(msg) raise ValidationError(msg)
return attrs return attrs
@ -37,7 +69,7 @@ class UserStoryStatusExistsValidator:
value = attrs[source] value = attrs[source]
if not models.UserStoryStatus.objects.filter(pk=value).exists(): if not models.UserStoryStatus.objects.filter(pk=value).exists():
msg = _("There's no user story status with that id") msg = _("There's no user story status with that id")
raise serializers.ValidationError(msg) raise ValidationError(msg)
return attrs return attrs
@ -46,5 +78,172 @@ class TaskStatusExistsValidator:
value = attrs[source] value = attrs[source]
if not models.TaskStatus.objects.filter(pk=value).exists(): if not models.TaskStatus.objects.filter(pk=value).exists():
msg = _("There's no task status with that id") msg = _("There's no task status with that id")
raise serializers.ValidationError(msg) raise ValidationError(msg)
return attrs return attrs
######################################################
# Custom values for selectors
######################################################
class PointsValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.Points
class UserStoryStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.UserStoryStatus
class TaskStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.TaskStatus
class SeverityValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.Severity
class PriorityValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.Priority
class IssueStatusValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.IssueStatus
class IssueTypeValidator(DuplicatedNameInProjectValidator, validators.ModelValidator):
class Meta:
model = models.IssueType
######################################################
# Members
######################################################
class MembershipValidator(validators.ModelValidator):
email = serializers.EmailField(required=True)
class Meta:
model = models.Membership
# IMPORTANT: Maintain the MembershipAdminSerializer Meta up to date
# with this info (excluding here user_email and email)
read_only_fields = ("user",)
exclude = ("token", "email")
def validate_email(self, attrs, source):
project = attrs.get("project", None)
if project is None:
project = self.object.project
email = attrs[source]
qs = models.Membership.objects.all()
# If self.object is not None, the serializer is in update
# mode, and for it, it should exclude self.
if self.object:
qs = qs.exclude(pk=self.object.pk)
qs = qs.filter(Q(project_id=project.id, user__email=email) |
Q(project_id=project.id, email=email))
if qs.count() > 0:
raise ValidationError(_("Email address is already taken"))
return attrs
def validate_role(self, attrs, source):
project = attrs.get("project", None)
if project is None:
project = self.object.project
role = attrs[source]
if project.roles.filter(id=role.id).count() == 0:
raise ValidationError(_("Invalid role for the project"))
return attrs
def validate_is_admin(self, attrs, source):
project = attrs.get("project", None)
if project is None:
project = self.object.project
if (self.object and self.object.user):
if self.object.user.id == project.owner_id and not attrs[source]:
raise ValidationError(_("The project owner must be admin."))
if not services.project_has_valid_admins(project, exclude_user=self.object.user):
raise ValidationError(
_("At least one user must be an active admin for this project.")
)
return attrs
class MembershipAdminValidator(MembershipValidator):
class Meta:
model = models.Membership
# IMPORTANT: Maintain the MembershipSerializer Meta up to date
# with this info (excluding there user_email and email)
read_only_fields = ("user",)
exclude = ("token",)
class MemberBulkValidator(RoleExistsValidator, validators.Validator):
email = serializers.EmailField()
role_id = serializers.IntegerField()
class MembersBulkValidator(ProjectExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
bulk_memberships = MemberBulkValidator(many=True)
invitation_extra_text = serializers.CharField(required=False, max_length=255)
######################################################
# Projects
######################################################
class ProjectValidator(validators.ModelValidator):
anon_permissions = PgArrayField(required=False)
public_permissions = PgArrayField(required=False)
tags = TagsField(default=[], required=False)
class Meta:
model = models.Project
read_only_fields = ("created_date", "modified_date", "slug", "blocked_code", "owner")
######################################################
# Project Templates
######################################################
class ProjectTemplateValidator(validators.ModelValidator):
default_options = JsonField(required=False, label=_("Default options"))
us_statuses = JsonField(required=False, label=_("User story's statuses"))
points = JsonField(required=False, label=_("Points"))
task_statuses = JsonField(required=False, label=_("Task's statuses"))
issue_statuses = JsonField(required=False, label=_("Issue's statuses"))
issue_types = JsonField(required=False, label=_("Issue's types"))
priorities = JsonField(required=False, label=_("Priorities"))
severities = JsonField(required=False, label=_("Severities"))
roles = JsonField(required=False, label=_("Roles"))
class Meta:
model = models.ProjectTemplate
read_only_fields = ("created_date", "modified_date")
######################################################
# Project order bulk serializers
######################################################
class UpdateProjectOrderBulkValidator(ProjectExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
order = serializers.IntegerField()

View File

@ -16,12 +16,14 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import serpy
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import MethodField
class BaseVoteResourceSerializerMixin(object): class VoteResourceSerializerMixin(serializers.LightSerializer):
is_voter = MethodField()
total_voters = MethodField()
def get_is_voter(self, obj): def get_is_voter(self, obj):
# The "is_voted" attribute is attached in the get_queryset of the viewset. # The "is_voted" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "is_voter", False) or False return getattr(obj, "is_voter", False) or False
@ -29,13 +31,3 @@ class BaseVoteResourceSerializerMixin(object):
def get_total_voters(self, obj): def get_total_voters(self, obj):
# The "total_voters" attribute is attached in the get_queryset of the viewset. # The "total_voters" attribute is attached in the get_queryset of the viewset.
return getattr(obj, "total_voters", 0) or 0 return getattr(obj, "total_voters", 0) or 0
class VoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serializers.ModelSerializer):
is_voter = serializers.SerializerMethodField("get_is_voter")
total_voters = serializers.SerializerMethodField("get_total_voters")
class ListVoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serpy.Serializer):
is_voter = serpy.MethodField("get_is_voter")
total_voters = serpy.MethodField("get_total_voters")

View File

@ -39,14 +39,6 @@ class VotedResourceMixin:
def pre_conditions_on_save(self, obj) def pre_conditions_on_save(self, obj)
""" """
def attach_votes_attrs_to_queryset(self, queryset):
qs = attach_total_voters_to_queryset(queryset)
if self.request.user.is_authenticated():
qs = attach_is_voter_to_queryset(self.request.user, qs)
return qs
@detail_route(methods=["POST"]) @detail_route(methods=["POST"])
def upvote(self, request, pk=None): def upvote(self, request, pk=None):
obj = self.get_object() obj = self.get_object()

View File

@ -17,14 +17,14 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.auth import get_user_model
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import Field, MethodField
class VoterSerializer(serializers.ModelSerializer): class VoterSerializer(serializers.LightSerializer):
full_name = serializers.CharField(source='get_full_name', required=False) id = Field()
username = Field()
full_name = MethodField()
class Meta: def get_full_name(self, obj):
model = get_user_model() return obj.get_full_name()
fields = ('id', 'username', 'full_name')

View File

@ -48,7 +48,7 @@ def attach_total_voters_to_queryset(queryset, as_field="total_voters"):
return qs return qs
def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"): def attach_is_voter_to_queryset(queryset, user, as_field="is_voter"):
"""Attach is_vote boolean to each object of the queryset. """Attach is_vote boolean to each object of the queryset.
Because of laziness of vote objects creation, this makes much simpler and more efficient to Because of laziness of vote objects creation, this makes much simpler and more efficient to
@ -57,22 +57,26 @@ def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"):
(The other way was to do it in the serializer with some try/except blocks and additional (The other way was to do it in the serializer with some try/except blocks and additional
queries) queries)
:param user: A users.User object model
:param queryset: A Django queryset object. :param queryset: A Django queryset object.
:param user: A users.User object model
:param as_field: Attach the boolean as an attribute with this name. :param as_field: Attach the boolean as an attribute with this name.
:return: Queryset object with the additional `as_field` field. :return: Queryset object with the additional `as_field` field.
""" """
model = queryset.model model = queryset.model
type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model)
sql = ("""SELECT CASE WHEN (SELECT count(*) if user is None or user.is_anonymous():
FROM votes_vote sql = """SELECT false"""
WHERE votes_vote.content_type_id = {type_id} else:
AND votes_vote.object_id = {tbl}.id sql = ("""SELECT CASE WHEN (SELECT count(*)
AND votes_vote.user_id = {user_id}) > 0 FROM votes_vote
THEN TRUE WHERE votes_vote.content_type_id = {type_id}
ELSE FALSE AND votes_vote.object_id = {tbl}.id
END""") AND votes_vote.user_id = {user_id}) > 0
sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) THEN TRUE
ELSE FALSE
END""")
sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id)
qs = queryset.extra(select={as_field: sql}) qs = queryset.extra(select={as_field: sql})
return qs return qs

View File

@ -24,7 +24,6 @@ from taiga.base import response
from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet from taiga.base.api import ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.api.permissions import IsAuthenticated
from taiga.base.api.utils import get_object_or_404 from taiga.base.api.utils import get_object_or_404
from taiga.base.decorators import list_route from taiga.base.decorators import list_route
@ -42,6 +41,8 @@ from taiga.projects.occ import OCCResourceMixin
from . import models from . import models
from . import permissions from . import permissions
from . import serializers from . import serializers
from . import validators
from . import utils as wiki_utils
class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
@ -49,6 +50,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
model = models.WikiPage model = models.WikiPage
serializer_class = serializers.WikiPageSerializer serializer_class = serializers.WikiPageSerializer
validator_class = validators.WikiPageValidator
permission_classes = (permissions.WikiPagePermission,) permission_classes = (permissions.WikiPagePermission,)
filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_backends = (filters.CanViewWikiPagesFilterBackend,)
filter_fields = ("project", "slug") filter_fields = ("project", "slug")
@ -56,7 +58,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = self.attach_watchers_attrs_to_queryset(qs) qs = wiki_utils.attach_extra_info(qs, user=self.request.user)
return qs return qs
@list_route(methods=["GET"]) @list_route(methods=["GET"])
@ -100,6 +102,7 @@ class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet):
class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet): class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.WikiLink model = models.WikiLink
serializer_class = serializers.WikiLinkSerializer serializer_class = serializers.WikiLinkSerializer
validator_class = validators.WikiLinkValidator
permission_classes = (permissions.WikiLinkPermission,) permission_classes = (permissions.WikiLinkPermission,)
filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_backends = (filters.CanViewWikiPagesFilterBackend,)
filter_fields = ["project"] filter_fields = ["project"]
@ -120,7 +123,7 @@ class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet):
wiki_page, created = models.WikiPage.objects.get_or_create( wiki_page, created = models.WikiPage.objects.get_or_create(
slug=wiki_link.href, slug=wiki_link.href,
project=wiki_link.project, project=wiki_link.project,
defaults={"owner": self.request.user,"last_modifier": self.request.user}) defaults={"owner": self.request.user, "last_modifier": self.request.user})
if created: if created:
# Creaste the new history entre, sSet watcher for the new wiki page # Creaste the new history entre, sSet watcher for the new wiki page

View File

@ -17,21 +17,26 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import Field, MethodField
from taiga.projects.history import services as history_service from taiga.projects.history import services as history_service
from taiga.projects.notifications.mixins import WatchedResourceModelSerializer from taiga.projects.notifications.mixins import WatchedResourceSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.mdrender.service import render as mdrender from taiga.mdrender.service import render as mdrender
from . import models
class WikiPageSerializer(WatchedResourceSerializer, serializers.LightSerializer):
id = Field()
project = Field(attr="project_id")
slug = Field()
content = Field()
owner = Field(attr="owner_id")
last_modifier = Field(attr="last_modifier_id")
created_date = Field()
modified_date = Field()
class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer): html = MethodField()
html = serializers.SerializerMethodField("get_html") editions = MethodField()
editions = serializers.SerializerMethodField("get_editions")
class Meta: version = Field()
model = models.WikiPage
read_only_fields = ('modified_date', 'created_date', 'owner')
def get_html(self, obj): def get_html(self, obj):
return mdrender(obj.project, obj.content) return mdrender(obj.project, obj.content)
@ -40,7 +45,9 @@ class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, seri
return history_service.get_history_queryset_by_model_instance(obj).count() + 1 # +1 for creation return history_service.get_history_queryset_by_model_instance(obj).count() + 1 # +1 for creation
class WikiLinkSerializer(serializers.ModelSerializer): class WikiLinkSerializer(serializers.LightSerializer):
class Meta: id = Field()
model = models.WikiLink project = Field(attr="project_id")
read_only_fields = ('href',) title = Field()
href = Field()
order = Field()

View File

@ -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

View File

@ -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',)

View File

@ -16,37 +16,48 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.projects.issues.serializers import IssueSerializer from taiga.base.api import serializers
from taiga.projects.userstories.serializers import UserStorySerializer from taiga.base.fields import Field, MethodField
from taiga.projects.tasks.serializers import TaskSerializer
from taiga.projects.wiki.serializers import WikiPageSerializer
from taiga.projects.issues.models import Issue
from taiga.projects.userstories.models import UserStory
from taiga.projects.tasks.models import Task
from taiga.projects.wiki.models import WikiPage
class IssueSearchResultsSerializer(IssueSerializer): class IssueSearchResultsSerializer(serializers.LightSerializer):
class Meta: id = Field()
model = Issue ref = Field()
fields = ('id', 'ref', 'subject', 'status', 'assigned_to') subject = Field()
status = Field(attr="status_id")
assigned_to = Field(attr="assigned_to_id")
class TaskSearchResultsSerializer(TaskSerializer): class TaskSearchResultsSerializer(serializers.LightSerializer):
class Meta: id = Field()
model = Task ref = Field()
fields = ('id', 'ref', 'subject', 'status', 'assigned_to') subject = Field()
status = Field(attr="status_id")
assigned_to = Field(attr="assigned_to_id")
class UserStorySearchResultsSerializer(UserStorySerializer): class UserStorySearchResultsSerializer(serializers.LightSerializer):
class Meta: id = Field()
model = UserStory ref = Field()
fields = ('id', 'ref', 'subject', 'status', 'total_points', subject = Field()
'milestone_name', 'milestone_slug') status = Field(attr="status_id")
total_points = MethodField()
milestone_name = MethodField()
milestone_slug = MethodField()
def get_milestone_name(self, obj):
return obj.milestone.name if obj.milestone else None
def get_milestone_slug(self, obj):
return obj.milestone.slug if obj.milestone else None
def get_total_points(self, obj):
assert hasattr(obj, "total_points_attr"), \
"instance must have a total_points_attr attribute"
return obj.total_points_attr
class WikiPageSearchResultsSerializer(WikiPageSerializer): class WikiPageSearchResultsSerializer(serializers.LightSerializer):
class Meta: id = Field()
model = WikiPage slug = Field()
fields = ('id', 'slug')

View File

@ -19,6 +19,7 @@
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from taiga.base.utils.db import to_tsquery from taiga.base.utils.db import to_tsquery
from taiga.projects.userstories.utils import attach_total_points
MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150) MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150)
@ -30,11 +31,13 @@ def search_user_stories(project, text):
"coalesce(userstories_userstory.description, '')) " "coalesce(userstories_userstory.description, '')) "
"@@ to_tsquery('english_nostop', %s)") "@@ to_tsquery('english_nostop', %s)")
if text: queryset = model_cls.objects.filter(project_id=project.pk)
return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)])
.filter(project_id=project.pk)[:MAX_RESULTS])
return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] if text:
queryset = queryset.extra(where=[where_clause], params=[to_tsquery(text)])
queryset = attach_total_points(queryset)
return queryset[:MAX_RESULTS]
def search_tasks(project, text): def search_tasks(project, text):

View File

@ -18,10 +18,8 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.apps import apps
from taiga.base import response from taiga.base import response
from taiga.base.api.utils import get_object_or_404
from taiga.base.api import ReadOnlyListViewSet from taiga.base.api import ReadOnlyListViewSet
from . import serializers from . import serializers
@ -36,7 +34,7 @@ class TimelineViewSet(ReadOnlyListViewSet):
def get_content_type(self): def get_content_type(self):
app_name, model = self.content_type.split(".", 1) app_name, model = self.content_type.split(".", 1)
return get_object_or_404(ContentType, app_label=app_name, model=model) return ContentType.objects.get_by_natural_key(app_name, model)
def get_queryset(self): def get_queryset(self):
ct = self.get_content_type() ct = self.get_content_type()

View File

@ -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),
),
]

View File

@ -20,13 +20,12 @@ from django.db import models
from django_pgjson.fields import JsonField from django_pgjson.fields import JsonField
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from taiga.projects.models import Project from taiga.projects.models import Project
class Timeline(models.Model): class Timeline(models.Model):
content_type = models.ForeignKey(ContentType, related_name="content_type_timelines") content_type = models.ForeignKey(ContentType, related_name="content_type_timelines")
object_id = models.PositiveIntegerField() object_id = models.PositiveIntegerField()
@ -36,12 +35,11 @@ class Timeline(models.Model):
project = models.ForeignKey(Project, null=True) project = models.ForeignKey(Project, null=True)
data = JsonField() data = JsonField()
data_content_type = models.ForeignKey(ContentType, related_name="data_timelines") data_content_type = models.ForeignKey(ContentType, related_name="data_timelines")
created = models.DateTimeField(default=timezone.now) created = models.DateTimeField(default=timezone.now, db_index=True)
class Meta: class Meta:
index_together = [('content_type', 'object_id', 'namespace'), ] index_together = [('content_type', 'object_id', 'namespace'), ]
# Register all implementations # Register all implementations
from .timeline_implementations import * from .timeline_implementations import *

View File

@ -16,26 +16,32 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.apps import apps
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.forms import widgets
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import JsonField from taiga.base.fields import Field, MethodField
from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
from . import models from . import models
from . import service
class TimelineSerializer(serializers.ModelSerializer): class TimelineSerializer(serializers.LightSerializer):
data = serializers.SerializerMethodField("get_data") data = serializers.SerializerMethodField("get_data")
id = Field()
content_type = Field(attr="content_type_id")
object_id = Field()
namespace = Field()
event_type = Field()
project = Field(attr="project_id")
data = MethodField()
data_content_type = Field(attr="data_content_type_id")
created = Field()
class Meta: class Meta:
model = models.Timeline model = models.Timeline
def get_data(self, obj): def get_data(self, obj):
#Updates the data user info saved if the user exists # Updates the data user info saved if the user exists
if hasattr(obj, "_prefetched_user"): if hasattr(obj, "_prefetched_user"):
user = obj._prefetched_user user = obj._prefetched_user
else: else:

View File

@ -27,33 +27,32 @@ from functools import partial, wraps
from taiga.base.utils.db import get_typename_for_model_class from taiga.base.utils.db import get_typename_for_model_class
from taiga.celery import app from taiga.celery import app
from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
_timeline_impl_map = {} _timeline_impl_map = {}
def _get_impl_key_from_model(model:Model, event_type:str): def _get_impl_key_from_model(model: Model, event_type: str):
if issubclass(model, Model): if issubclass(model, Model):
typename = get_typename_for_model_class(model) typename = get_typename_for_model_class(model)
return _get_impl_key_from_typename(typename, event_type) return _get_impl_key_from_typename(typename, event_type)
raise Exception("Not valid model parameter") raise Exception("Not valid model parameter")
def _get_impl_key_from_typename(typename:str, event_type:str): def _get_impl_key_from_typename(typename: str, event_type: str):
if isinstance(typename, str): if isinstance(typename, str):
return "{0}.{1}".format(typename, event_type) return "{0}.{1}".format(typename, event_type)
raise Exception("Not valid typename parameter") raise Exception("Not valid typename parameter")
def build_user_namespace(user:object): def build_user_namespace(user: object):
return "{0}:{1}".format("user", user.id) return "{0}:{1}".format("user", user.id)
def build_project_namespace(project:object): def build_project_namespace(project: object):
return "{0}:{1}".format("project", project.id) return "{0}:{1}".format("project", project.id)
def _add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): def _add_to_object_timeline(obj: object, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}):
assert isinstance(obj, Model), "obj must be a instance of Model" assert isinstance(obj, Model), "obj must be a instance of Model"
assert isinstance(instance, Model), "instance must be a instance of Model" assert isinstance(instance, Model), "instance must be a instance of Model"
from .models import Timeline from .models import Timeline
@ -75,12 +74,12 @@ def _add_to_object_timeline(obj:object, instance:object, event_type:str, created
) )
def _add_to_objects_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): def _add_to_objects_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}):
for obj in objects: for obj in objects:
_add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data) _add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data)
def _push_to_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): def _push_to_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}):
if isinstance(objects, Model): if isinstance(objects, Model):
_add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data) _add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data)
elif isinstance(objects, QuerySet) or isinstance(objects, list): elif isinstance(objects, QuerySet) or isinstance(objects, list):
@ -111,10 +110,10 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id
except projectModel.DoesNotExist: except projectModel.DoesNotExist:
return return
## Project timeline # Project timeline
_push_to_timeline(project, obj, event_type, created_datetime, _push_to_timeline(project, obj, event_type, created_datetime,
namespace=build_project_namespace(project), namespace=build_project_namespace(project),
extra_data=extra_data) extra_data=extra_data)
project.refresh_totals() project.refresh_totals()
@ -122,14 +121,14 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id
related_people = obj.get_related_people() related_people = obj.get_related_people()
_push_to_timeline(related_people, obj, event_type, created_datetime, _push_to_timeline(related_people, obj, event_type, created_datetime,
namespace=build_user_namespace(user), namespace=build_user_namespace(user),
extra_data=extra_data) extra_data=extra_data)
else: else:
# Actions not related with a project # Actions not related with a project
## - Me # - Me
_push_to_timeline(user, obj, event_type, created_datetime, _push_to_timeline(user, obj, event_type, created_datetime,
namespace=build_user_namespace(user), namespace=build_user_namespace(user),
extra_data=extra_data) extra_data=extra_data)
def get_timeline(obj, namespace=None): def get_timeline(obj, namespace=None):
@ -141,7 +140,6 @@ def get_timeline(obj, namespace=None):
if namespace is not None: if namespace is not None:
timeline = timeline.filter(namespace=namespace) timeline = timeline.filter(namespace=namespace)
timeline = timeline.select_related("project")
timeline = timeline.order_by("-created", "-id") timeline = timeline.order_by("-created", "-id")
return timeline return timeline
@ -156,22 +154,22 @@ def filter_timeline_for_user(timeline, user):
# Filtering private project with some public parts # Filtering private project with some public parts
content_types = { content_types = {
"view_project": ContentType.objects.get(app_label="projects", model="project"), "view_project": ContentType.objects.get_by_natural_key("projects", "project"),
"view_milestones": ContentType.objects.get(app_label="milestones", model="milestone"), "view_milestones": ContentType.objects.get_by_natural_key("milestones", "milestone"),
"view_us": ContentType.objects.get(app_label="userstories", model="userstory"), "view_us": ContentType.objects.get_by_natural_key("userstories", "userstory"),
"view_tasks": ContentType.objects.get(app_label="tasks", model="task"), "view_tasks": ContentType.objects.get_by_natural_key("tasks", "task"),
"view_issues": ContentType.objects.get(app_label="issues", model="issue"), "view_issues": ContentType.objects.get_by_natural_key("issues", "issue"),
"view_wiki_pages": ContentType.objects.get(app_label="wiki", model="wikipage"), "view_wiki_pages": ContentType.objects.get_by_natural_key("wiki", "wikipage"),
"view_wiki_links": ContentType.objects.get(app_label="wiki", model="wikilink"), "view_wiki_links": ContentType.objects.get_by_natural_key("wiki", "wikilink"),
} }
for content_type_key, content_type in content_types.items(): for content_type_key, content_type in content_types.items():
tl_filter |= Q(project__is_private=True, tl_filter |= Q(project__is_private=True,
project__anon_permissions__contains=[content_type_key], project__anon_permissions__contains=[content_type_key],
data_content_type=content_type) data_content_type=content_type)
# There is no specific permission for seeing new memberships # There is no specific permission for seeing new memberships
membership_content_type = ContentType.objects.get(app_label="projects", model="membership") membership_content_type = ContentType.objects.get_by_natural_key(app_label="projects", model="membership")
tl_filter |= Q(project__is_private=True, tl_filter |= Q(project__is_private=True,
project__anon_permissions__contains=["view_project"], project__anon_permissions__contains=["view_project"],
data_content_type=membership_content_type) data_content_type=membership_content_type)
@ -214,7 +212,7 @@ def get_project_timeline(project, accessing_user=None):
return timeline return timeline
def register_timeline_implementation(typename:str, event_type:str, fn=None): def register_timeline_implementation(typename: str, event_type: str, fn=None):
assert isinstance(typename, str), "typename must be a string" assert isinstance(typename, str), "typename must be a string"
assert isinstance(event_type, str), "event_type must be a string" assert isinstance(event_type, str), "event_type must be a string"
@ -231,7 +229,6 @@ def register_timeline_implementation(typename:str, event_type:str, fn=None):
return _wrapper return _wrapper
def extract_project_info(instance): def extract_project_info(instance):
return { return {
"id": instance.pk, "id": instance.pk,

View File

@ -19,7 +19,6 @@
import uuid import uuid
from django.apps import apps from django.apps import apps
from django.db.models import Q, F
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.validators import validate_email from django.core.validators import validate_email
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -28,21 +27,21 @@ from django.conf import settings
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
from taiga.base import filters from taiga.base import filters
from taiga.base import response from taiga.base import response
from taiga.base.utils.dicts import into_namedtuple
from taiga.auth.tokens import get_user_for_token from taiga.auth.tokens import get_user_for_token
from taiga.base.decorators import list_route from taiga.base.decorators import list_route
from taiga.base.decorators import detail_route from taiga.base.decorators import detail_route
from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelCrudViewSet
from taiga.base.api.mixins import BlockedByProjectMixin from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.base.filters import PermissionBasedFilterBackend
from taiga.base.api.utils import get_object_or_404 from taiga.base.api.utils import get_object_or_404
from taiga.base.filters import MembersFilterBackend from taiga.base.filters import MembersFilterBackend
from taiga.base.mails import mail_builder from taiga.base.mails import mail_builder
from taiga.projects.votes import services as votes_service
from taiga.users.services import get_user_by_username_or_email from taiga.users.services import get_user_by_username_or_email
from easy_thumbnails.source_generators import pil_image from easy_thumbnails.source_generators import pil_image
from . import models from . import models
from . import serializers from . import serializers
from . import validators
from . import permissions from . import permissions
from . import filters as user_filters from . import filters as user_filters
from . import services from . import services
@ -53,6 +52,8 @@ class UsersViewSet(ModelCrudViewSet):
permission_classes = (permissions.UserPermission,) permission_classes = (permissions.UserPermission,)
admin_serializer_class = serializers.UserAdminSerializer admin_serializer_class = serializers.UserAdminSerializer
serializer_class = serializers.UserSerializer serializer_class = serializers.UserSerializer
admin_validator_class = validators.UserAdminValidator
validator_class = validators.UserValidator
queryset = models.User.objects.all().prefetch_related("memberships") queryset = models.User.objects.all().prefetch_related("memberships")
filter_backends = (MembersFilterBackend,) filter_backends = (MembersFilterBackend,)
@ -64,6 +65,14 @@ class UsersViewSet(ModelCrudViewSet):
return self.serializer_class return self.serializer_class
def get_validator_class(self):
if self.action in ["partial_update", "update", "retrieve", "by_username"]:
user = self.object
if self.request.user == user or self.request.user.is_superuser:
return self.admin_validator_class
return self.validator_class
def create(self, *args, **kwargs): def create(self, *args, **kwargs):
raise exc.NotSupported() raise exc.NotSupported()
@ -86,7 +95,7 @@ class UsersViewSet(ModelCrudViewSet):
serializer = self.get_serializer(self.object) serializer = self.get_serializer(self.object)
return response.Ok(serializer.data) return response.Ok(serializer.data)
#TODO: commit_on_success # TODO: commit_on_success
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
""" """
We must detect if the user is trying to change his email so we can We must detect if the user is trying to change his email so we can
@ -96,12 +105,10 @@ class UsersViewSet(ModelCrudViewSet):
user = self.get_object() user = self.get_object()
self.check_permissions(request, "update", user) self.check_permissions(request, "update", user)
ret = super().partial_update(request, *args, **kwargs)
new_email = request.DATA.get('email', None) new_email = request.DATA.get('email', None)
if new_email is not None: if new_email is not None:
valid_new_email = True valid_new_email = True
duplicated_email = models.User.objects.filter(email = new_email).exists() duplicated_email = models.User.objects.filter(email=new_email).exists()
try: try:
validate_email(new_email) validate_email(new_email)
@ -115,14 +122,21 @@ class UsersViewSet(ModelCrudViewSet):
elif not valid_new_email: elif not valid_new_email:
raise exc.WrongArguments(_("Not valid email")) raise exc.WrongArguments(_("Not valid email"))
#We need to generate a token for the email # We need to generate a token for the email
request.user.email_token = str(uuid.uuid1()) request.user.email_token = str(uuid.uuid1())
request.user.new_email = new_email request.user.new_email = new_email
request.user.save(update_fields=["email_token", "new_email"]) request.user.save(update_fields=["email_token", "new_email"])
email = mail_builder.change_email(request.user.new_email, {"user": request.user, email = mail_builder.change_email(
"lang": request.user.lang}) request.user.new_email,
{
"user": request.user,
"lang": request.user.lang
}
)
email.send() email.send()
ret = super().partial_update(request, *args, **kwargs)
return ret return ret
def destroy(self, request, pk=None): def destroy(self, request, pk=None):
@ -165,16 +179,16 @@ class UsersViewSet(ModelCrudViewSet):
self.check_permissions(request, "change_password_from_recovery", None) self.check_permissions(request, "change_password_from_recovery", None)
serializer = serializers.RecoverySerializer(data=request.DATA, many=False) validator = validators.RecoveryValidator(data=request.DATA, many=False)
if not serializer.is_valid(): if not validator.is_valid():
raise exc.WrongArguments(_("Token is invalid")) raise exc.WrongArguments(_("Token is invalid"))
try: try:
user = models.User.objects.get(token=serializer.data["token"]) user = models.User.objects.get(token=validator.data["token"])
except models.User.DoesNotExist: except models.User.DoesNotExist:
raise exc.WrongArguments(_("Token is invalid")) raise exc.WrongArguments(_("Token is invalid"))
user.set_password(serializer.data["password"]) user.set_password(validator.data["password"])
user.token = None user.token = None
user.save(update_fields=["password", "token"]) user.save(update_fields=["password", "token"])
@ -247,13 +261,13 @@ class UsersViewSet(ModelCrudViewSet):
""" """
Verify the email change to current logged user. Verify the email change to current logged user.
""" """
serializer = serializers.ChangeEmailSerializer(data=request.DATA, many=False) validator = validators.ChangeEmailValidator(data=request.DATA, many=False)
if not serializer.is_valid(): if not validator.is_valid():
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you " raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
"didn't use it before?")) "didn't use it before?"))
try: try:
user = models.User.objects.get(email_token=serializer.data["email_token"]) user = models.User.objects.get(email_token=validator.data["email_token"])
except models.User.DoesNotExist: except models.User.DoesNotExist:
raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you " raise exc.WrongArguments(_("Invalid, are you sure the token is correct and you "
"didn't use it before?")) "didn't use it before?"))
@ -280,14 +294,14 @@ class UsersViewSet(ModelCrudViewSet):
""" """
Cancel an account via token Cancel an account via token
""" """
serializer = serializers.CancelAccountSerializer(data=request.DATA, many=False) validator = validators.CancelAccountValidator(data=request.DATA, many=False)
if not serializer.is_valid(): if not validator.is_valid():
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
try: try:
max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None) max_age_cancel_account = getattr(settings, "MAX_AGE_CANCEL_ACCOUNT", None)
user = get_user_for_token(serializer.data["cancel_token"], "cancel_account", user = get_user_for_token(validator.data["cancel_token"], "cancel_account",
max_age=max_age_cancel_account) max_age=max_age_cancel_account)
except exc.NotAuthenticated: except exc.NotAuthenticated:
raise exc.WrongArguments(_("Invalid, are you sure the token is correct?")) raise exc.WrongArguments(_("Invalid, are you sure the token is correct?"))
@ -305,7 +319,7 @@ class UsersViewSet(ModelCrudViewSet):
self.object_list = user_filters.ContactsFilterBackend().filter_queryset( self.object_list = user_filters.ContactsFilterBackend().filter_queryset(
user, request, self.get_queryset(), self).extra( user, request, self.get_queryset(), self).extra(
select={"complete_user_name":"concat(full_name, username)"}).order_by("complete_user_name") select={"complete_user_name": "concat(full_name, username)"}).order_by("complete_user_name")
page = self.paginate_queryset(self.object_list) page = self.paginate_queryset(self.object_list)
if page is not None: if page is not None:
@ -349,10 +363,10 @@ class UsersViewSet(ModelCrudViewSet):
for elem in elements: for elem in elements:
if elem["type"] == "project": if elem["type"] == "project":
# projects are liked objects # projects are liked objects
response_data.append(serializers.LikedObjectSerializer(elem, **extra_args_liked).data ) response_data.append(serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args_liked).data)
else: else:
# stories, tasks and issues are voted objects # stories, tasks and issues are voted objects
response_data.append(serializers.VotedObjectSerializer(elem, **extra_args_voted).data ) response_data.append(serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args_voted).data)
return response.Ok(response_data) return response.Ok(response_data)
@ -374,7 +388,7 @@ class UsersViewSet(ModelCrudViewSet):
"user_likes": services.get_liked_content_for_user(request.user), "user_likes": services.get_liked_content_for_user(request.user),
} }
response_data = [serializers.LikedObjectSerializer(elem, **extra_args).data for elem in elements] response_data = [serializers.LikedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements]
return response.Ok(response_data) return response.Ok(response_data)
@ -397,17 +411,18 @@ class UsersViewSet(ModelCrudViewSet):
"user_votes": services.get_voted_content_for_user(request.user), "user_votes": services.get_voted_content_for_user(request.user),
} }
response_data = [serializers.VotedObjectSerializer(elem, **extra_args).data for elem in elements] response_data = [serializers.VotedObjectSerializer(into_namedtuple(elem), **extra_args).data for elem in elements]
return response.Ok(response_data) return response.Ok(response_data)
######################################################
## Role
######################################################
######################################################
# Role
######################################################
class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet): class RolesViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Role model = models.Role
serializer_class = serializers.RoleSerializer serializer_class = serializers.RoleSerializer
validator_class = validators.RoleValidator
permission_classes = (permissions.RolesPermission, ) permission_classes = (permissions.RolesPermission, )
filter_backends = (filters.CanViewProjectFilterBackend,) filter_backends = (filters.CanViewProjectFilterBackend,)
filter_fields = ('project',) filter_fields = ('project',)

View File

@ -198,7 +198,7 @@ class User(AbstractBaseUser, PermissionsMixin):
def _fill_cached_memberships(self): def _fill_cached_memberships(self):
self._cached_memberships = {} self._cached_memberships = {}
qs = self.memberships.prefetch_related("user", "project", "role") qs = self.memberships.select_related("user", "project", "role")
for membership in qs.all(): for membership in qs.all():
self._cached_memberships[membership.project.id] = membership self._cached_memberships[membership.project.id] = membership

View File

@ -17,70 +17,45 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import PgArrayField from taiga.base.fields import PgArrayField, Field, MethodField, I18NField
from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.base.utils.thumbnails import get_thumbnail_url
from taiga.projects.models import Project from taiga.projects.models import Project
from .models import User, Role
from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url from .services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url
from .gravatar import get_gravatar_url from .gravatar import get_gravatar_url
from collections import namedtuple from collections import namedtuple
import re
import serpy
###################################################### ######################################################
## User # User
###################################################### ######################################################
class ContactProjectDetailSerializer(serializers.ModelSerializer): class ContactProjectDetailSerializer(serializers.LightSerializer):
class Meta: id = Field()
model = Project slug = Field()
fields = ("id", "slug", "name") name = Field()
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.LightSerializer):
full_name_display = serializers.SerializerMethodField("get_full_name_display") id = Field()
photo = serializers.SerializerMethodField("get_photo") username = Field()
big_photo = serializers.SerializerMethodField("get_big_photo") full_name = Field()
gravatar_url = serializers.SerializerMethodField("get_gravatar_url") full_name_display = MethodField()
roles = serializers.SerializerMethodField("get_roles") color = Field()
projects_with_me = serializers.SerializerMethodField("get_projects_with_me") bio = Field()
lang = Field()
class Meta: theme = Field()
model = User timezone = Field()
# IMPORTANT: Maintain the UserAdminSerializer Meta up to date is_active = Field()
# with this info (including there the email) photo = MethodField()
fields = ("id", "username", "full_name", "full_name_display", big_photo = MethodField()
"color", "bio", "lang", "theme", "timezone", "is_active", gravatar_url = MethodField()
"photo", "big_photo", "roles", "projects_with_me", roles = MethodField()
"gravatar_url") projects_with_me = MethodField()
read_only_fields = ("id",)
def validate_username(self, attrs, source):
value = attrs[source]
validator = validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"),
_("invalid"))
try:
validator(value)
except ValidationError:
raise serializers.ValidationError(_("Required. 255 characters or fewer. Letters, "
"numbers and /./-/_ characters'"))
if (self.object and
self.object.username != value and
User.objects.filter(username=value).exists()):
raise serializers.ValidationError(_("Invalid username. Try with a different one."))
return attrs
def get_full_name_display(self, obj): def get_full_name_display(self, obj):
return obj.get_full_name() if obj else "" return obj.get_full_name() if obj else ""
@ -113,24 +88,13 @@ class UserSerializer(serializers.ModelSerializer):
class UserAdminSerializer(UserSerializer): class UserAdminSerializer(UserSerializer):
total_private_projects = serializers.SerializerMethodField("get_total_private_projects") total_private_projects = MethodField()
total_public_projects = serializers.SerializerMethodField("get_total_public_projects") total_public_projects = MethodField()
email = Field()
class Meta: max_private_projects = Field()
model = User max_public_projects = Field()
# IMPORTANT: Maintain the UserSerializer Meta up to date max_memberships_private_projects = Field()
# with this info (including here the email) max_memberships_public_projects = Field()
fields = ("id", "username", "full_name", "full_name_display", "email",
"color", "bio", "lang", "theme", "timezone", "is_active", "photo",
"big_photo", "gravatar_url",
"max_private_projects", "max_public_projects",
"max_memberships_private_projects", "max_memberships_public_projects",
"total_private_projects", "total_public_projects")
read_only_fields = ("id", "email",
"max_private_projects", "max_public_projects",
"max_memberships_private_projects",
"max_memberships_public_projects")
def get_total_private_projects(self, user): def get_total_private_projects(self, user):
return user.owned_projects.filter(is_private=True).count() return user.owned_projects.filter(is_private=True).count()
@ -139,19 +103,13 @@ class UserAdminSerializer(UserSerializer):
return user.owned_projects.filter(is_private=False).count() return user.owned_projects.filter(is_private=False).count()
class UserBasicInfoSerializer(UserSerializer): class UserBasicInfoSerializer(serializers.LightSerializer):
class Meta: username = Field()
model = User full_name_display = MethodField()
fields = ("username", "full_name_display", "photo", "big_photo", "is_active", "id") photo = MethodField()
big_photo = MethodField()
is_active = Field()
class ListUserBasicInfoSerializer(serpy.Serializer): id = Field()
username = serpy.Field()
full_name_display = serpy.MethodField()
photo = serpy.MethodField()
big_photo = serpy.MethodField()
is_active = serpy.Field()
id = serpy.Field()
def get_full_name_display(self, obj): def get_full_name_display(self, obj):
return obj.get_full_name() return obj.get_full_name()
@ -162,76 +120,70 @@ class ListUserBasicInfoSerializer(serpy.Serializer):
def get_big_photo(self, obj): def get_big_photo(self, obj):
return get_big_photo_or_gravatar_url(obj) return get_big_photo_or_gravatar_url(obj)
def to_value(self, instance):
if instance is None:
return None
class RecoverySerializer(serializers.Serializer): return super().to_value(instance)
token = serializers.CharField(max_length=200)
password = serializers.CharField(min_length=6)
class ChangeEmailSerializer(serializers.Serializer):
email_token = serializers.CharField(max_length=200)
class CancelAccountSerializer(serializers.Serializer):
cancel_token = serializers.CharField(max_length=200)
###################################################### ######################################################
## Role # Role
###################################################### ######################################################
class RoleSerializer(serializers.ModelSerializer): class RoleSerializer(serializers.LightSerializer):
members_count = serializers.SerializerMethodField("get_members_count") id = Field()
name = Field()
computable = Field()
project = Field(attr="project_id")
order = Field()
members_count = MethodField()
permissions = PgArrayField(required=False) permissions = PgArrayField(required=False)
class Meta:
model = Role
fields = ('id', 'name', 'permissions', 'computable', 'project', 'order', 'members_count')
i18n_fields = ("name",)
def get_members_count(self, obj): def get_members_count(self, obj):
return obj.memberships.count() return obj.memberships.count()
class ProjectRoleSerializer(serializers.ModelSerializer): class ProjectRoleSerializer(serializers.LightSerializer):
class Meta: id = Field()
model = Role name = I18NField()
fields = ('id', 'name', 'slug', 'order', 'computable') slug = Field()
i18n_fields = ("name",) order = Field()
computable = Field()
###################################################### ######################################################
## Like # Like
###################################################### ######################################################
class HighLightedContentSerializer(serializers.Serializer): class HighLightedContentSerializer(serializers.LightSerializer):
type = serializers.CharField() type = Field()
id = serializers.IntegerField() id = Field()
ref = serializers.IntegerField() ref = Field()
slug = serializers.CharField() slug = Field()
name = serializers.CharField() name = Field()
subject = serializers.CharField() subject = Field()
description = serializers.SerializerMethodField("get_description") description = MethodField()
assigned_to = serializers.IntegerField() assigned_to = Field()
status = serializers.CharField() status = Field()
status_color = serializers.CharField() status_color = Field()
tags_colors = serializers.SerializerMethodField("get_tags_color") tags_colors = MethodField()
created_date = serializers.DateTimeField() created_date = Field()
is_private = serializers.SerializerMethodField("get_is_private") is_private = MethodField()
logo_small_url = serializers.SerializerMethodField("get_logo_small_url") logo_small_url = MethodField()
project = serializers.SerializerMethodField("get_project") project = MethodField()
project_name = serializers.SerializerMethodField("get_project_name") project_name = MethodField()
project_slug = serializers.SerializerMethodField("get_project_slug") project_slug = MethodField()
project_is_private = serializers.SerializerMethodField("get_project_is_private") project_is_private = MethodField()
project_blocked_code = serializers.CharField() project_blocked_code = Field()
assigned_to_username = serializers.CharField() assigned_to_username = Field()
assigned_to_full_name = serializers.CharField() assigned_to_full_name = Field()
assigned_to_photo = serializers.SerializerMethodField("get_photo") assigned_to_photo = MethodField()
is_watcher = serializers.SerializerMethodField("get_is_watcher") is_watcher = MethodField()
total_watchers = serializers.IntegerField() total_watchers = Field()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass # Don't pass the extra ids args up to the superclass
@ -241,18 +193,18 @@ class HighLightedContentSerializer(serializers.Serializer):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def _none_if_project(self, obj, property): def _none_if_project(self, obj, property):
type = obj.get("type", "") type = getattr(obj, "type", "")
if type == "project": if type == "project":
return None return None
return obj.get(property) return getattr(obj, property)
def _none_if_not_project(self, obj, property): def _none_if_not_project(self, obj, property):
type = obj.get("type", "") type = getattr(obj, "type", "")
if type != "project": if type != "project":
return None return None
return obj.get(property) return getattr(obj, property)
def get_project(self, obj): def get_project(self, obj):
return self._none_if_project(obj, "project") return self._none_if_project(obj, "project")
@ -278,29 +230,29 @@ class HighLightedContentSerializer(serializers.Serializer):
return get_thumbnail_url(logo, settings.THN_LOGO_SMALL) return get_thumbnail_url(logo, settings.THN_LOGO_SMALL)
return None return None
def get_photo(self, obj): def get_assigned_to_photo(self, obj):
type = obj.get("type", "") type = getattr(obj, "type", "")
if type == "project": if type == "project":
return None return None
UserData = namedtuple("UserData", ["photo", "email"]) UserData = namedtuple("UserData", ["photo", "email"])
user_data = UserData(photo=obj["assigned_to_photo"], email=obj.get("assigned_to_email") or "") user_data = UserData(photo=obj.assigned_to_photo, email=obj.assigned_to_email or "")
return get_photo_or_gravatar_url(user_data) return get_photo_or_gravatar_url(user_data)
def get_tags_color(self, obj): def get_tags_colors(self, obj):
tags = obj.get("tags", []) tags = getattr(obj, "tags", [])
tags = tags if tags is not None else [] tags = tags if tags is not None else []
tags_colors = obj.get("tags_colors", []) tags_colors = getattr(obj, "tags_colors", [])
tags_colors = tags_colors if tags_colors is not None else [] tags_colors = tags_colors if tags_colors is not None else []
return [{"name": tc[0], "color": tc[1]} for tc in tags_colors if tc[0] in tags] return [{"name": tc[0], "color": tc[1]} for tc in tags_colors if tc[0] in tags]
def get_is_watcher(self, obj): def get_is_watcher(self, obj):
return obj["id"] in self.user_watching.get(obj["type"], []) return obj.id in self.user_watching.get(obj.type, [])
class LikedObjectSerializer(HighLightedContentSerializer): class LikedObjectSerializer(HighLightedContentSerializer):
is_fan = serializers.SerializerMethodField("get_is_fan") is_fan = MethodField()
total_fans = serializers.IntegerField() total_fans = Field()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass # Don't pass the extra ids args up to the superclass
@ -310,12 +262,12 @@ class LikedObjectSerializer(HighLightedContentSerializer):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def get_is_fan(self, obj): def get_is_fan(self, obj):
return obj["id"] in self.user_likes.get(obj["type"], []) return obj.id in self.user_likes.get(obj.type, [])
class VotedObjectSerializer(HighLightedContentSerializer): class VotedObjectSerializer(HighLightedContentSerializer):
is_voter = serializers.SerializerMethodField("get_is_voter") is_voter = MethodField()
total_voters = serializers.IntegerField() total_voters = Field()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't pass the extra ids args up to the superclass # Don't pass the extra ids args up to the superclass
@ -325,4 +277,4 @@ class VotedObjectSerializer(HighLightedContentSerializer):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def get_is_voter(self, obj): def get_is_voter(self, obj):
return obj["id"] in self.user_votes.get(obj["type"], []) return obj.id in self.user_votes.get(obj.type, [])

View File

@ -3,7 +3,6 @@
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com> # Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com> # Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net> # Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the # published by the Free Software Foundation, either version 3 of the
@ -17,17 +16,92 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext as _ from django.core import validators as core_validators
from django.utils.translation import ugettext_lazy as _
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.exceptions import ValidationError
from taiga.base.fields import PgArrayField
from . import models from .models import User, Role
import re
class RoleExistsValidator: class RoleExistsValidator:
def validate_role_id(self, attrs, source): def validate_role_id(self, attrs, source):
value = attrs[source] value = attrs[source]
if not models.Role.objects.filter(pk=value).exists(): if not Role.objects.filter(pk=value).exists():
msg = _("There's no role with that id") msg = _("There's no role with that id")
raise serializers.ValidationError(msg) raise ValidationError(msg)
return attrs return attrs
######################################################
# User
######################################################
class UserValidator(validators.ModelValidator):
class Meta:
model = User
fields = ("username", "full_name", "color", "bio", "lang",
"theme", "timezone", "is_active")
def validate_username(self, attrs, source):
value = attrs[source]
validator = core_validators.RegexValidator(re.compile('^[\w.-]+$'), _("invalid username"),
_("invalid"))
try:
validator(value)
except ValidationError:
raise ValidationError(_("Required. 255 characters or fewer. Letters, "
"numbers and /./-/_ characters'"))
if (self.object and
self.object.username != value and
User.objects.filter(username=value).exists()):
raise ValidationError(_("Invalid username. Try with a different one."))
return attrs
class UserAdminValidator(UserValidator):
class Meta:
model = User
# IMPORTANT: Maintain the UserSerializer Meta up to date
# with this info (including here the email)
fields = ("username", "full_name", "color", "bio", "lang",
"theme", "timezone", "is_active", "email")
class RecoveryValidator(validators.Validator):
token = serializers.CharField(max_length=200)
password = serializers.CharField(min_length=6)
class ChangeEmailValidator(validators.Validator):
email_token = serializers.CharField(max_length=200)
class CancelAccountValidator(validators.Validator):
cancel_token = serializers.CharField(max_length=200)
######################################################
# Role
######################################################
class RoleValidator(validators.ModelValidator):
permissions = PgArrayField(required=False)
class Meta:
model = Role
fields = ('id', 'name', 'permissions', 'computable', 'project', 'order')
i18n_fields = ("name",)
class ProjectRoleValidator(validators.ModelValidator):
class Meta:
model = Role
fields = ('id', 'name', 'slug', 'order', 'computable')

View File

@ -17,7 +17,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.db import IntegrityError
from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelCrudViewSet
from taiga.base import exceptions as exc from taiga.base import exceptions as exc
@ -25,6 +24,7 @@ from taiga.base import exceptions as exc
from . import models from . import models
from . import filters from . import filters
from . import serializers from . import serializers
from . import validators
from . import permissions from . import permissions
@ -32,6 +32,7 @@ class StorageEntriesViewSet(ModelCrudViewSet):
model = models.StorageEntry model = models.StorageEntry
filter_backends = (filters.StorageEntriesFilterBackend,) filter_backends = (filters.StorageEntriesFilterBackend,)
serializer_class = serializers.StorageEntrySerializer serializer_class = serializers.StorageEntrySerializer
validator_class = validators.StorageEntryValidator
permission_classes = [permissions.StorageEntriesPermission] permission_classes = [permissions.StorageEntriesPermission]
lookup_field = "key" lookup_field = "key"
@ -45,9 +46,11 @@ class StorageEntriesViewSet(ModelCrudViewSet):
obj.owner = self.request.user obj.owner = self.request.user
def create(self, *args, **kwargs): def create(self, *args, **kwargs):
try: key = self.request.DATA.get("key", None)
return super().create(*args, **kwargs) if (key and self.request.user.is_authenticated() and
except IntegrityError: self.request.user.storage_entries.filter(key=key).exists()):
key = self.request.DATA.get("key", None) raise exc.BadRequest(
raise exc.IntegrityError(_("Duplicate key value violates unique constraint. " _("Duplicate key value violates unique constraint. "
"Key '{}' already exists.").format(key)) "Key '{}' already exists.").format(key)
)
return super().create(*args, **kwargs)

View File

@ -17,15 +17,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import JsonField from taiga.base.fields import Field
from . import models
class StorageEntrySerializer(serializers.ModelSerializer): class StorageEntrySerializer(serializers.LightSerializer):
value = JsonField(label="value") key = Field()
value = Field()
class Meta: created_date = Field()
model = models.StorageEntry modified_date = Field()
fields = ("key", "value", "created_date", "modified_date")
read_only_fields = ("created_date", "modified_date")

View File

@ -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")

View File

@ -30,6 +30,7 @@ from taiga.base.decorators import detail_route
from . import models from . import models
from . import serializers from . import serializers
from . import validators
from . import permissions from . import permissions
from . import tasks from . import tasks
@ -37,6 +38,7 @@ from . import tasks
class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet): class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Webhook model = models.Webhook
serializer_class = serializers.WebhookSerializer serializer_class = serializers.WebhookSerializer
validator_class = validators.WebhookValidator
permission_classes = (permissions.WebhookPermission,) permission_classes = (permissions.WebhookPermission,)
filter_backends = (filters.IsProjectAdminFilterBackend,) filter_backends = (filters.IsProjectAdminFilterBackend,)
filter_fields = ("project",) filter_fields = ("project",)

View File

@ -19,63 +19,55 @@
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from taiga.base.api import serializers from taiga.base.api import serializers
from taiga.base.fields import PgArrayField, JsonField from taiga.base.fields import Field, MethodField
from taiga.front.templatetags.functions import resolve as resolve_front_url from taiga.front.templatetags.functions import resolve as resolve_front_url
from taiga.projects.history import models as history_models
from taiga.projects.issues import models as issue_models
from taiga.projects.milestones import models as milestone_models
from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer
from taiga.projects.services import get_logo_big_thumbnail_url from taiga.projects.services import get_logo_big_thumbnail_url
from taiga.projects.tasks import models as task_models
from taiga.projects.tagging.fields import TagsField
from taiga.projects.userstories import models as us_models
from taiga.projects.wiki import models as wiki_models
from taiga.users.gravatar import get_gravatar_url from taiga.users.gravatar import get_gravatar_url
from taiga.users.services import get_photo_or_gravatar_url from taiga.users.services import get_photo_or_gravatar_url
from .models import Webhook, WebhookLog
######################################################################## ########################################################################
## WebHooks # WebHooks
######################################################################## ########################################################################
class WebhookSerializer(serializers.ModelSerializer): class WebhookSerializer(serializers.LightSerializer):
logs_counter = serializers.SerializerMethodField("get_logs_counter") id = Field()
project = Field(attr="project_id")
class Meta: name = Field()
model = Webhook url = Field()
key = Field()
logs_counter = MethodField()
def get_logs_counter(self, obj): def get_logs_counter(self, obj):
return obj.logs.count() return obj.logs.count()
class WebhookLogSerializer(serializers.ModelSerializer): class WebhookLogSerializer(serializers.LightSerializer):
request_data = JsonField() id = Field()
request_headers = JsonField() webhook = Field(attr="webhook_id")
response_headers = JsonField() url = Field()
status = Field()
class Meta: request_data = Field()
model = WebhookLog request_headers = Field()
response_data = Field()
response_headers = Field()
duration = Field()
created = Field()
######################################################################## ########################################################################
## User # User
######################################################################## ########################################################################
class UserSerializer(serializers.Serializer): class UserSerializer(serializers.LightSerializer):
id = serializers.SerializerMethodField("get_pk") id = Field(attr="pk")
permalink = serializers.SerializerMethodField("get_permalink") permalink = MethodField()
gravatar_url = serializers.SerializerMethodField("get_gravatar_url") gravatar_url = MethodField()
username = serializers.SerializerMethodField("get_username") username = MethodField()
full_name = serializers.SerializerMethodField("get_full_name") full_name = MethodField()
photo = serializers.SerializerMethodField("get_photo") photo = MethodField()
def get_pk(self, obj):
return obj.pk
def get_permalink(self, obj): def get_permalink(self, obj):
return resolve_front_url("user", obj.username) return resolve_front_url("user", obj.username)
@ -84,7 +76,7 @@ class UserSerializer(serializers.Serializer):
return get_gravatar_url(obj.email) return get_gravatar_url(obj.email)
def get_username(self, obj): def get_username(self, obj):
return obj.get_username return obj.get_username()
def get_full_name(self, obj): def get_full_name(self, obj):
return obj.get_full_name() return obj.get_full_name()
@ -92,18 +84,22 @@ class UserSerializer(serializers.Serializer):
def get_photo(self, obj): def get_photo(self, obj):
return get_photo_or_gravatar_url(obj) return get_photo_or_gravatar_url(obj)
def to_value(self, instance):
if instance is None:
return None
return super().to_value(instance)
######################################################################## ########################################################################
## Project # Project
######################################################################## ########################################################################
class ProjectSerializer(serializers.Serializer): class ProjectSerializer(serializers.LightSerializer):
id = serializers.SerializerMethodField("get_pk") id = Field(attr="pk")
permalink = serializers.SerializerMethodField("get_permalink") permalink = MethodField()
name = serializers.SerializerMethodField("get_name") name = MethodField()
logo_big_url = serializers.SerializerMethodField("get_logo_big_url") logo_big_url = MethodField()
def get_pk(self, obj):
return obj.pk
def get_permalink(self, obj): def get_permalink(self, obj):
return resolve_front_url("project", obj.slug) return resolve_front_url("project", obj.slug)
@ -116,11 +112,11 @@ class ProjectSerializer(serializers.Serializer):
######################################################################## ########################################################################
## History Serializer # History Serializer
######################################################################## ########################################################################
class HistoryDiffField(serializers.Field): class HistoryDiffField(Field):
def to_native(self, value): def to_value(self, value):
# Tip: 'value' is the object returned by # Tip: 'value' is the object returned by
# taiga.projects.history.models.HistoryEntry.values_diff() # taiga.projects.history.models.HistoryEntry.values_diff()
@ -137,21 +133,21 @@ class HistoryDiffField(serializers.Field):
return ret return ret
class HistoryEntrySerializer(serializers.ModelSerializer): class HistoryEntrySerializer(serializers.LightSerializer):
diff = HistoryDiffField(source="values_diff") comment = Field()
comment_html = Field()
class Meta: delete_comment_date = Field()
model = history_models.HistoryEntry comment_versions = Field()
exclude = ("id", "type", "key", "is_hidden", "is_snapshot", "snapshot", "user", "delete_comment_user", edit_comment_date = Field()
"values", "created_at") diff = HistoryDiffField(attr="values_diff")
######################################################################## ########################################################################
## _Misc_ # _Misc_
######################################################################## ########################################################################
class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): class CustomAttributesValuesWebhookSerializerMixin(serializers.LightSerializer):
custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") custom_attributes_values = MethodField()
def custom_attributes_queryset(self, project): def custom_attributes_queryset(self, project):
raise NotImplementedError() raise NotImplementedError()
@ -161,13 +157,13 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
ret = {} ret = {}
for attr in custom_attributes: for attr in custom_attributes:
value = values.get(str(attr["id"]), None) value = values.get(str(attr["id"]), None)
if value is not None: if value is not None:
ret[attr["name"]] = value ret[attr["name"]] = value
return ret return ret
try: try:
values = obj.custom_attributes_values.attributes_values values = obj.custom_attributes_values.attributes_values
custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name') custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name')
return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values)
@ -175,10 +171,10 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer):
return None return None
class RolePointsSerializer(serializers.Serializer): class RolePointsSerializer(serializers.LightSerializer):
role = serializers.SerializerMethodField("get_role") role = MethodField()
name = serializers.SerializerMethodField("get_name") name = MethodField()
value = serializers.SerializerMethodField("get_value") value = MethodField()
def get_role(self, obj): def get_role(self, obj):
return obj.role.name return obj.role.name
@ -190,16 +186,13 @@ class RolePointsSerializer(serializers.Serializer):
return obj.points.value return obj.points.value
class UserStoryStatusSerializer(serializers.Serializer): class UserStoryStatusSerializer(serializers.LightSerializer):
id = serializers.SerializerMethodField("get_pk") id = Field(attr="pk")
name = serializers.SerializerMethodField("get_name") name = MethodField()
slug = serializers.SerializerMethodField("get_slug") slug = MethodField()
color = serializers.SerializerMethodField("get_color") color = MethodField()
is_closed = serializers.SerializerMethodField("get_is_closed") is_closed = MethodField()
is_archived = serializers.SerializerMethodField("get_is_archived") is_archived = MethodField()
def get_pk(self, obj):
return obj.pk
def get_name(self, obj): def get_name(self, obj):
return obj.name return obj.name
@ -217,15 +210,12 @@ class UserStoryStatusSerializer(serializers.Serializer):
return obj.is_archived return obj.is_archived
class TaskStatusSerializer(serializers.Serializer): class TaskStatusSerializer(serializers.LightSerializer):
id = serializers.SerializerMethodField("get_pk") id = Field(attr="pk")
name = serializers.SerializerMethodField("get_name") name = MethodField()
slug = serializers.SerializerMethodField("get_slug") slug = MethodField()
color = serializers.SerializerMethodField("get_color") color = MethodField()
is_closed = serializers.SerializerMethodField("get_is_closed") is_closed = MethodField()
def get_pk(self, obj):
return obj.pk
def get_name(self, obj): def get_name(self, obj):
return obj.name return obj.name
@ -240,15 +230,12 @@ class TaskStatusSerializer(serializers.Serializer):
return obj.is_closed return obj.is_closed
class IssueStatusSerializer(serializers.Serializer): class IssueStatusSerializer(serializers.LightSerializer):
id = serializers.SerializerMethodField("get_pk") id = Field(attr="pk")
name = serializers.SerializerMethodField("get_name") name = MethodField()
slug = serializers.SerializerMethodField("get_slug") slug = MethodField()
color = serializers.SerializerMethodField("get_color") color = MethodField()
is_closed = serializers.SerializerMethodField("get_is_closed") is_closed = MethodField()
def get_pk(self, obj):
return obj.pk
def get_name(self, obj): def get_name(self, obj):
return obj.name return obj.name
@ -263,13 +250,10 @@ class IssueStatusSerializer(serializers.Serializer):
return obj.is_closed return obj.is_closed
class IssueTypeSerializer(serializers.Serializer): class IssueTypeSerializer(serializers.LightSerializer):
id = serializers.SerializerMethodField("get_pk") id = Field(attr="pk")
name = serializers.SerializerMethodField("get_name") name = MethodField()
color = serializers.SerializerMethodField("get_color") color = MethodField()
def get_pk(self, obj):
return obj.pk
def get_name(self, obj): def get_name(self, obj):
return obj.name return obj.name
@ -278,13 +262,10 @@ class IssueTypeSerializer(serializers.Serializer):
return obj.color return obj.color
class PrioritySerializer(serializers.Serializer): class PrioritySerializer(serializers.LightSerializer):
id = serializers.SerializerMethodField("get_pk") id = Field(attr="pk")
name = serializers.SerializerMethodField("get_name") name = MethodField()
color = serializers.SerializerMethodField("get_color") color = MethodField()
def get_pk(self, obj):
return obj.pk
def get_name(self, obj): def get_name(self, obj):
return obj.name return obj.name
@ -293,13 +274,10 @@ class PrioritySerializer(serializers.Serializer):
return obj.color return obj.color
class SeveritySerializer(serializers.Serializer): class SeveritySerializer(serializers.LightSerializer):
id = serializers.SerializerMethodField("get_pk") id = Field(attr="pk")
name = serializers.SerializerMethodField("get_name") name = MethodField()
color = serializers.SerializerMethodField("get_color") color = MethodField()
def get_pk(self, obj):
return obj.pk
def get_name(self, obj): def get_name(self, obj):
return obj.name return obj.name
@ -309,57 +287,90 @@ class SeveritySerializer(serializers.Serializer):
######################################################################## ########################################################################
## Milestone # Milestone
######################################################################## ########################################################################
class MilestoneSerializer(serializers.ModelSerializer): class MilestoneSerializer(serializers.LightSerializer):
id = Field()
name = Field()
slug = Field()
estimated_start = Field()
estimated_finish = Field()
created_date = Field()
modified_date = Field()
closed = Field()
disponibility = Field()
permalink = serializers.SerializerMethodField("get_permalink") permalink = serializers.SerializerMethodField("get_permalink")
project = ProjectSerializer() project = ProjectSerializer()
owner = UserSerializer() owner = UserSerializer()
class Meta:
model = milestone_models.Milestone
exclude = ("order", "watchers")
def get_permalink(self, obj): def get_permalink(self, obj):
return resolve_front_url("taskboard", obj.project.slug, obj.slug) return resolve_front_url("taskboard", obj.project.slug, obj.slug)
######################################################################## ########################################################################
## User Story # User Story
######################################################################## ########################################################################
class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
serializers.ModelSerializer): id = Field()
permalink = serializers.SerializerMethodField("get_permalink") ref = Field()
tags = TagsField(default=[], required=False)
external_reference = PgArrayField(required=False)
project = ProjectSerializer() project = ProjectSerializer()
is_closed = Field()
created_date = Field()
modified_date = Field()
finish_date = Field()
subject = Field()
client_requirement = Field()
team_requirement = Field()
generated_from_issue = Field(attr="generated_from_issue_id")
external_reference = Field()
tribe_gig = Field()
watchers = MethodField()
is_blocked = Field()
blocked_note = Field()
tags = Field()
permalink = serializers.SerializerMethodField("get_permalink")
owner = UserSerializer() owner = UserSerializer()
assigned_to = UserSerializer() assigned_to = UserSerializer()
points = RolePointsSerializer(source="role_points", many=True) points = MethodField()
status = UserStoryStatusSerializer() status = UserStoryStatusSerializer()
milestone = MilestoneSerializer() milestone = MilestoneSerializer()
class Meta:
model = us_models.UserStory
exclude = ("backlog_order", "sprint_order", "kanban_order", "version", "total_watchers", "is_watcher")
def get_permalink(self, obj): def get_permalink(self, obj):
return resolve_front_url("userstory", obj.project.slug, obj.ref) return resolve_front_url("userstory", obj.project.slug, obj.ref)
def custom_attributes_queryset(self, project): def custom_attributes_queryset(self, project):
return project.userstorycustomattributes.all() return project.userstorycustomattributes.all()
def get_watchers(self, obj):
return list(obj.get_watchers().values_list("id", flat=True))
def get_points(self, obj):
return RolePointsSerializer(obj.role_points.all(), many=True).data
######################################################################## ########################################################################
## Task # Task
######################################################################## ########################################################################
class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
serializers.ModelSerializer): id = Field()
ref = Field()
created_date = Field()
modified_date = Field()
finished_date = Field()
subject = Field()
us_order = Field()
taskboard_order = Field()
is_iocaine = Field()
external_reference = Field()
watchers = MethodField()
is_blocked = Field()
blocked_note = Field()
description = Field()
tags = Field()
permalink = serializers.SerializerMethodField("get_permalink") permalink = serializers.SerializerMethodField("get_permalink")
tags = TagsField(default=[], required=False)
project = ProjectSerializer() project = ProjectSerializer()
owner = UserSerializer() owner = UserSerializer()
assigned_to = UserSerializer() assigned_to = UserSerializer()
@ -367,25 +378,32 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatch
user_story = UserStorySerializer() user_story = UserStorySerializer()
milestone = MilestoneSerializer() milestone = MilestoneSerializer()
class Meta:
model = task_models.Task
exclude = ("version", "total_watchers", "is_watcher")
def get_permalink(self, obj): def get_permalink(self, obj):
return resolve_front_url("task", obj.project.slug, obj.ref) return resolve_front_url("task", obj.project.slug, obj.ref)
def custom_attributes_queryset(self, project): def custom_attributes_queryset(self, project):
return project.taskcustomattributes.all() return project.taskcustomattributes.all()
def get_watchers(self, obj):
return list(obj.get_watchers().values_list("id", flat=True))
######################################################################## ########################################################################
## Issue # Issue
######################################################################## ########################################################################
class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer):
serializers.ModelSerializer): id = Field()
ref = Field()
created_date = Field()
modified_date = Field()
finished_date = Field()
subject = Field()
external_reference = Field()
watchers = MethodField()
description = Field()
tags = Field()
permalink = serializers.SerializerMethodField("get_permalink") permalink = serializers.SerializerMethodField("get_permalink")
tags = TagsField(default=[], required=False)
project = ProjectSerializer() project = ProjectSerializer()
milestone = MilestoneSerializer() milestone = MilestoneSerializer()
owner = UserSerializer() owner = UserSerializer()
@ -395,30 +413,30 @@ class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatc
priority = PrioritySerializer() priority = PrioritySerializer()
severity = SeveritySerializer() severity = SeveritySerializer()
class Meta:
model = issue_models.Issue
exclude = ("version", "total_watchers", "is_watcher")
def get_permalink(self, obj): def get_permalink(self, obj):
return resolve_front_url("issue", obj.project.slug, obj.ref) return resolve_front_url("issue", obj.project.slug, obj.ref)
def custom_attributes_queryset(self, project): def custom_attributes_queryset(self, project):
return project.issuecustomattributes.all() return project.issuecustomattributes.all()
def get_watchers(self, obj):
return list(obj.get_watchers().values_list("id", flat=True))
######################################################################## ########################################################################
## Wiki Page # Wiki Page
######################################################################## ########################################################################
class WikiPageSerializer(serializers.ModelSerializer): class WikiPageSerializer(serializers.LightSerializer):
id = Field()
slug = Field()
content = Field()
created_date = Field()
modified_date = Field()
permalink = serializers.SerializerMethodField("get_permalink") permalink = serializers.SerializerMethodField("get_permalink")
project = ProjectSerializer() project = ProjectSerializer()
owner = UserSerializer() owner = UserSerializer()
last_modifier = UserSerializer() last_modifier = UserSerializer()
class Meta:
model = wiki_models.WikiPage
exclude = ("watchers", "total_watchers", "is_watcher", "version")
def get_permalink(self, obj): def get_permalink(self, obj):
return resolve_front_url("wiki", obj.project.slug, obj.slug) return resolve_front_url("wiki", obj.project.slug, obj.slug)

View File

@ -149,5 +149,4 @@ def test_webhook(webhook_id, url, key, by, date):
data['by'] = UserSerializer(by).data data['by'] = UserSerializer(by).data
data['date'] = date data['date'] = date
data['data'] = {"test": "test"} data['data'] = {"test": "test"}
return _send_request(webhook_id, url, key, data) return _send_request(webhook_id, url, key, data)

View File

@ -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

View File

@ -22,7 +22,11 @@ import uuid
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from taiga.projects import choices as project_choices from taiga.projects import choices as project_choices
from taiga.projects.models import Project
from taiga.projects.utils import attach_extra_info as attach_project_extra_info
from taiga.projects.issues.models import Issue
from taiga.projects.issues.serializers import IssueSerializer from taiga.projects.issues.serializers import IssueSerializer
from taiga.projects.issues.utils import attach_extra_info as attach_issue_extra_info
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.base.utils import json from taiga.base.utils import json
@ -61,22 +65,29 @@ def data():
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner, owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex) issues_csv_uuid=uuid.uuid4().hex)
m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id)
m.private_project1 = f.ProjectFactory(is_private=True, m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner, owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex) issues_csv_uuid=uuid.uuid4().hex)
m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
m.private_project2 = f.ProjectFactory(is_private=True, m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[], anon_permissions=[],
public_permissions=[], public_permissions=[],
owner=m.project_owner, owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex) issues_csv_uuid=uuid.uuid4().hex)
m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id)
m.blocked_project = f.ProjectFactory(is_private=True, m.blocked_project = f.ProjectFactory(is_private=True,
anon_permissions=[], anon_permissions=[],
public_permissions=[], public_permissions=[],
owner=m.project_owner, owner=m.project_owner,
issues_csv_uuid=uuid.uuid4().hex, issues_csv_uuid=uuid.uuid4().hex,
blocked_code=project_choices.BLOCKED_BY_STAFF) blocked_code=project_choices.BLOCKED_BY_STAFF)
m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id)
m.public_membership = f.MembershipFactory(project=m.public_project, m.public_membership = f.MembershipFactory(project=m.public_project,
user=m.project_member_with_perms, user=m.project_member_with_perms,
@ -129,24 +140,31 @@ def data():
priority__project=m.public_project, priority__project=m.public_project,
type__project=m.public_project, type__project=m.public_project,
milestone__project=m.public_project) milestone__project=m.public_project)
m.public_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.public_issue.id)
m.private_issue1 = f.IssueFactory(project=m.private_project1, m.private_issue1 = f.IssueFactory(project=m.private_project1,
status__project=m.private_project1, status__project=m.private_project1,
severity__project=m.private_project1, severity__project=m.private_project1,
priority__project=m.private_project1, priority__project=m.private_project1,
type__project=m.private_project1, type__project=m.private_project1,
milestone__project=m.private_project1) milestone__project=m.private_project1)
m.private_issue1 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue1.id)
m.private_issue2 = f.IssueFactory(project=m.private_project2, m.private_issue2 = f.IssueFactory(project=m.private_project2,
status__project=m.private_project2, status__project=m.private_project2,
severity__project=m.private_project2, severity__project=m.private_project2,
priority__project=m.private_project2, priority__project=m.private_project2,
type__project=m.private_project2, type__project=m.private_project2,
milestone__project=m.private_project2) milestone__project=m.private_project2)
m.private_issue2 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue2.id)
m.blocked_issue = f.IssueFactory(project=m.blocked_project, m.blocked_issue = f.IssueFactory(project=m.blocked_project,
status__project=m.blocked_project, status__project=m.blocked_project,
severity__project=m.blocked_project, severity__project=m.blocked_project,
priority__project=m.blocked_project, priority__project=m.blocked_project,
type__project=m.blocked_project, type__project=m.blocked_project,
milestone__project=m.blocked_project) milestone__project=m.blocked_project)
m.blocked_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.blocked_issue.id)
return m return m
@ -443,24 +461,28 @@ def test_issue_put_update_with_project_change(client):
project1.save() project1.save()
project2.save() project2.save()
membership1 = f.MembershipFactory(project=project1, project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id)
user=user1, project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id)
role__project=project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project1,
membership2 = f.MembershipFactory(project=project2, user=user1,
user=user1, role__project=project1,
role__project=project2, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project2,
membership3 = f.MembershipFactory(project=project1, user=user1,
user=user2, role__project=project2,
role__project=project1, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project1,
membership4 = f.MembershipFactory(project=project2, user=user2,
user=user3, role__project=project1,
role__project=project2, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project2,
user=user3,
role__project=project2,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
issue = f.IssueFactory.create(project=project1) issue = f.IssueFactory.create(project=project1)
issue = attach_issue_extra_info(Issue.objects.all()).get(id=issue.id)
url = reverse('issues-detail', kwargs={"pk": issue.pk}) url = reverse('issues-detail', kwargs={"pk": issue.pk})

View File

@ -22,8 +22,11 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects import choices as project_choices from taiga.projects import choices as project_choices
from taiga.projects.models import Project
from taiga.projects.utils import attach_extra_info as attach_project_extra_info
from taiga.projects.milestones.serializers import MilestoneSerializer from taiga.projects.milestones.serializers import MilestoneSerializer
from taiga.projects.milestones.models import Milestone from taiga.projects.milestones.models import Milestone
from taiga.projects.milestones.utils import attach_extra_info as attach_milestone_extra_info
from taiga.projects.notifications.services import add_watcher from taiga.projects.notifications.services import add_watcher
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
@ -56,44 +59,55 @@ def data():
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner) owner=m.project_owner)
m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id)
m.private_project1 = f.ProjectFactory(is_private=True, m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner) owner=m.project_owner)
m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
m.private_project2 = f.ProjectFactory(is_private=True, m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[], anon_permissions=[],
public_permissions=[], public_permissions=[],
owner=m.project_owner) owner=m.project_owner)
m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id)
m.blocked_project = f.ProjectFactory(is_private=True, m.blocked_project = f.ProjectFactory(is_private=True,
anon_permissions=[], anon_permissions=[],
public_permissions=[], public_permissions=[],
owner=m.project_owner, owner=m.project_owner,
blocked_code=project_choices.BLOCKED_BY_STAFF) blocked_code=project_choices.BLOCKED_BY_STAFF)
m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id)
m.public_membership = f.MembershipFactory(project=m.public_project, m.public_membership = f.MembershipFactory(
user=m.project_member_with_perms, project=m.public_project,
role__project=m.public_project, user=m.project_member_with_perms,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) role__project=m.public_project,
m.private_membership1 = f.MembershipFactory(project=m.private_project1, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
user=m.project_member_with_perms, m.private_membership1 = f.MembershipFactory(
role__project=m.private_project1, project=m.private_project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) 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, user=m.project_member_without_perms,
role__project=m.private_project1, role__project=m.private_project1,
role__permissions=[]) role__permissions=[])
m.private_membership2 = f.MembershipFactory(project=m.private_project2, m.private_membership2 = f.MembershipFactory(
user=m.project_member_with_perms, project=m.private_project2,
role__project=m.private_project2, user=m.project_member_with_perms,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) 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, user=m.project_member_without_perms,
role__project=m.private_project2, role__project=m.private_project2,
role__permissions=[]) role__permissions=[])
m.blocked_membership = f.MembershipFactory(project=m.blocked_project, m.blocked_membership = f.MembershipFactory(
user=m.project_member_with_perms, project=m.blocked_project,
role__project=m.blocked_project, user=m.project_member_with_perms,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) role__project=m.blocked_project,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.blocked_project, f.MembershipFactory(project=m.blocked_project,
user=m.project_member_without_perms, user=m.project_member_without_perms,
role__project=m.blocked_project, role__project=m.blocked_project,
@ -112,13 +126,17 @@ def data():
is_admin=True) is_admin=True)
f.MembershipFactory(project=m.blocked_project, f.MembershipFactory(project=m.blocked_project,
user=m.project_owner, user=m.project_owner,
is_admin=True) is_admin=True)
m.public_milestone = f.MilestoneFactory(project=m.public_project) m.public_milestone = f.MilestoneFactory(project=m.public_project)
m.public_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.public_milestone.id)
m.private_milestone1 = f.MilestoneFactory(project=m.private_project1) m.private_milestone1 = f.MilestoneFactory(project=m.private_project1)
m.private_milestone1 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone1.id)
m.private_milestone2 = f.MilestoneFactory(project=m.private_project2) m.private_milestone2 = f.MilestoneFactory(project=m.private_project2)
m.private_milestone2 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone2.id)
m.blocked_milestone = f.MilestoneFactory(project=m.blocked_project) m.blocked_milestone = f.MilestoneFactory(project=m.blocked_project)
m.blocked_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.blocked_milestone.id)
return m return m
@ -422,16 +440,16 @@ def test_milestone_watchers_list(client, data):
def test_milestone_watchers_retrieve(client, data): def test_milestone_watchers_retrieve(client, data):
add_watcher(data.public_milestone, data.project_owner) add_watcher(data.public_milestone, data.project_owner)
public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk, public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_watcher(data.private_milestone1, data.project_owner) add_watcher(data.private_milestone1, data.project_owner)
private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk, private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_watcher(data.private_milestone2, data.project_owner) add_watcher(data.private_milestone2, data.project_owner)
private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk, private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_watcher(data.blocked_milestone, data.project_owner) add_watcher(data.blocked_milestone, data.project_owner)
blocked_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.blocked_milestone.pk, blocked_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.blocked_milestone.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
users = [ users = [
None, None,

View File

@ -22,8 +22,10 @@ from django.apps import apps
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects import choices as project_choices from taiga.projects import choices as project_choices
from taiga.projects.serializers import ProjectDetailSerializer from taiga.projects import models as project_models
from taiga.projects.serializers import ProjectSerializer
from taiga.permissions.choices import MEMBERS_PERMISSIONS from taiga.permissions.choices import MEMBERS_PERMISSIONS
from taiga.projects.utils import attach_extra_info
from tests import factories as f from tests import factories as f
from tests.utils import helper_test_http_method, helper_test_http_method_and_count from tests.utils import helper_test_http_method, helper_test_http_method_and_count
@ -45,19 +47,26 @@ def data():
m.public_project = f.ProjectFactory(is_private=False, m.public_project = f.ProjectFactory(is_private=False,
anon_permissions=['view_project'], anon_permissions=['view_project'],
public_permissions=['view_project']) public_permissions=['view_project'])
m.public_project = attach_extra_info(project_models.Project.objects.all()).get(id=m.public_project.id)
m.private_project1 = f.ProjectFactory(is_private=True, m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=['view_project'], anon_permissions=['view_project'],
public_permissions=['view_project'], public_permissions=['view_project'],
owner=m.project_owner) owner=m.project_owner)
m.private_project1 = attach_extra_info(project_models.Project.objects.all()).get(id=m.private_project1.id)
m.private_project2 = f.ProjectFactory(is_private=True, m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[], anon_permissions=[],
public_permissions=[], public_permissions=[],
owner=m.project_owner) owner=m.project_owner)
m.private_project2 = attach_extra_info(project_models.Project.objects.all()).get(id=m.private_project2.id)
m.blocked_project = f.ProjectFactory(is_private=True, m.blocked_project = f.ProjectFactory(is_private=True,
anon_permissions=[], anon_permissions=[],
public_permissions=[], public_permissions=[],
owner=m.project_owner, owner=m.project_owner,
blocked_code=project_choices.BLOCKED_BY_STAFF) blocked_code=project_choices.BLOCKED_BY_STAFF)
m.blocked_project = attach_extra_info(project_models.Project.objects.all()).get(id=m.blocked_project.id)
f.RoleFactory(project=m.public_project) f.RoleFactory(project=m.public_project)
@ -153,12 +162,12 @@ def test_project_update(client, data):
data.project_owner data.project_owner
] ]
project_data = ProjectDetailSerializer(data.private_project2).data project_data = ProjectSerializer(data.private_project2).data
project_data["is_private"] = False project_data["is_private"] = False
results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users) results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users)
assert results == [401, 403, 403, 200] assert results == [401, 403, 403, 200]
project_data = ProjectDetailSerializer(data.blocked_project).data project_data = ProjectSerializer(data.blocked_project).data
project_data["is_private"] = False project_data["is_private"] = False
results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users) results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users)
assert results == [401, 403, 403, 451] assert results == [401, 403, 403, 451]

View File

@ -23,12 +23,16 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects import choices as project_choices from taiga.projects import choices as project_choices
from taiga.projects.models import Project
from taiga.projects.tasks.serializers import TaskSerializer from taiga.projects.tasks.serializers import TaskSerializer
from taiga.projects.tasks.models import Task
from taiga.projects.tasks.utils import attach_extra_info as attach_task_extra_info
from taiga.projects.utils import attach_extra_info as attach_project_extra_info
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin from taiga.projects.occ import OCCResourceMixin
from tests import factories as f from tests import factories as f
from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals from tests.utils import helper_test_http_method, reconnect_signals
from taiga.projects.votes.services import add_vote from taiga.projects.votes.services import add_vote
from taiga.projects.notifications.services import add_watcher from taiga.projects.notifications.services import add_watcher
@ -38,10 +42,6 @@ import pytest
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
def setup_function(function):
disconnect_signals()
def setup_function(function): def setup_function(function):
reconnect_signals() reconnect_signals()
@ -61,47 +61,61 @@ def data():
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner, owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex) tasks_csv_uuid=uuid.uuid4().hex)
m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id)
m.private_project1 = f.ProjectFactory(is_private=True, m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner, owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex) tasks_csv_uuid=uuid.uuid4().hex)
m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
m.private_project2 = f.ProjectFactory(is_private=True, m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[], anon_permissions=[],
public_permissions=[], public_permissions=[],
owner=m.project_owner, owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex) tasks_csv_uuid=uuid.uuid4().hex)
m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id)
m.blocked_project = f.ProjectFactory(is_private=True, m.blocked_project = f.ProjectFactory(is_private=True,
anon_permissions=[], anon_permissions=[],
public_permissions=[], public_permissions=[],
owner=m.project_owner, owner=m.project_owner,
tasks_csv_uuid=uuid.uuid4().hex, tasks_csv_uuid=uuid.uuid4().hex,
blocked_code=project_choices.BLOCKED_BY_STAFF) blocked_code=project_choices.BLOCKED_BY_STAFF)
m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id)
m.public_membership = f.MembershipFactory(project=m.public_project, m.public_membership = f.MembershipFactory(
user=m.project_member_with_perms, project=m.public_project,
role__project=m.public_project, user=m.project_member_with_perms,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) role__project=m.public_project,
m.private_membership1 = f.MembershipFactory(project=m.private_project1, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
user=m.project_member_with_perms,
role__project=m.private_project1, m.private_membership1 = f.MembershipFactory(
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) project=m.private_project1,
f.MembershipFactory(project=m.private_project1, user=m.project_member_with_perms,
user=m.project_member_without_perms, role__project=m.private_project1,
role__project=m.private_project1, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=[]) f.MembershipFactory(
m.private_membership2 = f.MembershipFactory(project=m.private_project2, project=m.private_project1,
user=m.project_member_with_perms, user=m.project_member_without_perms,
role__project=m.private_project2, role__project=m.private_project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) role__permissions=[])
f.MembershipFactory(project=m.private_project2, m.private_membership2 = f.MembershipFactory(
user=m.project_member_without_perms, project=m.private_project2,
role__project=m.private_project2, user=m.project_member_with_perms,
role__permissions=[]) role__project=m.private_project2,
m.blocked_membership = f.MembershipFactory(project=m.blocked_project, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
user=m.project_member_with_perms, f.MembershipFactory(
role__project=m.blocked_project, project=m.private_project2,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) user=m.project_member_without_perms,
role__project=m.private_project2,
role__permissions=[])
m.blocked_membership = f.MembershipFactory(
project=m.blocked_project,
user=m.project_member_with_perms,
role__project=m.blocked_project,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.blocked_project, f.MembershipFactory(project=m.blocked_project,
user=m.project_member_without_perms, user=m.project_member_without_perms,
role__project=m.blocked_project, role__project=m.blocked_project,
@ -120,8 +134,8 @@ def data():
is_admin=True) is_admin=True)
f.MembershipFactory(project=m.blocked_project, f.MembershipFactory(project=m.blocked_project,
user=m.project_owner, user=m.project_owner,
is_admin=True) is_admin=True)
milestone_public_task = f.MilestoneFactory(project=m.public_project) milestone_public_task = f.MilestoneFactory(project=m.public_project)
milestone_private_task1 = f.MilestoneFactory(project=m.private_project1) milestone_private_task1 = f.MilestoneFactory(project=m.private_project1)
@ -133,21 +147,28 @@ def data():
milestone=milestone_public_task, milestone=milestone_public_task,
user_story__project=m.public_project, user_story__project=m.public_project,
user_story__milestone=milestone_public_task) user_story__milestone=milestone_public_task)
m.public_task = attach_task_extra_info(Task.objects.all()).get(id=m.public_task.id)
m.private_task1 = f.TaskFactory(project=m.private_project1, m.private_task1 = f.TaskFactory(project=m.private_project1,
status__project=m.private_project1, status__project=m.private_project1,
milestone=milestone_private_task1, milestone=milestone_private_task1,
user_story__project=m.private_project1, user_story__project=m.private_project1,
user_story__milestone=milestone_private_task1) user_story__milestone=milestone_private_task1)
m.private_task1 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task1.id)
m.private_task2 = f.TaskFactory(project=m.private_project2, m.private_task2 = f.TaskFactory(project=m.private_project2,
status__project=m.private_project2, status__project=m.private_project2,
milestone=milestone_private_task2, milestone=milestone_private_task2,
user_story__project=m.private_project2, user_story__project=m.private_project2,
user_story__milestone=milestone_private_task2) user_story__milestone=milestone_private_task2)
m.private_task2 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task2.id)
m.blocked_task = f.TaskFactory(project=m.blocked_project, m.blocked_task = f.TaskFactory(project=m.blocked_project,
status__project=m.blocked_project, status__project=m.blocked_project,
milestone=milestone_blocked_task, milestone=milestone_blocked_task,
user_story__project=m.blocked_project, user_story__project=m.blocked_project,
user_story__milestone=milestone_blocked_task) user_story__milestone=milestone_blocked_task)
m.blocked_task = attach_task_extra_info(Task.objects.all()).get(id=m.blocked_task.id)
m.public_project.default_task_status = m.public_task.status m.public_project.default_task_status = m.public_task.status
m.public_project.save() m.public_project.save()
@ -404,24 +425,28 @@ def test_task_put_update_with_project_change(client):
project1.save() project1.save()
project2.save() project2.save()
membership1 = f.MembershipFactory(project=project1, project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id)
user=user1, project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id)
role__project=project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project1,
membership2 = f.MembershipFactory(project=project2, user=user1,
user=user1, role__project=project1,
role__project=project2, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project2,
membership3 = f.MembershipFactory(project=project1, user=user1,
user=user2, role__project=project2,
role__project=project1, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project1,
membership4 = f.MembershipFactory(project=project2, user=user2,
user=user3, role__project=project1,
role__project=project2, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project2,
user=user3,
role__project=project2,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
task = f.TaskFactory.create(project=project1) task = f.TaskFactory.create(project=project1)
task = attach_task_extra_info(Task.objects.all()).get(id=task.id)
url = reverse('tasks-detail', kwargs={"pk": task.pk}) url = reverse('tasks-detail', kwargs={"pk": task.pk})
@ -739,17 +764,17 @@ def test_task_voters_list(client, data):
def test_task_voters_retrieve(client, data): def test_task_voters_retrieve(client, data):
add_vote(data.public_task, data.project_owner) add_vote(data.public_task, data.project_owner)
public_url = reverse('task-voters-detail', kwargs={"resource_id": data.public_task.pk, public_url = reverse('task-voters-detail', kwargs={"resource_id": data.public_task.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_vote(data.private_task1, data.project_owner) add_vote(data.private_task1, data.project_owner)
private_url1 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task1.pk, private_url1 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task1.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_vote(data.private_task2, data.project_owner) add_vote(data.private_task2, data.project_owner)
private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk, private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_vote(data.blocked_task, data.project_owner) add_vote(data.blocked_task, data.project_owner)
blocked_url = reverse('task-voters-detail', kwargs={"resource_id": data.blocked_task.pk, blocked_url = reverse('task-voters-detail', kwargs={"resource_id": data.blocked_task.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
users = [ users = [
None, None,
@ -844,17 +869,17 @@ def test_task_watchers_list(client, data):
def test_task_watchers_retrieve(client, data): def test_task_watchers_retrieve(client, data):
add_watcher(data.public_task, data.project_owner) add_watcher(data.public_task, data.project_owner)
public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk, public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_watcher(data.private_task1, data.project_owner) add_watcher(data.private_task1, data.project_owner)
private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk, private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_watcher(data.private_task2, data.project_owner) add_watcher(data.private_task2, data.project_owner)
private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk, private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_watcher(data.blocked_task, data.project_owner) add_watcher(data.blocked_task, data.project_owner)
blocked_url = reverse('task-watchers-detail', kwargs={"resource_id": data.blocked_task.pk, blocked_url = reverse('task-watchers-detail', kwargs={"resource_id": data.blocked_task.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
users = [ users = [
None, None,
data.registered_user, data.registered_user,

View File

@ -23,7 +23,11 @@ from django.core.urlresolvers import reverse
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects import choices as project_choices from taiga.projects import choices as project_choices
from taiga.projects.models import Project
from taiga.projects.utils import attach_extra_info as attach_project_extra_info
from taiga.projects.userstories.models import UserStory
from taiga.projects.userstories.serializers import UserStorySerializer from taiga.projects.userstories.serializers import UserStorySerializer
from taiga.projects.userstories.utils import attach_extra_info as attach_userstory_extra_info
from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS
from taiga.projects.occ import OCCResourceMixin from taiga.projects.occ import OCCResourceMixin
@ -61,47 +65,58 @@ def data():
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner, owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex) userstories_csv_uuid=uuid.uuid4().hex)
m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id)
m.private_project1 = f.ProjectFactory(is_private=True, m.private_project1 = f.ProjectFactory(is_private=True,
anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)),
owner=m.project_owner, owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex) userstories_csv_uuid=uuid.uuid4().hex)
m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id)
m.private_project2 = f.ProjectFactory(is_private=True, m.private_project2 = f.ProjectFactory(is_private=True,
anon_permissions=[], anon_permissions=[],
public_permissions=[], public_permissions=[],
owner=m.project_owner, owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex) userstories_csv_uuid=uuid.uuid4().hex)
m.blocked_project = f.ProjectFactory(is_private=True, m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id)
anon_permissions=[],
public_permissions=[],
owner=m.project_owner,
userstories_csv_uuid=uuid.uuid4().hex,
blocked_code=project_choices.BLOCKED_BY_STAFF)
m.public_membership = f.MembershipFactory(project=m.public_project, m.blocked_project = f.ProjectFactory(is_private=True,
user=m.project_member_with_perms, anon_permissions=[],
role__project=m.public_project, public_permissions=[],
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) owner=m.project_owner,
m.private_membership1 = f.MembershipFactory(project=m.private_project1, userstories_csv_uuid=uuid.uuid4().hex,
user=m.project_member_with_perms, blocked_code=project_choices.BLOCKED_BY_STAFF)
role__project=m.private_project1, m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id)
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
m.public_membership = f.MembershipFactory(
project=m.public_project,
user=m.project_member_with_perms,
role__project=m.public_project,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
m.private_membership1 = f.MembershipFactory(
project=m.private_project1,
user=m.project_member_with_perms,
role__project=m.private_project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.private_project1, f.MembershipFactory(project=m.private_project1,
user=m.project_member_without_perms, user=m.project_member_without_perms,
role__project=m.private_project1, role__project=m.private_project1,
role__permissions=[]) role__permissions=[])
m.private_membership2 = f.MembershipFactory(project=m.private_project2, m.private_membership2 = f.MembershipFactory(
user=m.project_member_with_perms, project=m.private_project2,
role__project=m.private_project2, user=m.project_member_with_perms,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) 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, user=m.project_member_without_perms,
role__project=m.private_project2, role__project=m.private_project2,
role__permissions=[]) role__permissions=[])
m.blocked_membership = f.MembershipFactory(project=m.blocked_project, m.blocked_membership = f.MembershipFactory(
user=m.project_member_with_perms, project=m.blocked_project,
role__project=m.blocked_project, user=m.project_member_with_perms,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) role__project=m.blocked_project,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
f.MembershipFactory(project=m.blocked_project, f.MembershipFactory(project=m.blocked_project,
user=m.project_member_without_perms, user=m.project_member_without_perms,
role__project=m.blocked_project, role__project=m.blocked_project,
@ -120,8 +135,8 @@ def data():
is_admin=True) is_admin=True)
f.MembershipFactory(project=m.blocked_project, f.MembershipFactory(project=m.blocked_project,
user=m.project_owner, user=m.project_owner,
is_admin=True) is_admin=True)
m.public_points = f.PointsFactory(project=m.public_project) m.public_points = f.PointsFactory(project=m.public_project)
m.private_points1 = f.PointsFactory(project=m.private_project1) m.private_points1 = f.PointsFactory(project=m.private_project1)
@ -144,15 +159,19 @@ def data():
user_story__milestone__project=m.private_project2, user_story__milestone__project=m.private_project2,
user_story__status__project=m.private_project2) user_story__status__project=m.private_project2)
m.blocked_role_points = f.RolePointsFactory(role=m.blocked_project.roles.all()[0], m.blocked_role_points = f.RolePointsFactory(role=m.blocked_project.roles.all()[0],
points=m.blocked_points, points=m.blocked_points,
user_story__project=m.blocked_project, user_story__project=m.blocked_project,
user_story__milestone__project=m.blocked_project, user_story__milestone__project=m.blocked_project,
user_story__status__project=m.blocked_project) user_story__status__project=m.blocked_project)
m.public_user_story = m.public_role_points.user_story m.public_user_story = m.public_role_points.user_story
m.public_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.public_user_story.id)
m.private_user_story1 = m.private_role_points1.user_story m.private_user_story1 = m.private_role_points1.user_story
m.private_user_story1 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story1.id)
m.private_user_story2 = m.private_role_points2.user_story m.private_user_story2 = m.private_role_points2.user_story
m.private_user_story2 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story2.id)
m.blocked_user_story = m.blocked_role_points.user_story m.blocked_user_story = m.blocked_role_points.user_story
m.blocked_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.blocked_user_story.id)
return m return m
@ -380,24 +399,28 @@ def test_user_story_put_update_with_project_change(client):
project1.save() project1.save()
project2.save() project2.save()
membership1 = f.MembershipFactory(project=project1, project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id)
user=user1, project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id)
role__project=project1,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project1,
membership2 = f.MembershipFactory(project=project2, user=user1,
user=user1, role__project=project1,
role__project=project2, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project2,
membership3 = f.MembershipFactory(project=project1, user=user1,
user=user2, role__project=project2,
role__project=project1, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project1,
membership4 = f.MembershipFactory(project=project2, user=user2,
user=user3, role__project=project1,
role__project=project2, role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=project2,
user=user3,
role__project=project2,
role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS)))
us = f.UserStoryFactory.create(project=project1) us = f.UserStoryFactory.create(project=project1)
us = attach_userstory_extra_info(UserStory.objects.all()).get(id=us.id)
url = reverse('userstories-detail', kwargs={"pk": us.pk}) url = reverse('userstories-detail', kwargs={"pk": us.pk})
@ -592,7 +615,6 @@ def test_user_story_delete(client, data):
assert results == [401, 403, 403, 451] assert results == [401, 403, 403, 451]
def test_user_story_action_bulk_create(client, data): def test_user_story_action_bulk_create(client, data):
url = reverse('userstories-bulk-create') url = reverse('userstories-bulk-create')
@ -746,7 +768,7 @@ def test_user_story_voters_retrieve(client, data):
add_vote(data.blocked_user_story, data.project_owner) add_vote(data.blocked_user_story, data.project_owner)
blocked_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.blocked_user_story.pk, blocked_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.blocked_user_story.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
users = [ users = [
None, None,
data.registered_user, data.registered_user,
@ -840,16 +862,16 @@ def test_userstory_watchers_list(client, data):
def test_userstory_watchers_retrieve(client, data): def test_userstory_watchers_retrieve(client, data):
add_watcher(data.public_user_story, data.project_owner) add_watcher(data.public_user_story, data.project_owner)
public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk, public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_watcher(data.private_user_story1, data.project_owner) add_watcher(data.private_user_story1, data.project_owner)
private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk, private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_watcher(data.private_user_story2, data.project_owner) add_watcher(data.private_user_story2, data.project_owner)
private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk, private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
add_watcher(data.blocked_user_story, data.project_owner) add_watcher(data.blocked_user_story, data.project_owner)
blocked_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.blocked_user_story.pk, blocked_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.blocked_user_story.pk,
"pk": data.project_owner.pk}) "pk": data.project_owner.pk})
users = [ users = [
None, None,

View File

@ -242,16 +242,19 @@ def test_webhook_action_test(client, data):
] ]
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
_send_request_mock.return_value = data.webhooklog1
results = helper_test_http_method(client, 'post', url1, None, users) results = helper_test_http_method(client, 'post', url1, None, users)
assert results == [404, 404, 200] assert results == [404, 404, 200]
assert _send_request_mock.called is True assert _send_request_mock.called is True
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
_send_request_mock.return_value = data.webhooklog1
results = helper_test_http_method(client, 'post', url2, None, users) results = helper_test_http_method(client, 'post', url2, None, users)
assert results == [404, 404, 404] assert results == [404, 404, 404]
assert _send_request_mock.called is False assert _send_request_mock.called is False
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
_send_request_mock.return_value = data.webhooklog1
results = helper_test_http_method(client, 'post', blocked_url, None, users) results = helper_test_http_method(client, 'post', blocked_url, None, users)
assert results == [404, 404, 451] assert results == [404, 404, 451]
assert _send_request_mock.called is False assert _send_request_mock.called is False

View File

@ -43,7 +43,7 @@ def test_update_milestone_with_userstories_list(client):
form_data = { form_data = {
"name": "test", "name": "test",
"user_stories": [UserStorySerializer(us).data] "user_stories": [{"id": us.id}]
} }
client.login(user) client.login(user)

View File

@ -790,7 +790,7 @@ def test_watchers_assignation_for_issue(client):
assert response.status_code == 400 assert response.status_code == 400
issue = f.create_issue(project=project1, owner=user1) issue = f.create_issue(project=project1, owner=user1)
data = dict(IssueSerializer(issue).data) data = {}
data["id"] = None data["id"] = None
data["version"] = None data["version"] = None
data["watchers"] = [user1.pk, user2.pk] data["watchers"] = [user1.pk, user2.pk]
@ -802,8 +802,7 @@ def test_watchers_assignation_for_issue(client):
# Test the impossible case when project is not # Test the impossible case when project is not
# exists in create request, and validator works as expected # exists in create request, and validator works as expected
issue = f.create_issue(project=project1, owner=user1) issue = f.create_issue(project=project1, owner=user1)
data = dict(IssueSerializer(issue).data) data = {}
data["id"] = None data["id"] = None
data["watchers"] = [user1.pk, user2.pk] data["watchers"] = [user1.pk, user2.pk]
data["project"] = None data["project"] = None
@ -842,10 +841,11 @@ def test_watchers_assignation_for_task(client):
assert response.status_code == 400 assert response.status_code == 400
task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1)
data = dict(TaskSerializer(task).data) data = {
data["id"] = None "id": None,
data["version"] = None "version": None,
data["watchers"] = [user1.pk, user2.pk] "watchers": [user1.pk, user2.pk]
}
url = reverse("tasks-list") url = reverse("tasks-list")
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
@ -854,11 +854,11 @@ def test_watchers_assignation_for_task(client):
# Test the impossible case when project is not # Test the impossible case when project is not
# exists in create request, and validator works as expected # exists in create request, and validator works as expected
task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1)
data = dict(TaskSerializer(task).data) data = {
"id": None,
data["id"] = None "watchers": [user1.pk, user2.pk],
data["watchers"] = [user1.pk, user2.pk] "project": None
data["project"] = None }
url = reverse("tasks-list") url = reverse("tasks-list")
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
@ -894,10 +894,11 @@ def test_watchers_assignation_for_us(client):
assert response.status_code == 400 assert response.status_code == 400
us = f.create_userstory(project=project1, owner=user1, status__project=project1) us = f.create_userstory(project=project1, owner=user1, status__project=project1)
data = dict(UserStorySerializer(us).data) data = {
data["id"] = None "id": None,
data["version"] = None "version": None,
data["watchers"] = [user1.pk, user2.pk] "watchers": [user1.pk, user2.pk]
}
url = reverse("userstories-list") url = reverse("userstories-list")
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))
@ -906,11 +907,11 @@ def test_watchers_assignation_for_us(client):
# Test the impossible case when project is not # Test the impossible case when project is not
# exists in create request, and validator works as expected # exists in create request, and validator works as expected
us = f.create_userstory(project=project1, owner=user1, status__project=project1) us = f.create_userstory(project=project1, owner=user1, status__project=project1)
data = dict(UserStorySerializer(us).data) data = {
"id": None,
data["id"] = None "watchers": [user1.pk, user2.pk],
data["watchers"] = [user1.pk, user2.pk] "project": None
data["project"] = None }
url = reverse("userstories-list") url = reverse("userstories-list")
response = client.json.post(url, json.dumps(data)) response = client.json.post(url, json.dumps(data))

View File

@ -625,7 +625,7 @@ def test_projects_user_order(client):
#Testing user order #Testing user order
url = reverse("projects-list") url = reverse("projects-list")
url = "%s?member=%s&order_by=memberships__user_order" % (url, user.id) url = "%s?member=%s&order_by=user_order" % (url, user.id)
response = client.json.get(url) response = client.json.get(url)
response_content = response.data response_content = response.data
assert response.status_code == 200 assert response.status_code == 200

View File

@ -30,6 +30,7 @@ from ..utils import DUMMY_BMP_DATA
from taiga.base.utils import json from taiga.base.utils import json
from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.base.utils.thumbnails import get_thumbnail_url
from taiga.base.utils.dicts import into_namedtuple
from taiga.users import models from taiga.users import models
from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer from taiga.users.serializers import LikedObjectSerializer, VotedObjectSerializer
from taiga.auth.tokens import get_token_for_user from taiga.auth.tokens import get_token_for_user
@ -505,7 +506,7 @@ def test_get_watched_list_valid_info_for_project():
raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0] raw_project_watch_info = get_watched_list(fav_user, viewer_user)[0]
project_watch_info = LikedObjectSerializer(raw_project_watch_info).data project_watch_info = LikedObjectSerializer(into_namedtuple(raw_project_watch_info)).data
assert project_watch_info["type"] == "project" assert project_watch_info["type"] == "project"
assert project_watch_info["id"] == project.id assert project_watch_info["id"] == project.id
@ -559,7 +560,7 @@ def test_get_liked_list_valid_info():
project.refresh_totals() project.refresh_totals()
raw_project_like_info = get_liked_list(fan_user, viewer_user)[0] raw_project_like_info = get_liked_list(fan_user, viewer_user)[0]
project_like_info = LikedObjectSerializer(raw_project_like_info).data project_like_info = LikedObjectSerializer(into_namedtuple(raw_project_like_info)).data
assert project_like_info["type"] == "project" assert project_like_info["type"] == "project"
assert project_like_info["id"] == project.id assert project_like_info["id"] == project.id
@ -609,7 +610,7 @@ def test_get_watched_list_valid_info_for_not_project_types():
instance.add_watcher(fav_user) instance.add_watcher(fav_user)
raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0] raw_instance_watch_info = get_watched_list(fav_user, viewer_user, type=object_type)[0]
instance_watch_info = VotedObjectSerializer(raw_instance_watch_info).data instance_watch_info = VotedObjectSerializer(into_namedtuple(raw_instance_watch_info)).data
assert instance_watch_info["type"] == object_type assert instance_watch_info["type"] == object_type
assert instance_watch_info["id"] == instance.id assert instance_watch_info["id"] == instance.id
@ -666,7 +667,7 @@ def test_get_voted_list_valid_info():
f.VotesFactory(content_type=content_type, object_id=instance.id, count=3) f.VotesFactory(content_type=content_type, object_id=instance.id, count=3)
raw_instance_vote_info = get_voted_list(fav_user, viewer_user, type=object_type)[0] raw_instance_vote_info = get_voted_list(fav_user, viewer_user, type=object_type)[0]
instance_vote_info = VotedObjectSerializer(raw_instance_vote_info).data instance_vote_info = VotedObjectSerializer(into_namedtuple(raw_instance_vote_info)).data
assert instance_vote_info["type"] == object_type assert instance_vote_info["type"] == object_type
assert instance_vote_info["id"] == instance.id assert instance_vote_info["id"] == instance.id

Some files were not shown because too many files have changed in this diff Show More