Skip to content

Commit 2e4d4a2

Browse files
authored
Users can now update assets with attached media that contains images. (#649)
* do not restrict connectors to not uploading binaries * Allow users to update assets if they do not change binaries * Put back accidentally removed unit tests
1 parent 4e47763 commit 2e4d4a2

File tree

3 files changed

+100
-2
lines changed

3 files changed

+100
-2
lines changed

src/database/model/ai_asset/distribution.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Type
44

55
from fastapi.encoders import jsonable_encoder
6-
from pydantic import create_model
6+
from pydantic import create_model, validator
77
from sqlalchemy import Column, ForeignKey, String, LargeBinary
88
from sqlmodel import Field
99

@@ -64,6 +64,14 @@ class DistributionBase(AIoDConceptBase):
6464
sa_column=Column(LargeBinary),
6565
)
6666

67+
@validator("binary_blob", pre=True, always=True)
68+
def decode_string_to_bytes(cls, v) -> bytes | None:
69+
if v is None or isinstance(v, bytes):
70+
return v
71+
if not isinstance(v, str):
72+
raise TypeError("`binary_blob` can only be bytes, str, or None.")
73+
return base64.b64decode(v)
74+
6775
def dict(self, *args, **kwargs):
6876
# Defining it as a `Config` does not work for some reason.
6977
item_dict = super().dict(*args, **kwargs)

src/routers/resource_router.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,17 @@ def put_resource(
544544
with DbSession() as session:
545545
try:
546546
resource: Any = self._retrieve_resource(session, identifier)
547-
_raise_if_contains_binary_blob(resource_create_instance)
547+
if not user.is_connector:
548+
if hasattr(resource_create_instance, "media"):
549+
if not resource_create_instance.media: # type: ignore[attr-defined]
550+
# This does create the problem that a user cannot remove all media through this endpoint :/
551+
resource_create_instance.media = resource.media # type: ignore[attr-defined]
552+
elif set(m.binary_blob for m in resource_create_instance.media) != set( # type: ignore[attr-defined]
553+
m.binary_blob for m in resource.media
554+
):
555+
_raise_if_contains_binary_blob(resource_create_instance)
556+
else:
557+
_raise_if_contains_binary_blob(resource_create_instance)
548558
if not (
549559
user_can_write(user, resource.aiod_entry)
550560
or user.has_role(f"update_{self.resource_name_plural}")

src/tests/routers/resource_routers/test_router_organisation.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy
22
from unittest.mock import Mock
33

4+
from fastapi.encoders import jsonable_encoder
45
from starlette.testclient import TestClient
56

67
from database.model.agent.contact import Contact
@@ -351,3 +352,82 @@ def test_organisation_delete_image(
351352
)
352353
assert second_delete_response.status_code == HTTPStatus.NOT_FOUND
353354
assert "No image with the name" in second_delete_response.json()["detail"]
355+
356+
357+
def test_organisation_put_without_media_keeps_media(
358+
client: TestClient,
359+
organisation: Organisation,
360+
):
361+
organisation.media = []
362+
identifier = register_asset(organisation)
363+
364+
fake_image = io.BytesIO(b"\x89PNG\r\n\x1a\n...") # fake PNG bytes
365+
fake_image.name = "logo.png"
366+
367+
with logged_in_user():
368+
response = client.post(
369+
f"/organisations/{identifier}/image",
370+
params={"name": "logo"},
371+
files={"file": ("logo.png", fake_image, "image/png")},
372+
headers={"Authorization": "Fake token"},
373+
)
374+
assert response.status_code == HTTPStatus.OK, response.json()
375+
376+
organisation.name = "new name"
377+
response = client.put(
378+
f"/organisations/{identifier}",
379+
json=jsonable_encoder(organisation.dict()),
380+
headers={"Authorization": "Fake token"},
381+
)
382+
assert response.status_code == HTTPStatus.OK, response.json()
383+
response = client.get(
384+
f"/organisations/{identifier}",
385+
headers={"Authorization": "Fake token"},
386+
)
387+
assert response.status_code == HTTPStatus.OK, response.json()
388+
assert response.json()["name"] == "new name", response.json()
389+
assert response.json()["media"], response.json()
390+
391+
392+
def test_organisation_put_with_media_keeps_media_if_no_new_binary(
393+
client: TestClient,
394+
organisation: Organisation,
395+
):
396+
organisation.media = []
397+
identifier = register_asset(organisation)
398+
399+
fake_image = io.BytesIO(b"\x89PNG\r\n\x1a\n...") # fake PNG bytes
400+
fake_image.name = "logo.png"
401+
402+
with logged_in_user():
403+
response = client.post(
404+
f"/organisations/{identifier}/image",
405+
params={"name": "logo"},
406+
files={"file": ("logo.png", fake_image, "image/png")},
407+
headers={"Authorization": "Fake token"},
408+
)
409+
assert response.status_code == HTTPStatus.OK, response.json()
410+
411+
response = client.get(
412+
f"/organisations/{identifier}?get_image=true",
413+
headers={"Authorization": "Fake token"},
414+
)
415+
org = response.json()
416+
del org["aiod_entry"]
417+
org["media"].append(
418+
{"name": "foo", "binary_blob": "bar="},
419+
)
420+
response = client.put(
421+
f"/organisations/{identifier}",
422+
json=org,
423+
headers={"Authorization": "Fake token"},
424+
)
425+
assert response.status_code == HTTPStatus.BAD_REQUEST, "No new binary may be added through a PUT request"
426+
427+
org["media"].pop()
428+
response = client.put(
429+
f"/organisations/{identifier}",
430+
json=org,
431+
headers={"Authorization": "Fake token"},
432+
)
433+
assert response.status_code == HTTPStatus.OK, response.json()

0 commit comments

Comments
 (0)