From be6021a812e3b6b33c098b7585aba2c2f774742b Mon Sep 17 00:00:00 2001 From: krmax44 Date: Fri, 1 Nov 2024 12:05:34 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20split=20up=20foirequest=20?= =?UTF-8?q?api=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- froide/foirequest/api_views/__init__.py | 0 froide/foirequest/api_views/attachment.py | 74 ++ froide/foirequest/api_views/message.py | 38 + froide/foirequest/api_views/request.py | 241 +++++++ froide/foirequest/apps.py | 8 +- froide/foirequest/auth.py | 24 + .../{api_views.py => serializers.py} | 682 +++++------------- froide/foirequest/views/message.py | 2 +- 8 files changed, 557 insertions(+), 512 deletions(-) create mode 100644 froide/foirequest/api_views/__init__.py create mode 100644 froide/foirequest/api_views/attachment.py create mode 100644 froide/foirequest/api_views/message.py create mode 100644 froide/foirequest/api_views/request.py rename froide/foirequest/{api_views.py => serializers.py} (52%) diff --git a/froide/foirequest/api_views/__init__.py b/froide/foirequest/api_views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/froide/foirequest/api_views/attachment.py b/froide/foirequest/api_views/attachment.py new file mode 100644 index 000000000..2a882c824 --- /dev/null +++ b/froide/foirequest/api_views/attachment.py @@ -0,0 +1,74 @@ +from django.contrib.auth import get_user_model + +from django_filters import rest_framework as filters +from rest_framework import mixins, status, viewsets +from rest_framework.response import Response + +from ..auth import ( + CreateOnlyWithScopePermission, + get_read_foiattachment_queryset, +) +from ..models import FoiAttachment +from ..serializers import ( + FoiAttachmentSerializer, + FoiAttachmentTusSerializer, + FoiRequestListSerializer, +) + +User = get_user_model() + + +class FoiAttachmentFilter(filters.FilterSet): + class Meta: + model = FoiAttachment + fields = ( + "name", + "filetype", + "approved", + "is_redacted", + "belongs_to", + ) + + +class FoiAttachmentViewSet( + mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + viewsets.GenericViewSet, +): + serializer_action_classes = { + "create": FoiAttachmentTusSerializer, + "list": FoiAttachmentSerializer, + "retrieve": FoiAttachmentSerializer, + } + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = FoiAttachmentFilter + permission_classes = (CreateOnlyWithScopePermission,) + required_scopes = ["upload:message"] + + def get_serializer_class(self): + try: + return self.serializer_action_classes[self.action] + except (KeyError, AttributeError): + return FoiRequestListSerializer + + def get_queryset(self): + qs = get_read_foiattachment_queryset(self.request) + return self.optimize_query(qs) + + def optimize_query(self, qs): + return qs.prefetch_related( + "belongs_to", + "belongs_to__request", + "belongs_to__request__user", + ) + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = self.perform_create(serializer) + data = FoiAttachmentSerializer(instance, context={"request": request}).data + return Response(data, status=status.HTTP_201_CREATED) + + def perform_create(self, serializer): + return serializer.save() diff --git a/froide/foirequest/api_views/message.py b/froide/foirequest/api_views/message.py new file mode 100644 index 000000000..4b7f953c2 --- /dev/null +++ b/froide/foirequest/api_views/message.py @@ -0,0 +1,38 @@ +from django.contrib.auth import get_user_model + +from django_filters import rest_framework as filters +from rest_framework import viewsets + +from ..auth import ( + get_read_foimessage_queryset, +) +from ..models import FoiMessage +from ..serializers import FoiMessageSerializer, optimize_message_queryset + +User = get_user_model() + + +class FoiMessageFilter(filters.FilterSet): + class Meta: + model = FoiMessage + fields = ( + "request", + "kind", + "is_response", + ) + + +class FoiMessageViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = FoiMessageSerializer + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = FoiMessageFilter + + def get_queryset(self): + qs = get_read_foimessage_queryset(self.request).order_by() + return self.optimize_query(qs) + + def optimize_query(self, qs): + return optimize_message_queryset(self.request, qs) + + # @action(methods=["get", "post"], detail=False, url_name="draft") + # def get_or_create_draft(self, request): diff --git a/froide/foirequest/api_views/request.py b/froide/foirequest/api_views/request.py new file mode 100644 index 000000000..d01a4cc80 --- /dev/null +++ b/froide/foirequest/api_views/request.py @@ -0,0 +1,241 @@ +from django.contrib.auth import get_user_model +from django.db.models import Q + +from django_filters import rest_framework as filters +from rest_framework import mixins, status, throttling, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response +from taggit.models import Tag + +from froide.campaign.models import Campaign +from froide.helper.search.api_views import ESQueryMixin + +from ..auth import ( + CreateOnlyWithScopePermission, + get_read_foirequest_queryset, + throttle_action, +) +from ..documents import FoiRequestDocument +from ..filters import FoiRequestFilterSet +from ..models import FoiRequest +from ..serializers import ( + FoiRequestDetailSerializer, + FoiRequestListSerializer, + MakeRequestSerializer, +) +from ..utils import check_throttle + +User = get_user_model() + + +def filter_by_user_queryset(request): + user_filter = Q(is_active=True, private=False) + if request is None or not request.user.is_authenticated: + return User.objects.filter(user_filter) + + user = request.user + token = request.auth + + if not token and user.is_superuser: + return User.objects.all() + + # Either not OAuth or OAuth and valid token + if not token or token.is_valid(["read:request"]): + # allow filter by own user + user_filter |= Q(pk=request.user.pk) + + return User.objects.filter(user_filter) + + +def filter_by_authenticated_user_queryset(request): + if request is None or not request.user.is_authenticated: + return User.objects.none() + + user = request.user + token = request.auth + + if not token and user.is_superuser: + # Allow superusers complete access + return User.objects.all() + + if not token or token.is_valid(["read:request"]): + # allow filter by own user + return User.objects.filter(id=user.id) + return User.objects.none() + + +class FoiRequestFilter(filters.FilterSet): + user = filters.ModelChoiceFilter(queryset=filter_by_user_queryset) + tags = filters.CharFilter(method="tag_filter") + categories = filters.CharFilter(method="categories_filter") + classification = filters.CharFilter(method="classification_filter") + reference = filters.CharFilter(method="reference_filter") + follower = filters.ModelChoiceFilter( + queryset=filter_by_authenticated_user_queryset, method="follower_filter" + ) + costs = filters.RangeFilter() + campaign = filters.ModelChoiceFilter( + queryset=Campaign.objects.filter(public=True), + null_value="-", + null_label="No Campaign", + lookup_expr="isnull", + method="campaign_filter", + ) + created_at_after = filters.DateFilter(field_name="created_at", lookup_expr="gte") + created_at_before = filters.DateFilter(field_name="created_at", lookup_expr="lt") + has_same = filters.BooleanFilter( + field_name="same_as", lookup_expr="isnull", exclude=True + ) + + # FIXME: default ordering should be undetermined? + # ordering = filters.OrderingFilter( + # fields=( + # ('last_message', 'last_message'), + # ('first_message', 'first_message') + # ), + # field_labels={ + # '-last_message': 'By last message (latest first)', + # '-first_message': 'By first message (latest first)', + # 'last_message': 'By last message (oldest first)', + # 'first_message': 'By first message (oldest first)', + # } + # ) + + class Meta: + model = FoiRequest + fields = ( + "user", + "is_foi", + "checked", + "jurisdiction", + "tags", + "resolution", + "status", + "reference", + "classification", + "public_body", + "slug", + "costs", + "project", + "campaign", + "law", + ) + + def tag_filter(self, queryset, name, value): + return queryset.filter( + **{ + "tags__name": value, + } + ) + + def categories_filter(self, queryset, name, value): + return queryset.filter( + **{ + "public_body__categories__name": value, + } + ) + + def classification_filter(self, queryset, name, value): + return queryset.filter( + **{ + "public_body__classification__name": value, + } + ) + + def reference_filter(self, queryset, name, value): + return queryset.filter( + **{ + "reference__startswith": value, + } + ) + + def follower_filter(self, queryset, name, value): + return queryset.filter(followers__user=value) + + def campaign_filter(self, queryset, name, value): + if value == "-": + return queryset.filter(campaign__isnull=True) + return queryset.filter(campaign=value) + + +class MakeRequestThrottle(throttling.BaseThrottle): + def allow_request(self, request, view): + return not bool(check_throttle(request.user, FoiRequest)) + + +class FoiRequestViewSet( + mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + ESQueryMixin, + viewsets.GenericViewSet, +): + serializer_action_classes = { + "create": MakeRequestSerializer, + "list": FoiRequestListSerializer, + "retrieve": FoiRequestDetailSerializer, + } + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = FoiRequestFilter + permission_classes = (CreateOnlyWithScopePermission,) + required_scopes = ["make:request"] + search_model = FoiRequest + search_document = FoiRequestDocument + read_token_scopes = ["read:request"] + searchfilterset_class = FoiRequestFilterSet + + def get_serializer_class(self): + try: + return self.serializer_action_classes[self.action] + except (KeyError, AttributeError): + return FoiRequestListSerializer + + def get_queryset(self): + qs = get_read_foirequest_queryset(self.request) + return self.optimize_query(qs) + + def optimize_query(self, qs): + extras = () + if self.action == "retrieve": + extras = ("law",) + qs = qs.prefetch_related( + "public_body", "user", "tags", "public_body__jurisdiction", *extras + ) + return qs + + @action(detail=False, methods=["get"]) + def search(self, request): + return self.search_view(request) + + @action( + detail=False, + methods=["get"], + url_path="tags/autocomplete", + url_name="tags-autocomplete", + ) + def tags_autocomplete(self, request): + query = request.GET.get("q", "") + tags = Tag.objects.none() + if query: + tags = ( + Tag.objects.filter(name__istartswith=query) + .only("name") + .order_by("name") + ) + + page = self.paginate_queryset(tags) + return self.get_paginated_response( + [{"value": t.name, "label": t.name} for t in page] + ) + + @throttle_action((MakeRequestThrottle,)) + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = self.perform_create(serializer) + data = {"status": "success", "url": instance.get_absolute_domain_url()} + headers = {"Location": str(instance.get_absolute_url())} + return Response(data, status=status.HTTP_201_CREATED, headers=headers) + + def perform_create(self, serializer): + return serializer.save(user=self.request.user, request=self.request) diff --git a/froide/foirequest/apps.py b/froide/foirequest/apps.py index c8f95832d..c02d1e065 100644 --- a/froide/foirequest/apps.py +++ b/froide/foirequest/apps.py @@ -23,11 +23,9 @@ def ready(self): from froide.account.export import registry from froide.api import api_router from froide.foirequest import signals # noqa - from froide.foirequest.api_views import ( - FoiAttachmentViewSet, - FoiMessageViewSet, - FoiRequestViewSet, - ) + from froide.foirequest.api_views.attachment import FoiAttachmentViewSet + from froide.foirequest.api_views.message import FoiMessageViewSet + from froide.foirequest.api_views.request import FoiRequestViewSet from froide.helper.search import search_registry from froide.team import team_changed diff --git a/froide/foirequest/auth.py b/froide/foirequest/auth.py index 805e3c40b..698970604 100644 --- a/froide/foirequest/auth.py +++ b/froide/foirequest/auth.py @@ -9,6 +9,7 @@ from django.utils.translation import override from crossdomainmedia import CrossDomainMediaAuth +from oauth2_provider.contrib.rest_framework import TokenHasScope from froide.helper.auth import ( can_manage_object, @@ -345,3 +346,26 @@ def get_media_file_path(self) -> str: """ obj = self.context["object"] return obj.file.name + + +def throttle_action(throttle_classes): + def inner(method): + def _inner(self, request, *args, **kwargs): + for throttle_class in throttle_classes: + throttle = throttle_class() + if not throttle.allow_request(request, self): + self.throttled(request, throttle.wait()) + return method(self, request, *args, **kwargs) + + return _inner + + return inner + + +class CreateOnlyWithScopePermission(TokenHasScope): + def has_permission(self, request, view): + if view.action not in ("create", "update"): + return True + if not request.user.is_authenticated: + return False + return super().has_permission(request, view) diff --git a/froide/foirequest/api_views.py b/froide/foirequest/serializers.py similarity index 52% rename from froide/foirequest/api_views.py rename to froide/foirequest/serializers.py index 7cc46f014..5302d33fa 100644 --- a/froide/foirequest/api_views.py +++ b/froide/foirequest/serializers.py @@ -1,19 +1,11 @@ -from django.contrib.auth import get_user_model -from django.db.models import Prefetch, Q +from django.db.models import Prefetch -from django_filters import rest_framework as filters -from oauth2_provider.contrib.rest_framework import TokenHasScope -from rest_framework import mixins, serializers, status, throttling, viewsets -from rest_framework.decorators import action -from rest_framework.response import Response -from taggit.models import Tag +from rest_framework import serializers -from froide.campaign.models import Campaign from froide.document.api_views import DocumentSerializer from froide.foirequest.forms.message import TransferUploadForm from froide.foirequest.models.message import MessageKind from froide.helper.auth import get_write_queryset -from froide.helper.search.api_views import ESQueryMixin from froide.helper.text_diff import get_differences from froide.publicbody.api_views import ( FoiLawSerializer, @@ -25,206 +17,144 @@ from .auth import ( can_read_foirequest_authenticated, get_read_foiattachment_queryset, - get_read_foimessage_queryset, - get_read_foirequest_queryset, ) -from .documents import FoiRequestDocument -from .filters import FoiRequestFilterSet from .models import FoiAttachment, FoiMessage, FoiRequest from .services import CreateRequestService -from .utils import check_throttle from .validators import clean_reference -User = get_user_model() - - -def filter_by_user_queryset(request): - user_filter = Q(is_active=True, private=False) - if request is None or not request.user.is_authenticated: - return User.objects.filter(user_filter) - - user = request.user - token = request.auth - - if not token and user.is_superuser: - return User.objects.all() - - # Either not OAuth or OAuth and valid token - if not token or token.is_valid(["read:request"]): - # allow filter by own user - user_filter |= Q(pk=request.user.pk) - - return User.objects.filter(user_filter) - - -def filter_by_authenticated_user_queryset(request): - if request is None or not request.user.is_authenticated: - return User.objects.none() - - user = request.user - token = request.auth - - if not token and user.is_superuser: - # Allow superusers complete access - return User.objects.all() - - if not token or token.is_valid(["read:request"]): - # allow filter by own user - return User.objects.filter(id=user.id) - return User.objects.none() +class TagListField(serializers.CharField): + child = serializers.CharField() -class CreateOnlyWithScopePermission(TokenHasScope): - def has_permission(self, request, view): - if view.action not in ("create", "update"): - return True - if not request.user.is_authenticated: - return False - return super().has_permission(request, view) + def to_representation(self, data): + return [t.name for t in data.all()] -class FoiAttachmentSerializer(serializers.HyperlinkedModelSerializer): +class FoiRequestListSerializer(serializers.HyperlinkedModelSerializer): resource_uri = serializers.HyperlinkedIdentityField( - view_name="api:attachment-detail", lookup_field="pk" + view_name="api:request-detail", lookup_field="pk" ) - converted = serializers.HyperlinkedRelatedField( - view_name="api:attachment-detail", - lookup_field="pk", - read_only=True, + public_body = SimplePublicBodySerializer(read_only=True) + law = serializers.HyperlinkedRelatedField( + read_only=True, view_name="api:law-detail", lookup_field="pk" ) - redacted = serializers.HyperlinkedRelatedField( - view_name="api:attachment-detail", - lookup_field="pk", + jurisdiction = serializers.HyperlinkedRelatedField( + view_name="api:jurisdiction-detail", lookup_field="pk", read_only=True + ) + same_as = serializers.HyperlinkedRelatedField( + view_name="api:request-detail", lookup_field="pk", read_only=True + ) + user = serializers.SerializerMethodField(source="get_user") + project = serializers.PrimaryKeyRelatedField( read_only=True, ) - belongs_to = serializers.HyperlinkedRelatedField( - read_only=True, view_name="api:message-detail" + campaign = serializers.HyperlinkedRelatedField( + read_only=True, view_name="api:campaign-detail", lookup_field="pk" ) - document = DocumentSerializer() - site_url = serializers.CharField(source="get_absolute_domain_url", read_only=True) - anchor_url = serializers.CharField(source="get_domain_anchor_url", read_only=True) - file_url = serializers.SerializerMethodField(source="get_file_url", read_only=True) + tags = TagListField() + + description = serializers.CharField(source="get_description") + redacted_description = serializers.SerializerMethodField() + costs = serializers.SerializerMethodField() class Meta: - model = FoiAttachment + model = FoiRequest depth = 0 fields = ( "resource_uri", "id", - "belongs_to", - "name", - "filetype", - "size", - "site_url", - "anchor_url", - "file_url", - "pending", - "is_converted", - "converted", - "approved", - "can_approve", - "redacted", - "is_redacted", - "can_redact", - "can_delete", - "is_pdf", - "is_image", - "is_irrelevant", - "document", + "url", + "jurisdiction", + "is_foi", + "checked", + "refusal_reason", + "costs", + "public", + "law", + "description", + "redacted_description", + "summary", + "same_as_count", + "same_as", + "due_date", + "resolved_on", + "last_message", + "created_at", + "last_modified_at", + "status", + "public_body", + "resolution", + "slug", + "title", + "reference", + "user", + "project", + "campaign", + "tags", ) - def get_file_url(self, obj): - return obj.get_absolute_domain_file_url(authorized=True) - - -class FoiAttachmentTusSerializer(serializers.Serializer): - message = serializers.IntegerField() - upload = serializers.CharField() + def get_user(self, obj): + if obj.user is None: + return None + user = self.context["request"].user + if obj.user == user or user.is_superuser: + return obj.user.pk + if obj.user.private: + return None + return obj.user.pk - def validate_message(self, value): - writable_requests = get_write_queryset( - FoiRequest.objects.all(), - self.context["request"], - has_team=True, - scope="upload:message", + def get_redacted_description(self, obj): + request = self.context["request"] + authenticated_read = can_read_foirequest_authenticated( + obj, request, allow_code=False ) - try: - return FoiMessage.objects.get( - pk=value, - request__in=writable_requests, - kind=MessageKind.POST, - ) - except FoiMessage.DoesNotExist as e: - raise serializers.ValidationError("Message not found") from e + return obj.get_redacted_description(authenticated_read) - def validate(self, data): - self.form = TransferUploadForm( - data=data, foimessage=data["message"], user=self.context["request"].user - ) - if not self.form.is_valid(): - raise serializers.ValidationError("Invalid upload") + def get_costs(self, obj): + return float(obj.costs) - return data - def create(self, validated_data): - added = self.form.save(self.context["request"]) - return added[0] +class FoiRequestDetailSerializer(FoiRequestListSerializer): + public_body = PublicBodySerializer(read_only=True) + law = FoiLawSerializer(read_only=True) + messages = serializers.SerializerMethodField() + class Meta(FoiRequestListSerializer.Meta): + fields = FoiRequestListSerializer.Meta.fields + ("messages",) -class FoiAttachmentFilter(filters.FilterSet): - class Meta: - model = FoiAttachment - fields = ( - "name", - "filetype", - "approved", - "is_redacted", - "belongs_to", + def get_messages(self, obj): + qs = optimize_message_queryset( + self.context["request"], FoiMessage.objects.filter(request=obj) ) + return FoiMessageSerializer( + qs, read_only=True, many=True, context=self.context + ).data -class FoiAttachmentViewSet( - mixins.CreateModelMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - viewsets.GenericViewSet, -): - serializer_action_classes = { - "create": FoiAttachmentTusSerializer, - "list": FoiAttachmentSerializer, - "retrieve": FoiAttachmentSerializer, - } - filter_backends = (filters.DjangoFilterBackend,) - filterset_class = FoiAttachmentFilter - permission_classes = (CreateOnlyWithScopePermission,) - required_scopes = ["upload:message"] - - def get_serializer_class(self): - try: - return self.serializer_action_classes[self.action] - except (KeyError, AttributeError): - return FoiRequestListSerializer +class MakeRequestSerializer(serializers.Serializer): + publicbodies = serializers.PrimaryKeyRelatedField( + queryset=PublicBody.objects.all(), many=True + ) - def get_queryset(self): - qs = get_read_foiattachment_queryset(self.request) - return self.optimize_query(qs) + subject = serializers.CharField(max_length=230) + body = serializers.CharField() - def optimize_query(self, qs): - return qs.prefetch_related( - "belongs_to", - "belongs_to__request", - "belongs_to__request__user", - ) + full_text = serializers.BooleanField(required=False, default=False) + public = serializers.BooleanField(required=False, default=True) + reference = serializers.CharField(required=False, default="") + tags = serializers.ListField( + required=False, child=serializers.CharField(max_length=255) + ) - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - instance = self.perform_create(serializer) - data = FoiAttachmentSerializer(instance, context={"request": request}).data - return Response(data, status=status.HTTP_201_CREATED) + def validate_reference(self, value): + cleaned_reference = clean_reference(value) + if value and cleaned_reference != value: + raise serializers.ValidationError("Reference not clean") + return cleaned_reference - def perform_create(self, serializer): - return serializer.save() + def create(self, validated_data): + service = CreateRequestService(validated_data) + return service.execute(validated_data["request"]) class FoiMessageSerializer(serializers.HyperlinkedModelSerializer): @@ -325,362 +255,102 @@ def get_attachments(self, obj): return serializer.data -class FoiMessageFilter(filters.FilterSet): - class Meta: - model = FoiMessage - fields = ( - "request", - "kind", - "is_response", - ) - - -class FoiMessageViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = FoiMessageSerializer - filter_backends = (filters.DjangoFilterBackend,) - filterset_class = FoiMessageFilter - - def get_queryset(self): - qs = get_read_foimessage_queryset(self.request).order_by() - return self.optimize_query(qs) - - def optimize_query(self, qs): - return optimize_message_queryset(self.request, qs) - - -def optimize_message_queryset(request, qs): - atts = get_read_foiattachment_queryset( - request, queryset=FoiAttachment.objects.filter(belongs_to__in=qs) - ) - return qs.prefetch_related( - "request", - "request__user", - "sender_user", - "sender_public_body", - Prefetch("foiattachment_set", queryset=atts, to_attr="visible_attachments"), - ) - - -class TagListField(serializers.CharField): - child = serializers.CharField() - - def to_representation(self, data): - return [t.name for t in data.all()] - - -class FoiRequestListSerializer(serializers.HyperlinkedModelSerializer): +class FoiAttachmentSerializer(serializers.HyperlinkedModelSerializer): resource_uri = serializers.HyperlinkedIdentityField( - view_name="api:request-detail", lookup_field="pk" - ) - public_body = SimplePublicBodySerializer(read_only=True) - law = serializers.HyperlinkedRelatedField( - read_only=True, view_name="api:law-detail", lookup_field="pk" - ) - jurisdiction = serializers.HyperlinkedRelatedField( - view_name="api:jurisdiction-detail", lookup_field="pk", read_only=True + view_name="api:attachment-detail", lookup_field="pk" ) - same_as = serializers.HyperlinkedRelatedField( - view_name="api:request-detail", lookup_field="pk", read_only=True + converted = serializers.HyperlinkedRelatedField( + view_name="api:attachment-detail", + lookup_field="pk", + read_only=True, ) - user = serializers.SerializerMethodField(source="get_user") - project = serializers.PrimaryKeyRelatedField( + redacted = serializers.HyperlinkedRelatedField( + view_name="api:attachment-detail", + lookup_field="pk", read_only=True, ) - campaign = serializers.HyperlinkedRelatedField( - read_only=True, view_name="api:campaign-detail", lookup_field="pk" + belongs_to = serializers.HyperlinkedRelatedField( + read_only=True, view_name="api:message-detail" ) - tags = TagListField() - - description = serializers.CharField(source="get_description") - redacted_description = serializers.SerializerMethodField() - costs = serializers.SerializerMethodField() + document = DocumentSerializer() + site_url = serializers.CharField(source="get_absolute_domain_url", read_only=True) + anchor_url = serializers.CharField(source="get_domain_anchor_url", read_only=True) + file_url = serializers.SerializerMethodField(source="get_file_url", read_only=True) class Meta: - model = FoiRequest + model = FoiAttachment depth = 0 fields = ( "resource_uri", "id", - "url", - "jurisdiction", - "is_foi", - "checked", - "refusal_reason", - "costs", - "public", - "law", - "description", - "redacted_description", - "summary", - "same_as_count", - "same_as", - "due_date", - "resolved_on", - "last_message", - "created_at", - "last_modified_at", - "status", - "public_body", - "resolution", - "slug", - "title", - "reference", - "user", - "project", - "campaign", - "tags", - ) - - def get_user(self, obj): - if obj.user is None: - return None - user = self.context["request"].user - if obj.user == user or user.is_superuser: - return obj.user.pk - if obj.user.private: - return None - return obj.user.pk - - def get_redacted_description(self, obj): - request = self.context["request"] - authenticated_read = can_read_foirequest_authenticated( - obj, request, allow_code=False + "belongs_to", + "name", + "filetype", + "size", + "site_url", + "anchor_url", + "file_url", + "pending", + "is_converted", + "converted", + "approved", + "can_approve", + "redacted", + "is_redacted", + "can_redact", + "can_delete", + "is_pdf", + "is_image", + "is_irrelevant", + "document", ) - return obj.get_redacted_description(authenticated_read) - - def get_costs(self, obj): - return float(obj.costs) + def get_file_url(self, obj): + return obj.get_absolute_domain_file_url(authorized=True) -class FoiRequestDetailSerializer(FoiRequestListSerializer): - public_body = PublicBodySerializer(read_only=True) - law = FoiLawSerializer(read_only=True) - messages = serializers.SerializerMethodField() - class Meta(FoiRequestListSerializer.Meta): - fields = FoiRequestListSerializer.Meta.fields + ("messages",) +class FoiAttachmentTusSerializer(serializers.Serializer): + message = serializers.IntegerField() + upload = serializers.CharField() - def get_messages(self, obj): - qs = optimize_message_queryset( - self.context["request"], FoiMessage.objects.filter(request=obj) + def validate_message(self, value): + writable_requests = get_write_queryset( + FoiRequest.objects.all(), + self.context["request"], + has_team=True, + scope="upload:message", ) - return FoiMessageSerializer( - qs, read_only=True, many=True, context=self.context - ).data - - -class MakeRequestSerializer(serializers.Serializer): - publicbodies = serializers.PrimaryKeyRelatedField( - queryset=PublicBody.objects.all(), many=True - ) - - subject = serializers.CharField(max_length=230) - body = serializers.CharField() + try: + return FoiMessage.objects.get( + pk=value, + request__in=writable_requests, + kind=MessageKind.POST, + ) + except FoiMessage.DoesNotExist as e: + raise serializers.ValidationError("Message not found") from e - full_text = serializers.BooleanField(required=False, default=False) - public = serializers.BooleanField(required=False, default=True) - reference = serializers.CharField(required=False, default="") - tags = serializers.ListField( - required=False, child=serializers.CharField(max_length=255) - ) + def validate(self, data): + self.form = TransferUploadForm( + data=data, foimessage=data["message"], user=self.context["request"].user + ) + if not self.form.is_valid(): + raise serializers.ValidationError("Invalid upload") - def validate_reference(self, value): - cleaned_reference = clean_reference(value) - if value and cleaned_reference != value: - raise serializers.ValidationError("Reference not clean") - return cleaned_reference + return data def create(self, validated_data): - service = CreateRequestService(validated_data) - return service.execute(validated_data["request"]) + added = self.form.save(self.context["request"]) + return added[0] -class FoiRequestFilter(filters.FilterSet): - user = filters.ModelChoiceFilter(queryset=filter_by_user_queryset) - tags = filters.CharFilter(method="tag_filter") - categories = filters.CharFilter(method="categories_filter") - classification = filters.CharFilter(method="classification_filter") - reference = filters.CharFilter(method="reference_filter") - follower = filters.ModelChoiceFilter( - queryset=filter_by_authenticated_user_queryset, method="follower_filter" - ) - costs = filters.RangeFilter() - campaign = filters.ModelChoiceFilter( - queryset=Campaign.objects.filter(public=True), - null_value="-", - null_label="No Campaign", - lookup_expr="isnull", - method="campaign_filter", - ) - created_at_after = filters.DateFilter(field_name="created_at", lookup_expr="gte") - created_at_before = filters.DateFilter(field_name="created_at", lookup_expr="lt") - has_same = filters.BooleanFilter( - field_name="same_as", lookup_expr="isnull", exclude=True +def optimize_message_queryset(request, qs): + atts = get_read_foiattachment_queryset( + request, queryset=FoiAttachment.objects.filter(belongs_to__in=qs) ) - - # FIXME: default ordering should be undetermined? - # ordering = filters.OrderingFilter( - # fields=( - # ('last_message', 'last_message'), - # ('first_message', 'first_message') - # ), - # field_labels={ - # '-last_message': 'By last message (latest first)', - # '-first_message': 'By first message (latest first)', - # 'last_message': 'By last message (oldest first)', - # 'first_message': 'By first message (oldest first)', - # } - # ) - - class Meta: - model = FoiRequest - fields = ( - "user", - "is_foi", - "checked", - "jurisdiction", - "tags", - "resolution", - "status", - "reference", - "classification", - "public_body", - "slug", - "costs", - "project", - "campaign", - "law", - ) - - def tag_filter(self, queryset, name, value): - return queryset.filter( - **{ - "tags__name": value, - } - ) - - def categories_filter(self, queryset, name, value): - return queryset.filter( - **{ - "public_body__categories__name": value, - } - ) - - def classification_filter(self, queryset, name, value): - return queryset.filter( - **{ - "public_body__classification__name": value, - } - ) - - def reference_filter(self, queryset, name, value): - return queryset.filter( - **{ - "reference__startswith": value, - } - ) - - def follower_filter(self, queryset, name, value): - return queryset.filter(followers__user=value) - - def campaign_filter(self, queryset, name, value): - if value == "-": - return queryset.filter(campaign__isnull=True) - return queryset.filter(campaign=value) - - -class MakeRequestThrottle(throttling.BaseThrottle): - def allow_request(self, request, view): - return not bool(check_throttle(request.user, FoiRequest)) - - -def throttle_action(throttle_classes): - def inner(method): - def _inner(self, request, *args, **kwargs): - for throttle_class in throttle_classes: - throttle = throttle_class() - if not throttle.allow_request(request, self): - self.throttled(request, throttle.wait()) - return method(self, request, *args, **kwargs) - - return _inner - - return inner - - -class FoiRequestViewSet( - mixins.CreateModelMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - ESQueryMixin, - viewsets.GenericViewSet, -): - serializer_action_classes = { - "create": MakeRequestSerializer, - "list": FoiRequestListSerializer, - "retrieve": FoiRequestDetailSerializer, - } - filter_backends = (filters.DjangoFilterBackend,) - filterset_class = FoiRequestFilter - permission_classes = (CreateOnlyWithScopePermission,) - required_scopes = ["make:request"] - search_model = FoiRequest - search_document = FoiRequestDocument - read_token_scopes = ["read:request"] - searchfilterset_class = FoiRequestFilterSet - - def get_serializer_class(self): - try: - return self.serializer_action_classes[self.action] - except (KeyError, AttributeError): - return FoiRequestListSerializer - - def get_queryset(self): - qs = get_read_foirequest_queryset(self.request) - return self.optimize_query(qs) - - def optimize_query(self, qs): - extras = () - if self.action == "retrieve": - extras = ("law",) - qs = qs.prefetch_related( - "public_body", "user", "tags", "public_body__jurisdiction", *extras - ) - return qs - - @action(detail=False, methods=["get"]) - def search(self, request): - return self.search_view(request) - - @action( - detail=False, - methods=["get"], - url_path="tags/autocomplete", - url_name="tags-autocomplete", + return qs.prefetch_related( + "request", + "request__user", + "sender_user", + "sender_public_body", + Prefetch("foiattachment_set", queryset=atts, to_attr="visible_attachments"), ) - def tags_autocomplete(self, request): - query = request.GET.get("q", "") - tags = Tag.objects.none() - if query: - tags = ( - Tag.objects.filter(name__istartswith=query) - .only("name") - .order_by("name") - ) - - page = self.paginate_queryset(tags) - return self.get_paginated_response( - [{"value": t.name, "label": t.name} for t in page] - ) - - @throttle_action((MakeRequestThrottle,)) - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - instance = self.perform_create(serializer) - data = {"status": "success", "url": instance.get_absolute_domain_url()} - headers = {"Location": str(instance.get_absolute_url())} - return Response(data, status=status.HTTP_201_CREATED, headers=headers) - - def perform_create(self, serializer): - return serializer.save(user=self.request.user, request=self.request) diff --git a/froide/foirequest/views/message.py b/froide/foirequest/views/message.py index 378d1ed35..f4bfe7e62 100644 --- a/froide/foirequest/views/message.py +++ b/froide/foirequest/views/message.py @@ -23,7 +23,6 @@ from froide.proof.forms import handle_proof_form from froide.upload.forms import get_uppy_i18n -from ..api_views import FoiAttachmentSerializer, FoiMessageSerializer from ..decorators import ( allow_moderate_foirequest, allow_read_foirequest_authenticated, @@ -48,6 +47,7 @@ from ..models import FoiAttachment, FoiEvent, FoiMessage, FoiRequest from ..models.attachment import IMAGE_FILETYPES, PDF_FILETYPES, POSTAL_CONTENT_TYPES from ..models.message import MessageKind +from ..serializers import FoiAttachmentSerializer, FoiMessageSerializer from ..services import ResendBouncedMessageService from ..tasks import convert_images_to_pdf_task from ..utils import check_throttle