API performance

remotes/origin/issue/4795/notification_even_they_are_disabled
Alejandro Alonso 2016-06-23 15:11:16 +02:00 committed by Jesús Espino
parent 1252a08dbb
commit 78a2118e8e
12 changed files with 818 additions and 129 deletions

View File

@ -1228,4 +1228,6 @@ class LightSerializer(serpy.Serializer):
kwargs.pop("read_only", None) kwargs.pop("read_only", None)
kwargs.pop("partial", None) kwargs.pop("partial", None)
kwargs.pop("files", None) kwargs.pop("files", None)
context = kwargs.pop("context", {})
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.context = context

View File

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

View File

@ -61,7 +61,7 @@ from . import models
from . import permissions from . import permissions
from . import serializers from . import serializers
from . import services from . import services
from . import utils as project_utils
###################################################### ######################################################
## Project ## Project
@ -70,11 +70,9 @@ from . import services
class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin, class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMixin, BlockeableDeleteMixin,
TagsColorsResourceMixin, ModelCrudViewSet): TagsColorsResourceMixin, ModelCrudViewSet):
queryset = models.Project.objects.all() queryset = models.Project.objects.all()
serializer_class = serializers.ProjectDetailSerializer
admin_serializer_class = serializers.ProjectDetailAdminSerializer
list_serializer_class = serializers.ProjectSerializer
permission_classes = (permissions.ProjectPermission, ) permission_classes = (permissions.ProjectPermission, )
filter_backends = (project_filters.QFilterBackend, filter_backends = (project_filters.UserOrderFilterBackend,
project_filters.QFilterBackend,
project_filters.CanViewProjectObjFilterBackend, project_filters.CanViewProjectObjFilterBackend,
project_filters.DiscoverModeFilterBackend) project_filters.DiscoverModeFilterBackend)
@ -85,8 +83,7 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
"is_kanban_activated") "is_kanban_activated")
ordering = ("name", "id") ordering = ("name", "id")
order_by_fields = ("memberships__user_order", order_by_fields = ("total_fans",
"total_fans",
"total_fans_last_week", "total_fans_last_week",
"total_fans_last_month", "total_fans_last_month",
"total_fans_last_year", "total_fans_last_year",
@ -106,18 +103,8 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.select_related("owner") qs = qs.select_related("owner")
# Prefetch doesn"t work correctly if then if the field is filtered later (it generates more queries) qs = project_utils.attach_extra_info(qs, user=self.request.user)
# so we add some custom prefetching
qs = qs.prefetch_related("members")
qs = qs.prefetch_related("memberships")
qs = qs.prefetch_related(Prefetch("notify_policies",
NotifyPolicy.objects.exclude(notify_level=NotifyLevel.none), to_attr="valid_notify_policies"))
Milestone = apps.get_model("milestones", "Milestone")
qs = qs.prefetch_related(Prefetch("milestones",
Milestone.objects.filter(closed=True), to_attr="closed_milestones"))
# If filtering an activity period we must exclude the activities not updated recently enough # If filtering an activity period we must exclude the activities not updated recently enough
now = timezone.now() now = timezone.now()
@ -137,22 +124,20 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
return qs return qs
def retrieve(self, request, *args, **kwargs):
if self.action == "by_slug":
self.lookup_field = "slug"
return super().retrieve(request, *args, **kwargs)
def get_serializer_class(self): def get_serializer_class(self):
serializer_class = self.serializer_class
if self.action == "list": if self.action == "list":
serializer_class = self.list_serializer_class return serializers.LightProjectSerializer
elif self.action != "create":
if self.action == "by_slug":
slug = self.request.QUERY_PARAMS.get("slug", None)
project = get_object_or_404(models.Project, slug=slug)
else:
project = self.get_object()
if permissions_services.is_project_admin(self.request.user, project): if self.action in ["retrieve", "by_slug"]:
serializer_class = self.admin_serializer_class return serializers.LightProjectDetailSerializer
return serializer_class return serializers.ProjectSerializer
@detail_route(methods=["POST"]) @detail_route(methods=["POST"])
def change_logo(self, request, *args, **kwargs): def change_logo(self, request, *args, **kwargs):
@ -283,10 +268,9 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin, BlockeableSaveMix
return response.Ok(data) return response.Ok(data)
@list_route(methods=["GET"]) @list_route(methods=["GET"])
def by_slug(self, request): def by_slug(self, request, *args, **kwargs):
slug = request.QUERY_PARAMS.get("slug", None) slug = request.QUERY_PARAMS.get("slug", None)
project = get_object_or_404(models.Project, slug=slug) return self.retrieve(request, slug=slug)
return self.retrieve(request, pk=project.pk)
@detail_route(methods=["GET", "PATCH"]) @detail_route(methods=["GET", "PATCH"])
def modules(self, request, pk=None): def modules(self, request, pk=None):

View File

@ -45,7 +45,7 @@ class DiscoverModeFilterBackend(FilterBackend):
if request.QUERY_PARAMS.get("is_featured", None) == 'true': if request.QUERY_PARAMS.get("is_featured", None) == 'true':
qs = qs.order_by("?") qs = qs.order_by("?")
return super().filter_queryset(request, qs.distinct(), view) return super().filter_queryset(request, qs, view)
class CanViewProjectObjFilterBackend(FilterBackend): class CanViewProjectObjFilterBackend(FilterBackend):
@ -86,7 +86,7 @@ class CanViewProjectObjFilterBackend(FilterBackend):
# external users / anonymous # external users / anonymous
qs = qs.filter(anon_permissions__contains=["view_project"]) qs = qs.filter(anon_permissions__contains=["view_project"])
return super().filter_queryset(request, qs.distinct(), view) return super().filter_queryset(request, qs, view)
class QFilterBackend(FilterBackend): class QFilterBackend(FilterBackend):
@ -121,3 +121,34 @@ class QFilterBackend(FilterBackend):
params=params, params=params,
order_by=order_by) order_by=order_by)
return queryset return queryset
class UserOrderFilterBackend(FilterBackend):
def filter_queryset(self, request, queryset, view):
if request.user.is_anonymous():
return queryset
raw_fieldname = request.QUERY_PARAMS.get(self.order_by_query_param, None)
if not raw_fieldname:
return queryset
if raw_fieldname.startswith("-"):
field_name = raw_fieldname[1:]
else:
field_name = raw_fieldname
if field_name != "user_order":
return queryset
model = queryset.model
sql = """SELECT projects_membership.user_order
FROM projects_membership
WHERE
projects_membership.project_id = {tbl}.id AND
projects_membership.user_id = {user_id}
"""
sql = sql.format(tbl=model._meta.db_table, user_id=request.user.id)
queryset = queryset.extra(select={"user_order": sql})
queryset = queryset.order_by(raw_fieldname)
return queryset

