Skip to content

Commit

Permalink
Adding OrderedDict field
Browse files Browse the repository at this point in the history
  • Loading branch information
nilbacardit26 committed Oct 10, 2023
1 parent 9085dd8 commit a29e9c1
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 5 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ CHANGELOG
- Fix path__starts. Add a slash when parsing the path of the query if
the context of the search is not the container, to avoid getting the
results of contexts that starts with the same path.
[nbacardit26]
[nilbacardit26]
- Adding render_options when registering a user.
[nilbacardit26]
- Adding MultiImageOrderedAttachmentBehavior, and exposing
@orderImages to the images' api to order the keys of a field whose
behavior is MultiImageOrderedAttachment
[nilbacardit26]


6.4.2 (2022-08-25)
Expand Down
2 changes: 1 addition & 1 deletion contrib-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ html2text==2019.8.11
aiosmtplib==1.1.4
pre-commit==1.18.2
flake8==5.0.4
codecov==2.0.15
codecov==2.1.13
mypy-zope==0.3.2
black==22.3.0
isort==4.3.21
Expand Down
3 changes: 2 additions & 1 deletion guillotina/api/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,12 @@ async def prepare(self):
if self.behavior is not None and IAsyncBehavior.implementedBy(self.behavior.__class__):
# providedBy not working here?
await self.behavior.load()

if IDict.providedBy(field) and ICloudFileField.providedBy(field.value_type):
key = self.request.matchdict.get("file_key")
if key is not None:
self.field = CloudFileField(__name__=name).bind(DictFieldProxy(key, ctx, name))
else:
self.field = field.bind(ctx)
elif ICloudFileField.providedBy(field):
self.field = field.bind(ctx)

Expand Down
13 changes: 13 additions & 0 deletions guillotina/contrib/image/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,16 @@ async def __call__(self):
return await adapter.download()
else:
return HTTPNoContent()


@configure.service(
context=IResource,
method="PATCH",
permission="guillotina.ViewContent",
name="@orderImages/{field_name}",
**_traversed_file_doc("Order the keys of a field"),
)
class OrderMultiImage(TraversableFieldService):
async def __call__(self):
data = await self.request.json()
self.field.reorder_images(data)
21 changes: 21 additions & 0 deletions guillotina/contrib/image/behaviors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from collections import OrderedDict as NativeOrderedDict
from guillotina import configure
from guillotina.contrib.image.image import CloudImageFileField
from guillotina.schema import Dict
from guillotina.schema import OrderedDict
from guillotina.schema import TextLine
from zope.interface import Interface

Expand All @@ -13,6 +15,10 @@ class IMultiImageAttachmentMarker(Interface):
"""Marker interface for content with several image attachments."""


class IMultiImageOrderedAttachmentMarker(Interface):
"""Marker interface for content with several ordered image attachments."""


@configure.behavior(
title="ImageAttachment behavior", marker=IImageAttachmentMarker, for_="guillotina.interfaces.IResource"
)
Expand All @@ -29,3 +35,18 @@ class IMultiImageAttachment(Interface):
images = Dict(
key_type=TextLine(), value_type=CloudImageFileField(), default={}, missing_value={}, max_length=1000
)


@configure.behavior(
title="MultiImageAttachment behavior",
marker=IMultiImageAttachmentMarker,
for_="guillotina.interfaces.IResource",
)
class IMultiImageOrderedAttachment(Interface):
images = OrderedDict(
key_type=TextLine(),
value_type=CloudImageFileField(),
default=NativeOrderedDict(),
missing_value=NativeOrderedDict(),
max_length=1000,
)
12 changes: 12 additions & 0 deletions guillotina/json/serialize_value.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from collections import OrderedDict
from datetime import date
from datetime import datetime
from datetime import time
Expand Down Expand Up @@ -77,6 +78,17 @@ def dict_converter(value):
return dict(zip(keys, values))


@configure.value_serializer(OrderedDict)
def ordered_dict_converter(value):
if value == {}:
return {}

keys, values = zip(*value.items())
keys = map(json_compatible, keys)
values = map(json_compatible, values)
return dict(zip(keys, values))


@configure.value_serializer(datetime)
def python_datetime_converter(value):
try:
Expand Down
3 changes: 2 additions & 1 deletion guillotina/schema/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from guillotina.schema._field import NativeStringLine
from guillotina.schema._field import Object
from guillotina.schema._field import Orderable
from guillotina.schema._field import OrderedDict
from guillotina.schema._field import Password
from guillotina.schema._field import Set
from guillotina.schema._field import SourceText
Expand Down Expand Up @@ -71,7 +72,7 @@
Iterable, List, MaskTextLine, MinMaxLen, NativeString, NativeStringLine, Object, Orderable
Password, Set, SourceText, Text, TextLine, Time, Timedelta, Tuple, URI, UnionField
get_fields, get_fields_in_order, getFieldNames, getFieldNamesInOrder,
getValidationErrors, getSchemaValidationErrors, JSONField
getValidationErrors, getSchemaValidationErrors, JSONField, OrderedDict
accessors
ValidationError
NO_VALUE
21 changes: 21 additions & 0 deletions guillotina/schema/_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#
##############################################################################
from collections import namedtuple
from collections import OrderedDict as NativeOrderedDict
from datetime import date
from datetime import datetime
from datetime import time
Expand Down Expand Up @@ -65,6 +66,7 @@
from guillotina.schema.interfaces import IMinMaxLen
from guillotina.schema.interfaces import IObject
from guillotina.schema.interfaces import IObjectJSONField
from guillotina.schema.interfaces import IOrderedDict
from guillotina.schema.interfaces import IPassword
from guillotina.schema.interfaces import ISet
from guillotina.schema.interfaces import ISource
Expand Down Expand Up @@ -665,6 +667,25 @@ def bind(self, object):
return clone


