Skip to content
Open
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
11 changes: 10 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 2 additions & 3 deletions base.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +70 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

printf '%s\n' '--- tracked config files ---'
git ls-files '*base.cfg' '*sources.cfg' '*buildout*.cfg' | sed -n '1,120p'
printf '%s\n' '--- base.cfg relevant section ---'
base=$(git ls-files | awk '$0=="base.cfg"{print; exit}')
if [ -n "$base" ]; then
  cat -n "$base" | sed -n '55,85p'
else
  echo 'base.cfg not found at repository root'
fi
printf '%s\n' '--- source configuration references ---'
rg -n -C 3 'imio\.smartweb\.common|sources\.cfg|extends|WEBBDC-2835' --glob '*.cfg' --glob '*.ini' --glob '*.txt' --glob '*.md' . | sed -n '1,240p'

Repository: IMIO/imio.smartweb.core

Length of output: 4798


🏁 Script executed:

printf '%s\n' '--- repository state ---'
git status --short --branch
printf '%s\n' '--- inherited source configuration on main ---'
curl -fsSL https://raw.githubusercontent.com/IMIO/buildout.smartweb/main/sources.cfg |
  grep -n -C 2 'imio\.smartweb\.common' || true
printf '%s\n' '--- dependency branch metadata ---'
curl -fsSL 'https://api.github.com/repos/IMIO/imio.smartweb.common/branches/WEBBDC-2835' |
  jq '{name, sha: .commit.sha, protected}' || true
printf '%s\n' '--- current dependency main metadata ---'
curl -fsSL 'https://api.github.com/repos/IMIO/imio.smartweb.common/branches/main' |
  jq '{name, sha: .commit.sha, protected}' || true

Repository: IMIO/imio.smartweb.core

Length of output: 1193


Keep the default buildout on main.