View File

@ -144,7 +144,6 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
def get_queryset(self): def get_queryset(self):
qs = super().get_queryset() qs = super().get_queryset()
qs = qs.prefetch_related("attachments", "generated_user_stories")
qs = qs.select_related("owner", "assigned_to", "status", "project") qs = qs.select_related("owner", "assigned_to", "status", "project")
qs = self.attach_votes_attrs_to_queryset(qs) qs = self.attach_votes_attrs_to_queryset(qs)
return self.attach_watchers_attrs_to_queryset(qs) return self.attach_watchers_attrs_to_queryset(qs)

View File

@ -188,9 +188,10 @@ class WatchedModelMixin(object):
class BaseWatchedResourceModelSerializer(object): class BaseWatchedResourceModelSerializer(object):
def get_is_watcher(self, obj): def get_is_watcher(self, obj):
# The "is_watcher" attribute is attached in the get_queryset of the viewset.
if "request" in self.context: if "request" in self.context:
user = self.context["request"].user user = self.context["request"].user
return user.is_authenticated() and user.is_watcher(obj) return user.is_authenticated() and getattr(obj, "is_watcher", False)
return False return False
@ -205,8 +206,8 @@ class WatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, seriali
class ListWatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serpy.Serializer): class ListWatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serpy.Serializer):
is_watcher = serializers.SerializerMethodField("get_is_watcher") is_watcher = serpy.MethodField("get_is_watcher")
total_watchers = serializers.SerializerMethodField("get_total_watchers") total_watchers = serpy.MethodField("get_total_watchers")
class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer): class EditableWatchedResourceModelSerializer(WatchedResourceModelSerializer):

View File