@implementer(IOrderedDict)
class OrderedDict(Dict):
"""A field representing an OrderedDict."""

_type = NativeOrderedDict

def reorder_images(self, payload):
data_field = self.get(self.context)
# payload is an ordered list of the keys
if isinstance(payload, list):
if len(payload) != len(data_field):
raise ValueError("Length of the payload must be equal to the field")
for key in payload:
if key not in data_field:
raise ValueError("Key not found")
data_field.move_to_end(key)
self.set(self.context, data_field)


DEFAULT_JSON_SCHMEA = json.dumps({"type": "object", "properties": {}})


Expand Down
4 changes: 4 additions & 0 deletions guillotina/schema/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,10 @@ class IDict(IMinMaxLen, IIterable, IContainer):
)


class IOrderedDict(IDict):
"""Object representing an ordered dict"""


class ITerm(Interface):
"""Object representing a single value in a vocabulary."""

Expand Down
94 changes: 94 additions & 0 deletions guillotina/tests/image/test_field.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from guillotina.contrib.image.behaviors import IImageAttachment
from guillotina.contrib.image.behaviors import IMultiImageAttachment
from guillotina.contrib.image.behaviors import IMultiImageOrderedAttachment
from guillotina.tests.image import TEST_DATA_LOCATION

import json
Expand Down Expand Up @@ -135,3 +136,96 @@ async def test_multiimage_field_with_behavior(redis_container, container_request

response, status = await requester("GET", "/db/guillotina/foobar/@images/images/key1/thumb")
assert status == 200


@pytest.mark.app_settings(
{"applications": ["guillotina", "guillotina.contrib.image"], "cloud_datamanager": "db"}
)
async def test_multiimage_ordered_field_with_behavior(redis_container, container_requester):
async with container_requester as requester:
_, status = await requester("POST", "/db/guillotina/@addons", data=json.dumps({"id": "image"}))
assert status == 200

response, status = await requester(
"POST",
"/db/guillotina/",
data=json.dumps(
{"@type": "Item", "@behaviors": [IMultiImageOrderedAttachment.__identifier__], "id": "foobar"}
),
)
assert status == 201

with open(os.path.join(TEST_DATA_LOCATION, "profile.jpg"), "rb") as image:
data = image.read()
size = len(data)

response, status = await requester(
"GET",
"/db/guillotina/foobar",
)
assert status == 200

response, status = await requester(
"PATCH",
"/db/guillotina/foobar/@upload/images/key2",
data=data,
headers={"x-upload-size": f"{size}"},
)
assert status == 200

response, status = await requester(
"PATCH",
"/db/guillotina/foobar/@upload/images/key1",
data=data,
headers={"x-upload-size": f"{size}"},
)
assert status == 200

response, status = await requester(
"PATCH",
"/db/guillotina/foobar/@upload/images/key0",
data=data,
headers={"x-upload-size": f"{size}"},
)
assert status == 200

response, status = await requester(
"PATCH",
"/db/guillotina/foobar/@upload/images/key3",
data=data,
headers={"x-upload-size": f"{size}"},
)
assert status == 200

response, status = await requester("GET", "/db/guillotina/foobar")
behavior = response["guillotina.contrib.image.behaviors.IMultiImageOrderedAttachment"]
# First in first
keys_ordered = {"key2": 0, "key1": 1, "key0": 2, "key3": 3}
count = 0
for image in behavior["images"].keys():
assert count == keys_ordered[image]
count += 1

response, status = await requester("DELETE", "/db/guillotina/foobar/@delete/images/key1")
assert status == 200

response, status = await requester("GET", "/db/guillotina/foobar")
behavior = response["guillotina.contrib.image.behaviors.IMultiImageOrderedAttachment"]
keys_ordered = {"key2": 0, "key0": 1, "key3": 2}
count = 0
for image in behavior["images"].keys():
assert count == keys_ordered[image]
count += 1

response, status = await requester(
"PATCH", "/db/guillotina/foobar/@orderImages/images", data=json.dumps(["key3", "key2", "key0"])
)
assert status == 200

response, status = await requester("GET", "/db/guillotina/foobar")
behavior = response["guillotina.contrib.image.behaviors.IMultiImageOrderedAttachment"]
keys_ordered = {"key3": 0, "key2": 1, "key0": 2}
count = 0
for image in behavior["images"].keys():
assert count == keys_ordered[image]
count += 1
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
extras_require={
"test": [
"pytest>=3.8.0,<6.3.0",
"docker>=5.0.0,<6.0.0",
"docker>=6.0.0,<6.1.1", # https://github.com/docker/docker-py/pull/3116
"backoff",
"psycopg2-binary",
"pytest-asyncio<=0.13.0",
Expand Down

0 comments on commit a29e9c1

Please sign in to comment.