From 013b23d74d1b1c86416582a30a0efd6677eb1d2f Mon Sep 17 00:00:00 2001 From: boulch Date: Thu, 30 Jul 2026 14:03:52 +0200 Subject: [PATCH 1/2] WEBBDC-2835: Add read-only phones/mails/urls datagrids to the contact section --- CHANGES.rst | 6 +- .../core/contents/sections/contact/content.py | 161 ++++ .../core/contents/sections/contact/forms.py | 174 ++++- .../core/contents/sections/contact/macros.pt | 107 ++- .../core/contents/sections/contact/utils.py | 172 +++++ .../json_contact_informations_raw_mock.json | 64 ++ .../smartweb/core/tests/test_frozen_label.py | 49 ++ .../core/tests/test_section_contact.py | 718 +++++++++++++++++- .../core/tests/test_section_contact_forms.py | 426 +++++++++++ .../smartweb/core/tests/test_vocabularies.py | 25 + src/imio/smartweb/core/vocabularies.py | 49 ++ src/imio/smartweb/core/vocabularies.zcml | 18 + .../smartweb/core/widgets/frozen_label.py | 45 ++ 13 files changed, 1967 insertions(+), 47 deletions(-) create mode 100644 src/imio/smartweb/core/tests/resources/json_contact_informations_raw_mock.json create mode 100644 src/imio/smartweb/core/tests/test_frozen_label.py create mode 100644 src/imio/smartweb/core/tests/test_section_contact_forms.py create mode 100644 src/imio/smartweb/core/widgets/frozen_label.py diff --git a/CHANGES.rst b/CHANGES.rst index 97d929fb9..1f45312b0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,11 @@ Changelog 1.4.56 (unreleased) ------------------- -- Nothing changed yet. +- 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/src/imio/smartweb/core/contents/sections/contact/content.py b/src/imio/smartweb/core/contents/sections/contact/content.py index b9c327dc0..3564ef4b4 100644 --- a/src/imio/smartweb/core/contents/sections/contact/content.py +++ b/src/imio/smartweb/core/contents/sections/contact/content.py @@ -1,14 +1,112 @@ # -*- coding: utf-8 -*- +from collective.z3cform.datagridfield.datagridfield import DataGridFieldFactory +from collective.z3cform.datagridfield.row import DictRow 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 +from imio.smartweb.core.widgets.frozen_label import FrozenLabelTextFieldWidget from imio.smartweb.locales import SmartwebMessageFactory as _ from plone.autoform import directives from plone.supermodel import model from z3c.form.browser.checkbox import CheckBoxFieldWidget from zope import schema from zope.interface import implementer +from zope.interface import Interface + + +class IPhoneDisplayRow(Interface): + """One phone row of a related contact, plus the columns to display. + + Every column but `visible_columns` is remote directory data rendered as a + frozen label: read-only looking, yet still submitted, because DictRow + rejects a row whose keys are missing. Those stored copies are RESIDUE -- + the page render always reads the live directory payload. Never read them + back. + """ + + directives.mode(contact_uid="hidden") + contact_uid = schema.TextLine(title=_("Contact UID"), required=False) + + directives.widget("contact_title", FrozenLabelTextFieldWidget) + contact_title = schema.TextLine(title=_("Contact"), required=False) + + directives.widget("label", FrozenLabelTextFieldWidget) + label = schema.TextLine(title=_("Label"), required=False) + + directives.widget("type", FrozenLabelTextFieldWidget) + type = schema.TextLine(title=_("Type"), required=False) + + directives.widget("number", FrozenLabelTextFieldWidget) + number = schema.TextLine(title=_("Number"), required=False) + + directives.widget("visible_columns", CheckBoxFieldWidget) + visible_columns = schema.List( + title=_("Displayed columns"), + value_type=schema.Choice( + vocabulary="imio.smartweb.vocabulary.PhoneDisplayColumns" + ), + required=False, + ) + + +class IMailDisplayRow(Interface): + """One e-mail row of a related contact, plus the columns to display. + + See IPhoneDisplayRow: the data columns are residue, never read back. + """ + + directives.mode(contact_uid="hidden") + contact_uid = schema.TextLine(title=_("Contact UID"), required=False) + + directives.widget("contact_title", FrozenLabelTextFieldWidget) + contact_title = schema.TextLine(title=_("Contact"), required=False) + + directives.widget("label", FrozenLabelTextFieldWidget) + label = schema.TextLine(title=_("Label"), required=False) + + directives.widget("type", FrozenLabelTextFieldWidget) + type = schema.TextLine(title=_("Type"), required=False) + + directives.widget("mail_address", FrozenLabelTextFieldWidget) + mail_address = schema.TextLine(title=_("E-mail"), required=False) + + directives.widget("visible_columns", CheckBoxFieldWidget) + visible_columns = schema.List( + title=_("Displayed columns"), + value_type=schema.Choice( + vocabulary="imio.smartweb.vocabulary.MailDisplayColumns" + ), + required=False, + ) + + +class IUrlDisplayRow(Interface): + """One URL row of a related contact, plus the columns to display. + + See IPhoneDisplayRow: the data columns are residue, never read back. + """ + + directives.mode(contact_uid="hidden") + contact_uid = schema.TextLine(title=_("Contact UID"), required=False) + + directives.widget("contact_title", FrozenLabelTextFieldWidget) + contact_title = schema.TextLine(title=_("Contact"), required=False) + + directives.widget("type", FrozenLabelTextFieldWidget) + type = schema.TextLine(title=_("Type"), required=False) + + directives.widget("url", FrozenLabelTextFieldWidget) + url = schema.TextLine(title=_("Url"), required=False) + + directives.widget("visible_columns", CheckBoxFieldWidget) + visible_columns = schema.List( + title=_("Displayed columns"), + value_type=schema.Choice( + vocabulary="imio.smartweb.vocabulary.UrlDisplayColumns" + ), + required=False, + ) class ISectionContact(ISection): @@ -38,6 +136,69 @@ class ISectionContact(ISection): default=["address", "itinerary", "contact_informations", "schedule"], ) + model.fieldset( + "contact_informations", + label=_("Contact informations"), + fields=["phones_display", "mails_display", "urls_display"], + ) + + directives.widget( + "phones_display", + DataGridFieldFactory, + allow_insert=False, + allow_delete=False, + allow_reorder=False, + auto_append=False, + ) + phones_display = schema.List( + title=_("Phones"), + description=_( + "Read-only rows loaded from the related contacts with the button " + "at the bottom of this form. Check the columns you want to " + "display; uncheck them all to hide the row." + ), + value_type=DictRow(title="Value", schema=IPhoneDisplayRow), + required=False, + ) + + directives.widget( + "mails_display", + DataGridFieldFactory, + allow_insert=False, + allow_delete=False, + allow_reorder=False, + auto_append=False, + ) + mails_display = schema.List( + title=_("E-mails"), + description=_( + "Read-only rows loaded from the related contacts with the button " + "at the bottom of this form. Check the columns you want to " + "display; uncheck them all to hide the row." + ), + value_type=DictRow(title="Value", schema=IMailDisplayRow), + required=False, + ) + + directives.widget( + "urls_display", + DataGridFieldFactory, + allow_insert=False, + allow_delete=False, + allow_reorder=False, + auto_append=False, + ) + urls_display = schema.List( + title=_("URLs"), + description=_( + "Read-only rows loaded from the related contacts with the button " + "at the bottom of this form. Check the columns you want to " + "display; uncheck them all to hide the row." + ), + value_type=DictRow(title="Value", schema=IUrlDisplayRow), + required=False, + ) + model.fieldset( "layout", fields=[ diff --git a/src/imio/smartweb/core/contents/sections/contact/forms.py b/src/imio/smartweb/core/contents/sections/contact/forms.py index 4b3bea5d9..a310d70e7 100644 --- a/src/imio/smartweb/core/contents/sections/contact/forms.py +++ b/src/imio/smartweb/core/contents/sections/contact/forms.py @@ -1,17 +1,147 @@ # -*- coding: utf-8 -*- from imio.smartweb.common.browser.forms import CustomAddForm +from imio.smartweb.common.widgets.select import TranslatedAjaxSelectWidget from imio.smartweb.core.browser.forms import SmartwebCustomEditForm +from imio.smartweb.core.contents.sections.contact.utils import build_display_rows +from imio.smartweb.core.contents.sections.contact.utils import CONTACT_ROW_COLUMNS +from imio.smartweb.core.contents.sections.contact.utils import CONTACT_ROW_KEYS +from imio.smartweb.core.contents.sections.contact.utils import get_remote_contacts +from imio.smartweb.locales import SmartwebMessageFactory as _ +from plone import api from plone.dexterity.browser.add import DefaultAddView from plone.z3cform import layout +from z3c.form import button from z3c.form.interfaces import HIDDEN_MODE +DISPLAY_FIELDS = ("phones_display", "mails_display", "urls_display") -class ContactCustomAddForm(CustomAddForm): - portal_type = "imio.smartweb.SectionContact" +# related_contacts is an AjaxSelectWidget: it submits ONE text input holding +# every selected UID joined by this separator, not a list. Reading the raw +# request value would build a single bogus "uid1;uid2" UID. +RELATED_CONTACTS_SEPARATOR = TranslatedAjaxSelectWidget.separator + +KIND_BY_FIELD = { + "phones_display": "phones", + "mails_display": "mails", + "urls_display": "urls", +} + + +class ContactInformationsGridMixin: + """Contact-form specifics: the informations grids and the hidden hide_title. + + Repopulates the read-only contact-informations grids from the directory. + The grids are never "filled once": they are derived from the currently + selected related_contacts. Rather than rebuilding widgets, this rewrites + the request BEFORE super().update(), so the normal request -> widget path + regenerates names, ids, the .count marker and the patterns by construction. + + It also hides the `hide_title` field after the widgets exist, which both + concrete forms need and neither may forget. + """ def update(self): - super(ContactCustomAddForm, self).update() + if self.request.form.get(self._load_button_name): + self._reload_display_grids() + super().update() + self._hide_hide_title() + + @property + def _load_button_name(self): + return "{}buttons.load_contact_informations".format(self.prefix) + + def _reload_display_grids(self): + uids = self._submitted_contact_uids() + if not uids: + api.portal.show_message( + _("Please select a contact before loading its information."), + request=self.request, + type="info", + ) + contacts = [] + else: + contacts = get_remote_contacts(uids) + if not contacts: + # get_remote_contacts returns [] for a timeout, a non-200 and + # an unreachable host alike (utils.get_json swallows every + # exception), so "UIDs submitted but nothing came back" can + # only be a failure. Rewriting the grids here would empty them + # and destroy every recorded visible_columns preference on the + # next save, so leave the request untouched. + api.portal.show_message( + _( + "The contact directory could not be reached: contact " + "information was not loaded and nothing was changed." + ), + request=self.request, + type="error", + ) + return + api.portal.show_message( + _("Contact information has been loaded."), + request=self.request, + type="info", + ) + for field_name in DISPLAY_FIELDS: + kind = KIND_BY_FIELD[field_name] + prefix = "{}widgets.{}".format(self.prefix, field_name) + preferences = self._extract_preferences(prefix, kind) + rows = build_display_rows(kind, contacts, preferences) + self._write_grid(prefix, kind, rows) + + def _submitted_contact_uids(self): + """UIDs currently selected in related_contacts, in order. + + The AjaxSelectWidget submits them as a single separator-joined string; + a plain list is accepted too so the method does not depend on the + widget in use. + """ + uids = self.request.form.get("{}widgets.related_contacts".format(self.prefix)) + if isinstance(uids, str): + uids = uids.split(RELATED_CONTACTS_SEPARATOR) + return [uid.strip() for uid in uids or [] if uid and uid.strip()] + + def _extract_preferences(self, prefix, kind): + """Checkbox state already in the request, keyed (contact_uid, row_key). + + A row whose checkbox group submitted nothing yields an EMPTY list -- + "explicitly hidden" -- not a missing key. The widget was rendered (we + only look at indices whose contact_uid is present), so "nothing + submitted" can only mean "everything unchecked". + """ + form = self.request.form + key_column = CONTACT_ROW_KEYS[kind] + preferences = {} + index = 0 + while "{}.{}.widgets.contact_uid".format(prefix, index) in form: + row_prefix = "{}.{}.widgets".format(prefix, index) + key = (form.get("{}.{}".format(row_prefix, key_column)) or "").strip() + if key: + columns = form.get("{}.visible_columns".format(row_prefix)) + if columns is None: + columns = [] + elif isinstance(columns, str): + columns = [columns] + uid = form.get("{}.contact_uid".format(row_prefix)) or "" + preferences[(uid, key)] = list(columns) + index += 1 + return preferences + + def _write_grid(self, prefix, kind, rows): + form = self.request.form + for key in [key for key in form if key.startswith("{}.".format(prefix))]: + del form[key] + columns = ("contact_uid", "contact_title") + CONTACT_ROW_COLUMNS[kind] + for index, row in enumerate(rows): + row_prefix = "{}.{}.widgets".format(prefix, index) + for column in columns: + form["{}.{}".format(row_prefix, column)] = row.get(column) or "" + form["{}.visible_columns".format(row_prefix)] = list(row["visible_columns"]) + form["{}.visible_columns-empty-marker".format(row_prefix)] = "1" + form["{}.count".format(prefix)] = str(len(rows)) + + 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 +150,39 @@ def update(self): group.widgets["hide_title"].value = ["selected"] +class ContactCustomAddForm(ContactInformationsGridMixin, 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(ContactInformationsGridMixin, 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 8274a70cf..7cb11c88e 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 e543a922e..6dce7aaee 100644 --- a/src/imio/smartweb/core/contents/sections/contact/utils.py +++ b/src/imio/smartweb/core/contents/sections/contact/utils.py @@ -3,6 +3,7 @@ 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 +from imio.smartweb.core.config import DIRECTORY_URL from imio.smartweb.core.utils import batch_results from imio.smartweb.core.utils import get_json from imio.smartweb.core.utils import hash_md5 @@ -12,6 +13,123 @@ import json +# Human labels of the remote `type` tokens. The directory owns these +# vocabularies (imio/directory/core/vocabularies.py); their msgids live in the +# shared `imio.smartweb` domain, so they can be reused here without depending +# on imio.directory.core. If the directory adds a type, its label degrades to +# the raw token -- visible but harmless. +CONTACT_TYPE_LABELS = { + "phones": { + "fax": _("Fax"), + "cell": _("Mobile"), + "home": _("Personal phone"), + "work": _("Work phone"), + }, + "mails": { + "home": _("Personal email"), + "work": _("Work email"), + }, + "urls": { + "facebook": _("Facebook"), + "instagram": _("Instagram"), + "linkedin": _("Linkedin"), + "pinterest": _("Pinterest"), + "twitter": _("Twitter"), + "website": _("Website"), + "youtube": _("Youtube"), + }, +} + +# The remote column that identifies a row. A row without it cannot be keyed, +# so no preference can be recorded for it and it is skipped. +CONTACT_ROW_KEYS = { + "phones": "number", + "mails": "mail_address", + "urls": "url", +} + +# Columns of each row, in display order. Must mirror the *DisplayColumns +# vocabularies token for token. +CONTACT_ROW_COLUMNS = { + "phones": ("label", "type", "number"), + "mails": ("label", "type", "mail_address"), + "urls": ("type", "url"), +} + + +def translated_type_label(kind, token): + """Human label of a remote `type` token, or the raw token if unknown.""" + if not token: + return "" + msgid = CONTACT_TYPE_LABELS.get(kind, {}).get(token) + if msgid is None: + return token + current_lang = api.portal.get_current_language()[:2] + return translate(msgid, target_language=current_lang) + + +def row_key(kind, row): + """Identity of a remote row: its payload value, or "" when it has none.""" + return (row.get(CONTACT_ROW_KEYS[kind]) or "").strip() + + +def build_display_rows(kind, contacts, preferences=None): + """Build the DataGridField rows of `kind` from remote contact payloads. + + `contacts` is a list of contact dicts as returned by + `@search?UID=...&fullobjects=1`. `preferences` maps + `(contact_uid, row_key)` to a list of column names to carry over. + + A key ABSENT from `preferences` means "no preference recorded" and yields + every column. A key present with an EMPTY list means "explicitly hidden" + and is kept as such. The two are not interchangeable. + """ + preferences = preferences or {} + all_columns = CONTACT_ROW_COLUMNS[kind] + rows = [] + for contact in contacts: + uid = contact.get("UID") or "" + title = contact.get("title") or "" + for remote_row in contact.get(kind) or []: + key = row_key(kind, remote_row) + if not key: + continue + row = { + "contact_uid": uid, + "contact_title": title, + # list() so each row owns its default. + "visible_columns": list(preferences.get((uid, key), all_columns)), + } + for column in all_columns: + if column == "type": + row["type"] = translated_type_label(kind, remote_row.get("type")) + else: + row[column] = remote_row.get(column) or "" + rows.append(row) + return rows + + +def get_remote_contacts(uids): + """Live directory payload for `uids`, in that order. + + Deliberately uncached: this is only called from the "load contacts + informations" button, where the editor is asking for fresh data. + """ + if not uids: + return [] + url = "{}/@search?UID={}&fullobjects=1".format(DIRECTORY_URL, "&UID=".join(uids)) + current_lang = api.portal.get_current_language()[:2] + if current_lang != "fr": + url = f"{url}&translated_in_{current_lang}=1" + json_data = get_json(url) + if not json_data: + return [] + index_map = {uid: index for index, uid in enumerate(uids)} + items = [ + item for item in json_data.get("items") or [] if item.get("UID") in index_map + ] + return sorted(items, key=lambda item: index_map[item["UID"]]) + class ContactProperties(ContactSchedule): def __init__(self, json_dict, section): @@ -139,6 +257,60 @@ 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): + """{(contact_uid, row_key): [column, ...]} from the stored preferences. + + A key ABSENT from the returned map means "no preference recorded" and + yields every column at render time. A key present with an EMPTY list + means "explicitly hidden" and drops the row. The two are NOT + interchangeable: never normalise one into the other. A stored row whose + `visible_columns` is None is treated as "no preference", so its key is + deliberately left out of the map. + """ + stored = getattr(self.context, f"{kind}_display", None) or [] + result = {} + for row in stored: + key = row_key(kind, row) + if not key: + continue + columns = row.get("visible_columns") + if columns is None: + continue + result[(row.get("contact_uid") or "", key)] = list(columns) + return result + + def displayed_rows(self, kind): + """Remote rows of `kind`, each with the set of columns to render. + + Returns [{"data": , "columns": }, ...]. + Rows explicitly hidden are omitted, as are rows with no usable key. + + `self.contact` is the LIVE directory payload: the stored `*_display` + data columns are residue and are never read here. The remote row dict + is returned as-is and must not be mutated -- it belongs to cached JSON. + """ + preferences = self.visible_columns_map(kind) + uid = self.contact.get("UID") or "" + all_columns = set(CONTACT_ROW_COLUMNS[kind]) + rows = [] + for remote_row in self.contact.get(kind) or []: + key = row_key(kind, remote_row) + if not key: + continue + columns = preferences.get((uid, key)) + if columns is None: + columns = set(all_columns) + else: + columns = set(columns) & all_columns + if not columns: + continue + rows.append({"data": remote_row, "columns": columns}) + return rows + @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 000000000..34bc6b900 --- /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_frozen_label.py b/src/imio/smartweb/core/tests/test_frozen_label.py new file mode 100644 index 000000000..72de74da4 --- /dev/null +++ b/src/imio/smartweb/core/tests/test_frozen_label.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +from imio.smartweb.core.testing import IMIO_SMARTWEB_CORE_INTEGRATION_TESTING +from imio.smartweb.core.testing import ImioSmartwebTestCase +from imio.smartweb.core.widgets.frozen_label import FrozenLabelTextFieldWidget +from z3c.form.interfaces import NO_VALUE +from z3c.form.testing import TestRequest +from zope import schema + + +class TestFrozenLabelTextWidget(ImioSmartwebTestCase): + layer = IMIO_SMARTWEB_CORE_INTEGRATION_TESTING + + def _make_widget(self, value): + field = schema.TextLine(__name__="number", title="Number") + widget = FrozenLabelTextFieldWidget(field, TestRequest()) + widget.name = "form.widgets.phones_display.0.widgets.number" + widget.value = value + return widget + + def test_render_shows_the_value_as_a_label(self): + html = self._make_widget("+3287123456").render() + self.assertIn('+3287123456', html) + + def test_render_still_submits_the_value(self): + # The whole point: a display-mode widget would emit nothing and + # DictRow._validate would reject every row on save. + html = self._make_widget("+3287123456").render() + self.assertIn('type="hidden"', html) + self.assertIn('name="form.widgets.phones_display.0.widgets.number"', html) + self.assertIn('value="+3287123456"', html) + + def test_render_escapes_html(self): + html = self._make_widget("").render() + self.assertNotIn("").render() - self.assertNotIn("