@ -25,12 +25,15 @@ from taiga.base.api import serializers
from taiga.base.fields import JsonField from taiga.base.fields import JsonField
from taiga.base.fields import PgArrayField from taiga.base.fields import PgArrayField
from taiga.permissions import services as permissions_services
from taiga.users.services import get_photo_or_gravatar_url from taiga.users.services import get_photo_or_gravatar_url
from taiga.users.serializers import UserBasicInfoSerializer from taiga.users.serializers import UserBasicInfoSerializer
from taiga.users.serializers import ProjectRoleSerializer from taiga.users.serializers import ProjectRoleSerializer
from taiga.users.serializers import ListUserBasicInfoSerializer
from taiga.users.validators import RoleExistsValidator from taiga.users.validators import RoleExistsValidator
from taiga.permissions.services import get_user_project_permissions from taiga.permissions.services import get_user_project_permissions
from taiga.permissions.services import calculate_permissions
from taiga.permissions.services import is_project_admin, is_project_owner from taiga.permissions.services import is_project_admin, is_project_owner
from . import models from . import models
@ -46,6 +49,7 @@ from .tagging.fields import TagsField
from .tagging.fields import TagsColorsField from .tagging.fields import TagsColorsField
from .validators import ProjectExistsValidator from .validators import ProjectExistsValidator
import serpy
###################################################### ######################################################
## Custom values for selectors ## Custom values for selectors
@ -295,11 +299,6 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return False return False
def get_total_closed_milestones(self, obj): def get_total_closed_milestones(self, obj):
# The "closed_milestone" attribute can be attached in the get_queryset method of the viewset.
qs_closed_milestones = getattr(obj, "closed_milestones", None)
if qs_closed_milestones is not None:
return len(qs_closed_milestones)
return obj.milestones.filter(closed=True).count() return obj.milestones.filter(closed=True).count()
def get_notify_level(self, obj): def get_notify_level(self, obj):
@ -310,11 +309,6 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return None return None
def get_total_watchers(self, obj): def get_total_watchers(self, obj):
# The "valid_notify_policies" attribute can be attached in the get_queryset method of the viewset.
qs_valid_notify_policies = getattr(obj, "valid_notify_policies", None)
if qs_valid_notify_policies is not None:
return len(qs_valid_notify_policies)
return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count() return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count()
def get_logo_small_url(self, obj): def get_logo_small_url(self, obj):
@ -324,60 +318,253 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return services.get_logo_big_thumbnail_url(obj) return services.get_logo_big_thumbnail_url(obj)
class ProjectDetailSerializer(ProjectSerializer): class LightProjectSerializer(serializers.LightSerializer):
us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories id = serpy.Field()
points = PointsSerializer(many=True, required=False) 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()
task_statuses = TaskStatusSerializer(many=True, required=False) # Tasks tags = serpy.Field()
tags_colors = serpy.MethodField()
issue_statuses = IssueStatusSerializer(many=True, required=False) default_points = serpy.Field(attr="default_points_id")
issue_types = IssueTypeSerializer(many=True, required=False) default_us_status = serpy.Field(attr="default_us_status_id")
priorities = PrioritySerializer(many=True, required=False) # Issues default_task_status = serpy.Field(attr="default_task_status_id")
severities = SeveritySerializer(many=True, required=False) 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")
userstory_custom_attributes = UserStoryCustomAttributeSerializer(source="userstorycustomattributes", my_permissions = serpy.MethodField()
many=True, required=False)
task_custom_attributes = TaskCustomAttributeSerializer(source="taskcustomattributes",
many=True, required=False)
issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes",
many=True, required=False)
roles = ProjectRoleSerializer(source="roles", many=True, read_only=True) i_am_owner = serpy.MethodField()
members = serializers.SerializerMethodField(method_name="get_members") i_am_admin = serpy.MethodField()
total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships") i_am_member = serpy.MethodField()
is_out_of_owner_limits = serializers.SerializerMethodField(method_name="get_is_out_of_owner_limits")
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")
def get_members(self, obj): def get_members(self, obj):
qs = obj.memberships.filter(user__isnull=False) assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
qs = qs.extra(select={"complete_user_name":"concat(full_name, username)"}) if obj.members_attr is None:
qs = qs.order_by("complete_user_name") return []
qs = qs.select_related("role", "user")
serializer = ProjectMemberSerializer(qs, many=True) return [m.get("id") for m in obj.members_attr if m["id"] is not None]
return serializer.data
def get_i_am_member(self, obj):
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
if obj.members_attr is None:
return False
if "request" in self.context:
user = self.context["request"].user
if not user.is_anonymous() and user.id in [m.get("id") for m in obj.members_attr if m["id"] is not None]:
return True
return False
def get_tags_colors(self, obj):
return dict(obj.tags_colors)
def get_my_permissions(self, obj):
if "request" in self.context:
user = self.context["request"].user
return calculate_permissions(
is_authenticated = user.is_authenticated(),
is_superuser = user.is_superuser,
is_member = self.get_i_am_member(obj),
is_admin = self.get_i_am_admin(obj),
role_permissions = obj.my_role_permissions_attr,
anon_permissions = obj.anon_permissions,
public_permissions = obj.public_permissions)
return []
def get_owner(self, obj):
return ListUserBasicInfoSerializer(obj.owner).data
def get_i_am_owner(self, obj):
if "request" in self.context:
return is_project_owner(self.context["request"].user, obj)
return False
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_total_closed_milestones(self, obj):
assert hasattr(obj, "closed_milestones_attr"), "instance must have a closed_milestones_attr attribute"
return obj.closed_milestones_attr
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
def get_total_watchers(self, obj):
assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
if obj.notify_policies_attr is None:
return 0
valid_notify_policies = [np for np in obj.notify_policies_attr if np["notify_level"] != NotifyLevel.none]
return len(valid_notify_policies)
def get_notify_level(self, obj):
assert hasattr(obj, "notify_policies_attr"), "instance must have a notify_policies_attr attribute"
if obj.notify_policies_attr is None:
return None
if "request" in self.context:
user = self.context["request"].user
for np in obj.notify_policies_attr:
if np["user_id"] == user.id:
return np["notify_level"]
return None
def get_logo_small_url(self, obj):
return services.get_logo_small_thumbnail_url(obj)
def get_logo_big_url(self, obj):
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()
#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()
def to_value(self, instance):
# Name attributes must be translated
for attr in ["userstory_statuses_attr","points_attr", "task_statuses_attr",
"issue_statuses_attr", "issue_types_attr", "priorities_attr",
"severities_attr", "userstory_custom_attributes_attr",
"task_custom_attributes_attr","issue_custom_attributes_attr", "roles_attr"]:
assert hasattr(instance, attr), "instance must have a {} attribute".format(attr)
val = getattr(instance, attr)
if val is None:
continue
for elem in val:
elem["name"] = _(elem["name"])
ret = super().to_value(instance)
admin_fields = [
"is_private_extra_info", "max_memberships", "issues_csv_uuid",
"tasks_csv_uuid", "userstories_csv_uuid", "transfer_token"
]
is_admin_user = False
if "request" in self.context:
user = self.context["request"].user
is_admin_user = permissions_services.is_project_admin(user, instance)
if not is_admin_user:
for admin_field in admin_fields:
del(ret[admin_field])
return ret
def get_members(self, obj):
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
if obj.members_attr is None:
return []
ret = []
for m in obj.members_attr:
m["full_name_display"] = m["full_name"] or m["username"] or m["email"]
del(m["email"])
del(m["complete_user_name"])
if not m["id"] is None:
ret.append(m)
return ret
def get_total_memberships(self, obj): def get_total_memberships(self, obj):
return services.get_total_project_memberships(obj) if obj.members_attr is None:
return 0
return len(obj.members_attr)
def get_is_out_of_owner_limits(self, obj): def get_is_out_of_owner_limits(self, obj):
return services.check_if_project_is_out_of_owner_limits(obj) assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_is_out_of_owner_limits(obj,
class ProjectDetailAdminSerializer(ProjectDetailSerializer): current_memberships = self.get_total_memberships(obj),
is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info") current_private_projects=obj.private_projects_same_owner_attr,
max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships") current_public_projects=obj.public_projects_same_owner_attr
)
class Meta:
model = models.Project
read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref")
def get_is_private_extra_info(self, obj): def get_is_private_extra_info(self, obj):
return services.check_if_project_privacity_can_be_changed(obj) assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_privacity_can_be_changed(obj,
current_memberships = self.get_total_memberships(obj),
current_private_projects=obj.private_projects_same_owner_attr,
current_public_projects=obj.public_projects_same_owner_attr
)
def get_max_memberships(self, obj): def get_max_memberships(self, obj):
return services.get_max_memberships_for_project(obj) return services.get_max_memberships_for_project(obj)
###################################################### ######################################################
## Liked ## Liked
###################################################### ######################################################

