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("partial", None)
kwargs.pop("files", None)
context = kwargs.pop("context", {})
super().__init__(*args, **kwargs)
self.context = context

View File

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

View File

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

View File

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

View File

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

View File

@ -188,9 +188,10 @@ class WatchedModelMixin(object):
class BaseWatchedResourceModelSerializer(object):
def get_is_watcher(self, obj):
# The "is_watcher" attribute is attached in the get_queryset of the viewset.
if "request" in self.context:
user = self.context["request"].user
return user.is_authenticated() and user.is_watcher(obj)
return user.is_authenticated() and getattr(obj, "is_watcher", False)
return False
@ -205,8 +206,8 @@ class WatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, seriali
class ListWatchedResourceModelSerializer(BaseWatchedResourceModelSerializer, serpy.Serializer):
is_watcher = serializers.SerializerMethodField("get_is_watcher")
total_watchers = serializers.SerializerMethodField("get_total_watchers")
is_watcher = serpy.MethodField("get_is_watcher")
total_watchers = serpy.MethodField("get_total_watchers")
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 PgArrayField
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
@ -46,6 +49,7 @@ from .tagging.fields import TagsField
from .tagging.fields import TagsColorsField
from .validators import ProjectExistsValidator
import serpy
######################################################
## Custom values for selectors
@ -295,11 +299,6 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return False
def get_total_closed_milestones(self, obj):
# The "closed_milestone" attribute can be attached in the get_queryset method of the viewset.
qs_closed_milestones = getattr(obj, "closed_milestones", None)
if qs_closed_milestones is not None:
return len(qs_closed_milestones)
return obj.milestones.filter(closed=True).count()
def get_notify_level(self, obj):
@ -310,11 +309,6 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return None
def get_total_watchers(self, obj):
# The "valid_notify_policies" attribute can be attached in the get_queryset method of the viewset.
qs_valid_notify_policies = getattr(obj, "valid_notify_policies", None)
if qs_valid_notify_policies is not None:
return len(qs_valid_notify_policies)
return obj.notify_policies.exclude(notify_level=NotifyLevel.none).count()
def get_logo_small_url(self, obj):
@ -324,60 +318,253 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
return services.get_logo_big_thumbnail_url(obj)
class ProjectDetailSerializer(ProjectSerializer):
us_statuses = UserStoryStatusSerializer(many=True, required=False) # User Stories
points = PointsSerializer(many=True, required=False)
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()
task_statuses = TaskStatusSerializer(many=True, required=False) # Tasks
tags = serpy.Field()
tags_colors = serpy.MethodField()
issue_statuses = IssueStatusSerializer(many=True, required=False)
issue_types = IssueTypeSerializer(many=True, required=False)
priorities = PrioritySerializer(many=True, required=False) # Issues
severities = SeveritySerializer(many=True, required=False)
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")
userstory_custom_attributes = UserStoryCustomAttributeSerializer(source="userstorycustomattributes",
many=True, required=False)
task_custom_attributes = TaskCustomAttributeSerializer(source="taskcustomattributes",
many=True, required=False)
issue_custom_attributes = IssueCustomAttributeSerializer(source="issuecustomattributes",
many=True, required=False)
my_permissions = serpy.MethodField()
roles = ProjectRoleSerializer(source="roles", many=True, read_only=True)
members = serializers.SerializerMethodField(method_name="get_members")
total_memberships = serializers.SerializerMethodField(method_name="get_total_memberships")
is_out_of_owner_limits = serializers.SerializerMethodField(method_name="get_is_out_of_owner_limits")
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")
def get_members(self, obj):
qs = obj.memberships.filter(user__isnull=False)
qs = qs.extra(select={"complete_user_name":"concat(full_name, username)"})
qs = qs.order_by("complete_user_name")
qs = qs.select_related("role", "user")
serializer = ProjectMemberSerializer(qs, many=True)
return serializer.data
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
if obj.members_attr is None:
return []
return [m.get("id") for m in obj.members_attr if m["id"] is not None]
def get_i_am_member(self, obj):
assert hasattr(obj, "members_attr"), "instance must have a members_attr attribute"
if obj.members_attr is None:
return False
if "request" in self.context:
user = self.context["request"].user
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):
return services.get_total_project_memberships(obj)
if obj.members_attr is None:
return 0
return len(obj.members_attr)
def get_is_out_of_owner_limits(self, obj):
return services.check_if_project_is_out_of_owner_limits(obj)
class ProjectDetailAdminSerializer(ProjectDetailSerializer):
is_private_extra_info = serializers.SerializerMethodField(method_name="get_is_private_extra_info")
max_memberships = serializers.SerializerMethodField(method_name="get_max_memberships")
class Meta:
model = models.Project
read_only_fields = ("created_date", "modified_date", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref")
assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_is_out_of_owner_limits(obj,
current_memberships = self.get_total_memberships(obj),
current_private_projects=obj.private_projects_same_owner_attr,
current_public_projects=obj.public_projects_same_owner_attr
)
def get_is_private_extra_info(self, obj):
return services.check_if_project_privacity_can_be_changed(obj)
assert hasattr(obj, "private_projects_same_owner_attr"), "instance must have a private_projects_same_owner_attr attribute"
assert hasattr(obj, "public_projects_same_owner_attr"), "instance must have a public_projects_same_owner_attr attribute"
return services.check_if_project_privacity_can_be_changed(obj,
current_memberships = self.get_total_memberships(obj),
current_private_projects=obj.private_projects_same_owner_attr,
current_public_projects=obj.public_projects_same_owner_attr
)
def get_max_memberships(self, obj):
return services.get_max_memberships_for_project(obj)
######################################################
## Liked
######################################################

View File

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

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):
self._cached_memberships = {}
qs = self.memberships.prefetch_related("user", "project", "role")
qs = self.memberships.select_related("user", "project", "role")
for membership in qs.all():
self._cached_memberships[membership.project.id] = membership

View File

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

View File

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