source: multipleworkflowplugin/branches/multipleworkflow-1.3/multipleworkflow/web_ui.py

Last change on this file was 16593, checked in by Ryan J Ollos, 6 years ago

MultipleWorkflowPlugin 1.3.3: Fix regression in r15137

get_workflow_config_default was removed without
replacing all uses.

Tidy up codebase and require Trac >= 1.0.

Refs #12570, Fixes #12665.

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