imio.smartweb.common 1.2.36__py3-none-any.whl → 1.2.38__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,6 +3,7 @@
3
3
  xmlns:cache="http://namespaces.zope.org/cache"
4
4
  xmlns:zcml="http://namespaces.zope.org/zcml">
5
5
 
6
+ <include package="plone.app.caching" />
6
7
  <include package="z3c.unconfigure" file="meta.zcml" />
7
8
 
8
9
  <unconfigure>
@@ -10,4 +10,14 @@
10
10
  permission="zope2.View"
11
11
  />
12
12
 
13
+ <plone:service
14
+ name="@find"
15
+ method="GET"
16
+ accept="application/json"
17
+ for="zope.interface.Interface"
18
+ factory=".endpoint.FindEndpoint"
19
+ permission="zope2.View"
20
+ layer="imio.smartweb.common.interfaces.IImioSmartwebCommonLayer"
21
+ />
22
+
13
23
  </configure>
@@ -0,0 +1,192 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from collections import Counter
4
+ from imio.smartweb.common.rest.utils import get_json
5
+ from plone import api
6
+ from plone.restapi.search.handler import SearchHandler
7
+ from plone.restapi.search.utils import unflatten_dotted_dict
8
+ from plone.restapi.services import Service
9
+ from Products.CMFCore.utils import getToolByName
10
+ from zExceptions import Unauthorized
11
+
12
+ import json
13
+ import logging
14
+
15
+ logger = logging.getLogger("imio.smartweb.common")
16
+
17
+
18
+ class FindEndpointHandler(SearchHandler):
19
+ def search(self, query=None):
20
+ results = {"items": []}
21
+ portal = api.portal.get()
22
+ get_types_url = f"{portal.absolute_url()}/@types"
23
+ auth_header = self.request._orig_env.get("HTTP_AUTHORIZATION", None)
24
+ types = get_json(get_types_url, auth=auth_header, timeout=20)
25
+ if not types:
26
+ raise Unauthorized("No types found, you are not allowed to search")
27
+ self._constrain_query_by_path(query)
28
+ if not query.get("path"):
29
+ query["path"] = {"query": "/Plone"}
30
+ # query = self._parse_query(query)
31
+ if query.get("type_of_request") == "count_contents_types":
32
+ results = self.count_contents_types(query)
33
+ elif query.get("type_of_request") == "find_big_files_or_images":
34
+ results = self.find_big_files_or_images(query)
35
+ elif query.get("type_of_request") == "get_max_depth":
36
+ results = self.get_max_depth()
37
+ elif query.get("type_of_request") == "check_value_of_field":
38
+ results = self.check_value_of_field(
39
+ query.get("portal_type"),
40
+ query.get("field_name"),
41
+ query.get("expected_values"),
42
+ )
43
+ else:
44
+ return super().search(query)
45
+ return results
46
+
47
+ def count_contents_types(self, query):
48
+ results = {"items": []}
49
+ if query.get("operator") == "and":
50
+ query["path"]["depth"] = -1
51
+ lazy_resultset = self.catalog.searchResults(**query)
52
+ results = {
53
+ "items": [
54
+ {
55
+ "portal_type": query.get("portal_type"),
56
+ "nb_items": len(lazy_resultset),
57
+ }
58
+ ]
59
+ }
60
+ else:
61
+ portal_types = (
62
+ query.get("portal_type")
63
+ if isinstance(query.get("portal_type"), list)
64
+ else [query.get("portal_type")] if query.get("portal_type") else []
65
+ )
66
+ for portal_type in portal_types:
67
+ new_query = query.copy()
68
+ new_query["portal_type"] = portal_type
69
+ lazy_resultset = self.catalog.searchResults(**new_query)
70
+ results["items"].append(
71
+ {"portal_type": portal_type, "nb_items": len(lazy_resultset)}
72
+ )
73
+ # fullobjects = False
74
+ # results = getMultiAdapter((lazy_resultset, self.request), ISerializeToJson)(
75
+ # fullobjects=fullobjects
76
+ # )
77
+ return results
78
+
79
+ def find_big_files_or_images(self, query):
80
+ SIZE_LIMIT = query.get("size", "1000000")
81
+ results = {"items": []}
82
+ lazy_resultset = self.catalog.searchResults(**query)
83
+ for brain in lazy_resultset:
84
+ obj = brain.getObject()
85
+ blob = getattr(obj, "file", None) or getattr(obj, "image", None)
86
+ if blob and hasattr(blob, "getSize"):
87
+ size = blob.getSize()
88
+ if size > int(SIZE_LIMIT):
89
+ results["items"].append(
90
+ {
91
+ "title": obj.title,
92
+ "portal_type": obj.portal_type,
93
+ "url": obj.absolute_url(),
94
+ "size": round(size / (1024 * 1024), 2),
95
+ }
96
+ )
97
+ return results
98
+
99
+ def get_max_depth(self):
100
+ portal = api.portal.get()
101
+ catalog = getToolByName(portal, "portal_catalog")
102
+
103
+ max_depth = 0
104
+ results = {"items": []}
105
+
106
+ for brain in catalog():
107
+ path = brain.getPath()
108
+ depth = len(path.strip("/").split("/"))
109
+ if depth > max_depth:
110
+ max_depth = depth
111
+ results["items"] = [{"path": path, "depth": depth}]
112
+ elif depth == max_depth:
113
+ results["items"].append({"path": path, "depth": depth})
114
+ results["max_depth"] = max_depth
115
+ return results
116
+
117
+ def check_value_of_field(self, portal_type, field_name, expected_values):
118
+ """
119
+ Analyse la répartition des valeurs d'un champ dans les objets d'un content_type donné.
120
+ query sample : http://localhost:8085/Plone/@find?portal_type=imio.events.Event&field_name=event_type&expected_values=["activity", "event-driven"]&type_of_request=check_value_of_field
121
+
122
+ :param content_type: str, le portal_type (ex: "Event")
123
+ :param field_name: str, field name (ex: "event_type")
124
+ :param expected_values: list, list of values we want to check (ex: ["activity", "event-driven"])
125
+ :return: dict {valeur: {"count": n, "percent": x.xx}}
126
+ """
127
+ brains = self.catalog(portal_type=portal_type)
128
+
129
+ total = len(brains)
130
+ if total == 0:
131
+ return {}
132
+
133
+ # Récupération et normalisation des valeurs
134
+ values = []
135
+ for brain in brains:
136
+ obj = brain.getObject()
137
+ value = getattr(obj, field_name, None)
138
+ if isinstance(value, (list, tuple)):
139
+ if not value:
140
+ # liste/tuple vide => on compte 1 None
141
+ values.append(None)
142
+ else:
143
+ mapped = [
144
+ None if (v in ("", "None") or v is None) else v for v in value
145
+ ]
146
+ # si TOUT est None, on compte 1 None (au lieu d'en ajouter 0)
147
+ if all(v is None for v in mapped):
148
+ values.append(None)
149
+ else:
150
+ values.extend(mapped)
151
+ else:
152
+ v = None if (value in ("", "None") or value is None) else value
153
+ values.append(v)
154
+ counter = Counter(values)
155
+
156
+ # Normalisation des valeurs attendues
157
+ expected_values = normalize_query_param(expected_values)
158
+ # On garde les labels originaux pour l'affichage, mais on mappe "None" vers None réel pour le compteur
159
+ expected_mapping = {v: (None if v == "None" else v) for v in expected_values}
160
+
161
+ # Calcul stats
162
+ result = {}
163
+ for label, key in expected_mapping.items():
164
+ count = counter.get(key, 0)
165
+ percent = (count / total) * 100 if total > 0 else 0
166
+ result[label] = {"count": count, "percent": round(percent, 2)}
167
+
168
+ return result
169
+
170
+
171
+ class FindEndpoint(Service):
172
+ def reply(self):
173
+ query = self.request.form.copy()
174
+ query = unflatten_dotted_dict(query)
175
+ return FindEndpointHandler(self.context, self.request).search(query)
176
+
177
+
178
+ def normalize_query_param(value):
179
+ if isinstance(value, list):
180
+ return value
181
+
182
+ if isinstance(value, str):
183
+ val = value.strip()
184
+ # Try to parse json list
185
+ if val.startswith("[") and val.endswith("]"):
186
+ try:
187
+ return json.loads(val)
188
+ except json.JSONDecodeError:
189
+ return [val]
190
+ else:
191
+ return [val]
192
+ return [value]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imio.smartweb.common
3
- Version: 1.2.36
3
+ Version: 1.2.38
4
4
  Summary: Common utilities, vocabularies, taxonomies for imio.smartweb & co products
