diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py index 158d712d..31823945 100644 --- a/taiga/base/api/generics.py +++ b/taiga/base/api/generics.py @@ -62,6 +62,7 @@ class GenericAPIView(pagination.PaginationMixin, # or override `get_queryset()`/`get_serializer_class()`. queryset = None serializer_class = None + validator_class = None # This shortcut may be used instead of setting either or both # of the `queryset`/`serializer_class` attributes, although using @@ -79,6 +80,7 @@ class GenericAPIView(pagination.PaginationMixin, # The following attributes may be subject to change, # and should be considered private API. model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS + model_validator_class = api_settings.DEFAULT_MODEL_VALIDATOR_CLASS ###################################### # These are pending deprecation... @@ -88,7 +90,7 @@ class GenericAPIView(pagination.PaginationMixin, slug_field = 'slug' allow_empty = True - def get_serializer_context(self): + def get_extra_context(self): """ Extra context provided to the serializer class. """ @@ -101,14 +103,24 @@ class GenericAPIView(pagination.PaginationMixin, def get_serializer(self, instance=None, data=None, files=None, many=False, partial=False): """ - Return the serializer instance that should be used for validating and - deserializing input, and for serializing output. + Return the serializer instance that should be used for deserializing + input, and for serializing output. """ serializer_class = self.get_serializer_class() - context = self.get_serializer_context() + context = self.get_extra_context() return serializer_class(instance, data=data, files=files, many=many, partial=partial, context=context) + def get_validator(self, instance=None, data=None, + files=None, many=False, partial=False): + """ + Return the validator instance that should be used for validating the + input, and for serializing output. + """ + validator_class = self.get_validator_class() + context = self.get_extra_context() + return validator_class(instance, data=data, files=files, + many=many, partial=partial, context=context) def filter_queryset(self, queryset, filter_backends=None): """ @@ -119,7 +131,7 @@ class GenericAPIView(pagination.PaginationMixin, method if you want to apply the configured filtering backend to the default queryset. """ - #NOTE TAIGA: Added filter_backends to overwrite the default behavior. + # NOTE TAIGA: Added filter_backends to overwrite the default behavior. backends = filter_backends or self.get_filter_backends() for backend in backends: @@ -160,6 +172,22 @@ class GenericAPIView(pagination.PaginationMixin, model = self.model return DefaultSerializer + def get_validator_class(self): + validator_class = self.validator_class + serializer_class = self.get_serializer_class() + + # Situations where the validator is the rest framework serializer + if validator_class is None and serializer_class is not None: + return serializer_class + + if validator_class is not None: + return validator_class + + class DefaultValidator(self.model_validator_class): + class Meta: + model = self.model + return DefaultValidator + def get_queryset(self): """ Get the list of items for this view. diff --git a/taiga/base/api/mixins.py b/taiga/base/api/mixins.py index 89af6984..861d77ec 100644 --- a/taiga/base/api/mixins.py +++ b/taiga/base/api/mixins.py @@ -57,6 +57,7 @@ from .utils import get_object_or_404 from .. import exceptions as exc from ..decorators import model_pk_lock + def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None): """ Given a model instance, and an optional pk and slug field, @@ -89,19 +90,21 @@ class CreateModelMixin: Create a model instance. """ def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.DATA, files=request.FILES) + validator = self.get_validator(data=request.DATA, files=request.FILES) - if serializer.is_valid(): - self.check_permissions(request, 'create', serializer.object) + if validator.is_valid(): + self.check_permissions(request, 'create', validator.object) - self.pre_save(serializer.object) - self.pre_conditions_on_save(serializer.object) - self.object = serializer.save(force_insert=True) + self.pre_save(validator.object) + self.pre_conditions_on_save(validator.object) + self.object = validator.save(force_insert=True) self.post_save(self.object, created=True) + instance = self.get_queryset().get(id=self.object.id) + serializer = self.get_serializer(instance) headers = self.get_success_headers(serializer.data) return response.Created(serializer.data, headers=headers) - return response.BadRequest(serializer.errors) + return response.BadRequest(validator.errors) def get_success_headers(self, data): try: @@ -171,28 +174,32 @@ class UpdateModelMixin: if self.object is None: raise Http404 - serializer = self.get_serializer(self.object, data=request.DATA, - files=request.FILES, partial=partial) + validator = self.get_validator(self.object, data=request.DATA, + files=request.FILES, partial=partial) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + if not validator.is_valid(): + return response.BadRequest(validator.errors) # Hooks try: - self.pre_save(serializer.object) - self.pre_conditions_on_save(serializer.object) + self.pre_save(validator.object) + self.pre_conditions_on_save(validator.object) except ValidationError as err: # full_clean on model instance may be called in pre_save, # so we have to handle eventual errors. return response.BadRequest(err.message_dict) if self.object is None: - self.object = serializer.save(force_insert=True) + self.object = validator.save(force_insert=True) self.post_save(self.object, created=True) + instance = self.get_queryset().get(id=self.object.id) + serializer = self.get_serializer(instance) return response.Created(serializer.data) - self.object = serializer.save(force_update=True) + self.object = validator.save(force_update=True) self.post_save(self.object, created=False) + instance = self.get_queryset().get(id=self.object.id) + serializer = self.get_serializer(instance) return response.Ok(serializer.data) def partial_update(self, request, *args, **kwargs): @@ -251,7 +258,7 @@ class BlockeableModelMixin: raise NotImplementedError("is_blocked must be overridden") def pre_conditions_blocked(self, obj): - #Raises permission exception + # Raises permission exception if obj is not None and self.is_blocked(obj): raise exc.Blocked(_("Blocked element")) diff --git a/taiga/base/api/serializers.py b/taiga/base/api/serializers.py index 601c1753..82565b26 100644 --- a/taiga/base/api/serializers.py +++ b/taiga/base/api/serializers.py @@ -1229,5 +1229,7 @@ class LightSerializer(serpy.Serializer): kwargs.pop("partial", None) kwargs.pop("files", None) context = kwargs.pop("context", {}) + view = kwargs.pop("view", {}) super().__init__(*args, **kwargs) self.context = context + self.view = view diff --git a/taiga/base/api/settings.py b/taiga/base/api/settings.py index 1a3d01ba..75d204c9 100644 --- a/taiga/base/api/settings.py +++ b/taiga/base/api/settings.py @@ -98,6 +98,8 @@ DEFAULTS = { # Genric view behavior "DEFAULT_MODEL_SERIALIZER_CLASS": "taiga.base.api.serializers.ModelSerializer", + "DEFAULT_MODEL_VALIDATOR_CLASS": + "taiga.base.api.validators.ModelValidator", "DEFAULT_FILTER_BACKENDS": (), # Throttling diff --git a/taiga/projects/likes/mixins/serializers.py b/taiga/base/api/validators.py similarity index 72% rename from taiga/projects/likes/mixins/serializers.py rename to taiga/base/api/validators.py index 84d63b4e..3a8d6922 100644 --- a/taiga/projects/likes/mixins/serializers.py +++ b/taiga/base/api/validators.py @@ -16,15 +16,12 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.base.api import serializers +from . import serializers -class FanResourceSerializerMixin(serializers.ModelSerializer): - is_fan = serializers.SerializerMethodField("get_is_fan") +class Validator(serializers.Serializer): + pass - def get_is_fan(self, obj): - if "request" in self.context: - user = self.context["request"].user - return user.is_authenticated() and user.is_fan(obj) - return False +class ModelValidator(serializers.ModelSerializer): + pass diff --git a/taiga/base/fields.py b/taiga/base/fields.py index 5e5c4b5a..f0cf4ee2 100644 --- a/taiga/base/fields.py +++ b/taiga/base/fields.py @@ -20,11 +20,13 @@ from django.forms import widgets from django.utils.translation import ugettext as _ from taiga.base.api import serializers +import serpy #################################################################### -## Serializer fields +# Serializer fields #################################################################### + class JsonField(serializers.WritableField): """ Json objects serializer. @@ -38,40 +40,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 +72,49 @@ class WatchersField(serializers.WritableField): def from_native(self, data): return data + + +class Field(serpy.Field): + pass + + +class MethodField(serpy.MethodField): + pass + + +class I18NField(serpy.Field): + def to_value(self, value): + ret = super(I18NField, self).to_value(value) + return _(ret) + + +class I18NJsonField(serpy.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 diff --git a/taiga/base/neighbors.py b/taiga/base/neighbors.py index a57d2eeb..c8733ade 100644 --- a/taiga/base/neighbors.py +++ b/taiga/base/neighbors.py @@ -23,6 +23,7 @@ from django.db import connection from django.core.exceptions import ObjectDoesNotExist from django.db.models.sql.datastructures import EmptyResultSet from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField Neighbor = namedtuple("Neighbor", "left right") @@ -71,7 +72,6 @@ def get_neighbors(obj, results_set=None): if row is None: return Neighbor(None, None) - obj_position = row[1] - 1 left_object_id = row[2] right_object_id = row[3] @@ -88,13 +88,19 @@ def get_neighbors(obj, results_set=None): return Neighbor(left, right) -class NeighborsSerializerMixin: - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["neighbors"] = serializers.SerializerMethodField("get_neighbors") +class NeighborSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + + +class NeighborsSerializerMixin(serializers.LightSerializer): + neighbors = MethodField() def serialize_neighbor(self, neighbor): - raise NotImplementedError + if neighbor: + return NeighborSerializer(neighbor).data + return None def get_neighbors(self, obj): view, request = self.context.get("view", None), self.context.get("request", None) diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index d8453ad5..eaff499d 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -34,6 +34,7 @@ from taiga.base import exceptions as exc from taiga.base import response from taiga.base.api.mixins import CreateModelMixin from taiga.base.api.viewsets import GenericViewSet +from taiga.projects import utils as project_utils from taiga.projects.models import Project, Membership from taiga.projects.issues.models import Issue from taiga.projects.tasks.models import Task @@ -366,5 +367,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi return response.BadRequest({"error": e.message, "details": e.errors}) else: # On Success - response_data = ProjectSerializer(project).data + project_from_qs = project_utils.attach_extra_info(Project.objects.all()).get(id=project.id) + response_data = ProjectSerializer(project_from_qs).data + return response.Created(response_data) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index c6fbbe0d..c441e419 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -23,9 +23,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 +42,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,21 +50,24 @@ 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() permission_classes = (permissions.ProjectPermission, ) filter_backends = (project_filters.UserOrderFilterBackend, @@ -132,12 +131,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix def get_serializer_class(self): if self.action == "list": - return serializers.LightProjectSerializer + return serializers.ProjectSerializer - if self.action in ["retrieve", "by_slug"]: - return serializers.LightProjectDetailSerializer - - return serializers.ProjectSerializer + return serializers.ProjectDetailSerializer @detail_route(methods=["POST"]) def change_logo(self, request, *args, **kwargs): @@ -200,11 +196,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) @@ -346,7 +342,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"]) @@ -455,6 +451,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',) @@ -471,6 +468,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',) @@ -487,6 +485,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",) @@ -503,6 +502,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",) @@ -518,6 +518,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",) @@ -533,6 +534,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",) @@ -548,6 +550,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",) @@ -566,6 +569,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): @@ -579,7 +583,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") @@ -604,6 +610,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, @@ -618,11 +630,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) diff --git a/taiga/projects/attachments/serializers.py b/taiga/projects/attachments/serializers.py index 6c5ee05b..45e6be45 100644 --- a/taiga/projects/attachments/serializers.py +++ b/taiga/projects/attachments/serializers.py @@ -19,14 +19,12 @@ from django.conf import settings from taiga.base.api import serializers +from taiga.base.fields import MethodField 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") @@ -43,12 +41,11 @@ class AttachmentSerializer(serializers.ModelSerializer): 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 +53,7 @@ class ListBasicAttachmentsInfoSerializerMixin(serpy.Serializer): - The method attach_basic_attachments has been used to include the necessary json data about the attachments in the "attachments_attr" column """ - attachments = serpy.MethodField() + attachments = MethodField() def get_attachments(self, obj): include_attachments = getattr(obj, "include_attachments", False) diff --git a/taiga/projects/filters.py b/taiga/projects/filters.py index cbb692b8..fe720f97 100644 --- a/taiga/projects/filters.py +++ b/taiga/projects/filters.py @@ -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", ] @@ -142,11 +142,11 @@ class UserOrderFilterBackend(FilterBackend): 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} - """ + 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}) diff --git a/taiga/projects/history/api.py b/taiga/projects/history/api.py index a4c8199e..2119239a 100644 --- a/taiga/projects/history/api.py +++ b/taiga/projects/history/api.py @@ -23,7 +23,6 @@ from django.utils import timezone from taiga.base import response from taiga.base.decorators import detail_route from taiga.base.api import ReadOnlyListViewSet -from taiga.base.api.utils import get_object_or_404 from taiga.mdrender.service import render as mdrender from . import permissions @@ -38,7 +37,7 @@ class HistoryViewSet(ReadOnlyListViewSet): def get_content_type(self): app_name, model = self.content_type.split(".", 1) - return get_object_or_404(ContentType, app_label=app_name, model=model) + return ContentType.objects.get_by_natural_key(app_name, model) def get_queryset(self): ct = self.get_content_type() diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 558c5c25..d5c023a9 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -33,7 +33,8 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts # This keys has been removed from freeze_impl so we can have objects where the # previous diff has value for the attribute and we want to prevent their propagation -IGNORE_DIFF_FIELDS = [ "watchers", "description_diff", "content_diff", "blocked_note_diff"] +IGNORE_DIFF_FIELDS = ["watchers", "description_diff", "content_diff", "blocked_note_diff"] + def _generate_uuid(): return str(uuid.uuid1()) @@ -92,15 +93,15 @@ class HistoryEntry(models.Model): @cached_property def is_change(self): - return self.type == HistoryType.change + return self.type == HistoryType.change @cached_property def is_create(self): - return self.type == HistoryType.create + return self.type == HistoryType.create @cached_property def is_delete(self): - return self.type == HistoryType.delete + return self.type == HistoryType.delete @property def owner(self): @@ -185,7 +186,7 @@ class HistoryEntry(models.Model): role_name = resolve_value("roles", role_id) oldpoint_id = pointsold.get(role_id, None) points[role_name] = [resolve_value("points", oldpoint_id), - resolve_value("points", point_id)] + resolve_value("points", point_id)] # Process that removes points entries with # duplicate value. @@ -204,8 +205,8 @@ class HistoryEntry(models.Model): "deleted": [], } - oldattachs = {x["id"]:x for x in self.diff["attachments"][0]} - newattachs = {x["id"]:x for x in self.diff["attachments"][1]} + oldattachs = {x["id"]: x for x in self.diff["attachments"][0]} + newattachs = {x["id"]: x for x in self.diff["attachments"][1]} for aid in set(tuple(oldattachs.keys()) + tuple(newattachs.keys())): if aid in oldattachs and aid in newattachs: @@ -235,8 +236,8 @@ class HistoryEntry(models.Model): "deleted": [], } - oldcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][0] or []} - newcustattrs = {x["id"]:x for x in self.diff["custom_attributes"][1] or []} + oldcustattrs = {x["id"]: x for x in self.diff["custom_attributes"][0] or []} + newcustattrs = {x["id"]: x for x in self.diff["custom_attributes"][1] or []} for aid in set(tuple(oldcustattrs.keys()) + tuple(newcustattrs.keys())): if aid in oldcustattrs and aid in newcustattrs: diff --git a/taiga/projects/history/serializers.py b/taiga/projects/history/serializers.py index f1a10481..8407810f 100644 --- a/taiga/projects/history/serializers.py +++ b/taiga/projects/history/serializers.py @@ -17,28 +17,31 @@ # along with this program. If not, see . from taiga.base.api import serializers -from taiga.base.fields import JsonField, I18NJsonField +from taiga.base.fields import I18NJsonField, Field, MethodField from taiga.users.services import get_photo_or_gravatar_url -from . import models + +HISTORY_ENTRY_I18N_FIELDS = ("points", "status", "severity", "priority", "type") -HISTORY_ENTRY_I18N_FIELDS=("points", "status", "severity", "priority", "type") - - -class HistoryEntrySerializer(serializers.ModelSerializer): - diff = JsonField() - snapshot = JsonField() - values = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) - values_diff = I18NJsonField(i18n_fields=HISTORY_ENTRY_I18N_FIELDS) - user = serializers.SerializerMethodField("get_user") - delete_comment_user = JsonField() - comment_versions = JsonField() - - class Meta: - model = models.HistoryEntry - exclude = ("comment_versions",) +class HistoryEntrySerializer(serializers.LightSerializer): + id = Field() + user = MethodField() + created_at = Field() + type = Field() + key = Field() + diff = Field() + snapshot = Field() + values = Field() + values_diff = I18NJsonField() + comment = I18NJsonField() + comment_html = Field() + delete_comment_date = Field() + delete_comment_user = Field() + edit_comment_date = Field() + is_hidden = Field() + is_snapshot = Field() def get_user(self, entry): user = {"pk": None, "username": None, "name": None, "photo": None, "is_active": False} diff --git a/taiga/projects/history/services.py b/taiga/projects/history/services.py index 71b5bcf8..764cca39 100644 --- a/taiga/projects/history/services.py +++ b/taiga/projects/history/services.py @@ -34,12 +34,9 @@ from collections import namedtuple from copy import deepcopy from functools import partial from functools import wraps -from functools import lru_cache from django.conf import settings from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType -from django.core.paginator import Paginator, InvalidPage from django.apps import apps from django.db import transaction as tx from django_pglocks import advisory_lock @@ -50,6 +47,21 @@ from taiga.base.utils.diff import make_diff as make_diff_from_dicts from .models import HistoryType +# Freeze implementatitions +from .freeze_impl import project_freezer +from .freeze_impl import milestone_freezer +from .freeze_impl import userstory_freezer +from .freeze_impl import issue_freezer +from .freeze_impl import task_freezer +from .freeze_impl import wikipage_freezer + + +from .freeze_impl import project_values +from .freeze_impl import milestone_values +from .freeze_impl import userstory_values +from .freeze_impl import issue_values +from .freeze_impl import task_values +from .freeze_impl import wikipage_values # Type that represents a freezed object FrozenObj = namedtuple("FrozenObj", ["key", "snapshot"]) @@ -71,7 +83,7 @@ _not_important_fields = { log = logging.getLogger("taiga.history") -def make_key_from_model_object(obj:object) -> str: +def make_key_from_model_object(obj: object) -> str: """ Create unique key from model instance. """ @@ -79,7 +91,7 @@ def make_key_from_model_object(obj:object) -> str: return "{0}:{1}".format(tn, obj.pk) -def get_model_from_key(key:str) -> object: +def get_model_from_key(key: str) -> object: """ Get model from key """ @@ -87,7 +99,7 @@ def get_model_from_key(key:str) -> object: return apps.get_model(class_name) -def get_pk_from_key(key:str) -> object: +def get_pk_from_key(key: str) -> object: """ Get pk from key """ @@ -95,7 +107,7 @@ def get_pk_from_key(key:str) -> object: return pk -def get_instance_from_key(key:str) -> object: +def get_instance_from_key(key: str) -> object: """ Get instance from key """ @@ -109,7 +121,7 @@ def get_instance_from_key(key:str) -> object: return None -def register_values_implementation(typename:str, fn=None): +def register_values_implementation(typename: str, fn=None): """ Register values implementation for specified typename. This function can be used as decorator. @@ -128,7 +140,7 @@ def register_values_implementation(typename:str, fn=None): return _wrapper -def register_freeze_implementation(typename:str, fn=None): +def register_freeze_implementation(typename: str, fn=None): """ Register freeze implementation for specified typename. This function can be used as decorator. @@ -149,7 +161,7 @@ def register_freeze_implementation(typename:str, fn=None): # Low level api -def freeze_model_instance(obj:object) -> FrozenObj: +def freeze_model_instance(obj: object) -> FrozenObj: """ Creates a new frozen object from model instance. @@ -179,7 +191,7 @@ def freeze_model_instance(obj:object) -> FrozenObj: return FrozenObj(key, snapshot) -def is_hidden_snapshot(obj:FrozenDiff) -> bool: +def is_hidden_snapshot(obj: FrozenDiff) -> bool: """ Check if frozen object is considered hidden or not. @@ -199,7 +211,7 @@ def is_hidden_snapshot(obj:FrozenDiff) -> bool: return False -def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff: +def make_diff(oldobj: FrozenObj, newobj: FrozenObj) -> FrozenDiff: """ Compute a diff between two frozen objects. """ @@ -217,7 +229,7 @@ def make_diff(oldobj:FrozenObj, newobj:FrozenObj) -> FrozenDiff: return FrozenDiff(newobj.key, diff, newobj.snapshot) -def make_diff_values(typename:str, fdiff:FrozenDiff) -> dict: +def make_diff_values(typename: str, fdiff: FrozenDiff) -> dict: """ Given a typename and diff, build a values dict for it. If no implementation found for typename, warnig is raised in @@ -242,7 +254,7 @@ def _rebuild_snapshot_from_diffs(keysnapshot, partials): return result -def get_last_snapshot_for_key(key:str) -> FrozenObj: +def get_last_snapshot_for_key(key: str) -> FrozenObj: entry_model = apps.get_model("history", "HistoryEntry") # Search last snapshot @@ -271,17 +283,16 @@ def get_last_snapshot_for_key(key:str) -> FrozenObj: # Public api -def get_modified_fields(obj:object, last_modifications): +def get_modified_fields(obj: object, last_modifications): """ Get the modified fields for an object through his last modifications """ key = make_key_from_model_object(obj) entry_model = apps.get_model("history", "HistoryEntry") history_entries = (entry_model.objects - .filter(key=key) - .order_by("-created_at") - .values_list("diff", flat=True) - [0:last_modifications]) + .filter(key=key) + .order_by("-created_at") + .values_list("diff", flat=True)[0:last_modifications]) modified_fields = [] for history_entry in history_entries: @@ -291,7 +302,7 @@ def get_modified_fields(obj:object, last_modifications): @tx.atomic -def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): +def take_snapshot(obj: object, *, comment: str="", user=None, delete: bool=False): """ Given any model instance with registred content type, create new history entry of "change" type. @@ -301,7 +312,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): """ key = make_key_from_model_object(obj) - with advisory_lock(key) as acquired_key_lock: + with advisory_lock(key): typename = get_typename_for_model_class(obj.__class__) new_fobj = freeze_model_instance(obj) @@ -327,8 +338,8 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): # If diff and comment are empty, do # not create empty history entry if (not fdiff.diff and not comment - and old_fobj is not None - and entry_type != HistoryType.delete): + and old_fobj is not None + and entry_type != HistoryType.delete): return None @@ -358,7 +369,7 @@ def take_snapshot(obj:object, *, comment:str="", user=None, delete:bool=False): # High level query api -def get_history_queryset_by_model_instance(obj:object, types=(HistoryType.change,), +def get_history_queryset_by_model_instance(obj: object, types=(HistoryType.change,), include_hidden=False): """ Get one page of history for specified object. @@ -377,20 +388,12 @@ def prefetch_owners_in_history_queryset(qs): user_ids = [u["pk"] for u in qs.values_list("user", flat=True)] users = get_user_model().objects.filter(id__in=user_ids) users_by_id = {u.id: u for u in users} - for history_entry in qs: + for history_entry in qs: history_entry.prefetch_owner(users_by_id.get(history_entry.user["pk"], None)) return qs -# Freeze implementatitions -from .freeze_impl import project_freezer -from .freeze_impl import milestone_freezer -from .freeze_impl import userstory_freezer -from .freeze_impl import issue_freezer -from .freeze_impl import task_freezer -from .freeze_impl import wikipage_freezer - register_freeze_implementation("projects.project", project_freezer) register_freeze_implementation("milestones.milestone", milestone_freezer,) register_freeze_implementation("userstories.userstory", userstory_freezer) @@ -398,13 +401,6 @@ register_freeze_implementation("issues.issue", issue_freezer) register_freeze_implementation("tasks.task", task_freezer) register_freeze_implementation("wiki.wikipage", wikipage_freezer) -from .freeze_impl import project_values -from .freeze_impl import milestone_values -from .freeze_impl import userstory_values -from .freeze_impl import issue_values -from .freeze_impl import task_values -from .freeze_impl import wikipage_values - register_values_implementation("projects.project", project_values) register_values_implementation("milestones.milestone", milestone_values) register_values_implementation("userstories.userstory", userstory_values) diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 8da13476..f204fc13 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -34,14 +34,18 @@ from taiga.projects.occ import OCCResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin +from .utils import attach_extra_info + from . import models from . import services from . import permissions from . import serializers +from . import validators class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): + validator_class = validators.IssueValidator queryset = models.Issue.objects.all() permission_classes = (permissions.IssuePermission, ) filter_backends = (filters.CanViewIssuesFilterBackend, @@ -145,8 +149,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W def get_queryset(self): qs = super().get_queryset() 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: @@ -181,8 +185,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W def by_ref(self, request): 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) + return self.retrieve(request, project_id=project_id, ref=ref) @list_route(methods=["GET"]) def filters_data(self, request, *args, **kwargs): @@ -224,9 +227,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: @@ -237,11 +240,13 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W status=project.default_issue_status, severity=project.default_severity, priority=project.default_priority, type=project.default_issue_type, callback=self.post_save, precall=self.pre_save) + + issues = self.get_queryset().filter(id__in=[i.id for i in issues]) issues_serialized = self.get_serializer_class()(issues, many=True) return response.Ok(data=issues_serialized.data) - return response.BadRequest(serializer.errors) + return response.BadRequest(validator.errors) class IssueVotersViewSet(VotersViewSetMixin, ModelListViewSet): diff --git a/taiga/projects/issues/serializers.py b/taiga/projects/issues/serializers.py index 099171a1..2b773b81 100644 --- a/taiga/projects/issues/serializers.py +++ b/taiga/projects/issues/serializers.py @@ -17,56 +17,52 @@ # along with this program. If not, see . from taiga.base.api import serializers -from taiga.base.fields import PgArrayField +from taiga.base.fields import Field, MethodField from taiga.base.neighbors import NeighborsSerializerMixin from taiga.mdrender.service import render as mdrender -from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer -from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.tagging.fields import TagsAndTagsColorsField -from taiga.projects.serializers import BasicIssueStatusSerializer -from taiga.projects.validators import ProjectExistsValidator +from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin -from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin - -from taiga.users.serializers import UserBasicInfoSerializer - -from . import models - -import serpy -class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - tags = TagsAndTagsColorsField(default=[], required=False) - external_reference = PgArrayField(required=False) - is_closed = serializers.Field(source="is_closed") - comment = serializers.SerializerMethodField("get_comment") - generated_user_stories = serializers.SerializerMethodField("get_generated_user_stories") - blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") - description_html = serializers.SerializerMethodField("get_description_html") - status_extra_info = BasicIssueStatusSerializer(source="status", required=False, read_only=True) - assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) - owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) +class IssueListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, + OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, + StatusExtraInfoSerializerMixin, serializers.LightSerializer): + id = Field() + ref = Field() + severity = Field(attr="severity_id") + priority = Field(attr="priority_id") + type = Field(attr="type_id") + milestone = Field(attr="milestone_id") + project = Field(attr="project_id") + created_date = Field() + modified_date = Field() + finished_date = Field() + subject = Field() + external_reference = Field() + version = Field() + watchers = Field() + tags = Field() + is_closed = Field() - class Meta: - model = models.Issue - read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') + +class IssueSerializer(IssueListSerializer): + comment = MethodField() + generated_user_stories = MethodField() + blocked_note_html = MethodField() + description = Field() + description_html = MethodField() def get_comment(self, obj): # NOTE: This method and field is necessary to historical comments work return "" def get_generated_user_stories(self, obj): - return [{ - "id": us.id, - "ref": us.ref, - "subject": us.subject, - } for us in obj.generated_user_stories.all()] + assert hasattr(obj, "generated_user_stories_attr"), "instance must have a generated_user_stories_attr attribute" + return obj.generated_user_stories_attr def get_blocked_note_html(self, obj): return mdrender(obj.project, obj.blocked_note) @@ -75,39 +71,5 @@ class IssueSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWa return mdrender(obj.project, obj.description) -class IssueListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, - ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin, - ListStatusExtraInfoSerializerMixin, serializers.LightSerializer): - id = serpy.Field() - ref = serpy.Field() - severity = serpy.Field(attr="severity_id") - priority = serpy.Field(attr="priority_id") - type = serpy.Field(attr="type_id") - milestone = serpy.Field(attr="milestone_id") - project = serpy.Field(attr="project_id") - created_date = serpy.Field() - modified_date = serpy.Field() - finished_date = serpy.Field() - subject = serpy.Field() - external_reference = serpy.Field() - version = serpy.Field() - watchers = serpy.Field() - - class IssueNeighborsSerializer(NeighborsSerializerMixin, IssueSerializer): - def serialize_neighbor(self, neighbor): - if neighbor: - return NeighborIssueSerializer(neighbor).data - return None - - -class NeighborIssueSerializer(serializers.ModelSerializer): - class Meta: - model = models.Issue - fields = ("id", "ref", "subject") - depth = 0 - - -class IssuesBulkSerializer(ProjectExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - bulk_issues = serializers.CharField() + pass diff --git a/taiga/projects/issues/utils.py b/taiga/projects/issues/utils.py new file mode 100644 index 00000000..2053d923 --- /dev/null +++ b/taiga/projects/issues/utils.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset + + +def attach_generated_user_stories(queryset, as_field="generated_user_stories_attr"): + """Attach generated user stories json column to each object of the queryset. + + :param queryset: A Django issues queryset object. + :param as_field: Attach the generated user stories as an attribute with this name. + + :return: Queryset object with the additional `as_field` field. + """ + model = queryset.model + sql = """SELECT json_agg(row_to_json(t)) + FROM( + SELECT + userstories_userstory.id, + userstories_userstory.ref, + userstories_userstory.subject + FROM userstories_userstory + WHERE generated_from_issue_id = {tbl}.id) t""" + + sql = sql.format(tbl=model._meta.db_table) + queryset = queryset.extra(select={as_field: sql}) + return queryset + + +def attach_extra_info(queryset, user=None): + queryset = attach_generated_user_stories(queryset) + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_voter_to_queryset(queryset, user) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/issues/validators.py b/taiga/projects/issues/validators.py new file mode 100644 index 00000000..4c900c15 --- /dev/null +++ b/taiga/projects/issues/validators.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.base.fields import PgArrayField +from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer +from taiga.projects.notifications.validators import WatchersValidator +from taiga.projects.tagging.fields import TagsAndTagsColorsField +from taiga.projects.validators import ProjectExistsValidator + +from . import models + + +class IssueValidator(WatchersValidator, EditableWatchedResourceSerializer, + validators.ModelValidator): + + tags = TagsAndTagsColorsField(default=[], required=False) + external_reference = PgArrayField(required=False) + + class Meta: + model = models.Issue + read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') + + +class IssuesBulkValidator(ProjectExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + bulk_issues = serializers.CharField() diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index 1520f2c7..2e0047fc 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -17,7 +17,6 @@ # along with this program. If not, see . from django.apps import apps -from django.db.models import Prefetch from taiga.base import filters from taiga.base import response @@ -31,13 +30,9 @@ from taiga.base.utils.db import get_object_or_none from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.history.mixins import HistoryResourceMixin -from taiga.projects.votes.utils import attach_total_voters_to_queryset -from taiga.projects.votes.utils import attach_is_voter_to_queryset -from taiga.projects.notifications.utils import attach_watchers_to_queryset -from taiga.projects.notifications.utils import attach_is_watcher_to_queryset -from taiga.projects.userstories import utils as userstories_utils from . import serializers +from . import validators from . import models from . import permissions from . import utils as milestones_utils @@ -47,6 +42,8 @@ import datetime class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): + serializer_class = serializers.MilestoneSerializer + validator_class = validators.MilestoneValidator permission_classes = (permissions.MilestonePermission,) filter_backends = (filters.CanViewMilestonesFilterBackend,) filter_fields = ( @@ -56,12 +53,6 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ) queryset = models.Milestone.objects.all() - def get_serializer_class(self, *args, **kwargs): - if self.action == "list": - return serializers.MilestoneListSerializer - - return serializers.MilestoneSerializer - def list(self, request, *args, **kwargs): res = super().list(request, *args, **kwargs) self._add_taiga_info_headers() @@ -84,33 +75,8 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, def get_queryset(self): qs = super().get_queryset() - - # Userstories prefetching - UserStory = apps.get_model("userstories", "UserStory") - - us_qs = UserStory.objects.select_related("milestone", - "project", - "status", - "owner", - "assigned_to", - "generated_from_issue") - - us_qs = userstories_utils.attach_total_points(us_qs) - us_qs = userstories_utils.attach_role_points(us_qs) - us_qs = attach_total_voters_to_queryset(us_qs) - us_qs = self.attach_watchers_attrs_to_queryset(us_qs) - - if self.request.user.is_authenticated(): - us_qs = attach_is_voter_to_queryset(self.request.user, us_qs) - us_qs = attach_is_watcher_to_queryset(us_qs, self.request.user) - - qs = qs.prefetch_related(Prefetch("user_stories", queryset=us_qs)) - - # Milestones prefetching qs = qs.select_related("project", "owner") - qs = self.attach_watchers_attrs_to_queryset(qs) - qs = milestones_utils.attach_total_points(qs) - qs = milestones_utils.attach_closed_points(qs) + qs = milestones_utils.attach_extra_info(qs, user=self.request.user) qs = qs.order_by("-estimated_start") return qs diff --git a/taiga/projects/milestones/serializers.py b/taiga/projects/milestones/serializers.py index 724126fd..44b3e8f4 100644 --- a/taiga/projects/milestones/serializers.py +++ b/taiga/projects/milestones/serializers.py @@ -16,58 +16,29 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext as _ - from taiga.base.api import serializers -from taiga.base.utils import json -from taiga.projects.notifications.mixins import WatchedResourceModelSerializer -from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin +from taiga.base.fields import Field, MethodField +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.userstories.serializers import UserStoryListSerializer -from . import models -import serpy - - -class MilestoneSerializer(WatchersValidator, WatchedResourceModelSerializer, - ValidateDuplicatedNameInProjectMixin): - total_points = serializers.SerializerMethodField("get_total_points") - closed_points = serializers.SerializerMethodField("get_closed_points") - user_stories = serializers.SerializerMethodField("get_user_stories") - - class Meta: - model = models.Milestone - read_only_fields = ("id", "created_date", "modified_date") - - def get_total_points(self, obj): - return sum(obj.total_points.values()) - - def get_closed_points(self, obj): - return sum(obj.closed_points.values()) - - def get_user_stories(self, obj): - return UserStoryListSerializer(obj.user_stories.all(), many=True).data - - -class MilestoneListSerializer(ListWatchedResourceModelSerializer, serializers.LightSerializer): - id = serpy.Field() - name = serpy.Field() - slug = serpy.Field() - owner = serpy.Field(attr="owner_id") - project = serpy.Field(attr="project_id") - estimated_start = serpy.Field() - estimated_finish = serpy.Field() - created_date = serpy.Field() - modified_date = serpy.Field() - closed = serpy.Field() - disponibility = serpy.Field() - order = serpy.Field() - watchers = serpy.Field() - user_stories = serpy.MethodField("get_user_stories") - total_points = serpy.MethodField() - closed_points = serpy.MethodField() +class MilestoneSerializer(WatchedResourceSerializer, serializers.LightSerializer): + id = Field() + name = Field() + slug = Field() + owner = Field(attr="owner_id") + project = Field(attr="project_id") + estimated_start = Field() + estimated_finish = Field() + created_date = Field() + modified_date = Field() + closed = Field() + disponibility = Field() + order = Field() + watchers = Field() + user_stories = MethodField() + total_points = MethodField() + closed_points = MethodField() def get_user_stories(self, obj): return UserStoryListSerializer(obj.user_stories.all(), many=True).data diff --git a/taiga/projects/milestones/utils.py b/taiga/projects/milestones/utils.py index a32d7684..b292b1bd 100644 --- a/taiga/projects/milestones/utils.py +++ b/taiga/projects/milestones/utils.py @@ -17,6 +17,16 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.apps import apps +from django.db.models import Prefetch + +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.userstories import utils as userstories_utils +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset + def attach_total_points(queryset, as_field="total_points_attr"): """Attach total of point values to each object of the queryset. @@ -28,7 +38,7 @@ def attach_total_points(queryset, as_field="total_points_attr"): """ model = queryset.model sql = """SELECT SUM(projects_points.value) - FROM userstories_rolepoints + FROM userstories_rolepoints INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id WHERE userstories_userstory.milestone_id = {tbl}.id""" @@ -48,7 +58,7 @@ def attach_closed_points(queryset, as_field="closed_points_attr"): """ model = queryset.model sql = """SELECT SUM(projects_points.value) - FROM userstories_rolepoints + FROM userstories_rolepoints INNER JOIN userstories_userstory ON userstories_userstory.id = userstories_rolepoints.user_story_id INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id WHERE userstories_userstory.milestone_id = {tbl}.id AND userstories_userstory.is_closed=True""" @@ -56,3 +66,33 @@ def attach_closed_points(queryset, as_field="closed_points_attr"): sql = sql.format(tbl=model._meta.db_table) queryset = queryset.extra(select={as_field: sql}) return queryset + + +def attach_extra_info(queryset, user=None): + # Userstories prefetching + UserStory = apps.get_model("userstories", "UserStory") + us_queryset = UserStory.objects.select_related("milestone", + "project", + "status", + "owner", + "assigned_to", + "generated_from_issue") + + us_queryset = userstories_utils.attach_total_points(us_queryset) + us_queryset = userstories_utils.attach_role_points(us_queryset) + us_queryset = attach_total_voters_to_queryset(us_queryset) + us_queryset = attach_watchers_to_queryset(us_queryset) + us_queryset = attach_total_watchers_to_queryset(us_queryset) + us_queryset = attach_is_voter_to_queryset(us_queryset, user) + us_queryset = attach_is_watcher_to_queryset(us_queryset, user) + + queryset = queryset.prefetch_related(Prefetch("user_stories", queryset=us_queryset)) + queryset = attach_total_points(queryset) + queryset = attach_closed_points(queryset) + + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_voter_to_queryset(queryset, user) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/milestones/validators.py b/taiga/projects/milestones/validators.py index 3648a672..8de3174c 100644 --- a/taiga/projects/milestones/validators.py +++ b/taiga/projects/milestones/validators.py @@ -19,14 +19,23 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers +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") + msg = _("There's no milestone with that id") raise serializers.ValidationError(msg) return attrs + + +class MilestoneValidator(WatchersValidator, DuplicatedNameInProjectValidator, validators.ModelValidator): + class Meta: + model = models.Milestone + read_only_fields = ("id", "created_date", "modified_date") diff --git a/taiga/projects/mixins/serializers.py b/taiga/projects/mixins/serializers.py index a47d9bed..945c1119 100644 --- a/taiga/projects/mixins/serializers.py +++ b/taiga/projects/mixins/serializers.py @@ -17,34 +17,13 @@ # along with this program. If not, see . from taiga.base.api import serializers -from taiga.users.serializers import ListUserBasicInfoSerializer +from taiga.base.fields import Field, MethodField +from taiga.users.serializers import UserBasicInfoSerializer from django.utils.translation import ugettext as _ -import serpy -class ValidateDuplicatedNameInProjectMixin(serializers.ModelSerializer): - - def validate_name(self, attrs, source): - """ - Check the points name is not duplicated in the project on creation - """ - model = self.opts.model - qs = None - # If the object exists: - if self.object and attrs.get(source, None): - qs = model.objects.filter(project=self.object.project, name=attrs[source]).exclude(id=self.object.id) - - if not self.object and attrs.get("project", None) and attrs.get(source, None): - qs = model.objects.filter(project=attrs["project"], name=attrs[source]) - - if qs and qs.exists(): - raise serializers.ValidationError(_("Name duplicated for the project")) - - return attrs - - -class ListCachedUsersSerializerMixin(serpy.Serializer): +class CachedUsersSerializerMixin(serializers.LightSerializer): def to_value(self, instance): self._serialized_users = {} return super().to_value(instance) @@ -55,37 +34,40 @@ class ListCachedUsersSerializerMixin(serpy.Serializer): serialized_user = self._serialized_users.get(user.id, None) if serialized_user is None: - serializer_user = ListUserBasicInfoSerializer(user).data - self._serialized_users[user.id] = serializer_user + serialized_user = UserBasicInfoSerializer(user).data + self._serialized_users[user.id] = serialized_user return serialized_user -class ListOwnerExtraInfoSerializerMixin(ListCachedUsersSerializerMixin): - owner = serpy.Field(attr="owner_id") - owner_extra_info = serpy.MethodField() +class OwnerExtraInfoSerializerMixin(CachedUsersSerializerMixin): + owner = Field(attr="owner_id") + owner_extra_info = MethodField() def get_owner_extra_info(self, obj): return self.get_user_extra_info(obj.owner) -class ListAssignedToExtraInfoSerializerMixin(ListCachedUsersSerializerMixin): - assigned_to = serpy.Field(attr="assigned_to_id") - assigned_to_extra_info = serpy.MethodField() +class AssignedToExtraInfoSerializerMixin(CachedUsersSerializerMixin): + assigned_to = Field(attr="assigned_to_id") + assigned_to_extra_info = MethodField() def get_assigned_to_extra_info(self, obj): return self.get_user_extra_info(obj.assigned_to) -class ListStatusExtraInfoSerializerMixin(serpy.Serializer): - status = serpy.Field(attr="status_id") - status_extra_info = serpy.MethodField() +class StatusExtraInfoSerializerMixin(serializers.LightSerializer): + status = Field(attr="status_id") + status_extra_info = MethodField() def to_value(self, instance): self._serialized_status = {} return super().to_value(instance) def get_status_extra_info(self, obj): + if obj.status_id is None: + return None + serialized_status = self._serialized_status.get(obj.status_id, None) if serialized_status is None: serialized_status = { diff --git a/taiga/projects/notifications/mixins.py b/taiga/projects/notifications/mixins.py index 2cad1e97..e9dff950 100644 --- a/taiga/projects/notifications/mixins.py +++ b/taiga/projects/notifications/mixins.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import serpy - from functools import partial from operator import is_not @@ -28,16 +26,12 @@ from taiga.base import response from taiga.base.decorators import detail_route from taiga.base.api import serializers from taiga.base.api.utils import get_object_or_404 -from taiga.base.fields import WatchersField +from taiga.base.fields import WatchersField, MethodField from taiga.projects.notifications import services -from taiga.projects.notifications.utils import (attach_watchers_to_queryset, - attach_is_watcher_to_queryset, - attach_total_watchers_to_queryset) from . serializers import WatcherSerializer - class WatchedResourceMixin: """ Rest Framework resource mixin for resources susceptible @@ -54,14 +48,6 @@ class WatchedResourceMixin: _not_notify = False - def attach_watchers_attrs_to_queryset(self, queryset): - queryset = attach_watchers_to_queryset(queryset) - queryset = attach_total_watchers_to_queryset(queryset) - if self.request.user.is_authenticated(): - queryset = attach_is_watcher_to_queryset(queryset, self.request.user) - - return queryset - @detail_route(methods=["POST"]) def watch(self, request, pk=None): obj = self.get_object() @@ -186,7 +172,10 @@ 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: @@ -200,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 = serpy.MethodField("get_is_watcher") - total_watchers = serpy.MethodField("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 @@ -230,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: @@ -244,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()] @@ -254,10 +232,10 @@ class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): if user and user.is_authenticated(): obj.is_watcher = user.id in obj.watchers - return super(WatchedResourceModelSerializer, self).to_native(obj) + return super(WatchedResourceSerializer, self).to_native(obj) def save(self, **kwargs): - obj = super(EditableWatchedResourceModelSerializer, self).save(**kwargs) + obj = super(EditableWatchedResourceSerializer, self).save(**kwargs) self.fields["watchers"] = WatchersField(required=False) obj.watchers = [user.id for user in obj.get_watchers()] return obj diff --git a/taiga/projects/notifications/utils.py b/taiga/projects/notifications/utils.py index 00b98d63..ae6bd34c 100644 --- a/taiga/projects/notifications/utils.py +++ b/taiga/projects/notifications/utils.py @@ -53,15 +53,18 @@ def attach_is_watcher_to_queryset(queryset, user, as_field="is_watcher"): """ model = queryset.model type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) - sql = ("""SELECT CASE WHEN (SELECT count(*) - FROM notifications_watched - WHERE notifications_watched.content_type_id = {type_id} - AND notifications_watched.object_id = {tbl}.id - AND notifications_watched.user_id = {user_id}) > 0 - THEN TRUE - ELSE FALSE - END""") - sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) + if user is None or user.is_anonymous(): + sql = """SELECT false""" + else: + sql = ("""SELECT CASE WHEN (SELECT count(*) + FROM notifications_watched + WHERE notifications_watched.content_type_id = {type_id} + AND notifications_watched.object_id = {tbl}.id + AND notifications_watched.user_id = {user_id}) > 0 + THEN TRUE + ELSE FALSE + END""") + sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) qs = queryset.extra(select={as_field: sql}) return qs diff --git a/taiga/projects/serializers.py b/taiga/projects/serializers.py index c10a4810..96b8d0ba 100644 --- a/taiga/projects/serializers.py +++ b/taiga/projects/serializers.py @@ -16,135 +16,121 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import serpy - from django.utils.translation import ugettext as _ -from django.db.models import Q from taiga.base.api import serializers -from taiga.base.fields import JsonField -from taiga.base.fields import PgArrayField +from taiga.base.fields import Field, MethodField, I18NField from taiga.permissions import services as permissions_services from taiga.users.services import get_photo_or_gravatar_url from taiga.users.serializers import UserBasicInfoSerializer -from taiga.users.serializers import ProjectRoleSerializer -from taiga.users.serializers import ListUserBasicInfoSerializer -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 - -import serpy - -###################################################### -## Custom values for selectors -###################################################### - -class PointsSerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.Points - i18n_fields = ("name",) - - -class UserStoryStatusSerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.UserStoryStatus - i18n_fields = ("name",) - - -class BasicUserStoryStatusSerializer(serializers.ModelSerializer): - class Meta: - model = models.UserStoryStatus - i18n_fields = ("name",) - fields = ("name", "color") - - -class TaskStatusSerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.TaskStatus - i18n_fields = ("name",) - - -class BasicTaskStatusSerializerSerializer(serializers.ModelSerializer): - class Meta: - model = models.TaskStatus - i18n_fields = ("name",) - fields = ("name", "color") - - -class SeveritySerializer(ValidateDuplicatedNameInProjectMixin): - class Meta: - model = models.Severity - i18n_fields = ("name",) - - -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",) ###################################################### -## Members +# Custom values for selectors ###################################################### -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 PointsSerializer(serializers.LightSerializer): + name = I18NField() + order = Field() + value = Field() + project = Field(attr="project_id") - 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_photo(self, project): - return get_photo_or_gravatar_url(project.user) +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 TaskStatusSerializer(serializers.LightSerializer): + name = I18NField() + slug = Field() + order = Field() + is_closed = Field() + color = Field() + project = Field(attr="project_id") + + +class SeveritySerializer(serializers.LightSerializer): + name = I18NField() + order = Field() + color = Field() + project = Field(attr="project_id") + + +class PrioritySerializer(serializers.LightSerializer): + name = I18NField() + order = Field() + color = Field() + project = Field(attr="project_id") + + +class IssueStatusSerializer(serializers.LightSerializer): + name = I18NField() + slug = Field() + order = Field() + is_closed = Field() + color = Field() + project = Field(attr="project_id") + + +class IssueTypeSerializer(serializers.LightSerializer): + name = I18NField() + order = Field() + color = Field() + project = Field(attr="project_id") + + +###################################################### +# Members +###################################################### + +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() + + def get_role_name(self, obj): + return obj.role.name if obj.role else None + + 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 "" @@ -156,230 +142,84 @@ 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() - def get_my_permissions(self, obj): - if "request" in self.context: - return get_user_project_permissions(self.context["request"].user, obj) - return [] + is_watcher = MethodField() + total_watchers = MethodField() - def get_i_am_owner(self, obj): - if "request" in self.context: - return is_project_owner(self.context["request"].user, obj) - return False + logo_small_url = MethodField() + logo_big_url = MethodField() - def get_i_am_admin(self, obj): - if "request" in self.context: - 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): - 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_total_watchers(self, obj): - return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count() - - def get_logo_small_url(self, obj): - return services.get_logo_small_thumbnail_url(obj) - - def get_logo_big_url(self, obj): - return services.get_logo_big_thumbnail_url(obj) - - -class LightProjectSerializer(serializers.LightSerializer): - id = serpy.Field() - name = serpy.Field() - slug = serpy.Field() - description = serpy.Field() - created_date = serpy.Field() - modified_date = serpy.Field() - owner = serpy.MethodField() - members = serpy.MethodField() - total_milestones = serpy.Field() - total_story_points = serpy.Field() - is_backlog_activated = serpy.Field() - is_kanban_activated = serpy.Field() - is_wiki_activated = serpy.Field() - is_issues_activated = serpy.Field() - videoconferences = serpy.Field() - videoconferences_extra_data = serpy.Field() - creation_template = serpy.Field(attr="creation_template_id") - is_private = serpy.Field() - anon_permissions = serpy.Field() - public_permissions = serpy.Field() - is_featured = serpy.Field() - is_looking_for_people = serpy.Field() - looking_for_people_note = serpy.Field() - blocked_code = serpy.Field() - totals_updated_datetime = serpy.Field() - total_fans = serpy.Field() - total_fans_last_week = serpy.Field() - total_fans_last_month = serpy.Field() - total_fans_last_year = serpy.Field() - total_activity = serpy.Field() - total_activity_last_week = serpy.Field() - total_activity_last_month = serpy.Field() - total_activity_last_year = serpy.Field() - - tags = serpy.Field() - tags_colors = serpy.MethodField() - - default_points = serpy.Field(attr="default_points_id") - default_us_status = serpy.Field(attr="default_us_status_id") - default_task_status = serpy.Field(attr="default_task_status_id") - default_priority = serpy.Field(attr="default_priority_id") - default_severity = serpy.Field(attr="default_severity_id") - default_issue_status = serpy.Field(attr="default_issue_status_id") - default_issue_type = serpy.Field(attr="default_issue_type_id") - - my_permissions = serpy.MethodField() - - i_am_owner = serpy.MethodField() - i_am_admin = serpy.MethodField() - i_am_member = serpy.MethodField() - - notify_level = serpy.MethodField("get_notify_level") - total_closed_milestones = serpy.MethodField() - - is_watcher = serpy.MethodField() - total_watchers = serpy.MethodField() - - logo_small_url = serpy.MethodField() - logo_big_url = serpy.MethodField() - - is_fan = serpy.Field(attr="is_fan_attr") + is_fan = Field(attr="is_fan_attr") def get_members(self, obj): assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute" @@ -395,7 +235,8 @@ class LightProjectSerializer(serializers.LightSerializer): if "request" in self.context: user = self.context["request"].user - if not user.is_anonymous() and user.id in [m.get("id") for m in obj.members_attr if m["id"] is not None]: + 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 @@ -407,17 +248,17 @@ class LightProjectSerializer(serializers.LightSerializer): if "request" in self.context: 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) + 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 ListUserBasicInfoSerializer(obj.owner).data + return UserBasicInfoSerializer(obj.owner).data def get_i_am_owner(self, obj): if "request" in self.context: @@ -436,7 +277,7 @@ class LightProjectSerializer(serializers.LightSerializer): 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 != None and np != NotifyLevel.none + return np is not None and np != NotifyLevel.none def get_total_watchers(self, obj): assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute" @@ -466,36 +307,36 @@ class LightProjectSerializer(serializers.LightSerializer): return services.get_logo_big_thumbnail_url(obj) -class LightProjectDetailSerializer(LightProjectSerializer): - us_statuses = serpy.Field(attr="userstory_statuses_attr") - points = serpy.Field(attr="points_attr") - task_statuses = serpy.Field(attr="task_statuses_attr") - issue_statuses = serpy.Field(attr="issue_statuses_attr") - issue_types = serpy.Field(attr="issue_types_attr") - priorities = serpy.Field(attr="priorities_attr") - severities = serpy.Field(attr="severities_attr") - userstory_custom_attributes = serpy.Field(attr="userstory_custom_attributes_attr") - task_custom_attributes = serpy.Field(attr="task_custom_attributes_attr") - issue_custom_attributes = serpy.Field(attr="issue_custom_attributes_attr") - roles = serpy.Field(attr="roles_attr") - members = serpy.MethodField() - total_memberships = serpy.MethodField() - is_out_of_owner_limits = serpy.MethodField() +class ProjectDetailSerializer(ProjectSerializer): + 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() - #Admin fields - is_private_extra_info = serpy.MethodField() - max_memberships = serpy.MethodField() - issues_csv_uuid = serpy.Field() - tasks_csv_uuid = serpy.Field() - userstories_csv_uuid = serpy.Field() - transfer_token = serpy.Field() + # 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() def to_value(self, instance): # Name attributes must be translated - for attr in ["userstory_statuses_attr","points_attr", "task_statuses_attr", + 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"]: + "task_custom_attributes_attr", "issue_custom_attributes_attr", "roles_attr"]: assert hasattr(instance, attr), "instance must have a {} attribute".format(attr) val = getattr(instance, attr) @@ -547,8 +388,9 @@ class LightProjectDetailSerializer(LightProjectSerializer): def get_is_out_of_owner_limits(self, obj): assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute" assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute" - return services.check_if_project_is_out_of_owner_limits(obj, - current_memberships = self.get_total_memberships(obj), + 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 ) @@ -556,8 +398,9 @@ class LightProjectDetailSerializer(LightProjectSerializer): def get_is_private_extra_info(self, 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), + 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 ) @@ -565,41 +408,32 @@ class LightProjectDetailSerializer(LightProjectSerializer): def get_max_memberships(self, obj): return services.get_max_memberships_for_project(obj) -###################################################### -## Liked -###################################################### - -class LikedSerializer(serializers.ModelSerializer): - class Meta: - model = models.Project - fields = ['id', 'name', 'slug'] - - ###################################################### -## Project Templates +# Project Templates ###################################################### -class ProjectTemplateSerializer(serializers.ModelSerializer): - default_options = JsonField(required=False, label=_("Default options")) - us_statuses = JsonField(required=False, label=_("User story's statuses")) - points = JsonField(required=False, label=_("Points")) - task_statuses = JsonField(required=False, label=_("Task's statuses")) - issue_statuses = JsonField(required=False, label=_("Issue's statuses")) - issue_types = JsonField(required=False, label=_("Issue's types")) - priorities = JsonField(required=False, label=_("Priorities")) - severities = JsonField(required=False, label=_("Severities")) - roles = JsonField(required=False, label=_("Roles")) - - class Meta: - model = models.ProjectTemplate - read_only_fields = ("created_date", "modified_date") - i18n_fields = ("name", "description") - -###################################################### -## Project order bulk serializers -###################################################### - -class UpdateProjectOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - order = serializers.IntegerField() +class ProjectTemplateSerializer(serializers.LightSerializer): + id = Field() + name = I18NField() + slug = Field() + description = I18NField() + order = Field() + created_date = Field() + modified_date = Field() + default_owner_role = Field() + is_backlog_activated = Field() + is_kanban_activated = Field() + is_wiki_activated = Field() + is_issues_activated = Field() + videoconferences = Field() + videoconferences_extra_data = Field() + default_options = Field() + us_statuses = Field() + points = Field() + task_statuses = Field() + issue_statuses = Field() + issue_types = Field() + priorities = Field() + severities = Field() + roles = Field() diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index 01ae057e..bec134c5 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -26,7 +26,6 @@ from taiga.base.decorators import list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin -from taiga.projects.attachments.utils import attach_basic_attachments from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.models import Project, TaskStatus from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin @@ -38,10 +37,14 @@ from . import models from . import permissions from . import serializers from . import services +from . import validators +from . import utils as tasks_utils -class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, - TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): +class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, + WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin, + ModelCrudViewSet): + validator_class = validators.TaskValidator queryset = models.Task.objects.all() permission_classes = (permissions.TaskPermission,) filter_backends = (filters.CanViewTasksFilterBackend, @@ -74,17 +77,15 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa def get_queryset(self): qs = super().get_queryset() - qs = self.attach_votes_attrs_to_queryset(qs) qs = qs.select_related("milestone", "project", "status", "owner", "assigned_to") - qs = self.attach_watchers_attrs_to_queryset(qs) - if "include_attachments" in self.request.QUERY_PARAMS: - qs = attach_basic_attachments(qs) - qs = qs.extra(select={"include_attachments": "True"}) + include_attachments = "include_attachments" in self.request.QUERY_PARAMS + qs = tasks_utils.attach_extra_info(qs, user=self.request.user, + include_attachments=include_attachments) return qs @@ -164,8 +165,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa def by_ref(self, request): 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) + return self.retrieve(request, project_id=project_id, ref=ref) @list_route(methods=["GET"]) def csv(self, request): @@ -182,9 +182,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 +194,20 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa data["bulk_tasks"], milestone_id=data["sprint_id"], user_story_id=data["us_id"], status_id=data.get("status_id") or project.default_task_status_id, project=project, owner=request.user, callback=self.post_save, precall=self.pre_save) + + tasks = self.get_queryset().filter(id__in=[i.id for i in tasks]) tasks_serialized = self.get_serializer_class()(tasks, many=True) return response.Ok(tasks_serialized.data) - return response.BadRequest(serializer.errors) + return response.BadRequest(validator.errors) def _bulk_update_order(self, order_field, request, **kwargs): - serializer = serializers.UpdateTasksOrderBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.UpdateTasksOrderBulkValidator(data=request.DATA) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data project = get_object_or_404(Project, pk=data["project_id"]) self.check_permissions(request, "bulk_update_order", project) diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index ac82c570..cd649424 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -16,101 +16,44 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.contrib.auth import get_user_model -from django.utils.translation import ugettext_lazy as _ - from taiga.base.api import serializers -from taiga.base.fields import PgArrayField +from taiga.base.fields import Field, MethodField from taiga.base.neighbors import NeighborsSerializerMixin from taiga.mdrender.service import render as mdrender -from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin -from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer -from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.serializers import BasicTaskStatusSerializerSerializer -from taiga.mdrender.service import render as mdrender -from taiga.projects.tagging.fields import TagsAndTagsColorsField -from taiga.projects.tasks.validators import TaskExistsValidator -from taiga.projects.validators import ProjectExistsValidator +from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin +from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin -from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin - -from taiga.users.serializers import UserBasicInfoSerializer -from taiga.users.services import get_photo_or_gravatar_url -from taiga.users.services import get_big_photo_or_gravatar_url - -from . import models - -import serpy -class TaskSerializer(WatchersValidator, VoteResourceSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - - tags = TagsAndTagsColorsField(default=[], required=False) - external_reference = PgArrayField(required=False) - comment = serializers.SerializerMethodField("get_comment") - milestone_slug = serializers.SerializerMethodField("get_milestone_slug") - blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") - description_html = serializers.SerializerMethodField("get_description_html") - is_closed = serializers.SerializerMethodField("get_is_closed") - status_extra_info = BasicTaskStatusSerializerSerializer(source="status", required=False, read_only=True) - assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) - owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) - - class Meta: - model = models.Task - read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') - - def get_comment(self, obj): - return "" - - def get_milestone_slug(self, obj): - if obj.milestone: - return obj.milestone.slug - else: - return None - - def get_blocked_note_html(self, obj): - return mdrender(obj.project, obj.blocked_note) - - def get_description_html(self, obj): - return mdrender(obj.project, obj.description) - - def get_is_closed(self, obj): - return obj.status is not None and obj.status.is_closed - - -class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, - ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin, - ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin, +class TaskListSerializer(VoteResourceSerializerMixin, WatchedResourceSerializer, + OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, + StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, serializers.LightSerializer): - id = serpy.Field() - user_story = serpy.Field(attr="user_story_id") - ref = serpy.Field() - project = serpy.Field(attr="project_id") - milestone = serpy.Field(attr="milestone_id") - milestone_slug = serpy.MethodField("get_milestone_slug") - created_date = serpy.Field() - modified_date = serpy.Field() - finished_date = serpy.Field() - subject = serpy.Field() - us_order = serpy.Field() - taskboard_order = serpy.Field() - is_iocaine = serpy.Field() - external_reference = serpy.Field() - version = serpy.Field() - watchers = serpy.Field() - is_blocked = serpy.Field() - blocked_note = serpy.Field() - tags = serpy.Field() - is_closed = serpy.MethodField() + id = Field() + user_story = Field(attr="user_story_id") + ref = Field() + project = Field(attr="project_id") + milestone = Field(attr="milestone_id") + milestone_slug = MethodField() + created_date = Field() + modified_date = Field() + finished_date = Field() + subject = Field() + us_order = Field() + taskboard_order = Field() + is_iocaine = Field() + external_reference = Field() + version = Field() + watchers = Field() + is_blocked = Field() + blocked_note = Field() + tags = Field() + is_closed = MethodField() def get_milestone_slug(self, obj): return obj.milestone.slug if obj.milestone else None @@ -119,36 +62,21 @@ class TaskListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceMod return obj.status is not None and obj.status.is_closed +class TaskSerializer(TaskListSerializer): + comment = MethodField() + blocked_note_html = MethodField() + description = Field() + description_html = MethodField() + + def get_comment(self, obj): + return "" + + def get_blocked_note_html(self, obj): + return mdrender(obj.project, obj.blocked_note) + + def get_description_html(self, obj): + return mdrender(obj.project, obj.description) + + class TaskNeighborsSerializer(NeighborsSerializerMixin, TaskSerializer): - def serialize_neighbor(self, neighbor): - if neighbor: - return NeighborTaskSerializer(neighbor).data - return None - - -class NeighborTaskSerializer(serializers.ModelSerializer): - class Meta: - model = models.Task - fields = ("id", "ref", "subject") - depth = 0 - - -class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator, - TaskExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - sprint_id = serializers.IntegerField() - status_id = serializers.IntegerField(required=False) - us_id = serializers.IntegerField(required=False) - bulk_tasks = serializers.CharField() - - -## Order bulk serializers - -class _TaskOrderBulkSerializer(TaskExistsValidator, serializers.Serializer): - task_id = serializers.IntegerField() - order = serializers.IntegerField() - - -class UpdateTasksOrderBulkSerializer(ProjectExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - bulk_tasks = _TaskOrderBulkSerializer(many=True) + pass diff --git a/taiga/projects/tasks/utils.py b/taiga/projects/tasks/utils.py new file mode 100644 index 00000000..d10dddab --- /dev/null +++ b/taiga/projects/tasks/utils.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.projects.attachments.utils import attach_basic_attachments +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset + + +def attach_extra_info(queryset, user=None, include_attachments=False): + + if include_attachments: + queryset = attach_basic_attachments(queryset) + queryset = queryset.extra(select={"include_attachments": "True"}) + + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_voter_to_queryset(queryset, user) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py index 4a100779..7f71636c 100644 --- a/taiga/projects/tasks/validators.py +++ b/taiga/projects/tasks/validators.py @@ -19,7 +19,13 @@ from django.utils.translation import ugettext as _ from taiga.base.api import serializers - +from taiga.base.api import validators +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 @@ -30,3 +36,33 @@ class TaskExistsValidator: msg = _("There's no task with that id") raise serializers.ValidationError(msg) return attrs + + +class TaskValidator(WatchersValidator, EditableWatchedResourceSerializer, validators.ModelValidator): + tags = TagsAndTagsColorsField(default=[], required=False) + external_reference = PgArrayField(required=False) + + class Meta: + model = models.Task + read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner') + + +class TasksBulkValidator(ProjectExistsValidator, MilestoneExistsValidator, + TaskExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + sprint_id = serializers.IntegerField() + status_id = serializers.IntegerField(required=False) + us_id = serializers.IntegerField(required=False) + bulk_tasks = serializers.CharField() + + +# Order bulk validators + +class _TaskOrderBulkValidator(TaskExistsValidator, validators.Validator): + task_id = serializers.IntegerField() + order = serializers.IntegerField() + + +class UpdateTasksOrderBulkValidator(ProjectExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + bulk_tasks = _TaskOrderBulkValidator(many=True) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 87ecf18b..47487e88 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -16,12 +16,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from contextlib import closing -from collections import namedtuple - from django.apps import apps -from django.db import transaction, connection -from django.db.models.sql import datastructures +from django.db import transaction from django.utils.translation import ugettext as _ from django.http import HttpResponse @@ -36,7 +32,6 @@ from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelListViewSet from taiga.base.api.utils import get_object_or_404 -from taiga.projects.attachments.utils import attach_basic_attachments from taiga.projects.history.mixins import HistoryResourceMixin from taiga.projects.history.services import take_snapshot from taiga.projects.milestones.models import Milestone @@ -45,21 +40,20 @@ from taiga.projects.notifications.mixins import WatchedResourceMixin from taiga.projects.notifications.mixins import WatchersViewSetMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.tagging.api import TaggedResourceMixin -from taiga.projects.userstories.models import RolePoints from taiga.projects.votes.mixins.viewsets import VotedResourceMixin from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin -from taiga.projects.userstories.utils import attach_total_points -from taiga.projects.userstories.utils import attach_role_points -from taiga.projects.userstories.utils import attach_tasks +from taiga.projects.userstories.utils import attach_extra_info from . import models from . import permissions from . import serializers from . import services +from . import validators class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): + validator_class = validators.UserStoryValidator queryset = models.UserStory.objects.all() permission_classes = (permissions.UserStoryPermission,) filter_backends = (filters.CanViewUsFilterBackend, @@ -105,18 +99,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi "assigned_to", "generated_from_issue") - qs = self.attach_votes_attrs_to_queryset(qs) - qs = self.attach_watchers_attrs_to_queryset(qs) - qs = attach_total_points(qs) - qs = attach_role_points(qs) - - if "include_attachments" in self.request.QUERY_PARAMS: - qs = attach_basic_attachments(qs) - qs = qs.extra(select={"include_attachments": "True"}) - - if "include_tasks" in self.request.QUERY_PARAMS: - qs = attach_tasks(qs) - qs = qs.extra(select={"include_tasks": "True"}) + include_attachments = "include_attachments" in self.request.QUERY_PARAMS + include_tasks = "include_tasks" in self.request.QUERY_PARAMS + qs = attach_extra_info(qs, user=self.request.user, + include_attachments=include_attachments, + include_tasks=include_tasks) return qs @@ -239,8 +226,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi def by_ref(self, request): 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) + return self.retrieve(request, project_id=project_id, ref=ref) @list_route(methods=["GET"]) def csv(self, request): @@ -257,9 +243,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 +255,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 +280,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi return response.NoContent() def _bulk_update_order(self, order_field, request, **kwargs): - serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) + validator = validators.UpdateUserStoriesOrderBulkValidator(data=request.DATA) + if not validator.is_valid(): + return response.BadRequest(validator.errors) - data = serializer.data + data = validator.data project = get_object_or_404(Project, pk=data["project_id"]) self.check_permissions(request, "bulk_update_order", project) diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index 236a54d8..ef15eec6 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -16,96 +16,111 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from collections import ChainMap - -from django.contrib.auth import get_user_model -from django.utils.translation import ugettext_lazy as _ - from taiga.base.api import serializers -from taiga.base.api.utils import get_object_or_404 -from taiga.base.fields import PickledObjectField -from taiga.base.fields import PgArrayField +from taiga.base.fields import Field, MethodField from taiga.base.neighbors import NeighborsSerializerMixin -from taiga.base.utils import json from taiga.mdrender.service import render as mdrender -from taiga.projects.attachments.serializers import ListBasicAttachmentsInfoSerializerMixin -from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.mixins.serializers import ListOwnerExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListAssignedToExtraInfoSerializerMixin -from taiga.projects.mixins.serializers import ListStatusExtraInfoSerializerMixin -from taiga.projects.models import Project, UserStoryStatus -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer -from taiga.projects.notifications.mixins import ListWatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator -from taiga.projects.serializers import BasicUserStoryStatusSerializer -from taiga.projects.tagging.fields import TagsAndTagsColorsField -from taiga.projects.userstories.validators import UserStoryExistsValidator -from taiga.projects.validators import ProjectExistsValidator, UserStoryStatusExistsValidator +from taiga.projects.attachments.serializers import BasicAttachmentsInfoSerializerMixin +from taiga.projects.mixins.serializers import OwnerExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import AssignedToExtraInfoSerializerMixin +from taiga.projects.mixins.serializers import StatusExtraInfoSerializerMixin +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.projects.votes.mixins.serializers import VoteResourceSerializerMixin -from taiga.projects.votes.mixins.serializers import ListVoteResourceSerializerMixin - -from taiga.users.serializers import UserBasicInfoSerializer -from taiga.users.serializers import ListUserBasicInfoSerializer -from taiga.users.services import get_photo_or_gravatar_url -from taiga.users.services import get_big_photo_or_gravatar_url - -from . import models - -import serpy -class RolePointsField(serializers.WritableField): - def to_native(self, obj): - return {str(o.role.id): o.points.id for o in obj.all()} +class OriginIssueSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() - def from_native(self, obj): - if isinstance(obj, dict): - return obj - return json.loads(obj) + def to_value(self, instance): + if instance is None: + return None + + return super().to_value(instance) -class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, - EditableWatchedResourceModelSerializer, serializers.ModelSerializer): - tags = TagsAndTagsColorsField(default=[], required=False) - external_reference = PgArrayField(required=False) - points = RolePointsField(source="role_points", required=False) - total_points = serializers.SerializerMethodField("get_total_points") - comment = serializers.SerializerMethodField("get_comment") - milestone_slug = serializers.SerializerMethodField("get_milestone_slug") - milestone_name = serializers.SerializerMethodField("get_milestone_name") - origin_issue = serializers.SerializerMethodField("get_origin_issue") - blocked_note_html = serializers.SerializerMethodField("get_blocked_note_html") - description_html = serializers.SerializerMethodField("get_description_html") - status_extra_info = BasicUserStoryStatusSerializer(source="status", required=False, read_only=True) - assigned_to_extra_info = UserBasicInfoSerializer(source="assigned_to", required=False, read_only=True) - owner_extra_info = UserBasicInfoSerializer(source="owner", required=False, read_only=True) - tribe_gig = PickledObjectField(required=False) +class UserStoryListSerializer( + VoteResourceSerializerMixin, WatchedResourceSerializer, + OwnerExtraInfoSerializerMixin, AssignedToExtraInfoSerializerMixin, + StatusExtraInfoSerializerMixin, BasicAttachmentsInfoSerializerMixin, + serializers.LightSerializer): - class Meta: - model = models.UserStory - depth = 0 - read_only_fields = ('created_date', 'modified_date', 'owner') + id = Field() + ref = Field() + milestone = Field(attr="milestone_id") + milestone_slug = MethodField() + milestone_name = MethodField() + project = Field(attr="project_id") + is_closed = Field() + points = MethodField() + backlog_order = Field() + sprint_order = Field() + kanban_order = Field() + created_date = Field() + modified_date = Field() + finish_date = Field() + subject = Field() + client_requirement = Field() + team_requirement = Field() + generated_from_issue = Field(attr="generated_from_issue_id") + external_reference = Field() + tribe_gig = Field() + version = Field() + watchers = Field() + is_blocked = Field() + blocked_note = Field() + tags = Field() + total_points = MethodField() + comment = MethodField() + origin_issue = OriginIssueSerializer(attr="generated_from_issue") + + tasks = MethodField() + + def get_milestone_slug(self, obj): + return obj.milestone.slug if obj.milestone else None + + def get_milestone_name(self, obj): + return obj.milestone.name if obj.milestone else None def get_total_points(self, obj): - return obj.get_total_points() + assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute" + return obj.total_points_attr + + def get_points(self, obj): + assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute" + if obj.role_points_attr is None: + return {} + + return obj.role_points_attr + + def get_comment(self, obj): + return "" + + def get_tasks(self, obj): + include_tasks = getattr(obj, "include_tasks", False) + + if include_tasks: + assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute" + + if not include_tasks or obj.tasks_attr is None: + return [] + + return obj.tasks_attr + + +class UserStorySerializer(UserStoryListSerializer): + comment = MethodField() + origin_issue = MethodField() + blocked_note_html = MethodField() + description = Field() + description_html = MethodField() def get_comment(self, obj): # NOTE: This method and field is necessary to historical comments work return "" - def get_milestone_slug(self, obj): - if obj.milestone: - return obj.milestone.slug - else: - return None - - def get_milestone_name(self, obj): - if obj.milestone: - return obj.milestone.name - else: - return None - def get_origin_issue(self, obj): if obj.generated_from_issue: return { @@ -122,142 +137,5 @@ class UserStorySerializer(WatchersValidator, VoteResourceSerializerMixin, return mdrender(obj.project, obj.description) -class ListOriginIssueSerializer(serializers.LightSerializer): - id = serpy.Field() - ref = serpy.Field() - subject = serpy.Field() - - def to_value(self, instance): - if instance is None: - return None - - return super().to_value(instance) - - -class UserStoryListSerializer(ListVoteResourceSerializerMixin, ListWatchedResourceModelSerializer, - ListOwnerExtraInfoSerializerMixin, ListAssignedToExtraInfoSerializerMixin, - ListStatusExtraInfoSerializerMixin, ListBasicAttachmentsInfoSerializerMixin, - serializers.LightSerializer): - - id = serpy.Field() - ref = serpy.Field() - milestone = serpy.Field(attr="milestone_id") - milestone_slug = serpy.MethodField() - milestone_name = serpy.MethodField() - project = serpy.Field(attr="project_id") - is_closed = serpy.Field() - points = serpy.MethodField() - backlog_order = serpy.Field() - sprint_order = serpy.Field() - kanban_order = serpy.Field() - created_date = serpy.Field() - modified_date = serpy.Field() - finish_date = serpy.Field() - subject = serpy.Field() - client_requirement = serpy.Field() - team_requirement = serpy.Field() - generated_from_issue = serpy.Field(attr="generated_from_issue_id") - external_reference = serpy.Field() - tribe_gig = serpy.Field() - version = serpy.Field() - watchers = serpy.Field() - is_blocked = serpy.Field() - blocked_note = serpy.Field() - tags = serpy.Field() - total_points = serpy.MethodField() - comment = serpy.MethodField("get_comment") - origin_issue = ListOriginIssueSerializer(attr="generated_from_issue") - - tasks = serpy.MethodField() - - def get_milestone_slug(self, obj): - return obj.milestone.slug if obj.milestone else None - - def get_milestone_name(self, obj): - return obj.milestone.name if obj.milestone else None - - def get_total_points(self, obj): - assert hasattr(obj, "total_points_attr"), "instance must have a total_points_attr attribute" - return obj.total_points_attr - - def get_points(self, obj): - assert hasattr(obj, "role_points_attr"), "instance must have a role_points_attr attribute" - if obj.role_points_attr is None: - return {} - - return dict(ChainMap(*obj.role_points_attr)) - - def get_comment(self, obj): - return "" - - def get_tasks(self, obj): - include_tasks = getattr(obj, "include_tasks", False) - - if include_tasks: - assert hasattr(obj, "tasks_attr"), "instance must have a tasks_attr attribute" - - if not include_tasks or obj.tasks_attr is None: - return [] - - return obj.tasks_attr - - class UserStoryNeighborsSerializer(NeighborsSerializerMixin, UserStorySerializer): - def serialize_neighbor(self, neighbor): - if neighbor: - return NeighborUserStorySerializer(neighbor).data - return None - - -class NeighborUserStorySerializer(serializers.ModelSerializer): - class Meta: - model = models.UserStory - fields = ("id", "ref", "subject") - depth = 0 - - -class UserStoriesBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, - serializers.Serializer): - project_id = serializers.IntegerField() - status_id = serializers.IntegerField(required=False) - bulk_stories = serializers.CharField() - - -## Order bulk serializers - -class _UserStoryOrderBulkSerializer(UserStoryExistsValidator, serializers.Serializer): - us_id = serializers.IntegerField() - order = serializers.IntegerField() - - -class UpdateUserStoriesOrderBulkSerializer(ProjectExistsValidator, UserStoryStatusExistsValidator, - serializers.Serializer): - project_id = serializers.IntegerField() - bulk_stories = _UserStoryOrderBulkSerializer(many=True) - - -## Milestone bulk serializers - -class _UserStoryMilestoneBulkSerializer(UserStoryExistsValidator, serializers.Serializer): - us_id = serializers.IntegerField() - - -class UpdateMilestoneBulkSerializer(ProjectExistsValidator, SprintExistsValidator, serializers.Serializer): - project_id = serializers.IntegerField() - milestone_id = serializers.IntegerField() - bulk_stories = _UserStoryMilestoneBulkSerializer(many=True) - - def validate(self, data): - """ - All the userstories and the milestone are from the same project - """ - user_story_ids = [us["us_id"] for us in data["bulk_stories"]] - project = get_object_or_404(Project, pk=data["project_id"]) - - if project.user_stories.filter(id__in=user_story_ids).count() != len(user_story_ids): - raise serializers.ValidationError("all the user stories must be from the same project") - - if project.milestones.filter(id=data["milestone_id"]).count() != 1: - raise serializers.ValidationError("the milestone isn't valid for the project") - - return data + pass diff --git a/taiga/projects/userstories/utils.py b/taiga/projects/userstories/utils.py index 809248f7..87b8e094 100644 --- a/taiga/projects/userstories/utils.py +++ b/taiga/projects/userstories/utils.py @@ -17,6 +17,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from taiga.projects.attachments.utils import attach_basic_attachments +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset +from taiga.projects.votes.utils import attach_total_voters_to_queryset +from taiga.projects.votes.utils import attach_is_voter_to_queryset + def attach_total_points(queryset, as_field="total_points_attr"): """Attach total of point values to each object of the queryset. @@ -28,7 +35,7 @@ def attach_total_points(queryset, as_field="total_points_attr"): """ model = queryset.model sql = """SELECT SUM(projects_points.value) - FROM userstories_rolepoints + FROM userstories_rolepoints INNER JOIN projects_points ON userstories_rolepoints.points_id = projects_points.id WHERE userstories_rolepoints.user_story_id = {tbl}.id""" @@ -46,10 +53,15 @@ def attach_role_points(queryset, as_field="role_points_attr"): :return: Queryset object with the additional `as_field` field. """ model = queryset.model - sql = """SELECT json_agg((userstories_rolepoints.role_id, userstories_rolepoints.points_id)) - FROM userstories_rolepoints + sql = """SELECT FORMAT('{{%%s}}', + STRING_AGG(format( + '"%%s":%%s', + TO_JSON(userstories_rolepoints.role_id), + TO_JSON(userstories_rolepoints.points_id) + ), ',') + )::json + FROM userstories_rolepoints WHERE userstories_rolepoints.user_story_id = {tbl}.id""" - sql = sql.format(tbl=model._meta.db_table) queryset = queryset.extra(select={as_field: sql}) return queryset @@ -82,3 +94,23 @@ def attach_tasks(queryset, as_field="tasks_attr"): sql = sql.format(tbl=model._meta.db_table) queryset = queryset.extra(select={as_field: sql}) return queryset + + +def attach_extra_info(queryset, user=None, include_attachments=False, include_tasks=False): + queryset = attach_total_points(queryset) + queryset = attach_role_points(queryset) + + if include_attachments: + queryset = attach_basic_attachments(queryset) + queryset = queryset.extra(select={"include_attachments": "True"}) + + if include_tasks: + queryset = attach_tasks(queryset) + queryset = queryset.extra(select={"include_tasks": "True"}) + + queryset = attach_total_voters_to_queryset(queryset) + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_voter_to_queryset(queryset, user) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/userstories/validators.py b/taiga/projects/userstories/validators.py index 5ad5e7f4..4ea0b24a 100644 --- a/taiga/projects/userstories/validators.py +++ b/taiga/projects/userstories/validators.py @@ -19,9 +19,21 @@ 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.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): @@ -30,3 +42,72 @@ class UserStoryExistsValidator: msg = _("There's no user story with that id") raise serializers.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 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 diff --git a/taiga/projects/utils.py b/taiga/projects/utils.py index a05d8476..ee552136 100644 --- a/taiga/projects/utils.py +++ b/taiga/projects/utils.py @@ -375,7 +375,7 @@ def attach_private_projects_same_owner(queryset, user, as_field="private_project """ model = queryset.model if user is None or user.is_anonymous(): - sql = """SELECT '0'""" + sql = """SELECT 0""" else: sql = """SELECT COUNT(id) FROM projects_project p_aux @@ -399,7 +399,7 @@ def attach_public_projects_same_owner(queryset, user, as_field="public_projects_ """ model = queryset.model if user is None or user.is_anonymous(): - sql = """SELECT '0'""" + sql = """SELECT 0""" else: sql = """SELECT COUNT(id) FROM projects_project p_aux diff --git a/taiga/projects/validators.py b/taiga/projects/validators.py index 05866b66..c8ab21bb 100644 --- a/taiga/projects/validators.py +++ b/taiga/projects/validators.py @@ -16,11 +16,42 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.db.models import Q from django.utils.translation import ugettext as _ from taiga.base.api import serializers +from taiga.base.api import validators +from taiga.base.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 serializers.ValidationError(_("Name duplicated for the project")) + + return attrs class ProjectExistsValidator: @@ -48,3 +79,170 @@ class TaskStatusExistsValidator: msg = _("There's no task status with that id") raise serializers.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 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 not attrs[source]: + 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 MembershipAdminValidator(MembershipValidator): + class Meta: + model = models.Membership + # IMPORTANT: Maintain the MembershipSerializer Meta up to date + # with this info (excluding there user_email and email) + read_only_fields = ("user",) + exclude = ("token",) + + +class MemberBulkValidator(RoleExistsValidator, validators.Validator): + email = serializers.EmailField() + role_id = serializers.IntegerField() + + +class MembersBulkValidator(ProjectExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + bulk_memberships = MemberBulkValidator(many=True) + invitation_extra_text = serializers.CharField(required=False, max_length=255) + + +###################################################### +# Projects +###################################################### + +class ProjectValidator(validators.ModelValidator): + anon_permissions = PgArrayField(required=False) + public_permissions = PgArrayField(required=False) + tags = TagsField(default=[], required=False) + + class Meta: + model = models.Project + read_only_fields = ("created_date", "modified_date", "slug", "blocked_code", "owner") + + +###################################################### +# Project Templates +###################################################### + +class ProjectTemplateValidator(validators.ModelValidator): + default_options = JsonField(required=False, label=_("Default options")) + us_statuses = JsonField(required=False, label=_("User story's statuses")) + points = JsonField(required=False, label=_("Points")) + task_statuses = JsonField(required=False, label=_("Task's statuses")) + issue_statuses = JsonField(required=False, label=_("Issue's statuses")) + issue_types = JsonField(required=False, label=_("Issue's types")) + priorities = JsonField(required=False, label=_("Priorities")) + severities = JsonField(required=False, label=_("Severities")) + roles = JsonField(required=False, label=_("Roles")) + + class Meta: + model = models.ProjectTemplate + read_only_fields = ("created_date", "modified_date") + + +###################################################### +# Project order bulk serializers +###################################################### + +class UpdateProjectOrderBulkValidator(ProjectExistsValidator, validators.Validator): + project_id = serializers.IntegerField() + order = serializers.IntegerField() diff --git a/taiga/projects/votes/mixins/serializers.py b/taiga/projects/votes/mixins/serializers.py index fc7a988e..9f9d1049 100644 --- a/taiga/projects/votes/mixins/serializers.py +++ b/taiga/projects/votes/mixins/serializers.py @@ -16,12 +16,14 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import serpy - from taiga.base.api import serializers +from taiga.base.fields import MethodField -class BaseVoteResourceSerializerMixin(object): +class VoteResourceSerializerMixin(serializers.LightSerializer): + is_voter = MethodField() + total_voters = MethodField() + def get_is_voter(self, obj): # The "is_voted" attribute is attached in the get_queryset of the viewset. return getattr(obj, "is_voter", False) or False @@ -29,13 +31,3 @@ class BaseVoteResourceSerializerMixin(object): def get_total_voters(self, obj): # The "total_voters" attribute is attached in the get_queryset of the viewset. return getattr(obj, "total_voters", 0) or 0 - - -class VoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serializers.ModelSerializer): - is_voter = serializers.SerializerMethodField("get_is_voter") - total_voters = serializers.SerializerMethodField("get_total_voters") - - -class ListVoteResourceSerializerMixin(BaseVoteResourceSerializerMixin, serpy.Serializer): - is_voter = serpy.MethodField("get_is_voter") - total_voters = serpy.MethodField("get_total_voters") diff --git a/taiga/projects/votes/mixins/viewsets.py b/taiga/projects/votes/mixins/viewsets.py index 2456e375..50490ba7 100644 --- a/taiga/projects/votes/mixins/viewsets.py +++ b/taiga/projects/votes/mixins/viewsets.py @@ -39,14 +39,6 @@ class VotedResourceMixin: def pre_conditions_on_save(self, obj) """ - def attach_votes_attrs_to_queryset(self, queryset): - qs = attach_total_voters_to_queryset(queryset) - - if self.request.user.is_authenticated(): - qs = attach_is_voter_to_queryset(self.request.user, qs) - - return qs - @detail_route(methods=["POST"]) def upvote(self, request, pk=None): obj = self.get_object() diff --git a/taiga/projects/votes/utils.py b/taiga/projects/votes/utils.py index 291ee284..077abd46 100644 --- a/taiga/projects/votes/utils.py +++ b/taiga/projects/votes/utils.py @@ -48,7 +48,7 @@ def attach_total_voters_to_queryset(queryset, as_field="total_voters"): return qs -def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"): +def attach_is_voter_to_queryset(queryset, user, as_field="is_voter"): """Attach is_vote boolean to each object of the queryset. Because of laziness of vote objects creation, this makes much simpler and more efficient to @@ -57,22 +57,26 @@ def attach_is_voter_to_queryset(user, queryset, as_field="is_voter"): (The other way was to do it in the serializer with some try/except blocks and additional queries) - :param user: A users.User object model :param queryset: A Django queryset object. + :param user: A users.User object model :param as_field: Attach the boolean as an attribute with this name. :return: Queryset object with the additional `as_field` field. """ model = queryset.model type = apps.get_model("contenttypes", "ContentType").objects.get_for_model(model) - sql = ("""SELECT CASE WHEN (SELECT count(*) - FROM votes_vote - WHERE votes_vote.content_type_id = {type_id} - AND votes_vote.object_id = {tbl}.id - AND votes_vote.user_id = {user_id}) > 0 - THEN TRUE - ELSE FALSE - END""") - sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) + if user is None or user.is_anonymous(): + sql = """SELECT false""" + else: + sql = ("""SELECT CASE WHEN (SELECT count(*) + FROM votes_vote + WHERE votes_vote.content_type_id = {type_id} + AND votes_vote.object_id = {tbl}.id + AND votes_vote.user_id = {user_id}) > 0 + THEN TRUE + ELSE FALSE + END""") + sql = sql.format(type_id=type.id, tbl=model._meta.db_table, user_id=user.id) + qs = queryset.extra(select={as_field: sql}) return qs diff --git a/taiga/projects/wiki/api.py b/taiga/projects/wiki/api.py index 807d86c6..d6a3d44a 100644 --- a/taiga/projects/wiki/api.py +++ b/taiga/projects/wiki/api.py @@ -24,7 +24,6 @@ from taiga.base import response from taiga.base.api import ModelCrudViewSet from taiga.base.api import ModelListViewSet from taiga.base.api.mixins import BlockedByProjectMixin -from taiga.base.api.permissions import IsAuthenticated from taiga.base.api.utils import get_object_or_404 from taiga.base.decorators import list_route @@ -42,6 +41,8 @@ from taiga.projects.occ import OCCResourceMixin from . import models from . import permissions from . import serializers +from . import validators +from . import utils as wiki_utils class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, @@ -49,6 +50,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, model = models.WikiPage serializer_class = serializers.WikiPageSerializer + validator_class = validators.WikiPageValidator permission_classes = (permissions.WikiPagePermission,) filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_fields = ("project", "slug") @@ -56,7 +58,7 @@ class WikiViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, def get_queryset(self): qs = super().get_queryset() - qs = self.attach_watchers_attrs_to_queryset(qs) + qs = wiki_utils.attach_extra_info(qs, user=self.request.user) return qs @list_route(methods=["GET"]) @@ -100,6 +102,7 @@ class WikiWatchersViewSet(WatchersViewSetMixin, ModelListViewSet): class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.WikiLink serializer_class = serializers.WikiLinkSerializer + validator_class = validators.WikiLinkValidator permission_classes = (permissions.WikiLinkPermission,) filter_backends = (filters.CanViewWikiPagesFilterBackend,) filter_fields = ["project"] @@ -120,7 +123,7 @@ class WikiLinkViewSet(BlockedByProjectMixin, ModelCrudViewSet): wiki_page, created = models.WikiPage.objects.get_or_create( slug=wiki_link.href, project=wiki_link.project, - defaults={"owner": self.request.user,"last_modifier": self.request.user}) + defaults={"owner": self.request.user, "last_modifier": self.request.user}) if created: # Creaste the new history entre, sSet watcher for the new wiki page diff --git a/taiga/projects/wiki/serializers.py b/taiga/projects/wiki/serializers.py index 16de19df..a7e36c60 100644 --- a/taiga/projects/wiki/serializers.py +++ b/taiga/projects/wiki/serializers.py @@ -17,21 +17,26 @@ # along with this program. If not, see . from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField from taiga.projects.history import services as history_service -from taiga.projects.notifications.mixins import WatchedResourceModelSerializer -from taiga.projects.notifications.validators import WatchersValidator +from taiga.projects.notifications.mixins import WatchedResourceSerializer from taiga.mdrender.service import render as mdrender -from . import models +class WikiPageSerializer(WatchedResourceSerializer, serializers.LightSerializer): + id = Field() + project = Field(attr="project_id") + slug = Field() + content = Field() + owner = Field(attr="owner_id") + last_modifier = Field(attr="last_modifier_id") + created_date = Field() + modified_date = Field() -class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, serializers.ModelSerializer): - html = serializers.SerializerMethodField("get_html") - editions = serializers.SerializerMethodField("get_editions") + html = MethodField() + editions = MethodField() - class Meta: - model = models.WikiPage - read_only_fields = ('modified_date', 'created_date', 'owner') + version = Field() def get_html(self, obj): return mdrender(obj.project, obj.content) @@ -40,7 +45,9 @@ class WikiPageSerializer(WatchersValidator, WatchedResourceModelSerializer, seri return history_service.get_history_queryset_by_model_instance(obj).count() + 1 # +1 for creation -class WikiLinkSerializer(serializers.ModelSerializer): - class Meta: - model = models.WikiLink - read_only_fields = ('href',) +class WikiLinkSerializer(serializers.LightSerializer): + id = Field() + project = Field(attr="project_id") + title = Field() + href = Field() + order = Field() diff --git a/taiga/projects/wiki/utils.py b/taiga/projects/wiki/utils.py new file mode 100644 index 00000000..ecbf7602 --- /dev/null +++ b/taiga/projects/wiki/utils.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.projects.notifications.utils import attach_watchers_to_queryset +from taiga.projects.notifications.utils import attach_total_watchers_to_queryset +from taiga.projects.notifications.utils import attach_is_watcher_to_queryset + + +def attach_extra_info(queryset, user=None, include_attachments=False): + queryset = attach_watchers_to_queryset(queryset) + queryset = attach_total_watchers_to_queryset(queryset) + queryset = attach_is_watcher_to_queryset(queryset, user) + return queryset diff --git a/taiga/projects/wiki/validators.py b/taiga/projects/wiki/validators.py new file mode 100644 index 00000000..033fac1b --- /dev/null +++ b/taiga/projects/wiki/validators.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api import validators +from taiga.projects.notifications.validators import WatchersValidator + +from . import models + + +class WikiPageValidator(WatchersValidator, validators.ModelValidator): + class Meta: + model = models.WikiPage + read_only_fields = ('modified_date', 'created_date', 'owner') + + +class WikiLinkValidator(validators.ModelValidator): + class Meta: + model = models.WikiLink + read_only_fields = ('href',) diff --git a/taiga/searches/serializers.py b/taiga/searches/serializers.py index edc2d1ca..e96e1131 100644 --- a/taiga/searches/serializers.py +++ b/taiga/searches/serializers.py @@ -16,37 +16,48 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from taiga.projects.issues.serializers import IssueSerializer -from taiga.projects.userstories.serializers import UserStorySerializer -from taiga.projects.tasks.serializers import TaskSerializer -from taiga.projects.wiki.serializers import WikiPageSerializer - -from taiga.projects.issues.models import Issue -from taiga.projects.userstories.models import UserStory -from taiga.projects.tasks.models import Task -from taiga.projects.wiki.models import WikiPage +from taiga.base.api import serializers +from taiga.base.fields import Field, MethodField -class IssueSearchResultsSerializer(IssueSerializer): - class Meta: - model = Issue - fields = ('id', 'ref', 'subject', 'status', 'assigned_to') +class IssueSearchResultsSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + status = Field(attr="status_id") + assigned_to = Field(attr="assigned_to_id") -class TaskSearchResultsSerializer(TaskSerializer): - class Meta: - model = Task - fields = ('id', 'ref', 'subject', 'status', 'assigned_to') +class TaskSearchResultsSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + status = Field(attr="status_id") + assigned_to = Field(attr="assigned_to_id") -class UserStorySearchResultsSerializer(UserStorySerializer): - class Meta: - model = UserStory - fields = ('id', 'ref', 'subject', 'status', 'total_points', - 'milestone_name', 'milestone_slug') +class UserStorySearchResultsSerializer(serializers.LightSerializer): + id = Field() + ref = Field() + subject = Field() + status = Field(attr="status_id") + total_points = MethodField() + milestone_name = MethodField() + milestone_slug = MethodField() + + def get_milestone_name(self, obj): + return obj.milestone.name if obj.milestone else None + + def get_milestone_slug(self, obj): + return obj.milestone.slug if obj.milestone else None + + def get_total_points(self, obj): + assert hasattr(obj, "total_points_attr"), \ + "instance must have a total_points_attr attribute" + + return obj.total_points_attr -class WikiPageSearchResultsSerializer(WikiPageSerializer): - class Meta: - model = WikiPage - fields = ('id', 'slug') +class WikiPageSearchResultsSerializer(serializers.LightSerializer): + id = Field() + slug = Field() diff --git a/taiga/searches/services.py b/taiga/searches/services.py index f393844f..4dcda86f 100644 --- a/taiga/searches/services.py +++ b/taiga/searches/services.py @@ -19,6 +19,7 @@ from django.apps import apps from django.conf import settings from taiga.base.utils.db import to_tsquery +from taiga.projects.userstories.utils import attach_total_points MAX_RESULTS = getattr(settings, "SEARCHES_MAX_RESULTS", 150) @@ -30,11 +31,13 @@ def search_user_stories(project, text): "coalesce(userstories_userstory.description, '')) " "@@ to_tsquery('english_nostop', %s)") - if text: - return (model_cls.objects.extra(where=[where_clause], params=[to_tsquery(text)]) - .filter(project_id=project.pk)[:MAX_RESULTS]) + queryset = model_cls.objects.filter(project_id=project.pk) - return model_cls.objects.filter(project_id=project.pk)[:MAX_RESULTS] + if text: + queryset = queryset.extra(where=[where_clause], params=[to_tsquery(text)]) + + queryset = attach_total_points(queryset) + return queryset[:MAX_RESULTS] def search_tasks(project, text): diff --git a/taiga/timeline/api.py b/taiga/timeline/api.py index b0bf8e13..3e3bd6f4 100644 --- a/taiga/timeline/api.py +++ b/taiga/timeline/api.py @@ -18,10 +18,8 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from django.apps import apps from taiga.base import response -from taiga.base.api.utils import get_object_or_404 from taiga.base.api import ReadOnlyListViewSet from . import serializers @@ -36,7 +34,7 @@ class TimelineViewSet(ReadOnlyListViewSet): def get_content_type(self): app_name, model = self.content_type.split(".", 1) - return get_object_or_404(ContentType, app_label=app_name, model=model) + return ContentType.objects.get_by_natural_key(app_name, model) def get_queryset(self): ct = self.get_content_type() diff --git a/taiga/timeline/migrations/0005_auto_20160706_0723.py b/taiga/timeline/migrations/0005_auto_20160706_0723.py new file mode 100644 index 00000000..7ac9fa9c --- /dev/null +++ b/taiga/timeline/migrations/0005_auto_20160706_0723.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-07-06 07:23 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('timeline', '0004_auto_20150603_1312'), + ] + + operations = [ + migrations.AlterField( + model_name='timeline', + name='created', + field=models.DateTimeField(db_index=True, default=django.utils.timezone.now), + ), + ] diff --git a/taiga/timeline/models.py b/taiga/timeline/models.py index c71188f7..ebee7da5 100644 --- a/taiga/timeline/models.py +++ b/taiga/timeline/models.py @@ -20,13 +20,12 @@ from django.db import models from django_pgjson.fields import JsonField from django.utils import timezone -from django.core.exceptions import ValidationError - from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey from taiga.projects.models import Project + class Timeline(models.Model): content_type = models.ForeignKey(ContentType, related_name="content_type_timelines") object_id = models.PositiveIntegerField() @@ -36,12 +35,11 @@ class Timeline(models.Model): project = models.ForeignKey(Project, null=True) data = JsonField() data_content_type = models.ForeignKey(ContentType, related_name="data_timelines") - created = models.DateTimeField(default=timezone.now) + created = models.DateTimeField(default=timezone.now, db_index=True) class Meta: index_together = [('content_type', 'object_id', 'namespace'), ] - # Register all implementations from .timeline_implementations import * diff --git a/taiga/timeline/serializers.py b/taiga/timeline/serializers.py index a6be6944..07b1985a 100644 --- a/taiga/timeline/serializers.py +++ b/taiga/timeline/serializers.py @@ -16,26 +16,32 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.apps import apps from django.contrib.auth import get_user_model -from django.forms import widgets from taiga.base.api import serializers -from taiga.base.fields import JsonField +from taiga.base.fields import Field, MethodField from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url from . import models -from . import service -class TimelineSerializer(serializers.ModelSerializer): +class TimelineSerializer(serializers.LightSerializer): data = serializers.SerializerMethodField("get_data") + id = Field() + content_type = Field(attr="content_type_id") + object_id = Field() + namespace = Field() + event_type = Field() + project = Field(attr="project_id") + data = MethodField() + data_content_type = Field(attr="data_content_type_id") + created = Field() class Meta: model = models.Timeline def get_data(self, obj): - #Updates the data user info saved if the user exists + # Updates the data user info saved if the user exists if hasattr(obj, "_prefetched_user"): user = obj._prefetched_user else: diff --git a/taiga/timeline/service.py b/taiga/timeline/service.py index f99f795e..d3e81976 100644 --- a/taiga/timeline/service.py +++ b/taiga/timeline/service.py @@ -27,33 +27,32 @@ from functools import partial, wraps from taiga.base.utils.db import get_typename_for_model_class from taiga.celery import app -from taiga.users.services import get_photo_or_gravatar_url, get_big_photo_or_gravatar_url _timeline_impl_map = {} -def _get_impl_key_from_model(model:Model, event_type:str): +def _get_impl_key_from_model(model: Model, event_type: str): if issubclass(model, Model): typename = get_typename_for_model_class(model) return _get_impl_key_from_typename(typename, event_type) raise Exception("Not valid model parameter") -def _get_impl_key_from_typename(typename:str, event_type:str): +def _get_impl_key_from_typename(typename: str, event_type: str): if isinstance(typename, str): return "{0}.{1}".format(typename, event_type) raise Exception("Not valid typename parameter") -def build_user_namespace(user:object): +def build_user_namespace(user: object): return "{0}:{1}".format("user", user.id) -def build_project_namespace(project:object): +def build_project_namespace(project: object): return "{0}:{1}".format("project", project.id) -def _add_to_object_timeline(obj:object, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): +def _add_to_object_timeline(obj: object, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}): assert isinstance(obj, Model), "obj must be a instance of Model" assert isinstance(instance, Model), "instance must be a instance of Model" from .models import Timeline @@ -75,12 +74,12 @@ def _add_to_object_timeline(obj:object, instance:object, event_type:str, created ) -def _add_to_objects_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): +def _add_to_objects_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}): for obj in objects: _add_to_object_timeline(obj, instance, event_type, created_datetime, namespace, extra_data) -def _push_to_timeline(objects, instance:object, event_type:str, created_datetime:object, namespace:str="default", extra_data:dict={}): +def _push_to_timeline(objects, instance: object, event_type: str, created_datetime: object, namespace: str="default", extra_data: dict={}): if isinstance(objects, Model): _add_to_object_timeline(objects, instance, event_type, created_datetime, namespace, extra_data) elif isinstance(objects, QuerySet) or isinstance(objects, list): @@ -111,10 +110,10 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id except projectModel.DoesNotExist: return - ## Project timeline + # Project timeline _push_to_timeline(project, obj, event_type, created_datetime, - namespace=build_project_namespace(project), - extra_data=extra_data) + namespace=build_project_namespace(project), + extra_data=extra_data) project.refresh_totals() @@ -122,14 +121,14 @@ def push_to_timelines(project_id, user_id, obj_app_label, obj_model_name, obj_id related_people = obj.get_related_people() _push_to_timeline(related_people, obj, event_type, created_datetime, - namespace=build_user_namespace(user), - extra_data=extra_data) + namespace=build_user_namespace(user), + extra_data=extra_data) else: # Actions not related with a project - ## - Me + # - Me _push_to_timeline(user, obj, event_type, created_datetime, - namespace=build_user_namespace(user), - extra_data=extra_data) + namespace=build_user_namespace(user), + extra_data=extra_data) def get_timeline(obj, namespace=None): @@ -141,7 +140,6 @@ def get_timeline(obj, namespace=None): if namespace is not None: timeline = timeline.filter(namespace=namespace) - timeline = timeline.select_related("project") timeline = timeline.order_by("-created", "-id") return timeline @@ -156,22 +154,22 @@ def filter_timeline_for_user(timeline, user): # Filtering private project with some public parts content_types = { - "view_project": ContentType.objects.get(app_label="projects", model="project"), - "view_milestones": ContentType.objects.get(app_label="milestones", model="milestone"), - "view_us": ContentType.objects.get(app_label="userstories", model="userstory"), - "view_tasks": ContentType.objects.get(app_label="tasks", model="task"), - "view_issues": ContentType.objects.get(app_label="issues", model="issue"), - "view_wiki_pages": ContentType.objects.get(app_label="wiki", model="wikipage"), - "view_wiki_links": ContentType.objects.get(app_label="wiki", model="wikilink"), + "view_project": ContentType.objects.get_by_natural_key("projects", "project"), + "view_milestones": ContentType.objects.get_by_natural_key("milestones", "milestone"), + "view_us": ContentType.objects.get_by_natural_key("userstories", "userstory"), + "view_tasks": ContentType.objects.get_by_natural_key("tasks", "task"), + "view_issues": ContentType.objects.get_by_natural_key("issues", "issue"), + "view_wiki_pages": ContentType.objects.get_by_natural_key("wiki", "wikipage"), + "view_wiki_links": ContentType.objects.get_by_natural_key("wiki", "wikilink"), } for content_type_key, content_type in content_types.items(): tl_filter |= Q(project__is_private=True, - project__anon_permissions__contains=[content_type_key], - data_content_type=content_type) + project__anon_permissions__contains=[content_type_key], + data_content_type=content_type) # There is no specific permission for seeing new memberships - membership_content_type = ContentType.objects.get(app_label="projects", model="membership") + membership_content_type = ContentType.objects.get_by_natural_key(app_label="projects", model="membership") tl_filter |= Q(project__is_private=True, project__anon_permissions__contains=["view_project"], data_content_type=membership_content_type) @@ -214,7 +212,7 @@ def get_project_timeline(project, accessing_user=None): return timeline -def register_timeline_implementation(typename:str, event_type:str, fn=None): +def register_timeline_implementation(typename: str, event_type: str, fn=None): assert isinstance(typename, str), "typename must be a string" assert isinstance(event_type, str), "event_type must be a string" @@ -231,7 +229,6 @@ def register_timeline_implementation(typename:str, event_type:str, fn=None): return _wrapper - def extract_project_info(instance): return { "id": instance.pk, diff --git a/taiga/users/serializers.py b/taiga/users/serializers.py index 98903ec1..3d584c53 100644 --- a/taiga/users/serializers.py +++ b/taiga/users/serializers.py @@ -22,7 +22,8 @@ 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 + from taiga.base.utils.thumbnails import get_thumbnail_url from taiga.projects.models import Project @@ -33,11 +34,10 @@ from .gravatar import get_gravatar_url from collections import namedtuple import re -import serpy ###################################################### -## User +# User ###################################################### class ContactProjectDetailSerializer(serializers.ModelSerializer): @@ -139,19 +139,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,6 +156,12 @@ 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 + + return super().to_value(instance) + class RecoverySerializer(serializers.Serializer): token = serializers.CharField(max_length=200) @@ -177,7 +177,7 @@ class CancelAccountSerializer(serializers.Serializer): ###################################################### -## Role +# Role ###################################################### class RoleSerializer(serializers.ModelSerializer): @@ -201,7 +201,7 @@ class ProjectRoleSerializer(serializers.ModelSerializer): ###################################################### -## Like +# Like ###################################################### class HighLightedContentSerializer(serializers.Serializer): diff --git a/taiga/webhooks/api.py b/taiga/webhooks/api.py index f15021a0..4648a73a 100644 --- a/taiga/webhooks/api.py +++ b/taiga/webhooks/api.py @@ -30,6 +30,7 @@ from taiga.base.decorators import detail_route from . import models from . import serializers +from . import validators from . import permissions from . import tasks @@ -37,6 +38,7 @@ from . import tasks class WebhookViewSet(BlockedByProjectMixin, ModelCrudViewSet): model = models.Webhook serializer_class = serializers.WebhookSerializer + validator_class = validators.WebhookValidator permission_classes = (permissions.WebhookPermission,) filter_backends = (filters.IsProjectAdminFilterBackend,) filter_fields = ("project",) diff --git a/taiga/webhooks/serializers.py b/taiga/webhooks/serializers.py index ee0d8308..624e107c 100644 --- a/taiga/webhooks/serializers.py +++ b/taiga/webhooks/serializers.py @@ -19,63 +19,55 @@ from django.core.exceptions import ObjectDoesNotExist from taiga.base.api import serializers -from taiga.base.fields import PgArrayField, JsonField - +from taiga.base.fields import Field, MethodField from taiga.front.templatetags.functions import resolve as resolve_front_url -from taiga.projects.history import models as history_models -from taiga.projects.issues import models as issue_models -from taiga.projects.milestones import models as milestone_models -from taiga.projects.notifications.mixins import EditableWatchedResourceModelSerializer from taiga.projects.services import get_logo_big_thumbnail_url -from taiga.projects.tasks import models as task_models -from taiga.projects.tagging.fields import TagsField -from taiga.projects.userstories import models as us_models -from taiga.projects.wiki import models as wiki_models from taiga.users.gravatar import get_gravatar_url from taiga.users.services import get_photo_or_gravatar_url -from .models import Webhook, WebhookLog - ######################################################################## -## WebHooks +# WebHooks ######################################################################## -class WebhookSerializer(serializers.ModelSerializer): - logs_counter = serializers.SerializerMethodField("get_logs_counter") - - class Meta: - model = Webhook +class WebhookSerializer(serializers.LightSerializer): + id = Field() + project = Field(attr="project_id") + name = Field() + url = Field() + key = Field() + logs_counter = MethodField() def get_logs_counter(self, obj): return obj.logs.count() -class WebhookLogSerializer(serializers.ModelSerializer): - request_data = JsonField() - request_headers = JsonField() - response_headers = JsonField() - - class Meta: - model = WebhookLog +class WebhookLogSerializer(serializers.LightSerializer): + id = Field() + webhook = Field(attr="webhook_id") + url = Field() + status = Field() + request_data = Field() + request_headers = Field() + response_data = Field() + response_headers = Field() + duration = Field() + created = Field() ######################################################################## -## User +# User ######################################################################## -class UserSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - permalink = serializers.SerializerMethodField("get_permalink") - gravatar_url = serializers.SerializerMethodField("get_gravatar_url") - username = serializers.SerializerMethodField("get_username") - full_name = serializers.SerializerMethodField("get_full_name") - photo = serializers.SerializerMethodField("get_photo") - - def get_pk(self, obj): - return obj.pk +class UserSerializer(serializers.LightSerializer): + id = Field(attr="pk") + permalink = MethodField() + gravatar_url = MethodField() + username = MethodField() + full_name = MethodField() + photo = MethodField() def get_permalink(self, obj): return resolve_front_url("user", obj.username) @@ -84,7 +76,7 @@ class UserSerializer(serializers.Serializer): return get_gravatar_url(obj.email) def get_username(self, obj): - return obj.get_username + return obj.get_username() def get_full_name(self, obj): return obj.get_full_name() @@ -92,18 +84,22 @@ class UserSerializer(serializers.Serializer): def get_photo(self, obj): return get_photo_or_gravatar_url(obj) + def to_value(self, instance): + if instance is None: + return None + + return super().to_value(instance) + + ######################################################################## -## Project +# Project ######################################################################## -class ProjectSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - permalink = serializers.SerializerMethodField("get_permalink") - name = serializers.SerializerMethodField("get_name") - logo_big_url = serializers.SerializerMethodField("get_logo_big_url") - - def get_pk(self, obj): - return obj.pk +class ProjectSerializer(serializers.LightSerializer): + id = Field(attr="pk") + permalink = MethodField() + name = MethodField() + logo_big_url = MethodField() def get_permalink(self, obj): return resolve_front_url("project", obj.slug) @@ -116,11 +112,11 @@ class ProjectSerializer(serializers.Serializer): ######################################################################## -## History Serializer +# History Serializer ######################################################################## -class HistoryDiffField(serializers.Field): - def to_native(self, value): +class HistoryDiffField(Field): + def to_value(self, value): # Tip: 'value' is the object returned by # taiga.projects.history.models.HistoryEntry.values_diff() @@ -137,21 +133,21 @@ class HistoryDiffField(serializers.Field): return ret -class HistoryEntrySerializer(serializers.ModelSerializer): - diff = HistoryDiffField(source="values_diff") - - class Meta: - model = history_models.HistoryEntry - exclude = ("id", "type", "key", "is_hidden", "is_snapshot", "snapshot", "user", "delete_comment_user", - "values", "created_at") +class HistoryEntrySerializer(serializers.LightSerializer): + comment = Field() + comment_html = Field() + delete_comment_date = Field() + comment_versions = Field() + edit_comment_date = Field() + diff = HistoryDiffField(attr="values_diff") ######################################################################## -## _Misc_ +# _Misc_ ######################################################################## -class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): - custom_attributes_values = serializers.SerializerMethodField("get_custom_attributes_values") +class CustomAttributesValuesWebhookSerializerMixin(serializers.LightSerializer): + custom_attributes_values = MethodField() def custom_attributes_queryset(self, project): raise NotImplementedError() @@ -161,13 +157,13 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): ret = {} for attr in custom_attributes: value = values.get(str(attr["id"]), None) - if value is not None: + if value is not None: ret[attr["name"]] = value return ret try: - values = obj.custom_attributes_values.attributes_values + values = obj.custom_attributes_values.attributes_values custom_attributes = self.custom_attributes_queryset(obj.project).values('id', 'name') return _use_name_instead_id_as_key_in_custom_attributes_values(custom_attributes, values) @@ -175,10 +171,10 @@ class CustomAttributesValuesWebhookSerializerMixin(serializers.ModelSerializer): return None -class RolePointsSerializer(serializers.Serializer): - role = serializers.SerializerMethodField("get_role") - name = serializers.SerializerMethodField("get_name") - value = serializers.SerializerMethodField("get_value") +class RolePointsSerializer(serializers.LightSerializer): + role = MethodField() + name = MethodField() + value = MethodField() def get_role(self, obj): return obj.role.name @@ -190,16 +186,13 @@ class RolePointsSerializer(serializers.Serializer): return obj.points.value -class UserStoryStatusSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - slug = serializers.SerializerMethodField("get_slug") - color = serializers.SerializerMethodField("get_color") - is_closed = serializers.SerializerMethodField("get_is_closed") - is_archived = serializers.SerializerMethodField("get_is_archived") - - def get_pk(self, obj): - return obj.pk +class UserStoryStatusSerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + slug = MethodField() + color = MethodField() + is_closed = MethodField() + is_archived = MethodField() def get_name(self, obj): return obj.name @@ -217,15 +210,12 @@ class UserStoryStatusSerializer(serializers.Serializer): return obj.is_archived -class TaskStatusSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - slug = serializers.SerializerMethodField("get_slug") - color = serializers.SerializerMethodField("get_color") - is_closed = serializers.SerializerMethodField("get_is_closed") - - def get_pk(self, obj): - return obj.pk +class TaskStatusSerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + slug = MethodField() + color = MethodField() + is_closed = MethodField() def get_name(self, obj): return obj.name @@ -240,15 +230,12 @@ class TaskStatusSerializer(serializers.Serializer): return obj.is_closed -class IssueStatusSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - slug = serializers.SerializerMethodField("get_slug") - color = serializers.SerializerMethodField("get_color") - is_closed = serializers.SerializerMethodField("get_is_closed") - - def get_pk(self, obj): - return obj.pk +class IssueStatusSerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + slug = MethodField() + color = MethodField() + is_closed = MethodField() def get_name(self, obj): return obj.name @@ -263,13 +250,10 @@ class IssueStatusSerializer(serializers.Serializer): return obj.is_closed -class IssueTypeSerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - color = serializers.SerializerMethodField("get_color") - - def get_pk(self, obj): - return obj.pk +class IssueTypeSerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + color = MethodField() def get_name(self, obj): return obj.name @@ -278,13 +262,10 @@ class IssueTypeSerializer(serializers.Serializer): return obj.color -class PrioritySerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - color = serializers.SerializerMethodField("get_color") - - def get_pk(self, obj): - return obj.pk +class PrioritySerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + color = MethodField() def get_name(self, obj): return obj.name @@ -293,13 +274,10 @@ class PrioritySerializer(serializers.Serializer): return obj.color -class SeveritySerializer(serializers.Serializer): - id = serializers.SerializerMethodField("get_pk") - name = serializers.SerializerMethodField("get_name") - color = serializers.SerializerMethodField("get_color") - - def get_pk(self, obj): - return obj.pk +class SeveritySerializer(serializers.LightSerializer): + id = Field(attr="pk") + name = MethodField() + color = MethodField() def get_name(self, obj): return obj.name @@ -309,57 +287,90 @@ class SeveritySerializer(serializers.Serializer): ######################################################################## -## Milestone +# Milestone ######################################################################## -class MilestoneSerializer(serializers.ModelSerializer): +class MilestoneSerializer(serializers.LightSerializer): + id = Field() + name = Field() + slug = Field() + estimated_start = Field() + estimated_finish = Field() + created_date = Field() + modified_date = Field() + closed = Field() + disponibility = Field() permalink = serializers.SerializerMethodField("get_permalink") project = ProjectSerializer() owner = UserSerializer() - class Meta: - model = milestone_models.Milestone - exclude = ("order", "watchers") - def get_permalink(self, obj): return resolve_front_url("taskboard", obj.project.slug, obj.slug) ######################################################################## -## User Story +# User Story ######################################################################## -class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): - permalink = serializers.SerializerMethodField("get_permalink") - tags = TagsField(default=[], required=False) - external_reference = PgArrayField(required=False) +class UserStorySerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer): + id = Field() + ref = Field() project = ProjectSerializer() + is_closed = Field() + created_date = Field() + modified_date = Field() + finish_date = Field() + subject = Field() + client_requirement = Field() + team_requirement = Field() + generated_from_issue = Field(attr="generated_from_issue_id") + external_reference = Field() + tribe_gig = Field() + watchers = MethodField() + is_blocked = Field() + blocked_note = Field() + tags = Field() + permalink = serializers.SerializerMethodField("get_permalink") owner = UserSerializer() assigned_to = UserSerializer() - points = RolePointsSerializer(source="role_points", many=True) + points = MethodField() status = UserStoryStatusSerializer() milestone = MilestoneSerializer() - class Meta: - model = us_models.UserStory - exclude = ("backlog_order", "sprint_order", "kanban_order", "version", "total_watchers", "is_watcher") - def get_permalink(self, obj): return resolve_front_url("userstory", obj.project.slug, obj.ref) def custom_attributes_queryset(self, project): return project.userstorycustomattributes.all() + def get_watchers(self, obj): + return list(obj.get_watchers().values_list("id", flat=True)) + + def get_points(self, obj): + return RolePointsSerializer(obj.role_points.all(), many=True).data + ######################################################################## -## Task +# Task ######################################################################## -class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): +class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer): + id = Field() + ref = Field() + created_date = Field() + modified_date = Field() + finished_date = Field() + subject = Field() + us_order = Field() + taskboard_order = Field() + is_iocaine = Field() + external_reference = Field() + watchers = MethodField() + is_blocked = Field() + blocked_note = Field() + description = Field() + tags = Field() permalink = serializers.SerializerMethodField("get_permalink") - tags = TagsField(default=[], required=False) project = ProjectSerializer() owner = UserSerializer() assigned_to = UserSerializer() @@ -367,25 +378,32 @@ class TaskSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatch user_story = UserStorySerializer() milestone = MilestoneSerializer() - class Meta: - model = task_models.Task - exclude = ("version", "total_watchers", "is_watcher") - def get_permalink(self, obj): return resolve_front_url("task", obj.project.slug, obj.ref) def custom_attributes_queryset(self, project): return project.taskcustomattributes.all() + def get_watchers(self, obj): + return list(obj.get_watchers().values_list("id", flat=True)) + ######################################################################## -## Issue +# Issue ######################################################################## -class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatchedResourceModelSerializer, - serializers.ModelSerializer): +class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, serializers.LightSerializer): + id = Field() + ref = Field() + created_date = Field() + modified_date = Field() + finished_date = Field() + subject = Field() + external_reference = Field() + watchers = MethodField() + description = Field() + tags = Field() permalink = serializers.SerializerMethodField("get_permalink") - tags = TagsField(default=[], required=False) project = ProjectSerializer() milestone = MilestoneSerializer() owner = UserSerializer() @@ -395,30 +413,30 @@ class IssueSerializer(CustomAttributesValuesWebhookSerializerMixin, EditableWatc priority = PrioritySerializer() severity = SeveritySerializer() - class Meta: - model = issue_models.Issue - exclude = ("version", "total_watchers", "is_watcher") - def get_permalink(self, obj): return resolve_front_url("issue", obj.project.slug, obj.ref) def custom_attributes_queryset(self, project): return project.issuecustomattributes.all() + def get_watchers(self, obj): + return list(obj.get_watchers().values_list("id", flat=True)) + ######################################################################## -## Wiki Page +# Wiki Page ######################################################################## -class WikiPageSerializer(serializers.ModelSerializer): +class WikiPageSerializer(serializers.LightSerializer): + id = Field() + slug = Field() + content = Field() + created_date = Field() + modified_date = Field() permalink = serializers.SerializerMethodField("get_permalink") project = ProjectSerializer() owner = UserSerializer() last_modifier = UserSerializer() - class Meta: - model = wiki_models.WikiPage - exclude = ("watchers", "total_watchers", "is_watcher", "version") - def get_permalink(self, obj): return resolve_front_url("wiki", obj.project.slug, obj.slug) diff --git a/taiga/webhooks/tasks.py b/taiga/webhooks/tasks.py index 7990b928..334cd52d 100644 --- a/taiga/webhooks/tasks.py +++ b/taiga/webhooks/tasks.py @@ -149,5 +149,4 @@ def test_webhook(webhook_id, url, key, by, date): data['by'] = UserSerializer(by).data data['date'] = date data['data'] = {"test": "test"} - return _send_request(webhook_id, url, key, data) diff --git a/taiga/webhooks/validators.py b/taiga/webhooks/validators.py new file mode 100644 index 00000000..b95e2e64 --- /dev/null +++ b/taiga/webhooks/validators.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.api import validators + +from .models import Webhook + + +class WebhookValidator(validators.ModelValidator): + class Meta: + model = Webhook diff --git a/tests/integration/resources_permissions/test_issues_resources.py b/tests/integration/resources_permissions/test_issues_resources.py index d9b391f8..4f93ea0c 100644 --- a/tests/integration/resources_permissions/test_issues_resources.py +++ b/tests/integration/resources_permissions/test_issues_resources.py @@ -22,7 +22,11 @@ import uuid from django.core.urlresolvers import reverse from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.projects.issues.models import Issue from taiga.projects.issues.serializers import IssueSerializer +from taiga.projects.issues.utils import attach_extra_info as attach_issue_extra_info from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.base.utils import json @@ -61,22 +65,29 @@ def data(): public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, issues_csv_uuid=uuid.uuid4().hex, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) m.public_membership = f.MembershipFactory(project=m.public_project, user=m.project_member_with_perms, @@ -129,24 +140,31 @@ def data(): priority__project=m.public_project, type__project=m.public_project, milestone__project=m.public_project) + m.public_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.public_issue.id) + m.private_issue1 = f.IssueFactory(project=m.private_project1, status__project=m.private_project1, severity__project=m.private_project1, priority__project=m.private_project1, type__project=m.private_project1, milestone__project=m.private_project1) + m.private_issue1 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue1.id) + m.private_issue2 = f.IssueFactory(project=m.private_project2, status__project=m.private_project2, severity__project=m.private_project2, priority__project=m.private_project2, type__project=m.private_project2, milestone__project=m.private_project2) + m.private_issue2 = attach_issue_extra_info(Issue.objects.all()).get(id=m.private_issue2.id) + m.blocked_issue = f.IssueFactory(project=m.blocked_project, status__project=m.blocked_project, severity__project=m.blocked_project, priority__project=m.blocked_project, type__project=m.blocked_project, milestone__project=m.blocked_project) + m.blocked_issue = attach_issue_extra_info(Issue.objects.all()).get(id=m.blocked_issue.id) return m @@ -443,24 +461,28 @@ def test_issue_put_update_with_project_change(client): project1.save() project2.save() - membership1 = f.MembershipFactory(project=project1, - user=user1, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership2 = f.MembershipFactory(project=project2, - user=user1, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership3 = f.MembershipFactory(project=project1, - user=user2, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership4 = f.MembershipFactory(project=project2, - user=user3, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) issue = f.IssueFactory.create(project=project1) + issue = attach_issue_extra_info(Issue.objects.all()).get(id=issue.id) url = reverse('issues-detail', kwargs={"pk": issue.pk}) diff --git a/tests/integration/resources_permissions/test_milestones_resources.py b/tests/integration/resources_permissions/test_milestones_resources.py index a1a06172..1a3ab5cb 100644 --- a/tests/integration/resources_permissions/test_milestones_resources.py +++ b/tests/integration/resources_permissions/test_milestones_resources.py @@ -22,8 +22,11 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.utils import attach_extra_info as attach_project_extra_info from taiga.projects.milestones.serializers import MilestoneSerializer from taiga.projects.milestones.models import Milestone +from taiga.projects.milestones.utils import attach_extra_info as attach_milestone_extra_info from taiga.projects.notifications.services import add_watcher from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS @@ -56,44 +59,55 @@ def data(): anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project1, user=m.project_member_without_perms, role__project=m.private_project1, role__permissions=[]) - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project2, user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) - m.blocked_membership = f.MembershipFactory(project=m.blocked_project, - user=m.project_member_with_perms, - role__project=m.blocked_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.blocked_project, user=m.project_member_without_perms, role__project=m.blocked_project, @@ -112,13 +126,17 @@ def data(): is_admin=True) f.MembershipFactory(project=m.blocked_project, - user=m.project_owner, - is_admin=True) + user=m.project_owner, + is_admin=True) m.public_milestone = f.MilestoneFactory(project=m.public_project) + m.public_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.public_milestone.id) m.private_milestone1 = f.MilestoneFactory(project=m.private_project1) + m.private_milestone1 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone1.id) m.private_milestone2 = f.MilestoneFactory(project=m.private_project2) + m.private_milestone2 = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.private_milestone2.id) m.blocked_milestone = f.MilestoneFactory(project=m.blocked_project) + m.blocked_milestone = attach_milestone_extra_info(Milestone.objects.all()).get(id=m.blocked_milestone.id) return m @@ -422,16 +440,16 @@ def test_milestone_watchers_list(client, data): def test_milestone_watchers_retrieve(client, data): add_watcher(data.public_milestone, data.project_owner) public_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.public_milestone.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_milestone1, data.project_owner) private_url1 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_milestone2, data.project_owner) private_url2 = reverse('milestone-watchers-detail', kwargs={"resource_id": data.private_milestone2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.blocked_milestone, data.project_owner) blocked_url = reverse('milestone-watchers-detail', kwargs={"resource_id": data.blocked_milestone.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 949893b6..1410c86d 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -22,8 +22,10 @@ from django.apps import apps from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects 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) diff --git a/tests/integration/resources_permissions/test_tasks_resources.py b/tests/integration/resources_permissions/test_tasks_resources.py index 5eaf5243..4d6427dd 100644 --- a/tests/integration/resources_permissions/test_tasks_resources.py +++ b/tests/integration/resources_permissions/test_tasks_resources.py @@ -23,12 +23,16 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects.models import Project from taiga.projects.tasks.serializers import TaskSerializer +from taiga.projects.tasks.models import Task +from taiga.projects.tasks.utils import attach_extra_info as attach_task_extra_info +from taiga.projects.utils import attach_extra_info as attach_project_extra_info from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin from tests import factories as f -from tests.utils import helper_test_http_method, disconnect_signals, reconnect_signals +from tests.utils import helper_test_http_method, reconnect_signals from taiga.projects.votes.services import add_vote from taiga.projects.notifications.services import add_watcher @@ -38,10 +42,6 @@ import pytest pytestmark = pytest.mark.django_db -def setup_function(function): - disconnect_signals() - - def setup_function(function): reconnect_signals() @@ -61,47 +61,61 @@ def data(): public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) + m.blocked_project = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, tasks_csv_uuid=uuid.uuid4().hex, blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.MembershipFactory(project=m.private_project1, - user=m.project_member_without_perms, - role__project=m.private_project1, - role__permissions=[]) - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - f.MembershipFactory(project=m.private_project2, - user=m.project_member_without_perms, - role__project=m.private_project2, - role__permissions=[]) - m.blocked_membership = f.MembershipFactory(project=m.blocked_project, - user=m.project_member_with_perms, - role__project=m.blocked_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project1, + user=m.project_member_without_perms, + role__project=m.private_project1, + role__permissions=[]) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory( + project=m.private_project2, + user=m.project_member_without_perms, + role__project=m.private_project2, + role__permissions=[]) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.blocked_project, user=m.project_member_without_perms, role__project=m.blocked_project, @@ -120,8 +134,8 @@ def data(): is_admin=True) f.MembershipFactory(project=m.blocked_project, - user=m.project_owner, - is_admin=True) + user=m.project_owner, + is_admin=True) milestone_public_task = f.MilestoneFactory(project=m.public_project) milestone_private_task1 = f.MilestoneFactory(project=m.private_project1) @@ -133,21 +147,28 @@ def data(): milestone=milestone_public_task, user_story__project=m.public_project, user_story__milestone=milestone_public_task) + m.public_task = attach_task_extra_info(Task.objects.all()).get(id=m.public_task.id) + m.private_task1 = f.TaskFactory(project=m.private_project1, status__project=m.private_project1, milestone=milestone_private_task1, user_story__project=m.private_project1, user_story__milestone=milestone_private_task1) + m.private_task1 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task1.id) + m.private_task2 = f.TaskFactory(project=m.private_project2, status__project=m.private_project2, milestone=milestone_private_task2, user_story__project=m.private_project2, user_story__milestone=milestone_private_task2) + m.private_task2 = attach_task_extra_info(Task.objects.all()).get(id=m.private_task2.id) + m.blocked_task = f.TaskFactory(project=m.blocked_project, - status__project=m.blocked_project, - milestone=milestone_blocked_task, - user_story__project=m.blocked_project, - user_story__milestone=milestone_blocked_task) + status__project=m.blocked_project, + milestone=milestone_blocked_task, + user_story__project=m.blocked_project, + user_story__milestone=milestone_blocked_task) + m.blocked_task = attach_task_extra_info(Task.objects.all()).get(id=m.blocked_task.id) m.public_project.default_task_status = m.public_task.status m.public_project.save() @@ -404,24 +425,28 @@ def test_task_put_update_with_project_change(client): project1.save() project2.save() - membership1 = f.MembershipFactory(project=project1, - user=user1, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership2 = f.MembershipFactory(project=project2, - user=user1, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership3 = f.MembershipFactory(project=project1, - user=user2, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership4 = f.MembershipFactory(project=project2, - user=user3, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) task = f.TaskFactory.create(project=project1) + task = attach_task_extra_info(Task.objects.all()).get(id=task.id) url = reverse('tasks-detail', kwargs={"pk": task.pk}) @@ -739,17 +764,17 @@ def test_task_voters_list(client, data): def test_task_voters_retrieve(client, data): add_vote(data.public_task, data.project_owner) public_url = reverse('task-voters-detail', kwargs={"resource_id": data.public_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_vote(data.private_task1, data.project_owner) private_url1 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_vote(data.private_task2, data.project_owner) private_url2 = reverse('task-voters-detail', kwargs={"resource_id": data.private_task2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_vote(data.blocked_task, data.project_owner) blocked_url = reverse('task-voters-detail', kwargs={"resource_id": data.blocked_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, @@ -844,17 +869,17 @@ def test_task_watchers_list(client, data): def test_task_watchers_retrieve(client, data): add_watcher(data.public_task, data.project_owner) public_url = reverse('task-watchers-detail', kwargs={"resource_id": data.public_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_task1, data.project_owner) private_url1 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_task2, data.project_owner) private_url2 = reverse('task-watchers-detail', kwargs={"resource_id": data.private_task2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.blocked_task, data.project_owner) blocked_url = reverse('task-watchers-detail', kwargs={"resource_id": data.blocked_task.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, data.registered_user, diff --git a/tests/integration/resources_permissions/test_userstories_resources.py b/tests/integration/resources_permissions/test_userstories_resources.py index c9f95a31..4eb0c416 100644 --- a/tests/integration/resources_permissions/test_userstories_resources.py +++ b/tests/integration/resources_permissions/test_userstories_resources.py @@ -23,7 +23,11 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects import choices as project_choices +from taiga.projects.models import Project +from taiga.projects.utils import attach_extra_info as attach_project_extra_info +from taiga.projects.userstories.models import UserStory from taiga.projects.userstories.serializers import UserStorySerializer +from taiga.projects.userstories.utils import attach_extra_info as attach_userstory_extra_info from taiga.permissions.choices import MEMBERS_PERMISSIONS, ANON_PERMISSIONS from taiga.projects.occ import OCCResourceMixin @@ -61,47 +65,58 @@ def data(): public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) + m.public_project = attach_project_extra_info(Project.objects.all()).get(id=m.public_project.id) + m.private_project1 = f.ProjectFactory(is_private=True, anon_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), public_permissions=list(map(lambda x: x[0], ANON_PERMISSIONS)), owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) + m.private_project1 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project1.id) + m.private_project2 = f.ProjectFactory(is_private=True, anon_permissions=[], public_permissions=[], owner=m.project_owner, userstories_csv_uuid=uuid.uuid4().hex) - m.blocked_project = f.ProjectFactory(is_private=True, - anon_permissions=[], - public_permissions=[], - owner=m.project_owner, - userstories_csv_uuid=uuid.uuid4().hex, - blocked_code=project_choices.BLOCKED_BY_STAFF) + m.private_project2 = attach_project_extra_info(Project.objects.all()).get(id=m.private_project2.id) - m.public_membership = f.MembershipFactory(project=m.public_project, - user=m.project_member_with_perms, - role__project=m.public_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - m.private_membership1 = f.MembershipFactory(project=m.private_project1, - user=m.project_member_with_perms, - role__project=m.private_project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.blocked_project = f.ProjectFactory(is_private=True, + anon_permissions=[], + public_permissions=[], + owner=m.project_owner, + userstories_csv_uuid=uuid.uuid4().hex, + blocked_code=project_choices.BLOCKED_BY_STAFF) + m.blocked_project = attach_project_extra_info(Project.objects.all()).get(id=m.blocked_project.id) + + m.public_membership = f.MembershipFactory( + project=m.public_project, + user=m.project_member_with_perms, + role__project=m.public_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership1 = f.MembershipFactory( + project=m.private_project1, + user=m.project_member_with_perms, + role__project=m.private_project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project1, user=m.project_member_without_perms, role__project=m.private_project1, role__permissions=[]) - m.private_membership2 = f.MembershipFactory(project=m.private_project2, - user=m.project_member_with_perms, - role__project=m.private_project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.private_membership2 = f.MembershipFactory( + project=m.private_project2, + user=m.project_member_with_perms, + role__project=m.private_project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.private_project2, user=m.project_member_without_perms, role__project=m.private_project2, role__permissions=[]) - m.blocked_membership = f.MembershipFactory(project=m.blocked_project, - user=m.project_member_with_perms, - role__project=m.blocked_project, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + m.blocked_membership = f.MembershipFactory( + project=m.blocked_project, + user=m.project_member_with_perms, + role__project=m.blocked_project, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) f.MembershipFactory(project=m.blocked_project, user=m.project_member_without_perms, role__project=m.blocked_project, @@ -120,8 +135,8 @@ def data(): is_admin=True) f.MembershipFactory(project=m.blocked_project, - user=m.project_owner, - is_admin=True) + user=m.project_owner, + is_admin=True) m.public_points = f.PointsFactory(project=m.public_project) m.private_points1 = f.PointsFactory(project=m.private_project1) @@ -144,15 +159,19 @@ def data(): user_story__milestone__project=m.private_project2, user_story__status__project=m.private_project2) m.blocked_role_points = f.RolePointsFactory(role=m.blocked_project.roles.all()[0], - points=m.blocked_points, - user_story__project=m.blocked_project, - user_story__milestone__project=m.blocked_project, - user_story__status__project=m.blocked_project) + points=m.blocked_points, + user_story__project=m.blocked_project, + user_story__milestone__project=m.blocked_project, + user_story__status__project=m.blocked_project) m.public_user_story = m.public_role_points.user_story + m.public_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.public_user_story.id) m.private_user_story1 = m.private_role_points1.user_story + m.private_user_story1 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story1.id) m.private_user_story2 = m.private_role_points2.user_story + m.private_user_story2 = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.private_user_story2.id) m.blocked_user_story = m.blocked_role_points.user_story + m.blocked_user_story = attach_userstory_extra_info(UserStory.objects.all()).get(id=m.blocked_user_story.id) return m @@ -380,24 +399,28 @@ def test_user_story_put_update_with_project_change(client): project1.save() project2.save() - membership1 = f.MembershipFactory(project=project1, - user=user1, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership2 = f.MembershipFactory(project=project2, - user=user1, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership3 = f.MembershipFactory(project=project1, - user=user2, - role__project=project1, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) - membership4 = f.MembershipFactory(project=project2, - user=user3, - role__project=project2, - role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + project1 = attach_project_extra_info(Project.objects.all()).get(id=project1.id) + project2 = attach_project_extra_info(Project.objects.all()).get(id=project2.id) + + f.MembershipFactory(project=project1, + user=user1, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user1, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project1, + user=user2, + role__project=project1, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) + f.MembershipFactory(project=project2, + user=user3, + role__project=project2, + role__permissions=list(map(lambda x: x[0], MEMBERS_PERMISSIONS))) us = f.UserStoryFactory.create(project=project1) + us = attach_userstory_extra_info(UserStory.objects.all()).get(id=us.id) url = reverse('userstories-detail', kwargs={"pk": us.pk}) @@ -592,7 +615,6 @@ def test_user_story_delete(client, data): assert results == [401, 403, 403, 451] - def test_user_story_action_bulk_create(client, data): url = reverse('userstories-bulk-create') @@ -746,7 +768,7 @@ def test_user_story_voters_retrieve(client, data): add_vote(data.blocked_user_story, data.project_owner) blocked_url = reverse('userstory-voters-detail', kwargs={"resource_id": data.blocked_user_story.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, data.registered_user, @@ -840,16 +862,16 @@ def test_userstory_watchers_list(client, data): def test_userstory_watchers_retrieve(client, data): add_watcher(data.public_user_story, data.project_owner) public_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.public_user_story.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_user_story1, data.project_owner) private_url1 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story1.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.private_user_story2, data.project_owner) private_url2 = reverse('userstory-watchers-detail', kwargs={"resource_id": data.private_user_story2.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) add_watcher(data.blocked_user_story, data.project_owner) blocked_url = reverse('userstory-watchers-detail', kwargs={"resource_id": data.blocked_user_story.pk, - "pk": data.project_owner.pk}) + "pk": data.project_owner.pk}) users = [ None, diff --git a/tests/integration/resources_permissions/test_webhooks_resources.py b/tests/integration/resources_permissions/test_webhooks_resources.py index afc3597d..34d4cf00 100644 --- a/tests/integration/resources_permissions/test_webhooks_resources.py +++ b/tests/integration/resources_permissions/test_webhooks_resources.py @@ -242,16 +242,19 @@ def test_webhook_action_test(client, data): ] with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + _send_request_mock.return_value = data.webhooklog1 results = helper_test_http_method(client, 'post', url1, None, users) assert results == [404, 404, 200] assert _send_request_mock.called is True with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + _send_request_mock.return_value = data.webhooklog1 results = helper_test_http_method(client, 'post', url2, None, users) assert results == [404, 404, 404] assert _send_request_mock.called is False with mock.patch('taiga.webhooks.tasks._send_request') as _send_request_mock: + _send_request_mock.return_value = data.webhooklog1 results = helper_test_http_method(client, 'post', blocked_url, None, users) assert results == [404, 404, 451] assert _send_request_mock.called is False diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index ad28cc16..18562a8e 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -43,7 +43,7 @@ def test_update_milestone_with_userstories_list(client): form_data = { "name": "test", - "user_stories": [UserStorySerializer(us).data] + "user_stories": [{"id": us.id}] } client.login(user) diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index 661b68ff..d8943e71 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -790,7 +790,7 @@ def test_watchers_assignation_for_issue(client): assert response.status_code == 400 issue = f.create_issue(project=project1, owner=user1) - data = dict(IssueSerializer(issue).data) + data = {} data["id"] = None data["version"] = None data["watchers"] = [user1.pk, user2.pk] @@ -802,8 +802,7 @@ def test_watchers_assignation_for_issue(client): # Test the impossible case when project is not # exists in create request, and validator works as expected issue = f.create_issue(project=project1, owner=user1) - data = dict(IssueSerializer(issue).data) - + data = {} data["id"] = None data["watchers"] = [user1.pk, user2.pk] data["project"] = None @@ -842,10 +841,11 @@ def test_watchers_assignation_for_task(client): assert response.status_code == 400 task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) - data = dict(TaskSerializer(task).data) - data["id"] = None - data["version"] = None - data["watchers"] = [user1.pk, user2.pk] + data = { + "id": None, + "version": None, + "watchers": [user1.pk, user2.pk] + } url = reverse("tasks-list") response = client.json.post(url, json.dumps(data)) @@ -854,11 +854,11 @@ def test_watchers_assignation_for_task(client): # Test the impossible case when project is not # exists in create request, and validator works as expected task = f.create_task(project=project1, owner=user1, status__project=project1, milestone__project=project1) - data = dict(TaskSerializer(task).data) - - data["id"] = None - data["watchers"] = [user1.pk, user2.pk] - data["project"] = None + data = { + "id": None, + "watchers": [user1.pk, user2.pk], + "project": None + } url = reverse("tasks-list") response = client.json.post(url, json.dumps(data)) @@ -894,10 +894,11 @@ def test_watchers_assignation_for_us(client): assert response.status_code == 400 us = f.create_userstory(project=project1, owner=user1, status__project=project1) - data = dict(UserStorySerializer(us).data) - data["id"] = None - data["version"] = None - data["watchers"] = [user1.pk, user2.pk] + data = { + "id": None, + "version": None, + "watchers": [user1.pk, user2.pk] + } url = reverse("userstories-list") response = client.json.post(url, json.dumps(data)) @@ -906,11 +907,11 @@ def test_watchers_assignation_for_us(client): # Test the impossible case when project is not # exists in create request, and validator works as expected us = f.create_userstory(project=project1, owner=user1, status__project=project1) - data = dict(UserStorySerializer(us).data) - - data["id"] = None - data["watchers"] = [user1.pk, user2.pk] - data["project"] = None + data = { + "id": None, + "watchers": [user1.pk, user2.pk], + "project": None + } url = reverse("userstories-list") response = client.json.post(url, json.dumps(data)) diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 7eac9b06..10805f8e 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import copy import uuid import csv @@ -26,7 +25,6 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json from taiga.projects.userstories import services, models -from taiga.projects.userstories.serializers import UserStorySerializer from .. import factories as f @@ -108,7 +106,7 @@ def test_create_userstory_without_default_values(client): client.login(user) response = client.json.post(url, json.dumps(data)) assert response.status_code == 201 - assert response.data['status'] == None + assert response.data['status'] is None def test_api_delete_userstory(client): @@ -211,7 +209,7 @@ def test_api_update_milestone_in_bulk_invalid_milestone(client): f.MembershipFactory.create(project=project, user=project.owner, is_admin=True) us1 = f.create_userstory(project=project) us2 = f.create_userstory(project=project) - m1 = f.MilestoneFactory.create(project=project) + f.MilestoneFactory.create(project=project) m2 = f.MilestoneFactory.create() url = reverse("userstories-bulk-update-milestone") @@ -262,48 +260,53 @@ def test_update_userstory_points(client): f.MembershipFactory.create(project=project, user=user1, role=role1, is_admin=True) f.MembershipFactory.create(project=project, user=user2, role=role2) - f.PointsFactory.create(project=project, value=None) - f.PointsFactory.create(project=project, value=1) + points1 = f.PointsFactory.create(project=project, value=None) + points2 = f.PointsFactory.create(project=project, value=1) points3 = f.PointsFactory.create(project=project, value=2) - us = f.UserStoryFactory.create(project=project,owner=user1, status__project=project, + us = f.UserStoryFactory.create(project=project, owner=user1, status__project=project, milestone__project=project) - usdata = UserStorySerializer(us).data url = reverse("userstories-detail", args=[us.pk]) client.login(user1) # invalid role - data = {} - data["version"] = usdata["version"] - data["points"] = copy.copy(usdata["points"]) - data["points"].update({"222222": points3.pk}) + data = { + "version": us.version, + "points": { + str(role1.pk): points1.pk, + str(role2.pk): points2.pk, + "222222": points3.pk + } + } response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 # invalid point - data = {} - data["version"] = usdata["version"] - data["points"] = copy.copy(usdata["points"]) - data["points"].update({str(role1.pk): "999999"}) + data = { + "version": us.version, + "points": { + str(role1.pk): 999999, + str(role2.pk): points2.pk + } + } response = client.json.patch(url, json.dumps(data)) assert response.status_code == 400 # Api should save successful - data = {} - data["version"] = usdata["version"] - data["points"] = copy.copy(usdata["points"]) - data["points"].update({str(role1.pk): points3.pk}) + data = { + "version": us.version, + "points": { + str(role1.pk): points3.pk, + str(role2.pk): points2.pk + } + } response = client.json.patch(url, json.dumps(data)) - us = models.UserStory.objects.get(pk=us.pk) - usdatanew = UserStorySerializer(us).data - assert response.status_code == 200, str(response.content) - assert response.data["points"] == usdatanew['points'] - assert response.data["points"] != usdata['points'] + assert response.data["points"][str(role1.pk)] == points3.pk def test_update_userstory_rolepoints_on_add_new_role(client): @@ -438,32 +441,32 @@ def test_api_filters_data(client): # | 9 | user2 | user3 | tag0 | # ------------------------------------------------------ - user_story0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, - status=status3, tags=[tag1]) - user_story1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, - status=status3, tags=[tag2]) - user_story2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, - status=status1, tags=[tag1, tag2]) - user_story3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, - status=status0, tags=[tag3]) - user_story4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, - status=status0, tags=[tag1, tag2, tag3]) - user_story5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, - status=status2, tags=[tag3]) - user_story6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, - status=status3, tags=[tag1, tag2]) - user_story7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, - status=status0, tags=[tag3]) - user_story8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, - status=status3, tags=[tag1]) - user_story9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, - status=status1, tags=[tag0]) + f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status3, tags=[tag1]) + f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, + status=status3, tags=[tag2]) + f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, + status=status1, tags=[tag1, tag2]) + f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status0, tags=[tag3]) + f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, + status=status0, tags=[tag1, tag2, tag3]) + f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, + status=status2, tags=[tag3]) + f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, + status=status3, tags=[tag1, tag2]) + f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, + status=status0, tags=[tag3]) + f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, + status=status3, tags=[tag1]) + f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, + status=status1, tags=[tag0]) url = reverse("userstories-filters-data") + "?project={}".format(project.id) client.login(user1) - ## No filter + # No filter response = client.get(url) assert response.status_code == 200 @@ -471,7 +474,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4 assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3 - assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4 + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 4 assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3 assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1 @@ -486,7 +489,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 - ## Filter ((status0 or status3) + # Filter ((status0 or status3) response = client.get(url + "&status={},{}".format(status3.id, status0.id)) assert response.status_code == 200 @@ -494,7 +497,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 3 assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 - assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 3 assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 @@ -509,7 +512,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3 assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3 - ## Filter ((tag1 and tag2) and (user1 or user2)) + # Filter ((tag1 and tag2) and (user1 or user2)) response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) assert response.status_code == 200 @@ -517,7 +520,7 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1 assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 - assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] is None, response.data["assigned_to"]))["count"] == 0 assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0 assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 @@ -556,7 +559,7 @@ def test_custom_fields_csv_generation(): attr = f.UserStoryCustomAttributeFactory.create(project=project, name="attr1", description="desc") us = f.UserStoryFactory.create(project=project) attr_values = us.custom_attributes_values - attr_values.attributes_values = {str(attr.id):"val1"} + attr_values.attributes_values = {str(attr.id): "val1"} attr_values.save() queryset = project.user_stories.all() data = services.userstories_to_csv(project, queryset) @@ -595,7 +598,7 @@ def test_update_userstory_update_watchers(client): client.login(user=us.owner) url = reverse("userstories-detail", kwargs={"pk": us.pk}) - data = {"watchers": [watching_user.id], "version":1} + data = {"watchers": [watching_user.id], "version": 1} response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 @@ -614,7 +617,7 @@ def test_update_userstory_remove_watchers(client): client.login(user=us.owner) url = reverse("userstories-detail", kwargs={"pk": us.pk}) - data = {"watchers": [], "version":1} + data = {"watchers": [], "version": 1} response = client.json.patch(url, json.dumps(data)) assert response.status_code == 200 @@ -634,7 +637,7 @@ def test_update_userstory_update_tribe_gig(client): "id": 2, "title": "This is a gig test title" }, - "version":1 + "version": 1 } client.login(user=us.owner) diff --git a/tests/integration/test_webhooks_issues.py b/tests/integration/test_webhooks_issues.py index 491ec5b4..8789408d 100644 --- a/tests/integration/test_webhooks_issues.py +++ b/tests/integration/test_webhooks_issues.py @@ -19,7 +19,6 @@ import pytest from unittest.mock import patch -from unittest.mock import Mock from .. import factories as f @@ -29,8 +28,6 @@ from taiga.projects.history import services pytestmark = pytest.mark.django_db(transaction=True) -from taiga.base.utils import json - def test_webhooks_when_create_issue(settings): settings.WEBHOOKS_ENABLED = True project = f.ProjectFactory() @@ -79,7 +76,7 @@ def test_webhooks_when_update_issue(settings): assert data["data"]["subject"] == obj.subject assert data["change"]["comment"] == "test_comment" assert data["change"]["diff"]["subject"]["to"] == data["data"]["subject"] - assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] + assert data["change"]["diff"]["subject"]["from"] != data["data"]["subject"] def test_webhooks_when_delete_issue(settings): diff --git a/tests/unit/test_serializer_mixins.py b/tests/unit/test_serializer_mixins.py index 349a912c..cc88552f 100644 --- a/tests/unit/test_serializer_mixins.py +++ b/tests/unit/test_serializer_mixins.py @@ -19,46 +19,43 @@ import pytest -from .. import factories as f from django.db import models -from taiga.projects.mixins.serializers import ValidateDuplicatedNameInProjectMixin -from taiga.projects.models import Project +from taiga.base.api.validators import ModelValidator +from taiga.projects.validators import DuplicatedNameInProjectValidator pytestmark = pytest.mark.django_db(transaction=True) -import factory - class AuxProjectModel(models.Model): pass + class AuxModelWithNameAttribute(models.Model): name = models.CharField(max_length=255, null=False, blank=False) project = models.ForeignKey(AuxProjectModel, null=False, blank=False) -class AuxSerializer(ValidateDuplicatedNameInProjectMixin): +class AuxValidator(DuplicatedNameInProjectValidator, ModelValidator): class Meta: model = AuxModelWithNameAttribute - def test_duplicated_name_validation(): project = AuxProjectModel.objects.create() - instance_1 = AuxModelWithNameAttribute.objects.create(name="1", project=project) + AuxModelWithNameAttribute.objects.create(name="1", project=project) instance_2 = AuxModelWithNameAttribute.objects.create(name="2", project=project) # No duplicated_name - serializer = AuxSerializer(data={"name": "3", "project": project.id}) + validator = AuxValidator(data={"name": "3", "project": project.id}) - assert serializer.is_valid() + assert validator.is_valid() # Create duplicated_name - serializer = AuxSerializer(data={"name": "1", "project": project.id}) + validator = AuxValidator(data={"name": "1", "project": project.id}) - assert not serializer.is_valid() + assert not validator.is_valid() # Update name to existing one - serializer = AuxSerializer(data={"id": instance_2.id, "name": "1","project": project.id}) + validator = AuxValidator(data={"id": instance_2.id, "name": "1", "project": project.id}) - assert not serializer.is_valid() + assert not validator.is_valid()