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

View File

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

View File

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

View File

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

View File

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

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
model relationships, including hyperlinks, primary keys, or slugs.
"""
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
@ -59,6 +59,7 @@ from django.utils.translation import ugettext_lazy as _
from .fields import Field, WritableField, get_component, is_simple_callable
from .reverse import reverse
from taiga.base.exceptions import ValidationError
import warnings
from urllib import parse as urlparse

View File

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

View File

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

View File

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

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 ValidationError as DjangoValidationError
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.http import Http404
@ -224,6 +225,7 @@ class NotEnoughSlotsForProject(BaseException):
"total_memberships": total_memberships
}
def format_exception(exc):
if isinstance(exc.detail, (dict, list, tuple,)):
detail = exc.detail
@ -270,3 +272,6 @@ def exception_handler(exc):
# Note: Unhandled exceptions will raise a 500 error.
return None
ValidationError = DjangoValidationError

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.auth import get_user_model
from taiga.base.api import serializers
from taiga.base.fields import Field, MethodField
class FanSerializer(serializers.ModelSerializer):
full_name = serializers.CharField(source='get_full_name', required=False)
class FanSerializer(serializers.LightSerializer):
id = Field()
username = Field()
full_name = MethodField()
class Meta:
model = get_user_model()
fields = ('id', 'username', 'full_name')
def get_full_name(self, obj):
return obj.get_full_name()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 serializers
from . import validators
from . import permissions
from . import tasks
@ -37,6 +38,7 @@ from . import tasks
class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet):
model = models.Webhook
serializer_class = serializers.WebhookSerializer
validator_class = validators.WebhookValidator
permission_classes = (permissions.WebhookPermission,)
filter_backends = (filters.IsProjectAdminFilterBackend,)
filter_fields = ("project",)

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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:
_send_request_mock.return_value = data.webhooklog1
results = helper_test_http_method(client, 'post', url1, None, users)
assert results == [404, 404, 200]
assert _send_request_mock.called is True
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
_send_request_mock.return_value = data.webhooklog1
results = helper_test_http_method(client, 'post', url2, None, users)
assert results == [404, 404, 404]
assert _send_request_mock.called is False
with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock:
_send_request_mock.return_value = data.webhooklog1
results = helper_test_http_method(client, 'post', blocked_url, None, users)
assert results == [404, 404, 451]
assert _send_request_mock.called is False

View File

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

View File

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

View File

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

View File

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

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