base.cfg overrides the inherited imio.smartweb.common source with the unprotected WEBBDC-2835 branch. After the common-package changes merge, remove this override or set branch=main. Keep ticket-branch overrides in stack-specific configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@base.cfg` around lines 70 - 71, Update the imio.smartweb.common source entry
under [sources] in base.cfg to use the protected main branch, or remove the
override so the inherited main configuration applies. Keep WEBBDC-2835 branch
overrides only in stack-specific configuration.

11 changes: 9 additions & 2 deletions src/imio/smartweb/core/contents/sections/contact/content.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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",
Expand Down
60 changes: 48 additions & 12 deletions src/imio/smartweb/core/contents/sections/contact/forms.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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)
107 changes: 76 additions & 31 deletions src/imio/smartweb/core/contents/sections/contact/macros.pt
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,17 @@
tal:content="subtitle"></div>
</div>

<tal:informations tal:condition="python:'contact_informations' in visible_blocks">
<!-- The defines MUST live on a nested element: TAL evaluates `define`
BEFORE `condition` on the same element, so keeping them on
<tal:informations> would consult the grids even when the block is
unchecked. -->
<tal:def tal:define="phone_entries python:contact.displayed_rows('phones');
mail_entries python:contact.displayed_rows('mails');
url_entries python:contact.displayed_rows('urls');
vat_number python:contact.vat_number">
<div class="contact_informations"
tal:condition="python:'contact_informations' in visible_blocks">
tal:condition="python:phone_entries or mail_entries or url_entries or vat_number">

<h2 class="informations"
tal:condition="not: show_main_title"
Expand All @@ -61,34 +70,70 @@
tal:condition="show_main_title"
i18n:translate="">Contact</h3>

<span tal:replace="python:contact.vat_number"></span>
<span tal:condition="vat_number" tal:replace="vat_number"></span>
<ul class="contact_informations_genral"
tal:define="phones python:contact.phones;
mails python:contact.mails">
<li class="phones"
tal:repeat="phone phones"
tal:attributes="class python:phone.get('type') or ''">
<span tal:replace="python:phone.get('label') or ''"></span>
<a tal:attributes="href python:'tel:{}'.format(phone.get('number'))"
tal:content="python:'{}'.format(phone.get('number'))" />
</li>
<li class="mails"
tal:repeat="mail mails"
tal:attributes="class python:mail.get('type') or ''">
<span tal:replace="python:'{}'.format(mail.get('label') or '')"></span>
<a tal:attributes="href python:'mailto:{}'.format(mail.get('mail_address'))"
tal:content="python:'{}'.format(mail.get('mail_address'))" />
</li>
tal:condition="python:phone_entries or mail_entries">
<tal:phone tal:repeat="entry phone_entries">
<li tal:define="phone entry/data; columns entry/columns"
tal:attributes="class python:phone.get('type') or ''">
<span tal:condition="python:'label' in columns"
tal:replace="python:phone.get('label') or ''"></span>
<span tal:condition="python:'type' in columns"
tal:replace="python:contact.translated_type('phones', phone.get('type'))"></span>
<a tal:condition="python:'number' in columns"
tal:attributes="href python:'tel:{}'.format(phone.get('number'))"
tal:content="python:'{}'.format(phone.get('number'))" />
</li>
</tal:phone>
<tal:mail tal:repeat="entry mail_entries">
<li tal:define="mail entry/data; columns entry/columns"
tal:attributes="class python:mail.get('type') or ''">
<span tal:condition="python:'label' in columns"
tal:replace="python:mail.get('label') or ''"></span>
<span tal:condition="python:'type' in columns"
tal:replace="python:contact.translated_type('mails', mail.get('type'))"></span>
<a tal:condition="python:'mail_address' in columns"
tal:attributes="href python:'mailto:{}'.format(mail.get('mail_address'))"
tal:content="python:'{}'.format(mail.get('mail_address'))" />
</li>
</tal:mail>
Comment on lines +76 to +99

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard number/mail_address against missing values to avoid literal tel:None / mailto:None.

'tel:{}'.format(phone.get('number')) and '{}'.format(phone.get('number')) render the string "None" when the number key is missing/empty for a row even though the number column is selected — producing a broken tel:None link with visible text "None". Same issue for mail_address/mailto:. Note the label span two lines above already guards with or ''; number/mail_address lack the equivalent fallback.

🐛 Proposed fix
-                <a tal:condition="python:'number' in columns"
+                <a tal:condition="python:'number' in columns and phone.get('number')"
                    tal:attributes="href python:'tel:{}'.format(phone.get('number'))"
-                   tal:content="python:'{}'.format(phone.get('number'))" />
+                   tal:content="python:phone.get('number')" />
-                <a tal:condition="python:'mail_address' in columns"
+                <a tal:condition="python:'mail_address' in columns and mail.get('mail_address')"
                    tal:attributes="href python:'mailto:{}'.format(mail.get('mail_address'))"
-                   tal:content="python:'{}'.format(mail.get('mail_address'))" />
+                   tal:content="python:mail.get('mail_address')" />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<tal:phone tal:repeat="entry phone_entries">
<li tal:define="phone entry/data; columns entry/columns"
tal:attributes="class python:phone.get('type') or ''">
<span tal:condition="python:'label' in columns"
tal:replace="python:phone.get('label') or ''"></span>
<span tal:condition="python:'type' in columns"
tal:replace="python:contact.translated_type('phones', phone.get('type'))"></span>
<a tal:condition="python:'number' in columns"
tal:attributes="href python:'tel:{}'.format(phone.get('number'))"
tal:content="python:'{}'.format(phone.get('number'))" />
</li>
</tal:phone>
<tal:mail tal:repeat="entry mail_entries">
<li tal:define="mail entry/data; columns entry/columns"
tal:attributes="class python:mail.get('type') or ''">
<span tal:condition="python:'label' in columns"
tal:replace="python:mail.get('label') or ''"></span>
<span tal:condition="python:'type' in columns"
tal:replace="python:contact.translated_type('mails', mail.get('type'))"></span>
<a tal:condition="python:'mail_address' in columns"
tal:attributes="href python:'mailto:{}'.format(mail.get('mail_address'))"
tal:content="python:'{}'.format(mail.get('mail_address'))" />
</li>
</tal:mail>
<tal:phone tal:repeat="entry phone_entries">
<li tal:define="phone entry/data; columns entry/columns"
tal:attributes="class python:phone.get('type') or ''">
<span tal:condition="python:'label' in columns"
tal:replace="python:phone.get('label') or ''"></span>
<span tal:condition="python:'type' in columns"
tal:replace="python:contact.translated_type('phones', phone.get('type'))"></span>
<a tal:condition="python:'number' in columns and phone.get('number')"
tal:attributes="href python:'tel:{}'.format(phone.get('number'))"
tal:content="python:phone.get('number')" />
</li>
</tal:phone>
<tal:mail tal:repeat="entry mail_entries">
<li tal:define="mail entry/data; columns entry/columns"
tal:attributes="class python:mail.get('type') or ''">
<span tal:condition="python:'label' in columns"
tal:replace="python:mail.get('label') or ''"></span>
<span tal:condition="python:'type' in columns"
tal:replace="python:contact.translated_type('mails', mail.get('type'))"></span>
<a tal:condition="python:'mail_address' in columns and mail.get('mail_address')"
tal:attributes="href python:'mailto:{}'.format(mail.get('mail_address'))"
tal:content="python:mail.get('mail_address')" />
</li>
</tal:mail>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/imio/smartweb/core/contents/sections/contact/macros.pt` around lines 76 -
99, Update the phone and mail link attributes and contents in the repeated
phone/mail entries to use an empty-string fallback when number or mail_address
is missing, matching the existing label handling. Ensure missing values never
render as “None” or produce tel:None/mailto:None while preserving normal values.