View File

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

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

@ -0,0 +1,436 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2016 Anler Hernández <hello@anler.me>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
def attach_members(queryset, as_field="members_attr"):
"""Attach a json members representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the members as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(t))
FROM(
SELECT
users_user.id,
users_user.username,
users_user.full_name,
users_user.email,
concat(full_name, username) complete_user_name,
users_user.color,
users_user.photo,
users_user.is_active,
users_role.name role_name
FROM projects_membership
LEFT JOIN users_user ON projects_membership.user_id = users_user.id
LEFT JOIN users_role ON users_role.id = projects_membership.role_id
WHERE projects_membership.project_id = {tbl}.id
ORDER BY complete_user_name) t"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_closed_milestones(queryset, as_field="closed_milestones_attr"):
"""Attach a closed milestones counter to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the counter as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT COUNT(milestones_milestone.id)
FROM milestones_milestone
WHERE
milestones_milestone.project_id = {tbl}.id AND
milestones_milestone.closed = True
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_notify_policies(queryset, as_field="notify_policies_attr"):
"""Attach a json notification policies representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the notification policies as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(notifications_notifypolicy))
FROM notifications_notifypolicy
WHERE
notifications_notifypolicy.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_userstory_statuses(queryset, as_field="userstory_statuses_attr"):
"""Attach a json userstory statuses representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the userstory statuses as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(projects_userstorystatus))
FROM projects_userstorystatus
WHERE
projects_userstorystatus.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_points(queryset, as_field="points_attr"):
"""Attach a json points representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the points as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(projects_points))
FROM projects_points
WHERE
projects_points.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_task_statuses(queryset, as_field="task_statuses_attr"):
"""Attach a json task statuses representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the task statuses as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(projects_taskstatus))
FROM projects_taskstatus
WHERE
projects_taskstatus.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_issue_statuses(queryset, as_field="issue_statuses_attr"):
"""Attach a json issue statuses representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the statuses as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(projects_issuestatus))
FROM projects_issuestatus
WHERE
projects_issuestatus.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_issue_types(queryset, as_field="issue_types_attr"):
"""Attach a json issue types representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the types as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(projects_issuetype))
FROM projects_issuetype
WHERE
projects_issuetype.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_priorities(queryset, as_field="priorities_attr"):
"""Attach a json priorities representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the priorities as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(projects_priority))
FROM projects_priority
WHERE
projects_priority.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_severities(queryset, as_field="severities_attr"):
"""Attach a json severities representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the severities as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(projects_severity))
FROM projects_severity
WHERE
projects_severity.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_userstory_custom_attributes(queryset, as_field="userstory_custom_attributes_attr"):
"""Attach a json userstory custom attributes representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the userstory custom attributes as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(custom_attributes_userstorycustomattribute))
FROM custom_attributes_userstorycustomattribute
WHERE
custom_attributes_userstorycustomattribute.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_task_custom_attributes(queryset, as_field="task_custom_attributes_attr"):
"""Attach a json task custom attributes representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the task custom attributes as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(custom_attributes_taskcustomattribute))
FROM custom_attributes_taskcustomattribute
WHERE
custom_attributes_taskcustomattribute.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_issue_custom_attributes(queryset, as_field="issue_custom_attributes_attr"):
"""Attach a json issue custom attributes representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the issue custom attributes as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(custom_attributes_issuecustomattribute))
FROM custom_attributes_issuecustomattribute
WHERE
custom_attributes_issuecustomattribute.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_roles(queryset, as_field="roles_attr"):
"""Attach a json roles representation to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the roles as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
sql = """SELECT json_agg(row_to_json(users_role))
FROM users_role
WHERE
users_role.project_id = {tbl}.id
"""
sql = sql.format(tbl=model._meta.db_table)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_is_fan(queryset, user, as_field="is_fan_attr"):
"""Attach a is fan boolean to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the boolean as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
if user is None or user.is_anonymous():
sql = """SELECT false"""
else:
sql = """SELECT COUNT(likes_like.id) > 0
FROM likes_like
INNER JOIN django_content_type
ON likes_like.content_type_id = django_content_type.id
WHERE
django_content_type.model = 'project' AND
django_content_type.app_label = 'projects' AND
likes_like.user_id = {user_id} AND
likes_like.object_id = {tbl}.id"""
sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_my_role_permissions(queryset, user, as_field="my_role_permissions_attr"):
"""Attach a permission array to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the permissions as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
if user is None or user.is_anonymous():
sql = """SELECT '{}'"""
else:
sql = """SELECT users_role.permissions
FROM projects_membership
LEFT JOIN users_user ON projects_membership.user_id = users_user.id
LEFT JOIN users_role ON users_role.id = projects_membership.role_id
WHERE
projects_membership.project_id = {tbl}.id AND
users_user.id = {user_id}"""
sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_private_projects_same_owner(queryset, user, as_field="private_projects_same_owner_attr"):
"""Attach a private projects counter to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the counter as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
if user is None or user.is_anonymous():
sql = """SELECT '0'"""
else:
sql = """SELECT COUNT(id)
FROM projects_project p_aux
WHERE
p_aux.is_private = True AND
p_aux.owner_id = {tbl}.owner_id"""
sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_public_projects_same_owner(queryset, user, as_field="public_projects_same_owner_attr"):
"""Attach a public projects counter to each object of the queryset.
:param queryset: A Django projects queryset object.
:param as_field: Attach the counter as an attribute with this name.
:return: Queryset object with the additional `as_field` field.
"""
model = queryset.model
if user is None or user.is_anonymous():
sql = """SELECT '0'"""
else:
sql = """SELECT COUNT(id)
FROM projects_project p_aux
WHERE
p_aux.is_private = False AND
p_aux.owner_id = {tbl}.owner_id"""
sql = sql.format(tbl=model._meta.db_table, user_id=user.id)
queryset = queryset.extra(select={as_field: sql})
return queryset
def attach_extra_info(queryset, user=None):
queryset = attach_members(queryset)
queryset = attach_closed_milestones(queryset)
queryset = attach_notify_policies(queryset)
queryset = attach_userstory_statuses(queryset)
queryset = attach_points(queryset)
queryset = attach_task_statuses(queryset)
queryset = attach_issue_statuses(queryset)
queryset = attach_issue_types(queryset)
queryset = attach_priorities(queryset)
queryset = attach_severities(queryset)
queryset = attach_userstory_custom_attributes(queryset)
queryset = attach_task_custom_attributes(queryset)
queryset = attach_issue_custom_attributes(queryset)
queryset = attach_roles(queryset)
queryset = attach_is_fan(queryset, user)
queryset = attach_my_role_permissions(queryset, user)
queryset = attach_private_projects_same_owner(queryset, user)
queryset = attach_public_projects_same_owner(queryset, user)
return queryset

View File

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

View File

@ -22,7 +22,7 @@ from django.apps import apps
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects import choices as project_choices from taiga.projects import choices as project_choices
from taiga.projects.serializers import ProjectDetailSerializer from taiga.projects.serializers import ProjectSerializer
from taiga.permissions.choices import MEMBERS_PERMISSIONS from taiga.permissions.choices import MEMBERS_PERMISSIONS
from tests import factories as f from tests import factories as f
@ -153,12 +153,12 @@ def test_project_update(client, data):
data.project_owner data.project_owner
] ]
project_data = ProjectDetailSerializer(data.private_project2).data project_data = ProjectSerializer(data.private_project2).data
project_data["is_private"] = False project_data["is_private"] = False
results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users) results = helper_test_http_method(client, 'put', url, json.dumps(project_data), users)
assert results == [401, 403, 403, 200] assert results == [401, 403, 403, 200]
project_data = ProjectDetailSerializer(data.blocked_project).data project_data = ProjectSerializer(data.blocked_project).data
project_data["is_private"] = False project_data["is_private"] = False
results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users) results = helper_test_http_method(client, 'put', blocked_url, json.dumps(project_data), users)
assert results == [401, 403, 403, 451] assert results == [401, 403, 403, 451]

View File

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