diff --git a/CHANGES.rst b/CHANGES.rst index 97d929fb..4b48d841 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,16 @@ Changelog 1.4.56 (unreleased) ------------------- -- Nothing changed yet. +- WEBBDC-2835: REFACTOR : Move the contact-section row schemas, row helpers, grid form + mixin, frozen-label widget and ``*DisplayColumns`` vocabularies to + ``imio.smartweb.common`` so ``events`` and ``news`` can reuse them. + [boulch] + +- WEBBDC-2835: Add read-only phones/mails/urls datagrids to the contact section, + loaded from the related contacts with a dedicated button, with a per-row + checkbox column choosing which columns are displayed. Unchecking every column + hides the row. + [boulch] 1.4.55 (2026-07-28) diff --git a/base.cfg b/base.cfg index bb54f922..00d63bb3 100644 --- a/base.cfg +++ b/base.cfg @@ -67,6 +67,5 @@ scripts = # Don't use a released version of imio.smartweb.core imio.smartweb.core = -[remotes] -imio = https://github.com/IMIO -imio_push = git@github.com:IMIO +[sources] +imio.smartweb.common = git https://github.com/IMIO/imio.smartweb.common.git pushurl=git@github.com:IMIO/imio.smartweb.common.git branch=WEBBDC-2835 \ No newline at end of file diff --git a/src/imio/smartweb/core/contents/sections/contact/content.py b/src/imio/smartweb/core/contents/sections/contact/content.py index b9c327dc..bf3bd9ca 100644 --- a/src/imio/smartweb/core/contents/sections/contact/content.py +++ b/src/imio/smartweb/core/contents/sections/contact/content.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from imio.smartweb.common.contact.rows import IContactInformationsGrids from imio.smartweb.common.widgets.select import TranslatedAjaxSelectWidget from imio.smartweb.core.contents.sections.base import ISection from imio.smartweb.core.contents.sections.base import Section @@ -11,8 +12,14 @@ from zope.interface import implementer -class ISectionContact(ISection): - """Marker interface and Dexterity Python Schema for SectionContact""" +class ISectionContact(ISection, IContactInformationsGrids): + """Marker interface and Dexterity Python Schema for SectionContact + + The `contact_informations` fieldset and its three read-only datagrids come + from IContactInformationsGrids, shared with imio.events.core. Here the + stored data columns are RESIDUE: the page render always re-reads the live + directory payload and only `visible_columns` is authoritative. + """ directives.widget( "related_contacts", diff --git a/src/imio/smartweb/core/contents/sections/contact/forms.py b/src/imio/smartweb/core/contents/sections/contact/forms.py index 4b3bea5d..fc1b6840 100644 --- a/src/imio/smartweb/core/contents/sections/contact/forms.py +++ b/src/imio/smartweb/core/contents/sections/contact/forms.py @@ -1,17 +1,33 @@ # -*- coding: utf-8 -*- from imio.smartweb.common.browser.forms import CustomAddForm +from imio.smartweb.common.contact.forms import ContactInformationsGridMixin from imio.smartweb.core.browser.forms import SmartwebCustomEditForm +from imio.smartweb.locales import SmartwebMessageFactory as _ from plone.dexterity.browser.add import DefaultAddView from plone.z3cform import layout +from z3c.form import button from z3c.form.interfaces import HIDDEN_MODE -class ContactCustomAddForm(CustomAddForm): - portal_type = "imio.smartweb.SectionContact" +class SectionContactGridMixin(ContactInformationsGridMixin): + """The section's own bits: `related_contacts` and the hidden hide_title. + + The grid-reloading machinery itself lives in + imio.smartweb.common.contact.forms, shared with imio.events.core. + + `hide_title` is hidden after the widgets exist, which both concrete forms + need and neither may forget. It belongs to the Section base and does not + exist outside one, which is why it is not in the shared mixin. + """ + + contact_uids_field = "related_contacts" def update(self): - super(ContactCustomAddForm, self).update() + super().update() + self._hide_hide_title() + + def _hide_hide_title(self): # We hide hide_title field so no one can change the value for contact # and set True value (single checkbox) for group in self.groups: @@ -20,19 +36,39 @@ def update(self): group.widgets["hide_title"].value = ["selected"] +class ContactCustomAddForm(SectionContactGridMixin, CustomAddForm): + portal_type = "imio.smartweb.SectionContact" + + # Both MUST be copied before the decorator runs: @buttonAndHandler does a + # setdefault on the `buttons` AND on the `handlers` name of the class body + # being defined. Without the copies it would create fresh, empty managers + # that shadow the base ones -- the form would lose the Save / Cancel + # buttons (buttons) and, more silently, their handlers (handlers), so + # pressing Save would render the form again without saving anything. + buttons = CustomAddForm.buttons.copy() + handlers = CustomAddForm.handlers.copy() + + @button.buttonAndHandler( + _("Load contact information"), name="load_contact_informations" + ) + def handleLoadContactInformations(self, action): + """No-op: the grids were already rebuilt in update().""" + + class ContactCustomAddView(DefaultAddView): form = ContactCustomAddForm -class ContactCustomEditForm(SmartwebCustomEditForm): - def update(self): - super(ContactCustomEditForm, self).update() - # We hide hide_title field so no one can change the value for contact - # and set True value (single checkbox) - for group in self.groups: - if group.__name__ == "layout": - group.widgets["hide_title"].mode = HIDDEN_MODE - group.widgets["hide_title"].value = ["selected"] +class ContactCustomEditForm(SectionContactGridMixin, SmartwebCustomEditForm): + # See ContactCustomAddForm for why both managers are copied here. + buttons = SmartwebCustomEditForm.buttons.copy() + handlers = SmartwebCustomEditForm.handlers.copy() + + @button.buttonAndHandler( + _("Load contact information"), name="load_contact_informations" + ) + def handleLoadContactInformations(self, action): + """No-op: the grids were already rebuilt in update().""" ContactCustomEditView = layout.wrap_form(ContactCustomEditForm) diff --git a/src/imio/smartweb/core/contents/sections/contact/macros.pt b/src/imio/smartweb/core/contents/sections/contact/macros.pt index 8274a70c..7cb11c88 100644 --- a/src/imio/smartweb/core/contents/sections/contact/macros.pt +++ b/src/imio/smartweb/core/contents/sections/contact/macros.pt @@ -51,8 +51,17 @@ tal:content="subtitle"> + + + + +
diff --git a/src/imio/smartweb/core/contents/sections/contact/utils.py b/src/imio/smartweb/core/contents/sections/contact/utils.py index e543a922..e1a2f0e2 100644 --- a/src/imio/smartweb/core/contents/sections/contact/utils.py +++ b/src/imio/smartweb/core/contents/sections/contact/utils.py @@ -1,5 +1,13 @@ # -*- coding: utf-8 -*- +from imio.smartweb.common.contact.directory import build_display_rows # noqa: F401 +from imio.smartweb.common.contact.directory import displayed_rows as _displayed_rows +from imio.smartweb.common.contact.directory import get_remote_contacts # noqa: F401 +from imio.smartweb.common.contact.directory import row_key # noqa: F401 +from imio.smartweb.common.contact.directory import translated_type_label +from imio.smartweb.common.contact.directory import ( + visible_columns_map as _visible_columns_map, +) from imio.smartweb.common.contact_utils import ContactProperties as ContactSchedule from imio.smartweb.common.utils import get_term_from_vocabulary from imio.smartweb.common.utils import rich_description @@ -12,6 +20,10 @@ import json +# build_display_rows, get_remote_contacts and row_key are re-exported rather +# than used here: they moved to imio.smartweb.common but callers (the section +# forms and the tests) still import them from this module. + class ContactProperties(ContactSchedule): def __init__(self, json_dict, section): @@ -139,6 +151,22 @@ def formatted_address(self): return None return {"street": street, "entity": entity, "country": country} + def translated_type(self, kind, token): + """Human label of a remote `type` token. See translated_type_label.""" + return translated_type_label(kind, token) + + def visible_columns_map(self, kind): + """See imio.smartweb.common.contact.directory.visible_columns_map.""" + return _visible_columns_map(self.context, kind) + + def displayed_rows(self, kind): + """See imio.smartweb.common.contact.directory.displayed_rows. + + `self.contact` is the LIVE directory payload; the stored `*_display` + data columns are residue on this side and are never read here. + """ + return _displayed_rows(self.contact, self.context, kind) + @property def get_urls(self): if isinstance(self.urls, list): diff --git a/src/imio/smartweb/core/tests/resources/json_contact_informations_raw_mock.json b/src/imio/smartweb/core/tests/resources/json_contact_informations_raw_mock.json new file mode 100644 index 00000000..34bc6b90 --- /dev/null +++ b/src/imio/smartweb/core/tests/resources/json_contact_informations_raw_mock.json @@ -0,0 +1,64 @@ +{ + "@id": "http://localhost:8080/Plone/@search", + "items": [ + { + "@id": "http://localhost:8080/Plone/2dc381f0fb584381b8e4a19c84f53b35", + "@type": "imio.directory.Contact", + "UID": "2dc381f0fb584381b8e4a19c84f53b35", + "title": "Administration communale", + "subtitle": null, + "description": "", + "modified": "2026-07-29T08:00:00+00:00", + "type": {"token": "organization", "title": "Organization"}, + "vat_number": null, + "street": "Rue de la Paix", + "number": "1", + "complement": null, + "zipcode": "4000", + "city": "Liege", + "country": {"token": "be", "title": "Belgique"}, + "geolocation": {"latitude": 50.4, "longitude": 4.7}, + "logo": null, + "image": null, + "is_geolocated": true, + "phones": [ + {"label": "Secretariat", "type": "work", "number": "+3287123456"}, + {"label": "Direction", "type": "cell", "number": "+32475010203"} + ], + "mails": [ + {"label": "Accueil", "type": "work", "mail_address": "info@example.be"} + ], + "urls": [ + {"type": "website", "url": "https://example.be"}, + {"type": "facebook", "url": "https://facebook.com/example"} + ] + }, + { + "@id": "http://localhost:8080/Plone/af7bd1f547034b24a2e0da16c0ba0358", + "@type": "imio.directory.Contact", + "UID": "af7bd1f547034b24a2e0da16c0ba0358", + "title": "CPAS", + "subtitle": null, + "description": "", + "modified": "2026-07-29T08:00:00+00:00", + "type": {"token": "organization", "title": "Organization"}, + "vat_number": null, + "street": "Rue du Centre", + "number": "2", + "complement": null, + "zipcode": "4000", + "city": "Liege", + "country": {"token": "be", "title": "Belgique"}, + "geolocation": {"latitude": 50.5, "longitude": 4.8}, + "logo": null, + "image": null, + "is_geolocated": true, + "phones": [ + {"label": "Accueil", "type": "work", "number": "+3287654321"} + ], + "mails": [], + "urls": [] + } + ], + "items_total": 2 +} diff --git a/src/imio/smartweb/core/tests/test_section_contact.py b/src/imio/smartweb/core/tests/test_section_contact.py index 2c02b67d..708deced 100644 --- a/src/imio/smartweb/core/tests/test_section_contact.py +++ b/src/imio/smartweb/core/tests/test_section_contact.py @@ -6,7 +6,11 @@ from freezegun import freeze_time from imio.smartweb.common.contact_utils import formatted_schedule from imio.smartweb.common.contact_utils import get_schedule_for_today +from imio.smartweb.core.contents.sections.contact.utils import build_display_rows from imio.smartweb.core.contents.sections.contact.utils import ContactProperties +from imio.smartweb.core.contents.sections.contact.utils import get_remote_contacts +from imio.smartweb.core.contents.sections.contact.utils import row_key +from imio.smartweb.core.contents.sections.contact.utils import translated_type_label from imio.smartweb.core.contents.sections.views import SECTION_ITEMS_HASH_KEY from imio.smartweb.core.tests.utils import clear_cache from imio.smartweb.core.testing import IMIO_SMARTWEB_CORE_FUNCTIONAL_TESTING @@ -23,7 +27,9 @@ from zope.annotation.interfaces import IAnnotations from zope.component import queryMultiAdapter +import itertools import json +import re import requests import requests_mock import transaction @@ -84,7 +90,10 @@ def test_contact(self, m): self.assertEqual(json_contact.contact_type_class, "contact-type-organization") self.assertNotIn("contact_titles", view()) self.assertIn("contact_address", view()) - self.assertIn("contact_informations", view()) + # The fixture's phones/mails/urls are empty and vat_number is null, so + # the contact informations block now has no content at all and is + # omitted (no more orphan title / empty