source: multipleworkflowplugin/trunk/multipleworkflow/web_ui.py

Last change on this file was 18094, checked in by Cinc-th, 3 years ago

MultipleWorkflowPlugin: fixes for Python 3 support.

File size: 12.3 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2015-2021 Cinc
4#
5# All rights reserved.
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution.
9
10import io
11import json
12
13try:
14    from ConfigParser import SafeConfigParser, ParsingError
15except ImportError:
16    # Python 3
17    from configparser import SafeConfigParser, ParsingError
18from pkg_resources import resource_filename, get_distribution, parse_version
19try:
20    from genshi.output import HTMLSerializer
21except ImportError:
22    pass
23
24from trac.admin import IAdminPanelProvider
25from trac.core import Component, implements
26from trac.ticket.model import Type
27from trac.util.html import Markup, html as tag
28from trac.util.text import to_unicode
29from trac.util.translation import _, dgettext
30from trac.web.api import IRequestHandler
31from trac.web.chrome import (add_notice, add_warning,
32    ITemplateProvider, add_script_data, add_script)
33
34from multipleworkflow.workflow import get_workflow_config_by_type, parse_workflow_config
35
36
37def get_workflow_actions_for_error():
38    """This is a small workflow just showing an 'Error' state"""
39    txt = "? = Error -> Error"
40    return get_workflow_actions_from_text(txt, True)
41
42
43def get_workflow_actions_from_text(wf_txt, is_error_wf=False):
44    """Parse workflow actions in a text snippet
45    Note that no section header [ticket-workflow_xxx] must be provided"""
46
47    def get_line_txt(txt):
48        try:
49            msg = txt.split(']:')[1]
50        except IndexError:
51            msg = txt
52
53        return msg
54
55    error_txt = ""
56    config = SafeConfigParser()
57    try:
58        config.readfp(
59            io.StringIO("[ticket-workflow]\n" + to_unicode(wf_txt)))
60        raw_actions = [(key, config.get('ticket-workflow', key)) for key in
61                       config.options('ticket-workflow')]
62    except ParsingError as err:
63        error_txt = u"Parsing error: %s" % get_line_txt(
64            to_unicode(err).replace('\\n', '').replace('<???>', ''))
65
66        if not is_error_wf:  # prevent recursion
67            actions, tmp = get_workflow_actions_for_error()
68        else:
69            actions = []
70        return actions, error_txt
71
72    try:
73        actions = parse_workflow_config(raw_actions)
74    except BaseException as err:
75        error_txt = to_unicode(err)
76        if not is_error_wf:  # prevent recursion
77            actions, tmp = get_workflow_actions_for_error()
78        else:
79            actions = []
80
81    return actions, error_txt
82
83
84def create_workflow_name(name):
85    if name == 'default':
86        return 'ticket-workflow'
87    else:
88        return 'ticket-workflow-%s' % name
89
90
91# This function is taken from WorkflowMacro and modified for the multiple
92# workflow display
93def create_graph_data(self, req, name=''):
94    txt = req.args.get('text')
95    if txt:
96        actions, error_txt = get_workflow_actions_from_text(txt)
97        if error_txt:
98            txt = error_txt
99        else:
100            txt = "New custom workflow (not saved)"
101        if not actions:
102            # We should never end here...
103            actions = get_workflow_config_by_type(self.config, 'default')
104            txt = "Custom workflow is broken. Showing default workflow"
105    else:
106        txt = u""
107        print(name)
108        if name == 'default':
109            actions = get_workflow_config_by_type(self.config, 'default')
110        else:
111            actions = get_workflow_config_by_type(self.config, name)
112
113    states = list(
114        {state for action in actions.values()
115         for state in action['oldstates']} |
116        {action['newstate'] for action in actions.values()})
117    action_labels = [attrs['label'] for attrs in actions.values()]
118    action_names = list(actions)
119
120    edges = []
121    for name, action in actions.items():
122        new_index = states.index(action['newstate'])
123        name_index = action_names.index(name)
124        for old_state in action['oldstates']:
125            old_index = states.index(old_state)
126            edges.append((old_index, new_index, name_index))
127
128    args = {}
129    width = args.get('width', 800)
130    height = args.get('height', 600)
131    graph = {'nodes': states, 'actions': action_labels, 'edges': edges,
132             'width': width, 'height': height}
133    graph_id = '%012x' % id(self)  # id(graph)
134
135    scr_data = {'graph_%s' % graph_id: graph}
136
137    res = tag(
138        tag.p(txt),
139        tag.div('', class_='multiple-workflow-graph trac-noscript',
140                id='trac-workflow-graph-%s' % graph_id,
141                style="display:inline-block;width:%spx;height:%spx" %
142                      (width, height)),
143        tag.noscript(
144            tag.div(_("Enable JavaScript to display the workflow graph."),
145                    class_='system-message')))
146    return res, scr_data, graph
147
148
149def workflow_graph(self, req, name):
150    res, scr_data, graph = create_graph_data(self, req, name)
151
152    # add_script(req, 'multipleworkflow/js/excanvas.js', ie_if='IE')
153    add_script(req, 'multipleworkflow/js/workflow_graph.js')
154    add_script_data(req, scr_data)
155
156    return res
157
158
159def write_json_response(req, data_dict, httperror=200):
160    data = json.dumps(data_dict).encode('utf-8')
161    req.send_response(httperror)
162    req.send_header('Content-Type', 'application/json; charset=utf-8')
163    req.send_header('Content-Length', len(data))
164    req.end_headers()
165    req.write(data)
166
167
168class MultipleWorkflowAdminModule(Component):
169    """Implements the admin page for workflow editing. See 'Ticket System'
170    section.
171    """
172
173    implements(IAdminPanelProvider, ITemplateProvider, IRequestHandler)
174
175    # Api changes regarding Genshi started after v1.2. This not only affects templates but also fragment
176    # creation using trac.util.html.tag and friends
177    pre_1_3 = parse_version(get_distribution("Trac").version) < parse_version('1.3')
178
179    # IRequestHandler methods
180
181    def match_request(self, req):
182        return req.path_info == '/multipleworkflow/workflow_render'
183
184    def process_request(self, req):
185        req.perm.require('TICKET_ADMIN')
186
187        # div may be a Genshi fragment or a new Trac 1.3 fragment
188        div, scr_data, graph = create_graph_data(self, req)
189        if self.pre_1_3:
190            rendered = "".join(HTMLSerializer()(div.generate()))
191            data = {'html': rendered.encode("utf-8"), 'graph_data': graph}
192        else:
193            rendered = to_unicode(div)
194            data = {'html': rendered, 'graph_data': graph}
195        write_json_response(req, data)
196
197    # IAdminPanelProvider methods
198
199    def get_admin_panels(self, req):
200        if 'TICKET_ADMIN' in req.perm:
201            yield ('ticket', dgettext("messages", ("Ticket System")),
202                   'workflowadmin', _("Workflows"))
203
204    def _get_all_types_with_workflow(self, to_upper=False):
205        """Returns a list of all ticket types with custom workflow.
206
207        Note that a ticket type is not necessarily available during ticket
208        creation if it was deleted in the meantime.
209        """
210        types = []
211        for section in self.config.sections():
212            if section.startswith('ticket-workflow-'):
213                if to_upper:
214                    types.append(section[len('ticket-workflow-'):].upper())
215                else:
216                    types.append(section[len('ticket-workflow-'):])
217        return types
218
219    def install_workflow_controller(self, req):
220        """Set MultipleWorkflowPlugin as the current workflow controller in trac.ini.
221
222        Note that the current setting will be replaced and saved using the key 'workflow_mwf_install'. If
223        you want to use several workflow controllers at the same time you have to create a list on your own.
224
225        A notice will be shown to the user on success.
226        """
227        save_key = 'workflow_mwf_install'  # key in section [ticket] to keep the previous setting
228        prev_controller = self.config.get('ticket', 'workflow', '')
229        self.config.set('ticket', 'workflow', 'MultipleWorkflowPlugin')
230        self.config.set('ticket', save_key, prev_controller)
231        self.config.save()
232        add_notice(req, Markup(_(u'Workflow controller installed by setting <em>workflow=MultipleWorkflowPlugin</em> '
233                                 u'in section <em>[ticket]</em>.')))
234        add_notice(req, Markup(_(u'Previous workflow controller was saved as '
235                                 u'<em>%s=%s</em> in section <em>[ticket]</em>.') % (save_key, prev_controller)))
236
237    def render_admin_panel(self, req, cat, page, path_info):
238        req.perm.assert_permission('TICKET_ADMIN')
239
240        if req.method == 'POST':
241            if req.args.get('add'):
242                cur_types = self._get_all_types_with_workflow(True)
243                name = req.args.get('name')
244                if name.upper() in cur_types:
245                    add_warning(req, _(
246                        "There is already a workflow for ticket type '%s'. "
247                        "Note that upper/lowercase is ignored"), name)
248                else:
249                    src_section = create_workflow_name(req.args.get('type'))
250                    # Now copy the workflow
251                    section = 'ticket-workflow-%s' % name
252                    for key, val in self.config.options(src_section):
253                        self.config.set(section, key, val)
254                    self.config.save()
255            elif req.args.get('remove'):
256                workflow = 'ticket-workflow-%s' % req.args.get('sel')
257                for key, val in self.config.options(workflow):
258                    self.config.remove(workflow, key)
259                self.config.save()
260            elif req.args.get('save'):
261                name = req.args.get('name', '')
262                if name:
263                    section = 'ticket-workflow-%s' % name
264                else:
265                    # If it's the default workflow the input is disabled and
266                    # no value sent
267                    section = 'ticket-workflow'
268
269                # Change of workflow name. Remove old data from ini
270                if name and name != path_info:
271                    old_section = 'ticket-workflow-%s' % path_info
272                    for key, val in self.config.options(old_section):
273                        self.config.remove(old_section, key)
274
275                # Save new workflow
276                for key, val in self.config.options(section):
277                    self.config.remove(section, key)
278                for line in req.args.get('workflow-actions').split('\n'):
279                    try:
280                        key, val = line.split('=')
281                        self.config.set(section, key, val)
282                    except ValueError:
283                        # Empty line or missing val
284                        pass
285                self.config.save()
286            elif req.args.get('install'):
287                self.install_workflow_controller(req)
288
289            req.redirect(req.href.admin(cat, page))
290
291        # GET, show admin page
292        wf_controllers = self.config.getlist('ticket', 'workflow', [])
293        data = {'types': self._get_all_types_with_workflow(),
294                'trac_types': [enum.name for enum in Type.select(self.env)],
295                'wf_controller_installed': u'MultipleWorkflowPlugin' in wf_controllers}
296        if not path_info:
297            data.update({'view': 'list', 'name': 'default'})
298        else:
299            data.update({'view': 'detail',
300                         'name': path_info,
301                         'workflowgraph': workflow_graph(self, req, path_info)})
302            if path_info == 'default':
303                data['workflow'] = ["%s = %s\n" % (key, val) for key, val in
304                                    self.config.options('ticket-workflow')]
305            else:
306                data['workflow'] = ["%s = %s\n" % (key, val) for key, val in
307                                    self.config.options('ticket-workflow-%s' %
308                                                        path_info)]
309            add_script(req, 'common/js/resizer.js')
310            add_script_data(req, {
311                'auto_preview_timeout': 2,
312                'form_token': req.form_token,
313                'trac_types': data['trac_types']})
314
315        if self.pre_1_3:
316            return 'multipleworkflowadmin.html', data
317        else:
318            return 'multipleworkflowadmin_jinja.html', data, {}
319
320    # ITemplateProvider methods
321
322    def get_htdocs_dirs(self):
323        return [('multipleworkflow', resource_filename(__name__, 'htdocs'))]
324
325    def get_templates_dirs(self):
326        return [resource_filename(__name__, 'templates')]
Note: See TracBrowser for help on using the repository browser.