</ul>
<ul class="contact_informations_social"
tal:define="urls python:contact.get_urls"
tal:condition="urls">
<li class="urls"
tal:repeat="url urls"
tal:attributes="class python:url.get('type') or ''">
<a tal:attributes="href python:url.get('url');
title python:contact.get_translated_url_type(url.get('type'))"
target="_blank">
tal:condition="url_entries">
<tal:url tal:repeat="entry url_entries">
<li tal:define="url entry/data; columns entry/columns"
tal:attributes="class python:url.get('type') or ''">
<a tal:condition="python:'url' in columns"
tal:attributes="href python:url.get('url');
title python:contact.get_translated_url_type(url.get('type')) if ('type' in columns and url.get('type')) else nothing"
target="_blank">
Comment on lines 101 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Add rel="noopener noreferrer" to the target="_blank" social link.

Opening external URLs with target="_blank" and no rel="noopener noreferrer" exposes the page to reverse-tabnabbing (the opened page can access window.opener).

🛡️ Proposed fix
                 <a tal:condition="python:'url' in columns"
                    tal:attributes="href python:url.get('url');
                                    title python:contact.get_translated_url_type(url.get('type')) if ('type' in columns and url.get('type')) else nothing"
+                   rel="noopener noreferrer"
                    target="_blank">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ul class="contact_informations_social"
tal:define="urls python:contact.get_urls"
tal:condition="urls">
<li class="urls"
tal:repeat="url urls"
tal:attributes="class python:url.get('type') or ''">
<a tal:attributes="href python:url.get('url');
title python:contact.get_translated_url_type(url.get('type'))"
target="_blank">
tal:condition="url_entries">
<tal:url tal:repeat="entry url_entries">
<li tal:define="url entry/data; columns entry/columns"
tal:attributes="class python:url.get('type') or ''">
<a tal:condition="python:'url' in columns"
tal:attributes="href python:url.get('url');
title python:contact.get_translated_url_type(url.get('type')) if ('type' in columns and url.get('type')) else nothing"
target="_blank">
<ul class="contact_informations_social"
tal:condition="url_entries">
<tal:url tal:repeat="entry url_entries">
<li tal:define="url entry/data; columns entry/columns"
tal:attributes="class python:url.get('type') or ''">
<a tal:condition="python:'url' in columns"
tal:attributes="href python:url.get('url');
title python:contact.get_translated_url_type(url.get('type')) if ('type' in columns and url.get('type')) else nothing"
rel="noopener noreferrer"
target="_blank">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/imio/smartweb/core/contents/sections/contact/macros.pt` around lines 101
- 109, Update the social link rendered in the repeated url_entries block to
include rel="noopener noreferrer" alongside target="_blank" on the anchor
element, preserving its existing href, title, and condition behavior.

<tal:icon tal:condition="python:'type' in columns">
<tal:condition tal:condition="python:url.get('type') == 'facebook'">
<i class="bi bi-facebook"></i>
</tal:condition>
<tal:condition tal:condition="python:url.get('type') == 'twitter'">
<i class="bi bi-twitter"></i>
</tal:condition>
<tal:condition tal:condition="python:url.get('type') == 'instagram'">
<i class="bi bi-instagram"></i>
</tal:condition>
<tal:condition tal:condition="python:url.get('type') == 'youtube'">
<i class="bi bi-youtube"></i>
</tal:condition>
<tal:condition tal:condition="python:url.get('type') == 'pinterest'">
<i class="bi bi-pinterest"></i>
</tal:condition>
<tal:condition tal:condition="python:url.get('type') == 'linkedin'">
<i class="bi bi-linkedin"></i>
</tal:condition>
</tal:icon>
<span tal:attributes="class python:'sr-only' if url.get('type') != 'website' else ''"
tal:content="python:url.get('url')"/>
</a>
<!-- Type checked, Url unchecked: the editor still asked for
the icon, but there is no href to link, so render the
icon on its own, not wrapped in an <a>. -->
<tal:icon_only tal:condition="python:'type' in columns and 'url' not in columns">
<tal:condition tal:condition="python:url.get('type') == 'facebook'">
<i class="bi bi-facebook"></i>
</tal:condition>
Expand All @@ -107,13 +152,13 @@
<tal:condition tal:condition="python:url.get('type') == 'linkedin'">
<i class="bi bi-linkedin"></i>
</tal:condition>

<span tal:attributes="class python:'sr-only' if url.get('type') != 'website' else ''"
tal:content="python:url.get('url')"/>
</a>
</li>
</tal:icon_only>
</li>
</tal:url>
</ul>
</div>
</tal:def>
</tal:informations>
<div class="address"
tal:define="address python:contact.formatted_address()"
tal:condition="address">
Expand Down
28 changes: 28 additions & 0 deletions src/imio/smartweb/core/contents/sections/contact/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading