Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MVJ-112: area search attachments: hide user and attachment path from API response in public #819

Open
wants to merge 9 commits into
base: develop
Choose a base branch
from
12 changes: 12 additions & 0 deletions forms/serializers/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
RequiredFormFieldValidator,
)

# These fields include sensitive or unnecessary data for the public API
EXCLUDED_ATTACHMENT_FIELDS = ["attachment", "created_at"]


class RecursiveSerializer(serializers.Serializer):
def to_representation(self, value):
Expand Down Expand Up @@ -923,5 +926,14 @@ def save(self, **kwargs):
return super().save(**kwargs)


class AttachmentPublicSerializer(AttachmentSerializer):
def to_representation(self, instance):
representation = super().to_representation(instance)
for key in EXCLUDED_ATTACHMENT_FIELDS:
if key in representation:
representation.pop(key)
return representation


class ReadAttachmentSerializer(AttachmentSerializer):
field = serializers.CharField(source="field.identifier")
86 changes: 82 additions & 4 deletions forms/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
from faker import Faker

from forms.enums import FormState
from forms.models import Entry, Field
from forms.models import Entry
from forms.models.form import AnswerOpeningRecord, Attachment
from forms.serializers.form import EXCLUDED_ATTACHMENT_FIELDS
from plotsearch.enums import DeclineReason
from plotsearch.models import TargetStatus