5
5
  Home-page: https://github.com/imio/imio.smartweb.common
6
6
  Author: iMio
@@ -178,6 +178,23 @@ Changelog
178
178
  =========
179
179
 
180
180
 
181
+ 1.2.38 (2025-09-10)
182
+ -------------------
183
+
184
+ - @find : Process None value in a field or ["None", "other value"] for agatha stats
185
+ [boulch
186
+
187
+ - Add plone.app.caching include for futur proofing override
188
+ [jchandelle]
189
+
190
+
191
+ 1.2.37 (2025-09-03)
192
+ -------------------
193
+
194
+ - Add new @find endpoint to find content in instance
195
+ [boulch]
196
+
197
+
181
198
  1.2.36 (2025-06-25)
182
199
  -------------------
183
200
 
@@ -1,9 +1,9 @@
1
- imio.smartweb.common-1.2.36-py3.12-nspkg.pth,sha256=XZ3YhlzwpUCC8tXtelHRqxVxo3NWomIiMsUfUshrbeE,1011
1
+ imio.smartweb.common-1.2.38-py3.12-nspkg.pth,sha256=XZ3YhlzwpUCC8tXtelHRqxVxo3NWomIiMsUfUshrbeE,1011
2
2
  imio/smartweb/common/__init__.py,sha256=Na9XBfEQUMrm2c5jbqQgwWeg40ih0aXVG1vT8NeAjMQ,2709
3
3
  imio/smartweb/common/adapters.py,sha256=dG4MALuHPQI6lTeNvs5p4vVOxHoBBYaWN6g8J96KxzQ,1507
4
4
  imio/smartweb/common/adapters.zcml,sha256=VO3luZDtRL9FIIEghxPrqpx8paZF3m6MGEy-8kF1sOQ,437
5
5
  imio/smartweb/common/caching.py,sha256=aN7gX9rT63fnmUuZnx82Mbu3ghjJUeOiLvLEp9OiFm4,895
6
- imio/smartweb/common/caching_overrides.zcml,sha256=ysa5DgVGzO1Fp22d9Wg-vNyLlqfRR77WBV-7L6j0Hpo,526
6
+ imio/smartweb/common/caching_overrides.zcml,sha256=rpyKruOfuhAWfhtDVS9X54ibmNTZ9FmueH5Jz8aBf0w,570
7
7
  imio/smartweb/common/config.py,sha256=imgOo0_BBIbXGFNvBnhPReLOSY4JqIOH_QCAl0j0XGI,435
8
8
  imio/smartweb/common/configure.zcml,sha256=Ypqlo27RCC_w48QaQ0xucQRDjIBs9VpdtoJSjionK6Y,1128
9
9
  imio/smartweb/common/contact_utils.py,sha256=Pr-Nhz9MRnfirZ7RbmF5JZw9fX4gWa0dWkvJlQBiDKY,11341
@@ -75,7 +75,8 @@ imio/smartweb/common/profiles/testing/types/Document.xml,sha256=E3hhW_oL-WarUKcY
75
75
  imio/smartweb/common/profiles/testing/types/Folder.xml,sha256=RPxIPgViAK0GGGTPKbdlw53sywyQXPhVj6B8x0tYSD8,312
76
76
  imio/smartweb/common/profiles/uninstall/browserlayer.xml,sha256=glaCGlITwc36-1Fi2z0VCbLcoQ5GddkAhuW-SGYeZy8,130
77
77
  imio/smartweb/common/rest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
- imio/smartweb/common/rest/configure.zcml,sha256=8hQGCvo1EMGFe9cNUpGOcH7spKhUkglFRnRkd9IJ2eE,301
78
+ imio/smartweb/common/rest/configure.zcml,sha256=eT5gr-PcfMRxQzMME7T3nzz2Y0BVI4r9HyKHIccQC2s,575
79
+ imio/smartweb/common/rest/endpoint.py,sha256=6HUaGF4KgC7p0-0XSVGgUAP7WG8at1052l_zCjIkquE,7494
79
80
  imio/smartweb/common/rest/odwb.py,sha256=zRZxowUaE76xlW_blc92vRCzToUpnzw3O5mxFXaeScU,2249
80
81
  imio/smartweb/common/rest/search_filters.py,sha256=upZAZ6h50qiSXo0q22lUgNlcmX5elFtrIQs_jFBl1-s,3832
81
82
  imio/smartweb/common/rest/utils.py,sha256=9zc5ge_DmVuexxN09c3U6QAITAI1s2hc0m6eZuylSxM,1269
@@ -147,10 +148,10 @@ imio/smartweb/common/viewlets/skip_to_content.py,sha256=wm22NUf8Qh5uzz8p4vkLCdFN
147
148
  imio/smartweb/common/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
148
149
  imio/smartweb/common/widgets/select.py,sha256=5sV0UIgwu4AvwpgVmJeSZwUWz7OPD9VxYMowfmjcfbA,1313
149
150
  imio/smartweb/common/widgets/templates/ajaxselect_display.pt,sha256=v8TT2Xc19er6SUGWabgNfIdrayFYIjxyLK9H_XkIG3M,1209
150
- imio_smartweb_common-1.2.36.dist-info/licenses/LICENSE.GPL,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
151
- imio_smartweb_common-1.2.36.dist-info/licenses/LICENSE.rst,sha256=5dd78Fdt0e-oM2ICBrMpjHnT8vEP-jhBDF7akXni6B4,655
152
- imio_smartweb_common-1.2.36.dist-info/METADATA,sha256=5sVMJ5OumSl-oBKCj17pKY9mOwW_SXs38N_Z_etnIEQ,18706
153
- imio_smartweb_common-1.2.36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
154
- imio_smartweb_common-1.2.36.dist-info/namespace_packages.txt,sha256=Pg8AH8t9viMMW1hJbNZvTy_n2jXG2igIYUpon5RA4Js,19
155
- imio_smartweb_common-1.2.36.dist-info/top_level.txt,sha256=ZktC0EGzThvMTAin9_q_41rzvvfMT2FYbP8pbhSLMSA,5
156
- imio_smartweb_common-1.2.36.dist-info/RECORD,,
151
+ imio_smartweb_common-1.2.38.dist-info/licenses/LICENSE.GPL,sha256=gXf5dRMhNSbfLPYYTY_5hsZ1r7UU1OaKQEAQUhuIBkM,18092
152
+ imio_smartweb_common-1.2.38.dist-info/licenses/LICENSE.rst,sha256=5dd78Fdt0e-oM2ICBrMpjHnT8vEP-jhBDF7akXni6B4,655
153
+ imio_smartweb_common-1.2.38.dist-info/METADATA,sha256=fdTxjcsoQJD07U_hYF5HBvcyhNXJWkTHaKJfkIvycLI,19026
154
+ imio_smartweb_common-1.2.38.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
155
+ imio_smartweb_common-1.2.38.dist-info/namespace_packages.txt,sha256=Pg8AH8t9viMMW1hJbNZvTy_n2jXG2igIYUpon5RA4Js,19
156
+ imio_smartweb_common-1.2.38.dist-info/top_level.txt,sha256=ZktC0EGzThvMTAin9_q_41rzvvfMT2FYbP8pbhSLMSA,5
157
+ imio_smartweb_common-1.2.38.dist-info/RECORD,,