diff --git a/apps/accounts/views.py b/apps/accounts/views.py index 9e94705a..1b6f6fed 100644 --- a/apps/accounts/views.py +++ b/apps/accounts/views.py @@ -704,9 +704,7 @@ def remove_member(self, request, *args, **kwargs): def member(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.members() + queryset = group.modules_by_user(request.user).members() page = self.paginate_queryset(queryset) if page is not None: @@ -795,9 +793,7 @@ def remove_featured_project(self, request, *args, **kwargs): ) def project(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.featured_projects() + queryset = group.modules_by_user(request.user).featured_projects() page = self.paginate_queryset(queryset) project_serializer = ProjectLightSerializer( @@ -842,9 +838,7 @@ def hierarchy(self, request, *args, **kwargs): ) def subgroups(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.subgroups() + queryset = group.modules_by_user(request.user).subgroups() queryset_page = self.paginate_queryset(queryset) data = PeopleGroupLightSerializer( @@ -861,9 +855,7 @@ def subgroups(self, request, *args, **kwargs): ) def similars(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.similars() + queryset = group.modules_by_user(request.user).similars() queryset_page = self.paginate_queryset(queryset) data = PeopleGroupLightSerializer( @@ -880,9 +872,7 @@ def similars(self, request, *args, **kwargs): ) def locations(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.locations() + queryset = group.modules_by_user(request.user).locations() return Response( LocationSerializer(queryset, many=True, context={"request": request}).data, @@ -898,9 +888,7 @@ def locations(self, request, *args, **kwargs): ) def news(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.news() + queryset = group.modules_by_user(request.user).news() # use NewsViewSet to filter/order events queryset = NewsViewSet(request=self.request).filter_queryset(queryset) @@ -920,9 +908,7 @@ def news(self, request, *args, **kwargs): ) def event(self, request, *args, **kwargs): group = self.get_object() - modules_manager = group.get_related_module() - modules = modules_manager(group, request.user) - queryset = modules.event() + queryset = group.modules_by_user(request.user).event() # use EventViewSet to filter/order events queryset = EventViewSet(request=self.request).filter_queryset(queryset) diff --git a/apps/files/models.py b/apps/files/models.py index 6e9d1729..91590966 100644 --- a/apps/files/models.py +++ b/apps/files/models.py @@ -6,9 +6,10 @@ from azure.core.exceptions import ResourceNotFoundError from django.apps import apps from django.conf import settings +from django.core.cache import cache from django.core.files.uploadedfile import SimpleUploadedFile from django.db import models, transaction -from django.db.models import ForeignObjectRel, Model, Q, QuerySet +from django.db.models import ForeignObjectRel, ImageField, Model, Q, QuerySet from django.utils import timezone from simple_history.models import HistoricalRecords from stdimage import StdImageField @@ -253,6 +254,57 @@ class Meta: ordering = ("-created_at",) abstract = True + @staticmethod + def get_url(cache_key: str, field: ImageField) -> str: + """create cache for url file""" + url = cache.get(cache_key) + if url: + return url + + try: + url = field.url + except AttributeError: + return "" + + # expirations defined by azure/env - 60s + timeout = settings.STORAGE_EXPIRATION_SECS - 60 + cache.set(cache_key, url, timeout=timeout) + return url + + @property + def __url_key(self) -> str: + return f"image::url::{self.pk}" + + @property + def __url_variations_key(self) -> dict[str, str]: + field = self.file.field + + obj = {} + for name in field.variations.keys(): + obj[name] = f"image::url::{name}::{self.pk}" + return obj + + @property + def url(self) -> str: + """create cache for url file""" + return self.get_url(self.__url_key, self.file) + + @property + def variations(self) -> dict[str, str]: + varias = {"original": self.url} + + for name, key in self.__url_variations_key.items(): + field = getattr(self.file, name, None) + + url = "" + if field: + url = self.get_url(key, field) + varias[name] = url + return varias + + def clear_cache_urls(self): + cache.delete_many([self.__url_key, *list(self.__url_variations_key.values())]) + def duplicate(self, upload_to: str = "", **fields) -> None | Self: with suppress(ResourceNotFoundError): file_path = self.file.name.split("/") @@ -271,6 +323,10 @@ def duplicate(self, upload_to: str = "", **fields) -> None | Self: return super().duplicate(_upload_to=_upload_to, file=new_file, **fields) return None + def save(self, *ar, **kw): + self.clear_cache_urls() + return super().save(*ar, **kw) + class Image(BaseImage, HasOwner, ProjectRelated, OrganizationRelated): name = models.CharField(max_length=255) diff --git a/apps/files/serializers.py b/apps/files/serializers.py index a7076d4c..d6c10786 100644 --- a/apps/files/serializers.py +++ b/apps/files/serializers.py @@ -40,54 +40,6 @@ ) -# From https://github.com/glemmaPaul/django-stdimage-serializer (however the repo is not maintained anymore) -class StdImageField(serializers.ImageField): - """ - Get all the variations of the StdImageField - """ - - def to_native(self, obj): - return self.get_variations_urls(obj) - - def to_representation(self, obj): - return self.get_variations_urls(obj) - - def get_variations_urls(self, obj): - """ - Get all the logo urls. - """ - - # Initiate return object - return_object = {} - - # Get the field of the object - field = obj.field - - # A lot of ifs going araound, first check if it has the field variations - if hasattr(field, "variations"): - # Get the variations - variations = field.variations - # Go through the variations dict - for key in variations.keys(): - # Just to be sure if the stdimage object has it stored in the obj - if hasattr(obj, key): - # get the by stdimage properties - field_obj = getattr(obj, key, None) - if field_obj and hasattr(field_obj, "url"): - # store it, with the name of the variation type into our return object - return_object[key] = super( - StdImageField, self - ).to_representation(field_obj) - - # Also include the original (if possible) - if hasattr(obj, "url"): - return_object["original"] = super(StdImageField, self).to_representation( - obj - ) - - return return_object - - class AbstractAttachmentLink(metaclass=serializers.SerializerMetaclass): preview_image_url = serializers.URLField(max_length=2048, read_only=True) site_name = serializers.CharField(max_length=255, read_only=True) @@ -386,8 +338,8 @@ def get_related_project(self) -> Optional["Project"]: class ImageSerializer(serializers.ModelSerializer): url = serializers.SerializerMethodField() + variations = serializers.SerializerMethodField() file = serializers.ImageField(write_only=True) - variations = StdImageField(source="file", read_only=True) class Meta: model = Image @@ -407,6 +359,9 @@ class Meta: "variations", ] + def get_variations(self, file): + return file.variations + def validate_file(self, file): limit = settings.MAX_FILE_SIZE * 1024 * 1024 if file.size > limit: @@ -415,7 +370,7 @@ def validate_file(self, file): def get_url(self, image: Image) -> str | None: try: - url = image.file.url + url = image.url except AttributeError: return None request = self.context.get("request") diff --git a/apps/files/views.py b/apps/files/views.py index 80c04de1..5e35f0bb 100644 --- a/apps/files/views.py +++ b/apps/files/views.py @@ -290,9 +290,7 @@ def get_permissions(self): return super().get_permissions() def get_queryset(self): - modules_manager = self.people_group.get_related_module() - modules = modules_manager(self.people_group, self.request.user) - return modules.gallery() + return self.people_group.modules_by_user(self.request.user).gallery() def update(self, request, *ar, **kw): request.data["people_group"] = self.people_group.id diff --git a/apps/modules/group.py b/apps/modules/group.py index 6604fe43..7af569ba 100644 --- a/apps/modules/group.py +++ b/apps/modules/group.py @@ -1,4 +1,12 @@ -from django.db.models import Case, Prefetch, Q, QuerySet, Value, When +from django.db.models import ( + Case, + CharField, + IntegerField, + Q, + QuerySet, + Value, + When, +) from apps.accounts.models import PeopleGroup, PeopleGroupLocation, ProjectUser from apps.commons.models import GroupData @@ -6,7 +14,6 @@ from apps.modules.base import AbstractModules, register_module from apps.newsfeed.models import Event, EventLocation, NewsLocation from apps.projects.models import Location, Project -from apps.skills.models import Skill from services.crisalid.models import Document, DocumentTypeCentralized @@ -15,33 +22,44 @@ class PeopleGroupModules(AbstractModules): instance: PeopleGroup def members(self) -> QuerySet[ProjectUser]: - skills_prefetch = Prefetch( - "skills", queryset=Skill.objects.select_related("tag") - ) - - leaders = self.instance.leaders.all() - managers = self.instance.managers.all() - members = self.instance.members.all() - - all_members = leaders | managers | members return ( - all_members.distinct() - .filter(pk__in=self.user.get_user_queryset()) + self.user.get_user_queryset() + .filter( + groups__data__role__in=( + GroupData.Role.LEADERS, + GroupData.Role.MANAGERS, + GroupData.Role.MEMBERS, + ), + groups__people_groups=self.instance, + ) .annotate( role=Case( - When(pk__in=leaders, then=Value(GroupData.Role.LEADERS)), - When(pk__in=managers, then=Value(GroupData.Role.MANAGERS)), - When(pk__in=members, then=Value(GroupData.Role.MEMBERS)), + When( + groups__data__role=GroupData.Role.LEADERS, + then=Value(GroupData.Role.LEADERS.value), + ), + When( + groups__data__role=GroupData.Role.MANAGERS, + then=Value(GroupData.Role.MANAGERS.value), + ), + When( + groups__data__role=GroupData.Role.MEMBERS, + then=Value(GroupData.Role.MEMBERS.value), + ), + output_field=CharField(), ), - # add sort order priority (first leader, manager and members) priority_role_order=Case( - When(pk__in=leaders, then=1), - When(pk__in=managers, then=2), - When(pk__in=members, then=3), + When(groups__data__role=GroupData.Role.LEADERS, then=Value(1)), + When(groups__data__role=GroupData.Role.MANAGERS, then=Value(2)), + When( + groups__data__role=GroupData.Role.MEMBERS, + then=Value(3), + ), + output_field=IntegerField(), ), ) .order_by("priority_role_order") - .prefetch_related(skills_prefetch, "groups") + .distinct() ) def featured_projects(self) -> QuerySet[Project]: diff --git a/apps/modules/project.py b/apps/modules/project.py index 9c7e8aa5..f9c4239a 100644 --- a/apps/modules/project.py +++ b/apps/modules/project.py @@ -1,4 +1,11 @@ -from django.db.models import Case, QuerySet, Value, When +from django.db.models import ( + Case, + CharField, + IntegerField, + QuerySet, + Value, + When, +) from apps.accounts.models import PeopleGroup, ProjectUser from apps.announcements.models import Announcement @@ -20,27 +27,37 @@ class ProjectModules(AbstractModules): instance: Project def members(self) -> QuerySet[ProjectUser]: - # get all members and annote role - owners = self.instance.owners.all() - members = self.instance.members.all() - reviewers = self.instance.reviewers.all() - - # union all and filter by request.user - all_members = owners | members | reviewers return ( - all_members.distinct() - .filter(pk__in=self.user.get_user_queryset()) + self.user.get_user_queryset() + .filter( + groups__data__role__in=( + GroupData.Role.OWNERS, + GroupData.Role.MEMBERS, + GroupData.Role.REVIEWERS, + ), + groups__projects=self.instance, + ) .annotate( role=Case( - When(pk__in=owners, then=Value(GroupData.Role.OWNERS)), - When(pk__in=members, then=Value(GroupData.Role.MEMBERS)), - When(pk__in=reviewers, then=Value(GroupData.Role.REVIEWERS)), + When( + groups__data__role=GroupData.Role.OWNERS, + then=Value(GroupData.Role.OWNERS.value), + ), + When( + groups__data__role=GroupData.Role.MEMBERS, + then=Value(GroupData.Role.MEMBERS.value), + ), + When( + groups__data__role=GroupData.Role.REVIEWERS, + then=Value(GroupData.Role.REVIEWERS.value), + ), + output_field=CharField(), ), - # add sort order priority (first leader, manager and members) priority_role_order=Case( - When(pk__in=owners, then=1), - When(pk__in=members, then=2), - When(pk__in=reviewers, then=3), + When(groups__data__role=GroupData.Role.OWNERS, then=Value(1)), + When(groups__data__role=GroupData.Role.MEMBERS, then=Value(2)), + When(groups__data__role=GroupData.Role.REVIEWERS, then=Value(3)), + output_field=IntegerField(), ), ) .order_by("priority_role_order") @@ -48,32 +65,42 @@ def members(self) -> QuerySet[ProjectUser]: ) def groups(self) -> QuerySet[PeopleGroup]: - # get all members and annote role - owner_groups = self.instance.owner_groups.all() - member_groups = self.instance.member_groups.all() - reviewer_groups = self.instance.reviewer_groups.all() - - # union all and filter by request.user - all_members = owner_groups | member_groups | reviewer_groups return ( - all_members.distinct() - .filter(pk__in=self.user.get_people_group_queryset()) + self.user.get_people_group_queryset() + .filter( + groups__data__role__in=( + GroupData.Role.OWNER_GROUPS, + GroupData.Role.MEMBER_GROUPS, + GroupData.Role.REVIEWER_GROUPS, + ), + groups__projects=self.instance, + ) .annotate( role=Case( - When(pk__in=owner_groups, then=Value(GroupData.Role.OWNER_GROUPS)), When( - pk__in=member_groups, then=Value(GroupData.Role.MEMBER_GROUPS) + groups__data__role=GroupData.Role.OWNER_GROUPS, + then=Value(GroupData.Role.OWNER_GROUPS.value), + ), + When( + groups__data__role=GroupData.Role.MEMBER_GROUPS, + then=Value(GroupData.Role.MEMBER_GROUPS.value), ), When( - pk__in=reviewer_groups, - then=Value(GroupData.Role.REVIEWER_GROUPS), + groups__data__role=GroupData.Role.REVIEWER_GROUPS, + then=Value(GroupData.Role.REVIEWER_GROUPS.value), ), + output_field=CharField(), ), - # add sort order priority (first leader, manager and members) priority_role_order=Case( - When(pk__in=owner_groups, then=1), - When(pk__in=member_groups, then=2), - When(pk__in=reviewer_groups, then=3), + When(groups__data__role=GroupData.Role.OWNER_GROUPS, then=Value(1)), + When( + groups__data__role=GroupData.Role.MEMBER_GROUPS, then=Value(2) + ), + When( + groups__data__role=GroupData.Role.REVIEWER_GROUPS, + then=Value(3), + ), + output_field=IntegerField(), ), ) .order_by("priority_role_order") diff --git a/apps/organizations/serializers.py b/apps/organizations/serializers.py index 451c8ba6..b8368b99 100644 --- a/apps/organizations/serializers.py +++ b/apps/organizations/serializers.py @@ -367,7 +367,7 @@ def get_attachment_files_count(self, organization: Organization) -> int: def create(self, validated_data): team = validated_data.pop("team", {}) - organization = super(OrganizationSerializer, self).create(validated_data) + organization = super().create(validated_data) OrganizationAddTeamMembersSerializer().create( {"organization": organization, **team} ) @@ -375,16 +375,13 @@ def create(self, validated_data): def update(self, instance, validated_data): validated_data.pop("team", {}) - return super(OrganizationSerializer, self).update(instance, validated_data) + return super().update(instance, validated_data) @auto_translated class OrganizationLightSerializer( - OrganizationRelatedSerializer, - serializers.ModelSerializer, + OrganizationSerializer, ): - logo_image = ImageSerializer(read_only=True) - class Meta: model = Organization fields = [ @@ -399,15 +396,6 @@ class Meta: "is_logo_visible_on_parent_dashboard", ] - def get_related_organizations(self) -> Organization: - # We're not supposed to be here since only super admin can create - # organization and this function should not be called in this case. - # see the view and permissions associated with this serializer. - # - # We return a dummy value so that the permission process can continue - # and return a `False`. - return [SimpleNamespace(code=uuid.uuid4(), pk=uuid.uuid4())] - def create(self, validated_data): """Create the instance's permissions and default groups.""" organization = super().create(validated_data) diff --git a/apps/projects/serializers.py b/apps/projects/serializers.py index e9177de5..09fbf4f3 100644 --- a/apps/projects/serializers.py +++ b/apps/projects/serializers.py @@ -34,7 +34,7 @@ from apps.notifications.tasks import notify_new_project, notify_project_changes from apps.organizations.models import Organization, ProjectCategory, Template from apps.organizations.serializers import ( - OrganizationSerializer, + OrganizationLightSerializer, ProjectCategoryLightSerializer, ProjectTemplateSerializer, ) @@ -199,7 +199,7 @@ class ProjectSerializer( # read_only header_image = ImageSerializer(read_only=True) categories = ProjectCategoryLightSerializer(many=True, read_only=True) - organizations = OrganizationSerializer(many=True, read_only=True) + organizations = OrganizationLightSerializer(many=True, read_only=True) template = ProjectTemplateSerializer(read_only=True) views = serializers.SerializerMethodField() diff --git a/apps/projects/views.py b/apps/projects/views.py index c0d273f4..997b3f81 100644 --- a/apps/projects/views.py +++ b/apps/projects/views.py @@ -117,7 +117,7 @@ def get_queryset(self) -> QuerySet: .prefetch_related( "categories", "tags", - "organizations", + "organizations__logo_image", ) ) diff --git a/projects/settings/base.py b/projects/settings/base.py index eb163556..106e3f8c 100644 --- a/projects/settings/base.py +++ b/projects/settings/base.py @@ -387,6 +387,7 @@ # STORAGES # ############## +STORAGE_EXPIRATION_SECS = int(os.getenv("AZURE_URL_EXPIRATION_SECS", "3600")) STORAGES = { "default": { "BACKEND": os.getenv( @@ -396,8 +397,8 @@ "account_name": os.getenv("AZURE_ACCOUNT_NAME", "criparisdevlabprojects"), "account_key": os.getenv("AZURE_ACCOUNT_KEY", ""), "azure_container": os.getenv("AZURE_CONTAINER", "projects"), - "expiration_secs": int(os.getenv("AZURE_URL_EXPIRATION_SECS", "3600")), - "cache_control": f"private,max-age={os.getenv('AZURE_URL_EXPIRATION_SECS', '3600')},must-revalidate", + "expiration_secs": STORAGE_EXPIRATION_SECS, + "cache_control": f"private,max-age={STORAGE_EXPIRATION_SECS},must-revalidate", }, }, "staticfiles": { diff --git a/services/crisalid/views.py b/services/crisalid/views.py index f4dcb139..3138bf5e 100644 --- a/services/crisalid/views.py +++ b/services/crisalid/views.py @@ -211,9 +211,9 @@ class AbstractGroupDocumentViewSet( NestedPeopleGroupViewMixins, AbstractDocumentViewSet ): def get_queryset(self): - modules_manager = self.people_group.get_related_module() - modules = modules_manager(self.people_group, self.request.user) - return getattr(modules, self.document_name)() + return getattr( + self.people_group.modules_by_user(self.request.user), self.document_name + )() class AbstractResearcherDocumentViewSet(