Skip to content

Commit

Permalink
✨ add create + update endpoints to message api, refactor api code
Browse files Browse the repository at this point in the history
  • Loading branch information
krmax44 committed Nov 11, 2024
1 parent 3f093af commit 8ff43c8
Show file tree
Hide file tree
Showing 29 changed files with 652 additions and 407 deletions.
File renamed without changes.
16 changes: 16 additions & 0 deletions froide/foirequest/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from rest_framework import permissions
from rest_framework.views import Request

from ..auth import can_write_foirequest


class OwnsFoiRequestPermission(permissions.BasePermission):
def __init__(self, request_field: str = "request") -> None:
self.request_field = request_field
super().__init__()

def has_object_permission(self, request: Request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True

return can_write_foirequest(getattr(obj, self.request_field), request)
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
from django.db.models import Prefetch
from django.utils import timezone
from django.utils.translation import gettext as _

from rest_framework import serializers
from rest_framework.views import PermissionDenied

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.foirequest.models.message import MESSAGE_KIND_USER_ALLOWED
from froide.helper.text_diff import get_differences
from froide.publicbody.api_views import (
from froide.publicbody.api.serializers import (
FoiLawSerializer,
PublicBodySerializer,
SimplePublicBodySerializer,
)
from froide.publicbody.models import PublicBody

from .auth import (
from ..auth import (
can_read_foirequest_authenticated,
can_write_foirequest,
get_read_foiattachment_queryset,
get_read_foirequest_queryset,
)
from .models import FoiAttachment, FoiMessage, FoiRequest
from .services import CreateRequestService
from .validators import clean_reference
from ..models import FoiAttachment, FoiMessage, FoiRequest
from ..services import CreateRequestService
from ..validators import clean_reference


class TagListField(serializers.CharField):
Expand Down Expand Up @@ -157,13 +161,16 @@ def create(self, validated_data):
return service.execute(validated_data["request"])


class FoiRequestRelatedField(serializers.HyperlinkedRelatedField):
view_name = "api:request-detail"

def get_queryset(self):
return get_read_foirequest_queryset(self.context["request"])


class FoiMessageSerializer(serializers.HyperlinkedModelSerializer):
resource_uri = serializers.HyperlinkedIdentityField(
view_name="api:message-detail", lookup_field="pk"
)
request = serializers.HyperlinkedRelatedField(
read_only=True, view_name="api:request-detail"
)
resource_uri = serializers.HyperlinkedIdentityField(view_name="api:message-detail")
request = FoiRequestRelatedField()
attachments = serializers.SerializerMethodField(source="get_attachments")
sender_public_body = serializers.HyperlinkedRelatedField(
read_only=True, view_name="api:publicbody-detail"
Expand All @@ -176,9 +183,13 @@ class FoiMessageSerializer(serializers.HyperlinkedModelSerializer):
content = serializers.SerializerMethodField(source="get_content")
redacted_subject = serializers.SerializerMethodField(source="get_redacted_subject")
redacted_content = serializers.SerializerMethodField(source="get_redacted_content")
sender = serializers.CharField()
url = serializers.CharField(source="get_absolute_domain_url")
status_name = serializers.CharField(source="get_status_display")
sender = serializers.CharField(read_only=True)
url = serializers.CharField(source="get_absolute_domain_url", read_only=True)
status = serializers.ChoiceField(choices=FoiRequest.STATUS.choices, required=False)
is_draft = serializers.BooleanField(required=False)
status_name = serializers.CharField(source="get_status_display", read_only=True)
not_publishable = serializers.BooleanField(read_only=True)
timestamp = serializers.DateTimeField(default=timezone.now)

class Meta:
model = FoiMessage
Expand Down Expand Up @@ -254,6 +265,32 @@ def get_attachments(self, obj):
)
return serializer.data

def validate_kind(self, value):
# forbid users from e.g. creating a fake e-mail message
if value not in MESSAGE_KIND_USER_ALLOWED:
raise serializers.ValidationError(
"This message kind can not be created via the API."
)
return value

def validate_request(self, value):
if not can_write_foirequest(value, self.context["request"]):
raise PermissionDenied(
_("You do not have permission to add a message to this request.")
)
return value

def ensure_draft(self, instance):
if not instance.is_draft:
raise serializers.ValidationError("Only draft messages can be altered.")

def create(self, validated_data):
return super().create(validated_data)

def update(self, instance, validated_data):
self.ensure_draft(instance)
return super().update(instance, validated_data)


class FoiAttachmentSerializer(serializers.HyperlinkedModelSerializer):
resource_uri = serializers.HyperlinkedIdentityField(
Expand Down Expand Up @@ -310,25 +347,13 @@ def get_file_url(self, obj):


class FoiAttachmentTusSerializer(serializers.Serializer):
message = serializers.IntegerField()
message = serializers.HyperlinkedRelatedField(
view_name="api:message-detail",
lookup_field="pk",
queryset=FoiMessage.objects.all(),
)
upload = serializers.CharField()

def validate_message(self, value):
writable_requests = get_write_queryset(
FoiRequest.objects.all(),
self.context["request"],
has_team=True,
scope="upload:message",
)
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

def validate(self, data):
self.form = TransferUploadForm(
data=data, foimessage=data["message"], user=self.context["request"].user
Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from rest_framework import mixins, status, viewsets
from rest_framework.response import Response

from ..auth import (
from froide.foirequest.api.permissions import OwnsFoiRequestPermission

from ...auth import (
CreateOnlyWithScopePermission,
get_read_foiattachment_queryset,
)
from ..models import FoiAttachment
from ...models import FoiAttachment
from ..serializers import (
FoiAttachmentSerializer,
FoiAttachmentTusSerializer,
Expand Down Expand Up @@ -43,8 +45,15 @@ class FoiAttachmentViewSet(
}
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = FoiAttachmentFilter
permission_classes = (CreateOnlyWithScopePermission,)
required_scopes = ["upload:message"]
required_scopes = ["make:message"]

def get_permissions(self):
if self.action == "list" or self.action == "retrieve":
return []
return [
CreateOnlyWithScopePermission(),
OwnsFoiRequestPermission("belongs_to__request"),
]

def get_serializer_class(self):
try:
Expand Down
55 changes: 55 additions & 0 deletions froide/foirequest/api/views/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _

from django_filters import rest_framework as filters
from rest_framework import permissions, viewsets
from rest_framework.exceptions import PermissionDenied
from rest_framework.views import Request

from froide.foirequest.api.permissions import OwnsFoiRequestPermission

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", "is_draft")


class FoiMessageViewSet(viewsets.ModelViewSet):
serializer_class = FoiMessageSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = FoiMessageFilter
required_scopes = ["make:message"]

def get_permissions(self):
if self.action in ["list", "retrieve"]:
return []
return [permissions.IsAuthenticated(), OwnsFoiRequestPermission()]

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 ensure_draft(self):
# only drafts can be edited
if not self.get_object().is_draft:
raise PermissionDenied(_("Cannot alter non-draft messages"))

def update(self, request: Request, *args, **kwargs):
self.ensure_draft()
return super().update(request, *args, **kwargs)

def destroy(self, request: Request, *args, **kwargs):
self.ensure_draft()
return super().destroy(request, *args, **kwargs)
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@
from froide.campaign.models import Campaign
from froide.helper.search.api_views import ESQueryMixin

from ..auth import (
from ...auth import (
CreateOnlyWithScopePermission,
get_read_foirequest_queryset,
throttle_action,
)
from ..documents import FoiRequestDocument
from ..filters import FoiRequestFilterSet
from ..models import FoiRequest
from ...documents import FoiRequestDocument
from ...filters import FoiRequestFilterSet
from ...models import FoiRequest
from ...utils import check_throttle
from ..serializers import (
FoiRequestDetailSerializer,
FoiRequestListSerializer,
MakeRequestSerializer,
)
from ..utils import check_throttle

User = get_user_model()

Expand Down
38 changes: 0 additions & 38 deletions froide/foirequest/api_views/message.py

This file was deleted.

6 changes: 3 additions & 3 deletions froide/foirequest/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +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.attachment import FoiAttachmentViewSet
from froide.foirequest.api_views.message import FoiMessageViewSet
from froide.foirequest.api_views.request import 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

Expand Down
13 changes: 9 additions & 4 deletions froide/foirequest/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@


def get_campaign_auth_foirequests_filter(request: HttpRequest, fk_path=None):
if not request.user.is_staff:
# request is not available when called from manage.py generateschema
if not request or not request.user.is_staff:
return None

# staff user can act on all campaign-requests
Expand Down Expand Up @@ -157,8 +158,10 @@ def can_read_foiproject_authenticated(


@lru_cache()
def can_write_foirequest(foirequest: FoiRequest, request: HttpRequest) -> bool:
if can_write_object(foirequest, request):
def can_write_foirequest(
foirequest: FoiRequest, request: HttpRequest, scope=None
) -> bool:
if can_write_object(foirequest, request, scope):
return True

if foirequest.project:
Expand Down Expand Up @@ -366,6 +369,8 @@ class CreateOnlyWithScopePermission(TokenHasScope):
def has_permission(self, request, view):
if view.action not in ("create", "update"):
return True
if request.user.is_authenticated:
if request.user.is_authenticated and request.auth is None:
# allow api use with session authentication
# see https://www.django-rest-framework.org/api-guide/authentication/#sessionauthentication
return True
return super().has_permission(request, view)
Loading

0 comments on commit 8ff43c8

Please sign in to comment.