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 3b8d831
Show file tree
Hide file tree
Showing 29 changed files with 616 additions and 380 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 ..auth import can_write_foirequest


class OwnsFoiRequestPermission(permissions.BasePermission):
def __init__(self, foirequest_field="request"):
self.foirequest_field = foirequest_field
super().__init__()

def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS and obj.is_draft is False:
return True

foirequest = getattr(obj, self.foirequest_field)
return can_write_foirequest(foirequest, request)
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
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.foirequest.models.message import MESSAGE_KIND_USER_ALLOWED, MessageKind
from froide.helper.auth import get_write_queryset
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 +162,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 +184,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 +266,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
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,8 @@ class FoiAttachmentViewSet(
}
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = FoiAttachmentFilter
permission_classes = (CreateOnlyWithScopePermission,)
required_scopes = ["upload:message"]
permission_classes = [CreateOnlyWithScopePermission, OwnsFoiRequestPermission]
required_scopes = ["make:message"]

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

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

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_queryset(self):
qs = get_read_foimessage_queryset(self.request).order_by()
return self.optimize_query(qs)

def get_permissions(self):
return [
permissions.IsAuthenticated(),
OwnsFoiRequestPermission("belongs_to__request"),
]

def optimize_query(self, qs):
return optimize_message_queryset(self.request, qs)

def destroy(self, request, *args, **kwargs):
if not self.get_object().is_draft:
raise BadRequest(_("Cannot delete non-draft messages"))

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
3 changes: 2 additions & 1 deletion 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
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 self.is_draft is False

@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
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,26 @@ def test_search_similar(self):
self.assertContains(response, "title")
self.assertContains(response, "description")

def test_create_message(self):
self.client.login(email="[email protected]", password="froide")
req = factories.FoiRequestFactory.create()

response = self.client.post(
"/api/v1/message/",
data={
"request": reverse("api:request-detail", kwargs={"pk": req.pk}),
},
)
self.assertEqual(response.status_code, 201)

message_id = json.loads(response.content)["id"]
resource_uri = reverse("api:message-detail", kwargs={"pk": message_id})

response = self.client.delete(resource_uri)
self.assertEqual(response.status_code, 403)

response = self.client.patch(resource_uri, data={})


class OAuthAPIMixin:
def setUp(self):
Expand Down
Loading

0 comments on commit 3b8d831

Please sign in to comment.