imio.smartweb.common 1.2.41__py3-none-any.whl → 1.2.43__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.
Files changed (37) hide show
  1. imio/smartweb/common/browser/tiny/process.py +1 -1
  2. imio/smartweb/common/configure.zcml +1 -0
  3. imio/smartweb/common/ia/__init__.py +0 -0
  4. imio/smartweb/common/ia/behaviors/__init__.py +0 -0
  5. imio/smartweb/common/ia/behaviors/configure.zcml +15 -0
  6. imio/smartweb/common/ia/behaviors/title.py +17 -0
  7. imio/smartweb/common/ia/browser/__init__.py +0 -0
  8. imio/smartweb/common/ia/browser/categorization_button_add.py +63 -0
  9. imio/smartweb/common/ia/browser/categorization_button_edit.py +87 -0
  10. imio/smartweb/common/ia/browser/configure.zcml +43 -0
  11. imio/smartweb/common/ia/browser/static/Makefile +12 -0
  12. imio/smartweb/common/ia/browser/static/package.json +28 -0
  13. imio/smartweb/common/ia/browser/static/smartweb-common-ia_suggested_titles-compiled.js +1 -0
  14. imio/smartweb/common/ia/browser/static/src/ia_suggested_titles.js +247 -0
  15. imio/smartweb/common/ia/browser/static/webpack.config.js +41 -0
  16. imio/smartweb/common/ia/browser/views.py +150 -0
  17. imio/smartweb/common/ia/configure.zcml +10 -0
  18. imio/smartweb/common/ia/widgets/configure.zcml +17 -0
  19. imio/smartweb/common/ia/widgets/html_snippet_widget.pt +352 -0
  20. imio/smartweb/common/ia/widgets/html_snippet_widget.py +65 -0
  21. imio/smartweb/common/ia/widgets/suggested_ia_titles_input.pt +45 -0
  22. imio/smartweb/common/ia/widgets/widget.py +40 -0
  23. imio/smartweb/common/profiles/default/metadata.xml +1 -1
  24. imio/smartweb/common/profiles/default/registry/registry.xml +9 -0
  25. imio/smartweb/common/rest/endpoint.py +80 -0
  26. imio/smartweb/common/upgrades/configure.zcml +19 -0
  27. imio/smartweb/common/upgrades/profiles/1035_to_1036/registry/bundles.xml +17 -0
  28. imio/smartweb/common/utils.py +4 -2
  29. {imio_smartweb_common-1.2.41.dist-info → imio_smartweb_common-1.2.43.dist-info}/METADATA +19 -1
  30. {imio_smartweb_common-1.2.41.dist-info → imio_smartweb_common-1.2.43.dist-info}/RECORD +36 -16
  31. {imio_smartweb_common-1.2.41.dist-info → imio_smartweb_common-1.2.43.dist-info}/WHEEL +1 -1
  32. imio/smartweb/common/browser/ia.py +0 -33
  33. /imio.smartweb.common-1.2.41-py3.12-nspkg.pth → /imio.smartweb.common-1.2.43-py3.12-nspkg.pth +0 -0
  34. {imio_smartweb_common-1.2.41.dist-info → imio_smartweb_common-1.2.43.dist-info}/licenses/LICENSE.GPL +0 -0
  35. {imio_smartweb_common-1.2.41.dist-info → imio_smartweb_common-1.2.43.dist-info}/licenses/LICENSE.rst +0 -0
  36. {imio_smartweb_common-1.2.41.dist-info → imio_smartweb_common-1.2.43.dist-info}/namespace_packages.txt +0 -0
  37. {imio_smartweb_common-1.2.41.dist-info → imio_smartweb_common-1.2.43.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,150 @@
1
+ from imio.smartweb.common.config import IPA_URL
2
+ from imio.smartweb.common.config import APPLICATION_ID
3
+ from imio.smartweb.common.config import PROJECT_ID
4
+ from imio.smartweb.common.utils import get_vocabulary
5
+ from plone import api
6
+ from Products.Five import BrowserView
7
+ from zope.i18n import translate
8
+
9
+ import json
10
+ import requests
11
+
12
+
13
+ class BaseIAView(BrowserView):
14
+ """
15
+ Base view providing common headers and configuration for IA-related features.
16
+ This class is shared across multiple projects, including imio.smartweb.core.
17
+ """
18
+
19
+ _headers = None
20
+
21
+ @property
22
+ def headers(self):
23
+ if self._headers is None:
24
+ self._headers = {
25
+ "accept": "application/json",
26
+ "Content-Type": "application/json",
27
+ "x-imio-application": APPLICATION_ID,
28
+ "x-imio-municipality": PROJECT_ID,
29
+ }
30
+ return self._headers
31
+
32
+ @property
33
+ def headers_json(self):
34
+ return json.dumps(self.headers)
35
+
36
+
37
+ class ProcessSuggestedTitlesView(BaseIAView):
38
+
39
+ def __call__(self):
40
+ self.request.response.setHeader(
41
+ "Content-Type", "application/json; charset=utf-8"
42
+ )
43
+ current_html = self.request.form.get("text", "")
44
+ payload = {
45
+ "input": current_html,
46
+ "expansion_target": 50,
47
+ }
48
+ url = f"{IPA_URL}/suggest-titles"
49
+ response = requests.post(url, headers=self.headers, json=payload)
50
+ if response.status_code != 200:
51
+ return current_html
52
+ data = response.json()
53
+ if not data:
54
+ return current_html
55
+ return json.dumps(data)
56
+
57
+
58
+ class BaseProcessCategorizeContentView(BaseIAView):
59
+
60
+ timeout = (10, 30)
61
+
62
+ def __init__(self, context, request):
63
+ super().__init__(context, request)
64
+ self.current_lang = api.portal.get_current_language()[:2]
65
+
66
+ def _get_structured_data_from_vocabulary(self, vocabulary_name, obj=None):
67
+ voc = get_vocabulary(vocabulary_name, obj)
68
+ voc_translated_dict = [
69
+ {
70
+ "title": translate(t.title, target_language=self.current_lang),
71
+ "token": t.token,
72
+ }
73
+ for t in voc
74
+ ]
75
+ return voc_translated_dict
76
+
77
+ def _ask_categorization_to_ia(self, text, voc):
78
+ payload = {"input": text, "vocabulary": voc, "unique": False}
79
+ url = f"{IPA_URL}/categorize-content"
80
+ try:
81
+ response = requests.post(
82
+ url, headers=self.headers, json=payload, timeout=self.timeout
83
+ )
84
+ response.raise_for_status()
85
+ return response.json() or {}
86
+ except requests.RequestException:
87
+ return {}
88
+
89
+ def _merge_existing_tokens(self, ia_list, existing_tokens, full_voc):
90
+ """Ajoute les tokens déjà enregistrés mais absents de la réponse IA."""
91
+ ia_tokens = {t.get("token") for t in ia_list}
92
+ for token in existing_tokens or []:
93
+ if token not in ia_tokens:
94
+ item = next((t for t in full_voc if t.get("token") == token), None)
95
+ if item:
96
+ ia_list.append(item)
97
+ return ia_list
98
+
99
+ def _process_iam(self, all_text, results):
100
+ iam_voc = self._get_structured_data_from_vocabulary(
101
+ "imio.smartweb.vocabulary.IAm"
102
+ )
103
+ data = self._ask_categorization_to_ia(all_text, iam_voc)
104
+ if not data:
105
+ return
106
+ ia_iam = [
107
+ {"title": r.get("title"), "token": r.get("token")}
108
+ for r in data.get("result", [])
109
+ ]
110
+ ia_iam = self._merge_existing_tokens(
111
+ ia_iam, getattr(self.context, "iam", []), iam_voc
112
+ )
113
+ results["form-widgets-IAm-iam"] = ia_iam
114
+
115
+ def _process_topics(self, all_text, results):
116
+ topics_voc = self._get_structured_data_from_vocabulary(
117
+ "imio.smartweb.vocabulary.Topics"
118
+ )
119
+ data = self._ask_categorization_to_ia(all_text, topics_voc)
120
+ if not data:
121
+ return
122
+ ia_topics = [
123
+ {"title": r.get("title"), "token": r.get("token")}
124
+ for r in data.get("result", [])
125
+ ]
126
+ ia_topics = self._merge_existing_tokens(
127
+ ia_topics, getattr(self.context, "topics", []), topics_voc
128
+ )
129
+ results["form-widgets-ITopics-topics"] = ia_topics
130
+
131
+ def _get_all_text(self):
132
+ """To implement in child class"""
133
+ raise NotImplementedError("_get_all_text must be defined in subclasses")
134
+
135
+ def _process_specific(self, all_text="", results={}):
136
+ """To implement in child class"""
137
+ raise NotImplementedError("_process_specific must be defined in subclasses")
138
+
139
+ def __call__(self):
140
+ self.request.response.setHeader(
141
+ "Content-Type", "application/json; charset=utf-8"
142
+ )
143
+ all_text = self._get_all_text()
144
+ results = {}
145
+ self._process_iam(all_text, results)
146
+ self._process_topics(all_text, results)
147
+ self._process_specific(all_text, results)
148
+ return json.dumps(
149
+ {"ok": True, "message": "Catégorisation calculée", "data": results}
150
+ )
@@ -0,0 +1,10 @@
1
+ <configure
2
+ xmlns="http://namespaces.zope.org/zope"
3
+ xmlns:browser="http://namespaces.zope.org/browser"
4
+ xmlns:plone="http://namespaces.plone.org/plone">
5
+
6
+ <include package=".behaviors" />
7
+ <include package=".browser" />
8
+ <include package=".widgets" />
9
+
10
+ </configure>
@@ -0,0 +1,17 @@
1
+ <configure
2
+ xmlns="http://namespaces.zope.org/zope"
3
+ xmlns:z3c="http://namespaces.zope.org/z3c"
4
+ xmlns:browser="http://namespaces.zope.org/browser">
5
+
6
+ <adapter factory=".widget.SuggestedIATitlesWidget"
7
+ for="z3c.form.interfaces.IFormLayer
8
+ imio.smartweb.common.interfaces.IImioSmartwebCommonLayer"
9
+ />
10
+
11
+ <z3c:widgetTemplate
12
+ widget=".widget.ISuggestedIATitlesWidget"
13
+ mode="input"
14
+ layer="z3c.form.interfaces.IFormLayer"
15
+ template="suggested_ia_titles_input.pt" />
16
+
17
+ </configure>
@@ -0,0 +1,352 @@
1
+ <tal:root xmlns="http://www.w3.org/1999/xhtml" i18n:domain="imio.smartweb"
2
+ xmlns:tal="http://xml.zope.org/namespaces/tal">
3
+ <div class="autotoc-section">
4
+ <!-- ATTENTION,
5
+ dans Agenda et actualités, le text est directement sur le context, du coups, on ne peut pas disabled le bouton
6
+ ou si on veut le dire il faut faire un callback pour récupérer si il y a qqchose dans le text.
7
+ disabled python:view.is_disabled and 'disabled' or nothing"
8
+ -->
9
+ <button type="button"
10
+ tal:attributes="id string:${view/wid}-btn;
11
+ data-endpoint view/endpoint;
12
+ class python:view.klass and f'suggestedcategorization-generate-button {view.klass}' or 'suggestedcategorization-generate-button';"
13
+ i18n:translate="">
14
+ Catégoriser via IA
15
+ </button>
16
+ <span tal:attributes="id string:${view/wid}-status"
17
+ style="margin-left:.5rem;"></span>
18
+ <span tal:condition="view/is_disabled"
19
+ style="margin-left:.1rem;font-size: 14px;"
20
+ i18n:translate="">(Contenu insuffisant pour catégoriser)</span>
21
+ </div>
22
+
23
+ <script type="text/javascript">
24
+ (function(){
25
+ var btnId = '${view/wid}-btn';
26
+ var statusId = '${view/wid}-status';
27
+ var btn = document.getElementById(btnId);
28
+ if(!btn) return;
29
+
30
+ var status = document.getElementById(statusId);
31
+ var endpoint = btn.getAttribute('data-endpoint');
32
+ var form = btn.closest('form') || document.querySelector('form');
33
+
34
+ // ---------- UI helpers ----------
35
+ function setStatus(msg){ if(status){ status.textContent = msg; } }
36
+ function ok(msg){ setStatus(msg || 'Mis à jour ✔'); }
37
+ function err(msg){ setStatus('Erreur : ' + (msg || 'inconnue')); }
38
+
39
+ // ---------- CSRF ----------
40
+ function getCsrf(){
41
+ var t = form && form.querySelector('input[name="_authenticator"]');
42
+ return t ? t.value : '';
43
+ }
44
+
45
+ // ---------- Helpers IDs / champs ----------
46
+ function resolveTarget(id){
47
+ if (!id) return null;
48
+ id = (id[0] === '#') ? id.slice(1) : id;
49
+
50
+ var el = document.getElementById(id);
51
+ if (el && (id.indexOf('formfield-') === 0 || el.matches('div, fieldset, section'))) {
52
+ var inner = el.querySelector('select, input, textarea');
53
+ if (inner) el = inner;
54
+ }
55
+ if (!el && form){
56
+ try { el = form.querySelector('#'+(window.CSS && CSS.escape ? CSS.escape(id) : id)+', [name="'+id+'"]'); }
57
+ catch(e){ el = form.querySelector('#'+id+', [name="'+id+'"]'); }
58
+ }
59
+ return el || null;
60
+ }
61
+
62
+ function isPatSelect2Input(el){
63
+ return el && el.tagName === 'INPUT' && el.classList.contains('pat-select2');
64
+ }
65
+
66
+ function normalizeEntry(item){
67
+ // Supporte: "token" OU {token,title} OU {value,text}
68
+ if (item && typeof item === 'object'){
69
+ var v = item.value || item.token || '';
70
+ var t = item.text || item.title || v;
71
+ return {value: String(v), text: String(t)};
72
+ }
73
+ var s = String(item == null ? '' : item);
74
+ return {value: s, text: s};
75
+ }
76
+
77
+ // ---------- SELECT classique (<select>) ----------
78
+ function setSelectSequential(selectEl, values, options){
79
+ options = options || {};
80
+ var replace = (options.replace !== false);
81
+ var delay = typeof options.delay === 'number' ? options.delay : 0;
82
+ var isMulti = !!selectEl.multiple;
83
+
84
+ var seq = (Array.isArray(values) ? values : [values]).map(normalizeEntry);
85
+
86
+ if (replace){
87
+ for (var i=0; i<selectEl.options.length; i++){ selectEl.options[i].selected = false; }
88
+ try { selectEl.dispatchEvent(new Event('change', {bubbles:true})); } catch(e){}
89
+ }
90
+
91
+ var i = 0;
92
+ function addNext(){
93
+ if (i >= seq.length){
94
+ if (!isMulti && seq.length){
95
+ selectEl.value = seq[0].value;
96
+ try { selectEl.dispatchEvent(new Event('change', {bubbles:true})); } catch(e){}
97
+ }
98
+ return;
99
+ }
100
+ var item = seq[i++]; // {value, text}
101
+
102
+ var opt = null;
103
+ for (var j=0; j<selectEl.options.length; j++){
104
+ if (selectEl.options[j].value === item.value){ opt = selectEl.options[j]; break; }
105
+ }
106
+ if (!opt){
107
+ // fallback label
108
+ for (var k=0; k<selectEl.options.length; k++){
109
+ if (selectEl.options[k].text === item.text){ opt = selectEl.options[k]; break; }
110
+ }
111
+ }
112
+ if (!opt){
113
+ opt = new Option(item.text, item.value, false, false);
114
+ selectEl.add(opt);
115
+ }
116
+
117
+ opt.selected = true;
118
+ try { selectEl.dispatchEvent(new Event('change', {bubbles:true})); } catch(e){}
119
+
120
+ if (delay > 0) setTimeout(addNext, delay);
121
+ else if (window.requestAnimationFrame) requestAnimationFrame(addNext);
122
+ else setTimeout(addNext, 0);
123
+ }
124
+ addNext();
125
+ }
126
+
127
+ // ---------- INPUT pat-select2 (remote) ----------
128
+ function setPatSelect2(el, values, options){
129
+ options = options || {};
130
+ var replace = (options.replace !== false);
131
+
132
+ // Lire le séparateur depuis la pattern (défaut ;)
133
+ var sep = ';';
134
+ var cfg = el.getAttribute('data-pat-select2');
135
+ if (cfg){
136
+ try { var parsed = JSON.parse(cfg); if (parsed && parsed.separator) sep = parsed.separator; } catch(e){}
137
+ }
138
+
139
+ // Normaliser en {value, text}
140
+ function norm(item){
141
+ if (item && typeof item === 'object'){
142
+ var v = item.value || item.token || '';
143
+ var t = item.text || item.title || v;
144
+ return {value:String(v), text:String(t)};
145
+ }
146
+ var s = String(item == null ? '' : item);
147
+ return {value:s, text:s};
148
+ }
149
+ var seq = (Array.isArray(values) ? values : [values]).map(norm);
150
+ var tokens = seq.map(function(x){ return x.value; });
151
+
152
+ // Append vs replace
153
+ if (!replace && el.value){
154
+ var current = el.value.split(sep).filter(Boolean);
155
+ var set = Object.create(null);
156
+ current.forEach(function(v){ set[v] = true; });
157
+ tokens.forEach(function(v){ if (!set[v]) current.push(v); });
158
+ tokens = current;
159
+ }
160
+
161
+ // 1) Écrire les tokens dans l'input (ce qui sera soumis)
162
+ el.value = tokens.join(sep);
163
+
164
+ // 2) Si Select2 (v3) est présent, pousser *puis* figer les labels
165
+ var hadSelect2 = false;
166
+ if (window.jQuery){
167
+ var $el = window.jQuery(el);
168
+ var api = $el.data('select2');
169
+ if (api){
170
+ hadSelect2 = true;
171
+ var data = seq.map(function(x){ return {id: x.value, text: x.text}; });
172
+
173
+ try {
174
+ // Important: ne PAS déclencher de change ici avant d'avoir injecté les labels
175
+ // Fixer d'abord les ids (facultatif, mais garde la cohérence interne)
176
+ $el.select2('val', tokens);
177
+
178
+ // Puis injecter les labels
179
+ $el.select2('data', data);
180
+
181
+ // Enfin, notifier (léger) : input + (éventuellement) change
182
+ el.dispatchEvent(new Event('input', {bubbles:true}));
183
+ // Si tu veux éviter que pat-select2 réécrase juste derrière, commente la ligne suivante :
184
+ el.dispatchEvent(new Event('change', {bubbles:true}));
185
+ } catch(e){}
186
+ }
187
+ }
188
+
189
+ // 3) Fallback DOM si l'API select2 n'est pas dispo OU si la pattern a écrasé les labels
190
+ // => on remplace manuellement le texte des chips par les labels
191
+ try {
192
+ var container = document.getElementById('s2id_' + el.id);
193
+ if (container){
194
+ var chips = container.querySelectorAll('.select2-search-choice > div');
195
+ if (chips && chips.length){
196
+ // Construire un map token->label
197
+ var map = Object.create(null);
198
+ seq.forEach(function(x){ map[x.value] = x.text; });
199
+
200
+ chips.forEach(function(div){
201
+ var current = (div.textContent || '').trim();
202
+ if (map[current]) {
203
+ div.textContent = map[current];
204
+ }
205
+ });
206
+ }
207
+ }
208
+ } catch(e){}
209
+ }
210
+
211
+
212
+
213
+ // ---------- Checkboxes / Radios ----------
214
+ function setCheckboxGroup(el, value){
215
+ var name = el.name;
216
+ if (!name || !form){ el.checked = !!value; return; }
217
+ var vals = Array.isArray(value) ? value.map(String) : (value == null ? [] : [String(value)]);
218
+ var boxes = form.querySelectorAll('input[type="checkbox"][name="'+name+'"]');
219
+ boxes.forEach(function(b){
220
+ b.checked = vals.indexOf(b.value) !== -1 || (vals.length === 0 && !!value === true);
221
+ try { b.dispatchEvent(new Event('change', {bubbles:true})); } catch(e){}
222
+ });
223
+ }
224
+ function setRadioGroup(el, value){
225
+ var name = el.name;
226
+ if (!name || !form) return;
227
+ var v = (value == null) ? '' : String(value);
228
+ var radios = form.querySelectorAll('input[type="radio"][name="'+name+'"]');
229
+ radios.forEach(function(r){ r.checked = (r.value === v); });
230
+ try { el.dispatchEvent(new Event('change', {bubbles:true})); } catch(e){}
231
+ }
232
+
233
+ // ---------- Dispatcher ----------
234
+ function setFieldById(id, value){
235
+ var el = resolveTarget(id);
236
+ if (!el) return;
237
+
238
+ // Support format objet {values:[...], append:true}
239
+ var values = value;
240
+ var replace = true;
241
+ if (value && typeof value === 'object' && !Array.isArray(value) && ('values' in value || 'append' in value)){
242
+ values = value.values != null ? value.values : value;
243
+ replace = value.append ? false : true;
244
+ }
245
+
246
+ if (el.tagName === 'SELECT'){ setSelectSequential(el, values, {replace:replace, delay:0}); return; }
247
+ if (isPatSelect2Input(el)){ setPatSelect2(el, values, {replace:replace}); return; }
248
+
249
+ if (el.tagName === 'INPUT'){
250
+ var t = (el.getAttribute('type') || '').toLowerCase();
251
+ if (t === 'checkbox'){ setCheckboxGroup(el, values); return; }
252
+ if (t === 'radio'){ setRadioGroup(el, values); return; }
253
+ el.value = Array.isArray(values) ? values.join(', ') : (values == null ? '' : String(values));
254
+ try { el.dispatchEvent(new Event('input', {bubbles:true})); } catch(e){}
255
+ try { el.dispatchEvent(new Event('change', {bubbles:true})); } catch(e){}
256
+ return;
257
+ }
258
+ if (el.tagName === 'TEXTAREA'){
259
+ el.value = Array.isArray(values) ? values.join('\n') : (values == null ? '' : String(values));
260
+ try { el.dispatchEvent(new Event('input', {bubbles:true})); } catch(e){}
261
+ try { el.dispatchEvent(new Event('change', {bubbles:true})); } catch(e){}
262
+ return;
263
+ }
264
+ if (el.hasAttribute('contenteditable')){
265
+ el.textContent = Array.isArray(values) ? values.join(', ') : (values == null ? '' : String(values));
266
+ }
267
+ }
268
+
269
+ function applyValues(data){
270
+ if (!data) return;
271
+ Object.keys(data).forEach(function(id){
272
+ setFieldById(id, data[id]);
273
+ });
274
+ }
275
+
276
+ /* BASIC collect
277
+ function collect(){
278
+ var out = {};
279
+ var t = document.getElementById('form-widgets-IBasic-title');
280
+ if (t) out['form-widgets-IBasic-title'] = t.value;
281
+ return out;
282
+ }*/
283
+
284
+ // (optionnel) collecte minimale pour le backend
285
+ function collect() {
286
+ var out = {};
287
+ var fields = [
288
+ 'form-widgets-IBasic-title',
289
+ 'form-widgets-IIASmartTitle-title',
290
+ 'form-widgets-IRichTextBehavior-text'
291
+ ];
292
+
293
+ fields.forEach(function (id) {
294
+ var el = document.getElementById(id);
295
+ if (el && el.value) {
296
+ out[id] = el.value;
297
+ }
298
+ });
299
+
300
+ return out;
301
+ }
302
+
303
+ // ---------- Action bouton ----------
304
+ btn.addEventListener('click', function(){
305
+
306
+ if (!endpoint){ err('Endpoint manquant'); return; }
307
+ btn.disabled = true;
308
+ setStatus('Traitement…');
309
+
310
+ // edit ici; si tu réutilises en add, adapte si besoin
311
+ var payload = {
312
+ mode: 'edit',
313
+ formdata: collect()
314
+ };
315
+
316
+ fetch(endpoint, {
317
+ method: 'POST',
318
+ headers: {
319
+ 'accept': 'application/json',
320
+ 'Content-Type': 'application/json',
321
+ 'x-imio-application': '${view/x_imio_application}',
322
+ 'x-imio-municipality': '${view/x_imio_municipality}',
323
+ 'X-CSRF-TOKEN': getCsrf()
324
+ },
325
+ body: JSON.stringify(payload),
326
+ credentials: 'same-origin'
327
+ })
328
+ .then(function(resp){
329
+ console.log(resp.status);
330
+ if (!resp.ok) throw new Error('HTTP '+resp.status);
331
+ return resp.text().then(function(t){ return t ? JSON.parse(t) : {}; });
332
+ })
333
+ .then(function(data){
334
+ var payload = data && (data.data || data);
335
+ applyValues(payload);
336
+ ok((data && data.message) || 'Appliqué');
337
+ })
338
+ .catch(function(e){
339
+ err(e && e.message);
340
+ })
341
+ .finally(function(){
342
+ btn.disabled = false;
343
+ });
344
+ });
345
+ })();
346
+ </script>
347
+
348
+
349
+
350
+
351
+
352
+ </tal:root>
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ # imio/smartweb/core/browser/categorization_button_edit.py
3
+ from imio.smartweb.common.config import APPLICATION_ID
4
+ from imio.smartweb.common.config import PROJECT_ID
5
+ from z3c.form import widget as z3c_widget
6
+ from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
7
+
8
+ FIELD_NAME = "categorization_ia_link" # Internal id for dummy field
9
+
10
+
11
+ class EditHtmlSnippetWidget(z3c_widget.Widget):
12
+ """Widget HTML (bouton + JS) avec template ZPT."""
13
+
14
+ template = ViewPageTemplateFile("html_snippet_widget.pt")
15
+ x_imio_application = APPLICATION_ID
16
+ x_imio_municipality = PROJECT_ID
17
+
18
+ def update(self):
19
+ # edit : context == objet
20
+ base = self.context.absolute_url()
21
+ self.endpoint = f"{base}/@@ProcessCategorizeContent"
22
+ # Unique id for button + status zone
23
+ self.wid = getattr(self, "name", FIELD_NAME)
24
+
25
+ # Désactiver le bouton si aucune section texte avec contenu n'est présente
26
+ self.klass = getattr(self, "klass", "")
27
+ has_text_content = False
28
+
29
+ # Vérifie si le contexte contient au moins une section texte avec du contenu
30
+ try:
31
+ for item in getattr(self.context, "objectItems", lambda: [])():
32
+ obj = item[1]
33
+ # Vérifier si la section texte a du contenu (non vide)
34
+ text_output = getattr(getattr(obj, "text", None), "output", "")
35
+ if text_output and text_output.strip():
36
+ has_text_content = True
37
+ break
38
+ except Exception:
39
+ pass
40
+
41
+ if not has_text_content:
42
+ self.klass = f"{self.klass} disabled".strip() if self.klass else "disabled"
43
+ self.is_disabled = True
44
+ else:
45
+ self.is_disabled = False
46
+
47
+ def render(self):
48
+ return self.template()
49
+
50
+
51
+ class AddHtmlSnippetWidget(z3c_widget.Widget):
52
+ template = ViewPageTemplateFile("html_snippet_widget.pt")
53
+ x_imio_application = APPLICATION_ID
54
+ x_imio_municipality = PROJECT_ID
55
+
56
+ def update(self):
57
+ # ++add++ : context = container ; edit : context = object
58
+ base = self.context.absolute_url()
59
+ self.endpoint = f"{base}/@@ProcessCategorizeContent"
60
+ self.wid = getattr(self, "name", "categorization_ia_link")
61
+ self.klass = getattr(self, "klass", "")
62
+ self.is_disabled = False
63
+
64
+ def render(self):
65
+ return self.template()
@@ -0,0 +1,45 @@
1
+ <tal:domain i18n:domain="imio.smartweb">
2
+ <div class="suggestedtitles-with-button"
3
+ data-slug-url="${context/absolute_url}/@@processsuggestedtitles"
4
+ data-source-selector="#form-widgets-title">
5
+
6
+ <input id="" name="" class="" title="" lang="" disabled=""
7
+ readonly="" alt="" tabindex="" accesskey="" size="" maxlength=""
8
+ style="" value="" type="text"
9
+ tal:attributes="id view/id;
10
+ name view/name;
11
+ class view/klass;
12
+ style view/style;
13
+ title view/title;
14
+ lang view/lang;
15
+ value view/value;
16
+ disabled view/disabled;
17
+ tabindex view/tabindex;
18
+ readonly view/readonly;
19
+ size view/size;
20
+ maxlength view/maxlength;
21
+ placeholder view/placeholder;
22
+ autocapitalize view/autocapitalize;" />
23
+
24
+ <a id="ia-suggest-open"
25
+ href="#ia-suggest-modal"
26
+ class="suggestedtitles-generate-button pat-plone-modal"
27
+ tal:attributes="data-pat-plone-modal view/data_pat_plone_modal;"
28
+ i18n:translate="">
29
+ Générer via IA
30
+ </a>
31
+ </div>
32
+
33
+ <div id="ia-suggest-modal" style="display:none">
34
+ <p class="formHelp ia-modale" i18n:translate="">Select a title :</p>
35
+ <div class="ia-suggest-list"></div>
36
+ <button type="button"
37
+ class="btn btn-secondary plone-btn me-1 plone-modal-close"
38
+ data-bs-dismiss="modal"
39
+ data-dismiss="modal"
40
+ aria-label="Close"
41
+ i18n:translate="">
42
+ Close
43
+ </button>
44
+ </div>
45
+ </tal:domain>
@@ -0,0 +1,40 @@
1
+ from imio.smartweb.locales import SmartwebMessageFactory as _
2
+ from plone import api
3
+ from plone.app.z3cform.interfaces import IPloneFormLayer
4
+ from z3c.form.browser.text import TextWidget
5
+ from z3c.form.interfaces import IFieldWidget
6
+ from z3c.form.interfaces import IWidget
7
+ from z3c.form.widget import FieldWidget
8
+ from zope.component import adapter
9
+ from zope.i18n import translate
10
+ from zope.interface import implementer
11
+ from zope.interface import implementer_only
12
+ from zope.schema.interfaces import ITextLine
13
+
14
+
15
+ class ISuggestedIATitlesWidget(IWidget):
16
+ pass
17
+
18
+
19
+ @implementer_only(ISuggestedIATitlesWidget)
20
+ class SuggestedIATitlesWidget(TextWidget):
21
+ klass = "text-widget form-control suggestedtitles-with-button"
22
+
23
+ def update(self):
24
+ super().update()
25
+ self.pattern_options = {
26
+ "sourceSelector": "#form-widgets-title",
27
+ "buttonLabel": "Get titles",
28
+ }
29
+
30
+ @property
31
+ def data_pat_plone_modal(self):
32
+ language = api.portal.get_current_language(context=self.context)
33
+ title = translate(_("Title suggestions"), target_language=language)
34
+ return f"title: {title}; width: 600; loadLinksWithinModal: false"
35
+
36
+
37
+ @implementer(IFieldWidget)
38
+ @adapter(ITextLine, IPloneFormLayer)
39
+ def SuggestedIATitlesFieldWidget(field, request) -> IFieldWidget:
40
+ return FieldWidget(field, SuggestedIATitlesWidget(request))
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
2
  <metadata>
3
- <version>1035</version>
3
+ <version>1036</version>
4
4
  <dependencies>
5
5
  <dependency>profile-plone.restapi:default</dependency>
6
6
  <dependency>profile-eea.facetednavigation:default</dependency>