Mercurial > libervia-backend
annotate frontends/src/jp/xmlui_manager.py @ 2420:03da3ef5fb5b
plugin tickets: added ticketsSet and ticketsSchemaGet methods:
those methods are high level methods specialised for tickets.
ticketsSet will use default tickets node if node is not set, set "created" and "updated" fields, create comments node if the ticket is new, and associate it with "comments_uri" field.
ticketsSchemaGet is like getUISchema with node defaulting to tickets default node.
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 05 Nov 2017 15:36:06 +0100 |
parents | c38c54c47e16 |
children | 0046283a285d |
rev | line source |
---|---|
2408 | 1 #!/usr/bin/env python2 |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # JP: a SàT frontend | |
2414
8b37a62336c3
misc: date update (yes it's a bit late :p )
Goffi <goffi@goffi.org>
parents:
2412
diff
changeset
|
5 # Copyright (C) 2009-2017 Jérôme Poisson (goffi@goffi.org) |
2408 | 6 |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.log import getLogger | |
21 log = getLogger(__name__) | |
22 from sat_frontends.tools import xmlui as xmlui_manager | |
23 from sat_frontends.jp.constants import Const as C | |
24 from sat.tools.common.ansi import ANSI as A | |
25 from sat.core.i18n import _ | |
26 from functools import partial | |
27 | |
28 # workflow constants | |
29 | |
30 SUBMIT = 'SUBMIT' # submit form | |
31 | |
32 | |
33 | |
34 ## Widgets ## | |
35 | |
36 class Base(object): | |
37 """Base for Widget and Container""" | |
38 type = None | |
39 _root = None | |
40 | |
41 def __init__(self, xmlui_parent): | |
42 self.xmlui_parent = xmlui_parent | |
43 self.host = self.xmlui_parent.host | |
44 | |
45 @property | |
46 def root(self): | |
47 """retrieve main XMLUI parent class""" | |
48 if self._root is not None: | |
49 return self._root | |
50 root = self | |
51 while not isinstance(root, xmlui_manager.XMLUIBase): | |
52 root = root.xmlui_parent | |
53 self._root = root | |
54 return root | |
55 | |
56 def disp(self, *args, **kwargs): | |
57 self.host.disp(*args, **kwargs) | |
58 | |
59 | |
60 class Widget(Base): | |
61 category = u'widget' | |
62 enabled = True | |
63 | |
64 @property | |
65 def name(self): | |
66 return self._xmlui_name | |
67 | |
68 def show(self): | |
69 """display current widget | |
70 | |
71 must be overriden by subclasses | |
72 """ | |
73 raise NotImplementedError(self.__class__) | |
74 | |
75 def verboseName(self, elems=None, value=None): | |
76 """add name in color to the elements | |
77 | |
78 helper method to display name which can then be used to automate commands | |
79 elems is only modified if verbosity is > 0 | |
80 @param elems(list[unicode], None): elements to display | |
81 None to display name directly | |
82 @param value(unicode, None): value to show | |
83 use self.name if None | |
84 """ | |
85 if value is None: | |
86 value = self.name | |
87 if self.host.verbosity: | |
88 to_disp = [A.FG_MAGENTA, | |
89 u' ' if elems else u'', | |
90 u'({})'.format(value), A.RESET] | |
91 if elems is None: | |
92 self.host.disp(A.color(*to_disp)) | |
93 else: | |
94 elems.extend(to_disp) | |
95 | |
96 class ValueWidget(Widget): | |
97 | |
98 def __init__(self, xmlui_parent, value): | |
99 super(ValueWidget, self).__init__(xmlui_parent) | |
100 self.value = value | |
101 | |
102 @property | |
103 def values(self): | |
104 return [self.value] | |
105 | |
106 | |
107 class InputWidget(ValueWidget): | |
108 | |
109 def __init__(self, xmlui_parent, value, read_only=False): | |
110 super(InputWidget, self).__init__(xmlui_parent, value) | |
111 self.read_only = read_only | |
112 | |
113 def _xmluiGetValue(self): | |
114 return self.value | |
115 | |
116 | |
117 class OptionsWidget(Widget): | |
118 | |
119 def __init__(self, xmlui_parent, options, selected, style): | |
120 super(OptionsWidget, self).__init__(xmlui_parent) | |
121 self.options = options | |
122 self.selected = selected | |
123 self.style = style | |
124 | |
125 @property | |
126 def values(self): | |
127 return self.selected | |
128 | |
129 @values.setter | |
130 def values(self, values): | |
131 self.selected = values | |
132 | |
133 @property | |
134 def value(self): | |
135 return self.selected[0] | |
136 | |
137 @value.setter | |
138 def value(self, value): | |
139 self.selected = [value] | |
140 | |
141 def _xmluiSelectValue(self, value): | |
142 self.value = value | |
143 | |
144 def _xmluiSelectValues(self, values): | |
145 self.values = values | |
146 | |
147 def _xmluiGetSelectedValues(self): | |
148 return self.values | |
149 | |
150 @property | |
151 def labels(self): | |
152 """return only labels from self.items""" | |
153 for value, label in self.items: | |
154 yield label | |
155 | |
156 @property | |
157 def items(self): | |
158 """return suitable items, according to style""" | |
159 no_select = self.no_select | |
160 for value,label in self.options: | |
161 if no_select or value in self.selected: | |
162 yield value,label | |
163 | |
164 @property | |
165 def inline(self): | |
166 return u'inline' in self.style | |
167 | |
168 @property | |
169 def no_select(self): | |
170 return u'noselect' in self.style | |
171 | |
172 | |
173 class EmptyWidget(xmlui_manager.EmptyWidget, Widget): | |
174 | |
175 def __init__(self, _xmlui_parent): | |
176 Widget.__init__(self) | |
177 | |
178 | |
179 class TextWidget(xmlui_manager.TextWidget, ValueWidget): | |
180 type = u"text" | |
181 | |
182 def show(self): | |
183 self.host.disp(self.value) | |
184 | |
185 | |
186 class LabelWidget(xmlui_manager.LabelWidget, ValueWidget): | |
187 type = u"label" | |
188 | |
189 @property | |
190 def for_name(self): | |
191 try: | |
192 return self._xmlui_for_name | |
193 except AttributeError: | |
194 return None | |
195 | |
196 def show(self, no_lf=False, ansi=u''): | |
197 """show label | |
198 | |
199 @param no_lf(bool): same as for [JP.disp] | |
200 @param ansi(unicode): ansi escape code to print before label | |
201 """ | |
202 self.disp(A.color(ansi, self.value), no_lf=no_lf) | |
203 | |
204 | |
205 class StringWidget(xmlui_manager.StringWidget, InputWidget): | |
206 type = u"string" | |
207 | |
208 def show(self): | |
209 if self.read_only: | |
210 self.disp(self.value) | |
211 else: | |
212 elems = [] | |
213 self.verboseName(elems) | |
214 if self.value: | |
215 elems.append(_(u'(enter: {default})').format(default=self.value)) | |
216 elems.extend([C.A_HEADER, u'> ']) | |
217 value = raw_input(A.color(*elems)) | |
218 if value: | |
219 # TODO: empty value should be possible | |
220 # an escape key should be used for default instead of enter with empty value | |
221 self.value = value | |
222 | |
223 | |
224 | |
225 class JidInputWidget(xmlui_manager.JidInputWidget, StringWidget): | |
226 type = u'jid_input' | |
227 | |
228 | |
229 class TextBoxWidget(xmlui_manager.TextWidget, StringWidget): | |
230 type = u"textbox" | |
231 | |
232 | |
233 class ListWidget(xmlui_manager.ListWidget, OptionsWidget): | |
234 type = u'list' | |
235 # TODO: handle flags, notably multi | |
236 | |
237 def show(self): | |
238 if not self.options: | |
239 return | |
240 | |
241 # list display | |
242 self.verboseName() | |
243 | |
244 for idx, (value, label) in enumerate(self.options): | |
245 elems = [] | |
246 if not self.root.readonly: | |
247 elems.extend([C.A_SUBHEADER, unicode(idx), A.RESET, u': ']) | |
248 elems.append(label) | |
249 self.verboseName(elems, value) | |
250 self.disp(A.color(*elems)) | |
251 | |
252 if self.root.readonly: | |
253 return | |
254 | |
255 if len(self.options) == 1: | |
256 # we have only one option, no need to ask | |
257 self.value = self.options[0][0] | |
258 return | |
259 | |
260 # we ask use to choose an option | |
261 choice = None | |
262 limit_max = len(self.options)-1 | |
263 while choice is None or choice<0 or choice>limit_max: | |
264 choice = raw_input(A.color(C.A_HEADER, _(u'your choice (0-{max}): ').format(max=limit_max))) | |
265 try: | |
266 choice = int(choice) | |
267 except ValueError: | |
268 choice = None | |
269 self.value = self.options[choice][0] | |
270 self.disp('') | |
271 | |
272 | |
273 class BoolWidget(xmlui_manager.BoolWidget, InputWidget): | |
274 type = u'bool' | |
275 | |
276 def show(self): | |
277 disp_true = A.color(A.FG_GREEN, u'TRUE') | |
278 disp_false = A.color(A.FG_RED,u'FALSE') | |
279 if self.read_only: | |
280 self.disp(disp_true if self.value else disp_false) | |
281 else: | |
282 self.disp(A.color(C.A_HEADER, u'0: ', disp_false)) | |
283 self.disp(A.color(C.A_HEADER, u'1: ', disp_true)) | |
284 choice = None | |
285 while choice not in ('0', '1'): | |
286 elems = [C.A_HEADER, _(u'your choice (0,1): ')] | |
287 self.verboseName(elems) | |
288 choice = raw_input(A.color(*elems)) | |
289 self.value = bool(int(choice)) | |
290 self.disp('') | |
291 | |
292 def _xmluiGetValue(self): | |
293 return C.boolConst(self.value) | |
294 | |
295 ## Containers ## | |
296 | |
297 class Container(Base): | |
298 category = u'container' | |
299 | |
300 def __init__(self, xmlui_parent): | |
301 super(Container, self).__init__(xmlui_parent) | |
302 self.children = [] | |
303 | |
304 def __iter__(self): | |
305 return iter(self.children) | |
306 | |
307 def _xmluiAppend(self, widget): | |
308 self.children.append(widget) | |
309 | |
310 def show(self): | |
311 for child in self.children: | |
312 child.show() | |
313 | |
314 | |
315 class VerticalContainer(xmlui_manager.VerticalContainer, Container): | |
316 type = u'vertical' | |
317 | |
318 | |
319 class PairsContainer(xmlui_manager.PairsContainer, Container): | |
320 type = u'pairs' | |
321 | |
322 | |
323 class LabelContainer(xmlui_manager.PairsContainer, Container): | |
324 type = u'label' | |
325 | |
326 def show(self): | |
327 for child in self.children: | |
328 no_lf = False | |
329 # we check linked widget type | |
330 # to see if we want the label on the same line or not | |
331 if child.type == u'label': | |
332 for_name = child.for_name | |
333 if for_name is not None: | |
334 for_widget = self.root.widgets[for_name] | |
335 wid_type = for_widget.type | |
336 if wid_type in ('text', 'string', 'jid_input'): | |
337 no_lf = True | |
338 elif wid_type == 'bool' and for_widget.read_only: | |
339 no_lf = True | |
340 child.show(no_lf=no_lf, ansi=A.FG_CYAN) | |
341 else: | |
342 child.show() | |
343 | |
344 ## Dialogs ## | |
345 | |
346 | |
347 class Dialog(object): | |
348 | |
349 def __init__(self, xmlui_parent): | |
350 self.xmlui_parent = xmlui_parent | |
351 self.host = self.xmlui_parent.host | |
352 | |
353 def disp(self, *args, **kwargs): | |
354 self.host.disp(*args, **kwargs) | |
355 | |
356 def show(self): | |
357 """display current dialog | |
358 | |
359 must be overriden by subclasses | |
360 """ | |
361 raise NotImplementedError(self.__class__) | |
362 | |
363 | |
364 class NoteDialog(xmlui_manager.NoteDialog, Dialog): | |
365 | |
366 def show(self): | |
367 # TODO: handle title and level | |
368 self.disp(self.message) | |
369 | |
370 def __init__(self, _xmlui_parent, title, message, level): | |
371 Dialog.__init__(self, _xmlui_parent) | |
372 xmlui_manager.NoteDialog.__init__(self, _xmlui_parent) | |
373 self.title, self.message, self.level = title, message, level | |
374 | |
375 ## Factory ## | |
376 | |
377 | |
378 class WidgetFactory(object): | |
379 | |
380 def __getattr__(self, attr): | |
381 if attr.startswith("create"): | |
382 cls = globals()[attr[6:]] | |
383 return cls | |
384 | |
385 | |
386 class XMLUIPanel(xmlui_manager.XMLUIPanel): | |
387 widget_factory = WidgetFactory() | |
388 _actions = 0 # use to keep track of bridge's launchAction calls | |
389 readonly = False | |
390 workflow = None | |
391 _submit_cb = None | |
392 | |
2419
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
393 def __init__(self, host, parsed_dom, title=None, flags=None, callback=None, ignore=None, profile=None): |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
394 xmlui_manager.XMLUIPanel.__init__(self, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
395 host, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
396 parsed_dom, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
397 title = title, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
398 flags = flags, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
399 ignore = ignore, |
c38c54c47e16
frontends (xmlui): added an attribute to ignore some widgets (and their label) in create
Goffi <goffi@goffi.org>
parents:
2414
diff
changeset
|
400 profile=host.profile) |
2408 | 401 self.submitted = False |
402 | |
403 @property | |
404 def command(self): | |
405 return self.host.command | |
406 | |
407 def show(self, workflow=None): | |
408 """display the panel | |
409 | |
410 @param workflow(list, None): command to execute if not None | |
411 put here for convenience, the main workflow is the class attribute | |
412 (because workflow can continue in subclasses) | |
413 command are a list of consts or lists: | |
414 - SUBMIT is the only constant so far, it submits the XMLUI | |
415 - list must contain widget name/widget value to fill | |
416 """ | |
417 if workflow: | |
418 XMLUIPanel.workflow = workflow | |
419 if XMLUIPanel.workflow: | |
420 self.runWorkflow() | |
421 else: | |
422 self.main_cont.show() | |
423 | |
424 def runWorkflow(self): | |
425 """loop into workflow commands and execute commands | |
426 | |
427 SUBMIT will interrupt workflow (which will be continue on callback) | |
428 @param workflow(list): same as [show] | |
429 """ | |
430 workflow = XMLUIPanel.workflow | |
431 while True: | |
432 try: | |
433 cmd = workflow.pop(0) | |
434 except IndexError: | |
435 break | |
436 if cmd == SUBMIT: | |
437 self.onFormSubmitted() | |
438 self.submit_id = None # avoid double submit | |
439 return | |
440 elif isinstance(cmd, list): | |
441 name, value = cmd | |
2412
7641bef56dcd
jp (xmlui): fixed workflow when value is for a BoolWidget
Goffi <goffi@goffi.org>
parents:
2410
diff
changeset
|
442 widget = self.widgets[name] |
7641bef56dcd
jp (xmlui): fixed workflow when value is for a BoolWidget
Goffi <goffi@goffi.org>
parents:
2410
diff
changeset
|
443 if widget.type == 'bool': |
7641bef56dcd
jp (xmlui): fixed workflow when value is for a BoolWidget
Goffi <goffi@goffi.org>
parents:
2410
diff
changeset
|
444 value = C.bool(value) |
7641bef56dcd
jp (xmlui): fixed workflow when value is for a BoolWidget
Goffi <goffi@goffi.org>
parents:
2410
diff
changeset
|
445 widget.value = value |
2408 | 446 self.show() |
447 | |
448 def submitForm(self, callback=None): | |
449 XMLUIPanel._submit_cb = callback | |
450 self.onFormSubmitted() | |
451 | |
452 def onFormSubmitted(self, ignore=None): | |
453 # self.submitted is a Q&D workaround to avoid | |
454 # double submit when a workflow is set | |
455 if self.submitted: | |
456 return | |
457 self.submitted = True | |
458 super(XMLUIPanel, self).onFormSubmitted(ignore) | |
459 | |
460 def _xmluiClose(self): | |
461 pass | |
462 | |
463 def _launchActionCb(self, data): | |
464 XMLUIPanel._actions -= 1 | |
465 assert XMLUIPanel._actions >= 0 | |
466 if u'xmlui' in data: | |
467 xmlui_raw = data['xmlui'] | |
468 xmlui = xmlui_manager.create(self.host, xmlui_raw) | |
469 xmlui.show() | |
470 if xmlui.submit_id: | |
471 xmlui.onFormSubmitted() | |
472 # TODO: handle data other than XMLUI | |
473 if not XMLUIPanel._actions: | |
474 if self._submit_cb is None: | |
475 self.host.quit() | |
476 else: | |
477 self._submit_cb() | |
478 | |
479 def _xmluiLaunchAction(self, action_id, data): | |
480 XMLUIPanel._actions += 1 | |
481 self.host.bridge.launchAction( | |
482 action_id, | |
483 data, | |
484 self.profile, | |
485 callback=self._launchActionCb, | |
486 errback=partial(self.command.errback, | |
487 msg=_(u"can't launch XMLUI action: {}"), | |
488 exit_code=C.EXIT_BRIDGE_ERRBACK)) | |
489 | |
490 | |
491 class XMLUIDialog(xmlui_manager.XMLUIDialog): | |
492 type = 'dialog' | |
493 dialog_factory = WidgetFactory() | |
494 readonly = False | |
495 | |
2410
40e6e779a253
jp (xmlui): XMLUIDialog.show has now a dummy argument, so workflow arguments can be put there
Goffi <goffi@goffi.org>
parents:
2408
diff
changeset
|
496 def show(self, dummy=None): |
2408 | 497 self.dlg.show() |
498 | |
499 def _xmluiClose(self): | |
500 pass | |
501 | |
502 | |
503 xmlui_manager.registerClass(xmlui_manager.CLASS_PANEL, XMLUIPanel) | |
504 xmlui_manager.registerClass(xmlui_manager.CLASS_DIALOG, XMLUIDialog) | |
505 create = xmlui_manager.create |