Splitting validators and serializers

remotes/origin/issue/4795/notification_even_they_are_disabled
Alejandro Alonso 2016-06-29 09:45:00 +02:00 committed by Jesús Espino
parent 78a2118e8e
commit 4864b9f957
67 changed files with 2077 additions and 1740 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,14 +34,18 @@ from taiga.projects.occ import OCCResourceMixin
from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin, VotersViewSetMixin
from .utils import attach_extra_info
from . import models
from . import services
from . import permissions
from . import serializers
from . import validators
class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
validator_class = validators.IssueValidator
queryset = models.Issue.objects.all()
permission_classes = (permissions.IssuePermission, )
filter_backends = (filters.CanViewIssuesFilterBackend,
@ -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):

View File

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

View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.projects.notifications.utils import attach_watchers_to_queryset
from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.projects.votes.utils import attach_is_voter_to_queryset
def attach_generated_user_stories(queryset, as_field="generated_user_stories_attr"):
"""Attach generated user stories json column to each object of the queryset.
:param queryset: A Django issues queryset object.
:param as_field: Attach the generated user stories as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(t))
FROM(
SELECT
userstories_userstory.id,
userstories_userstory.ref,
userstories_userstory.subject
FROM userstories_userstory
WHERE generated_from_issue_id = {tbl}.id) t"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_extra_info(queryset, user=None):
queryset = attach_generated_user_stories(queryset)
queryset = attach_total_voters_to_queryset(queryset)
queryset = attach_watchers_to_queryset(queryset)
queryset = attach_total_watchers_to_queryset(queryset)
queryset = attach_is_voter_to_queryset(queryset, user)
queryset = attach_is_watcher_to_queryset(queryset, user)
return queryset

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.fields import PgArrayField
from taiga.projects.notifications.mixins import EditableWatchedResourceSerializer
from taiga.projects.notifications.validators import WatchersValidator
from taiga.projects.tagging.fields import TagsAndTagsColorsField
from taiga.projects.validators import ProjectExistsValidator
from . import models
class IssueValidator(WatchersValidator, EditableWatchedResourceSerializer,
validators.ModelValidator):
tags = TagsAndTagsColorsField(default=[], required=False)
external_reference = PgArrayField(required=False)
class Meta:
model = models.Issue
read_only_fields = ('id', 'ref', 'created_date', 'modified_date', 'owner')
class IssuesBulkValidator(ProjectExistsValidator, validators.Validator):
project_id = serializers.IntegerField()
bulk_issues = serializers.CharField()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,8 +16,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import serpy
from functools import partial
from operator import is_not
@ -28,16 +26,12 @@ from taiga.base import response
from taiga.base.decorators import detail_route
from taiga.base.api import serializers
from taiga.base.api.utils import get_object_or_404
from taiga.base.fields import WatchersField
from taiga.base.fields import WatchersField, MethodField
from taiga.projects.notifications import services
from taiga.projects.notifications.utils import (attach_watchers_to_queryset,
attach_is_watcher_to_queryset,
attach_total_watchers_to_queryset)
from . serializers import WatcherSerializer
class WatchedResourceMixin:
"""
Rest Framework resource mixin for resources susceptible
@ -54,14 +48,6 @@ class WatchedResourceMixin:
_not_notify = False
def attach_watchers_attrs_to_queryset(self, queryset):
queryset = attach_watchers_to_queryset(queryset)
queryset = attach_total_watchers_to_queryset(queryset)
if self.request.user.is_authenticated():
queryset = attach_is_watcher_to_queryset(queryset, self.request.user)
return queryset
@detail_route(methods=["POST"])
def watch(self, request, pk=None):
obj = self.get_object()
@ -186,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

View File

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

View File

@ -16,135 +16,121 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import serpy
from django.utils.translation import ugettext as _
from django.db.models import Q
from taiga.base.api import serializers
from taiga.base.fields import JsonField
from taiga.base.fields import PgArrayField
from taiga.base.fields import Field, MethodField, I18NField
from taiga.permissions import services as permissions_services
from taiga.users.services import get_photo_or_gravatar_url
from taiga.users.serializers import UserBasicInfoSerializer
from taiga.users.serializers import ProjectRoleSerializer
from taiga.users.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()

View File

@ -26,7 +26,6 @@ from taiga.base.decorators import list_route
from taiga.base.api import ModelCrudViewSet, ModelListViewSet
from taiga.base.api.mixins import BlockedByProjectMixin
from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.models import Project, TaskStatus
from taiga.projects.notifications.mixins import WatchedResourceMixin, WatchersViewSetMixin
@ -38,10 +37,14 @@ from . import models
from . import permissions
from . import serializers
from . import services
from . import validators
from . import utils as tasks_utils
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin,
WatchedResourceMixin, TaggedResourceMixin, BlockedByProjectMixin,
ModelCrudViewSet):
validator_class = validators.TaskValidator
queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,)
filter_backends = (filters.CanViewTasksFilterBackend,
@ -74,17 +77,15 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
def get_queryset(self):
qs = super().get_queryset()
qs = self.attach_votes_attrs_to_queryset(qs)
qs = qs.select_related("milestone",
"project",
"status",
"owner",
"assigned_to")
qs = self.attach_watchers_attrs_to_queryset(qs)
if "include_attachments" in self.request.QUERY_PARAMS:
qs = attach_basic_attachments(qs)
qs = qs.extra(select={"include_attachments": "True"})
include_attachments = "include_attachments" in self.request.QUERY_PARAMS
qs = tasks_utils.attach_extra_info(qs, user=self.request.user,
include_attachments=include_attachments)
return qs
@ -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)

