Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Changelog
1.2.27 (unreleased)
-------------------

- CITIBDC-568 : Allow non-ASCII characters in schedule comments by overriding DictFieldDeserializer for ISchedule fields.
[remdub]

- Fix CSRF error : Wrong factory! Should be ContactCategoriesVocabularyFactory() instead of ContactCategoriesDe...
[boulch]

Expand Down
1 change: 1 addition & 0 deletions src/imio/directory/core/contents/contact/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
for=".content.IContact plone.restapi.interfaces.IPloneRestapiLayer" />

<adapter factory=".serializer.ContactJSONSummarySerializer" />
<adapter factory=".deserializer.ScheduleFieldDeserializer" />

<unconfigure>
<adapter factory="plone.app.imagecropping.dx.CroppingUtilsDexterity" />
Expand Down
29 changes: 29 additions & 0 deletions src/imio/directory/core/contents/contact/deserializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from collective.schedulefield.schedule import ISchedule
from plone.dexterity.interfaces import IDexterityContent
from plone.restapi.deserializer.dxfields import DefaultFieldDeserializer
from plone.restapi.interfaces import IFieldDeserializer
from zope.component import adapter
from zope.interface import implementer
from zope.publisher.interfaces.browser import IBrowserRequest

import json


@implementer(IFieldDeserializer)
@adapter(ISchedule, IDexterityContent, IBrowserRequest)
class ScheduleFieldDeserializer(DefaultFieldDeserializer):
"""Allow non-ASCII characters (e.g. accented) in schedule comments.
The default DictFieldDeserializer recursively validates inner dict values
against ASCIILine(), rejecting accented characters. Schedule.validate()
already skips the 'comment' key, so we use it directly instead."""

def __call__(self, value):
if not value:
return value
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON for schedule field: {e}") from e
self.field.validate(value)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return value
100 changes: 100 additions & 0 deletions src/imio/directory/core/tests/test_contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,106 @@ def test_overall_response_format(self):
"items_total property should match actual item count.",
)

def _make_schedule(self, comment=""):
days = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
]
return {
day: {
"morningstart": "09:00" if day not in ("saturday", "sunday") else "",
"morningend": "12:00" if day not in ("saturday", "sunday") else "",
"afternoonstart": "13:00" if day not in ("saturday", "sunday") else "",
"afternoonend": "17:00" if day not in ("saturday", "sunday") else "",
"comment": comment if day == "monday" else "",
}
for day in days
}

def test_schedule_patch_with_non_ascii_comment(self):
contact = api.content.create(
container=self.entity,
type="imio.directory.Contact",
title="contact",
)
transaction.commit()
comment = "uniquement sur rendez-vous à partir de 14h (été)"
response = self.api_session.patch(
contact.absolute_url(),
json={"schedule": self._make_schedule(comment)},
)
self.assertEqual(response.status_code, 204)
result = self.api_session.get(contact.absolute_url()).json()
self.assertEqual(result["schedule"]["monday"]["comment"], comment)

def test_schedule_patch_with_ascii_comment(self):
contact = api.content.create(
container=self.entity,
type="imio.directory.Contact",
title="contact",
)
transaction.commit()
comment = "by appointment only"
response = self.api_session.patch(
contact.absolute_url(),
json={"schedule": self._make_schedule(comment)},
)
self.assertEqual(response.status_code, 204)
result = self.api_session.get(contact.absolute_url()).json()
self.assertEqual(result["schedule"]["monday"]["comment"], comment)

def test_schedule_patch_with_empty_value(self):
contact = api.content.create(
container=self.entity,
type="imio.directory.Contact",
title="contact",
)
transaction.commit()
response = self.api_session.patch(
contact.absolute_url(),
json={"schedule": {}},
)
self.assertEqual(response.status_code, 204)

def test_schedule_patch_as_json_string(self):
import json

contact = api.content.create(
container=self.entity,
type="imio.directory.Contact",
title="contact",
)
transaction.commit()
comment = "réunion hebdomadaire"
schedule = self._make_schedule(comment)
response = self.api_session.patch(
contact.absolute_url(),
json={"schedule": json.dumps(schedule)},
)
self.assertEqual(response.status_code, 204)
result = self.api_session.get(contact.absolute_url()).json()
self.assertEqual(result["schedule"]["monday"]["comment"], comment)

def test_schedule_patch_with_non_ascii_time_field(self):
contact = api.content.create(
container=self.entity,
type="imio.directory.Contact",
title="contact",
)
transaction.commit()
schedule = self._make_schedule()
schedule["monday"]["afternoonend"] = "17:é0"
response = self.api_session.patch(
contact.absolute_url(),
json={"schedule": schedule},
)
self.assertEqual(response.status_code, 400)

def test_subscriber_to_select_current_entity(self):
contact = api.content.create(
container=self.entity,
Expand Down
Loading