Expand Down Expand Up @@ -343,12 +344,18 @@ def test_meeting_memo_create(


@pytest.mark.django_db
def test_attachment_post(
def test_attachment_upload(
django_db_setup, admin_client, admin_user, plot_search_target, basic_form
):
"""
should upload an attachment, create a form answer, and the attachment should be linked to the form answer
"""
example_file = SimpleUploadedFile(name="example.txt", content=b"Lorem lipsum")
field = basic_form.sections.get(identifier="application-target").fields.get(
identifier="reference-attachments"
)
payload = {
"field": Field.objects.all().first().id,
"field": field.id,
"name": fake.name(),
"attachment": example_file,
}
Expand Down Expand Up @@ -418,7 +425,7 @@ def test_attachment_post(
def test_attachment_delete(
django_db_setup, admin_client, admin_user, plot_search_target, basic_form
):
test_attachment_post(
test_attachment_upload(
django_db_setup, admin_client, admin_user, plot_search_target, basic_form
)
attachment = Attachment.objects.all().first()
Expand All @@ -430,6 +437,77 @@ def test_attachment_delete(
assert os.path.isfile(file_path) is False


@pytest.mark.django_db
def test_attachment_upload_public(
django_db_setup, admin_client, admin_user, plot_search_target, basic_form
):
"""
should upload an attachment with the public API and not return any sensitive or unnecessary fields in the HTTP,
create a form answer, and the attachment should be linked to a form answer
"""
example_file = SimpleUploadedFile(name="example.txt", content=b"Lorem lipsum")
field = basic_form.sections.get(identifier="application-target").fields.get(
identifier="reference-attachments"
)
payload = {
"field": field.id,
"name": fake.name(),
"attachment": example_file,
}
url = reverse("v1:pub_attachment-list")
response = admin_client.post(url, data=payload)
assert response.status_code == 201

attachment_keys = response.json().keys()
assert len(set(EXCLUDED_ATTACHMENT_FIELDS).intersection(set(attachment_keys))) == 0

attachment_id = response.json()["id"]
url = reverse("v1:pub_answer-list")
payload = {
"form": basic_form.id,
"user": admin_user.pk,
"targets": [
plot_search_target.pk,
], # noqa: E231
"entries": {
"sections": {
"company-information": [
{
"sections": {},
"fields": {"company-name": {"value": "", "extraValue": ""}},
},
{
"sections": {},
"fields": {
"business-id": {"value": "", "extraValue": ""},
"hallintaosuus": {"value": "1/1", "extraValue": ""},
},
},
],
"contact-person": {
"sections": {},
"fields": {
"first-name": {"value": "False", "extraValue": ""},
"last-name": {
"value": "99",
"extraValue": "developers developers developers",
},
},
},
},
"fields": {},
},
"attachments": [
attachment_id,
], # noqa: E231
"ready": True,
}
response = admin_client.post(url, data=payload, content_type="application/json")

assert response.status_code == 201
assert Attachment.objects.filter(answer=response.json()["id"]).exists()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also test that it doesn't return any unwanted fields?



@pytest.mark.django_db
def test_opening_record_create(
django_db_setup,
Expand Down
3 changes: 2 additions & 1 deletion forms/viewsets/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
AnswerOpeningRecordSerializer,
AnswerPublicSerializer,
AnswerSerializer,
AttachmentPublicSerializer,
AttachmentSerializer,
FormSerializer,
MeetingMemoSerializer,
Expand Down Expand Up @@ -178,7 +179,7 @@ def download(self, request, pk=None):
class AttachmentPublicViewSet(FileExtensionFileMixin, AttachmentViewSet):
"""Includes FileExtensionFileMixin to validate file extensions."""

pass
serializer_class = AttachmentPublicSerializer


class TargetStatusViewset(
Expand Down
28 changes: 28 additions & 0 deletions plotsearch/serializers/plot_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@
from users.models import User
from users.serializers import UserSerializer

# These fields include sensitive or unnecessary data for the public API
EXCLUDED_AREA_SEARCH_ATTACHMENT_FIELDS = [
"attachment",
"user",
"area_search",
"created_at",
]


class PlotSearchSubTypeLinkedSerializer(NameModelSerializer):
class Meta:
Expand Down Expand Up @@ -840,6 +848,15 @@ def create(self, validated_data):
return attachment


class AreaSearchAttachmentPublicSerializer(AreaSearchAttachmentSerializer):
def to_representation(self, instance):
representation = super().to_representation(instance)
for key in EXCLUDED_AREA_SEARCH_ATTACHMENT_FIELDS:
if key in representation:
representation.pop(key)
return representation


class AreaSearchStatusNoteSerializer(serializers.ModelSerializer):
preparer = UserSerializer(read_only=True)
time_stamp = serializers.DateTimeField(read_only=True)
Expand Down Expand Up @@ -1075,6 +1092,17 @@ def update(self, instance, validated_data):
return instance


class AreaSearchPublicSerializer(AreaSearchSerializer):
area_search_attachments = InstanceDictPrimaryKeyRelatedField(
instance_class=AreaSearchAttachment,
queryset=AreaSearchAttachment.objects.all(),
related_serializer=AreaSearchAttachmentPublicSerializer,
required=False,
allow_null=True,
many=True,
)


class AreaSearchDetailSerializer(AreaSearchSerializer):
answer = AnswerSerializer(read_only=True, required=False)
plot = serializers.ListField(child=serializers.CharField(), read_only=True)
Expand Down
76 changes: 75 additions & 1 deletion plotsearch/tests/api/test_plot_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from leasing.models import PlanUnit
from plotsearch.enums import SearchClass, SearchStage
from plotsearch.models import AreaSearch, PlotSearch, PlotSearchTarget
from plotsearch.models.plot_search import PlotSearchStage
from plotsearch.models.plot_search import AreaSearchAttachment, PlotSearchStage
from plotsearch.serializers.plot_search import EXCLUDED_AREA_SEARCH_ATTACHMENT_FIELDS

fake = Faker("fi_FI")

Expand Down Expand Up @@ -798,6 +799,48 @@ def test_area_search_create_simple(
)


@pytest.mark.django_db
def test_area_search_create_simple_public(
django_db_setup, admin_client, area_search_test_data, area_search_attachment_factory
):
url = reverse("v1:pub_area_search-list") # list == create

attachment = area_search_attachment_factory(
area_search=area_search_test_data,
attachment=SimpleUploadedFile(content=b"Lorem Impsum", name="test.txt"),
name="test.txt",
)

data = {
"description_area": get_random_string(length=12),
"description_intended_use": get_random_string(length=12),
"intended_use": area_search_test_data.intended_use.pk,
"geometry": area_search_test_data.geometry.geojson,
"attachments": 1,
"area_search_attachments": [attachment.id],
}

response = admin_client.post(
url, json.dumps(data, cls=DjangoJSONEncoder), content_type="application/json"
)
attachments = response.json().get("area_search_attachments")
assert (
len(
set(attachments[0].keys()).intersection(
set(EXCLUDED_AREA_SEARCH_ATTACHMENT_FIELDS)
)
)
== 0
)

assert response.status_code == 201, "%s %s" % (response.status_code, response.data)
assert AreaSearch.objects.filter(id=response.data["id"]).exists()
assert (
AreaSearch.objects.get(id=response.data["id"]).intended_use.id
== area_search_test_data.intended_use.pk
)


@pytest.mark.django_db
def test_area_search_attachment_create(
django_db_setup, admin_client, area_search_test_data
Expand All @@ -824,6 +867,37 @@ def test_area_search_attachment_create(
assert len(response.data["area_search_attachments"]) == 1


@pytest.mark.django_db
def test_area_search_attachment_create_public(
django_db_setup, admin_client, area_search_test_data
):
attachment = {
"area_search": area_search_test_data.id,
"attachment": SimpleUploadedFile(content=b"Lorem Impsum", name="test.txt"),
"name": fake.name(),
}

url = reverse(
"v1:pub_area_search_attachment-list",
)
response = admin_client.post(url, data=attachment)

assert response.status_code == 201

attachment_keys = response.json().keys()
assert (
len(
set(EXCLUDED_AREA_SEARCH_ATTACHMENT_FIELDS).intersection(
set(attachment_keys)
)
)
== 0
)
assert AreaSearchAttachment.objects.filter(
area_search=area_search_test_data
).exists()


@pytest.mark.django_db
def test_opening_record_create(
django_db_setup,
Expand Down
5 changes: 4 additions & 1 deletion plotsearch/views/plot_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,11 @@
PlotSearchOpeningRecordPermissions,
)
from plotsearch.serializers.plot_search import (
AreaSearchAttachmentPublicSerializer,
AreaSearchAttachmentSerializer,
AreaSearchDetailSerializer,
AreaSearchListSerializer,
AreaSearchPublicSerializer,
AreaSearchSerializer,
DirectReservationLinkSerializer,
FAQSerializer,
Expand Down Expand Up @@ -363,7 +365,7 @@ class AreaSearchPublicViewSet(
mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet
):
queryset = AreaSearch.objects.all()
serializer_class = AreaSearchSerializer
serializer_class = AreaSearchPublicSerializer
permission_classes = (
AreaSearchPublicPermissions,
IsAuthenticated,
Expand Down Expand Up @@ -411,6 +413,7 @@ class AreaSearchAttachmentPublicViewset(
):
"""Includes FileExtensionFileMixin to validate file extensions."""

serializer_class = AreaSearchAttachmentPublicSerializer
permission_classes = (AreaSearchAttachmentPublicPermissions,)

def destroy(self, request, *args, **kwargs):
Expand Down
Loading