View File

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

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.notifications.utils import attach_watchers_to_queryset
from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.projects.votes.utils import attach_is_voter_to_queryset
def attach_extra_info(queryset, user=None, include_attachments=False):
if include_attachments:
queryset = attach_basic_attachments(queryset)
queryset = queryset.extra(select={"include_attachments": "True"})
queryset = attach_total_voters_to_queryset(queryset)
queryset = attach_watchers_to_queryset(queryset)
queryset = attach_total_watchers_to_queryset(queryset)
queryset = attach_is_voter_to_queryset(queryset, user)
queryset = attach_is_watcher_to_queryset(queryset, user)
return queryset

View File

@ -19,7 +19,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)

View File

@ -16,12 +16,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from contextlib import closing
from collections import namedtuple
from django.apps import apps
from django.db import transaction, connection
from django.db.models.sql import datastructures
from django.db import transaction
from django.utils.translation import ugettext as _
from django.http import HttpResponse
@ -36,7 +32,6 @@ from taiga.base.api import ModelCrudViewSet
from taiga.base.api import ModelListViewSet
from taiga.base.api.utils import get_object_or_404
from taiga.projects.attachments.utils import attach_basic_attachments
from taiga.projects.history.mixins import HistoryResourceMixin
from taiga.projects.history.services import take_snapshot
from taiga.projects.milestones.models import Milestone
@ -45,21 +40,20 @@ from taiga.projects.notifications.mixins import WatchedResourceMixin
from taiga.projects.notifications.mixins import WatchersViewSetMixin
from taiga.projects.occ import OCCResourceMixin
from taiga.projects.tagging.api import TaggedResourceMixin
from taiga.projects.userstories.models import RolePoints
from taiga.projects.votes.mixins.viewsets import VotedResourceMixin
from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
from taiga.projects.userstories.utils import attach_total_points
from taiga.projects.userstories.utils import attach_role_points
from taiga.projects.userstories.utils import attach_tasks
from taiga.projects.userstories.utils import attach_extra_info
from . import models
from . import permissions
from . import serializers
from . import services
from . import validators
class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, WatchedResourceMixin,
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
validator_class = validators.UserStoryValidator
queryset = models.UserStory.objects.all()
permission_classes = (permissions.UserStoryPermission,)
filter_backends = (filters.CanViewUsFilterBackend,
@ -105,18 +99,11 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
"assigned_to",
"generated_from_issue")
qs = self.attach_votes_attrs_to_queryset(qs)
qs = self.attach_watchers_attrs_to_queryset(qs)
qs = attach_total_points(qs)
qs = attach_role_points(qs)
if "include_attachments" in self.request.QUERY_PARAMS:
qs = attach_basic_attachments(qs)
qs = qs.extra(select={"include_attachments": "True"})
if "include_tasks" in self.request.QUERY_PARAMS:
qs = attach_tasks(qs)
qs = qs.extra(select={"include_tasks": "True"})
include_attachments = "include_attachments" in self.request.QUERY_PARAMS
include_tasks = "include_tasks" in self.request.QUERY_PARAMS
qs = attach_extra_info(qs, user=self.request.user,
include_attachments=include_attachments,
include_tasks=include_tasks)
return qs
@ -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)

View File

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

View File

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

View File

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

View File

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

View File

@ -16,11 +16,42 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.db.models import Q
from django.utils.translation import ugettext as _
from taiga.base.api import serializers
from taiga.base.api import validators
from taiga.base.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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.projects.notifications.utils import attach_watchers_to_queryset
from taiga.projects.notifications.utils import attach_total_watchers_to_queryset
from taiga.projects.notifications.utils import attach_is_watcher_to_queryset
def attach_extra_info(queryset, user=None, include_attachments=False):
queryset = attach_watchers_to_queryset(queryset)
queryset = attach_total_watchers_to_queryset(queryset)
queryset = attach_is_watcher_to_queryset(queryset, user)
return queryset

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import validators
from taiga.projects.notifications.validators import WatchersValidator
from . import models
class WikiPageValidator(WatchersValidator, validators.ModelValidator):
class Meta:
model = models.WikiPage
read_only_fields = ('modified_date', 'created_date', 'owner')
class WikiLinkValidator(validators.ModelValidator):
class Meta:
model = models.WikiLink
read_only_fields = ('href',)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.2 on 2016-07-06 07:23
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('timeline', '0004_auto_20150603_1312'),
]
operations = [
migrations.AlterField(
model_name='timeline',
name='created',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from taiga.base.api import validators
from .models import Webhook
class WebhookValidator(validators.ModelValidator):
class Meta:
model = Webhook

View File

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

View File

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

View File

@ -22,8 +22,10 @@ from django.apps import apps
from taiga.base.utils import json
from taiga.projects import choices as project_choices
from taiga.projects 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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,6 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import 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)

View File

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

View File

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