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 Dec 6, 2024
1 parent 9440f6f commit 545799b
Show file tree
Hide file tree
Showing 28 changed files with 635 additions and 386 deletions.
File renamed without changes.
33 changes: 33 additions & 0 deletions froide/foirequest/api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from rest_framework import permissions
from rest_framework.views import Request

from froide.foirequest.models.attachment import FoiAttachment
from froide.foirequest.models.message import FoiMessage
from froide.foirequest.models.request import FoiRequest

from ..auth import can_write_foirequest


class WriteFoiRequestPermission(permissions.BasePermission):
def get_foirequest(self, obj) -> FoiRequest:
if isinstance(obj, FoiRequest):
return obj
elif isinstance(obj, FoiMessage):
return obj.request
elif isinstance(obj, FoiAttachment):
return obj.belongs_to.request
raise ValueError("Cannot determine request from object")

def has_object_permission(self, request: Request, view, obj) -> bool:
if request.method in permissions.SAFE_METHODS:
return True
foirequest = self.get_foirequest(obj)
return can_write_foirequest(foirequest, request)


class OnlyEditableWhenDraftPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
else:
return obj.is_draft
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
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 import permissions, 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, MessageKind
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_moderate_pii_foirequest,
can_read_foirequest_authenticated,
can_write_foirequest,
get_read_foiattachment_queryset,
get_read_foirequest_queryset,
get_write_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 @@ -96,8 +102,9 @@ class Meta:
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:
request = self.context["request"]
user = request.user
if obj.user == user or can_moderate_pii_foirequest(obj, request):
return obj.user.pk
if obj.user.private:
return None
Expand Down Expand Up @@ -157,13 +164,20 @@ def create(self, validated_data):
return service.execute(validated_data["request"])


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

def get_queryset(self):
request = self.context["request"]
if request.method in permissions.SAFE_METHODS:
return get_read_foirequest_queryset(request)
else:
return get_write_foirequest_queryset(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 +190,14 @@ 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)
kind = serializers.ChoiceField(choices=MessageKind.choices, required=True)
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 +273,21 @@ 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


class FoiAttachmentSerializer(serializers.HyperlinkedModelSerializer):
resource_uri = serializers.HyperlinkedIdentityField(
Expand Down Expand Up @@ -310,25 +344,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 WriteFoiRequestPermission

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,11 @@ class FoiAttachmentViewSet(
}
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = FoiAttachmentFilter
permission_classes = (CreateOnlyWithScopePermission,)
required_scopes = ["upload:message"]
permission_classes = [
CreateOnlyWithScopePermission,
WriteFoiRequestPermission,
]
required_scopes = ["make:message"]

def get_serializer_class(self):
try:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from django.contrib.auth import get_user_model

from django_filters import rest_framework as filters
from rest_framework import viewsets
from rest_framework import permissions, viewsets

from ..auth import (
from froide.foirequest.api.permissions import (
OnlyEditableWhenDraftPermission,
WriteFoiRequestPermission,
)

from ...auth import (
get_read_foimessage_queryset,
)
from ..models import FoiMessage
from ...models import FoiMessage
from ..serializers import FoiMessageSerializer, optimize_message_queryset

User = get_user_model()
Expand All @@ -15,24 +20,23 @@
class FoiMessageFilter(filters.FilterSet):
class Meta:
model = FoiMessage
fields = (
"request",
"kind",
"is_response",
)
fields = ("request", "kind", "is_response", "is_draft")


class FoiMessageViewSet(viewsets.ReadOnlyModelViewSet):
class FoiMessageViewSet(viewsets.ModelViewSet):
serializer_class = FoiMessageSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = FoiMessageFilter
required_scopes = ["make:message"]
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
WriteFoiRequestPermission,
OnlyEditableWhenDraftPermission,
]

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):
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
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
9 changes: 6 additions & 3 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
19 changes: 14 additions & 5 deletions froide/foirequest/models/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,8 @@ def get_throttle_filter(self, queryset, user, extra_filters=None):
qs = qs.filter(**extra_filters)
return qs, "timestamp"


class FoiMessageNoDraftsManager(FoiMessageManager):
def get_queryset(self):
return super().get_queryset().filter(is_draft=False)
def get_drafts(self, drafts=True):
return super().get_queryset().filter(is_draft=drafts)


class MessageTag(TagBase):
Expand All @@ -64,12 +62,21 @@ class MessageKind(models.TextChoices):
EMAIL = ("email", _("email"))
POST = ("post", _("postal mail"))
FAX = ("fax", _("fax"))
# uploads by public bodies using link in foirequest
UPLOAD = ("upload", _("upload"))
PHONE = ("phone", _("phone call"))
VISIT = ("visit", _("visit in person"))
IMPORT = ("import", _("automatically imported"))


# users are allowed to only create messages of these kinds
# the other kinds can only be created by the system
MESSAGE_KIND_USER_ALLOWED = [
MessageKind.POST,
MessageKind.PHONE,
MessageKind.VISIT,
]

MESSAGE_KIND_ICONS = {
MessageKind.EMAIL: "mail",
MessageKind.POST: "newspaper-o",
Expand Down Expand Up @@ -176,7 +183,6 @@ class FoiMessage(models.Model):
confirmation_sent = models.BooleanField(_("Confirmation sent?"), default=False)

objects = FoiMessageManager()
no_drafts = FoiMessageNoDraftsManager()

class Meta:
get_latest_by = "timestamp"
Expand All @@ -201,6 +207,9 @@ def save(self, *args, **kwargs):

super().save(*args, **kwargs)

def is_public(self) -> bool:
return not self.is_draft

@property
def is_postal(self):
return self.kind == MessageKind.POST
Expand Down
2 changes: 1 addition & 1 deletion froide/foirequest/models/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ def ident(self):

def get_messages(self, with_tags=False):
qs = (
self.foimessage_set(manager="no_drafts")
self.foimessage_set.filter(is_draft=False)
.select_related(
"sender_user", "sender_public_body", "recipient_public_body"
)
Expand Down
2 changes: 1 addition & 1 deletion froide/foirequest/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .test_admin import * # noqa
from .test_api import * # noqa
from .test_api_request import * # noqa
from .test_mail import * # noqa
from .test_request import * # noqa
from .test_web import * # noqa
Loading

0 comments on commit 545799b

Please sign in to comment.