source: milestonetemplateplugin/trunk/milestonetemplate/web_ui.py

Last change on this file was 17778, checked in by Cinc-th, 3 years ago
  • Don't use filter_stream() from ITemplateStreamFilter anymore.
  • Disable preview for milestone description for Trac>1.2 because Trac 1.4 comes with a preview implementation

Plugin is now compatible with Trac >=1.2 without using Genshi.

Closes #13833

File size: 13.1 KB
RevLine 
[15153]1# -*- coding: utf-8 -*-
[17777]2# Copyright (c) 2016-2020 Cinc
3# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions
7# are met:
8# 1. Redistributions of source code must retain the above copyright
9#    notice, this list of conditions and the following disclaimer.
10# 2. Redistributions in binary form must reproduce the above copyright
11#    notice, this list of conditions and the following disclaimer in the
12#    documentation and/or other materials provided with the distribution.
13# 3. The name of the author may not be used to endorse or promote products
14#    derived from this software without specific prior written permission.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[15153]26
27__author__ = 'Cinc'
28
29from trac.core import implements, TracError
30from trac.resource import ResourceNotFound
31from trac.ticket.admin import MilestoneAdminPanel
32from trac.ticket.model import Milestone
33from trac.util.datefmt import parse_date
[16172]34from trac.util.translation import _
[15153]35from trac.web.api import IRequestFilter
[17778]36from trac.web.chrome import Chrome, ITemplateProvider, \
[16623]37    add_script, add_script_data, add_notice, add_stylesheet
[15153]38from trac.wiki import WikiSystem, WikiPage
39from string import Template
40
[17778]41class JTransformer(object):
42    """Class modelled after the Genshi Transformer class. Instead of an xpath it uses a
43       selector usable by jQuery.
44       You may use cssify (https://github.com/santiycr/cssify) to convert a xpath to a selector."""
[15438]45
[17778]46    def __init__(self, xpath):
47        self.css = xpath  # xpath must be a css selector for jQuery
48
49    def after(self, html):
50        return {'pos': 'after', 'css': self.css, 'html': html}
51
52    def before(self, html):
53        return {'pos': 'before', 'css': self.css, 'html': html}
54
55
[15153]56class MilestoneTemplatePlugin(MilestoneAdminPanel):
57    """Use templates when creating milestones.
58
[15438]59    Any wiki page with a name starting with ''!MilestoneTemplates/'' can be used as a template. This works in a
[15153]60    similar way as wiki templates work (see PageTemplates).
61
62    Examples for milestone templates:
63
[15438]64    * !MilestoneTemplates/MsTemplate
65    * !MilestoneTemplates/MyMilestoneTemplate
[15153]66
67    Milestone template may use the variable ''$MILESTONE'' for inserting the chosen milestone name into the
68    description. This may be useful if the template contains a TracQuery with milestone parameter.
69
70    {{{
71    [[TicketQuery(max=5,status=closed,milestone=$MILESTONE,format=table,col=resolution|summary|milestone)]]
72    }}}
73    """
74
[17778]75    implements(IRequestFilter, ITemplateProvider)
[15153]76
77    MILESTONE_TEMPLATES_PREFIX = u'MilestoneTemplates/'
78
[17777]79    admin_page_template = u"""<div class="field">
[15153]80            <label>Template:
81            <select id="ms-templates" name="template">
82                <option value="">(blank)</option>
[17777]83                {options}
[15153]84            </select>
85            <span class="hint">For Milestone description</span>
86            </label>
[17777]87        </div>"""
88    admin_option_tmpl = u"""<option value="{templ}">
89                    {templ}
90                </option>"""
91
92    milestone_page_template = u"""<div class="field">
[15153]93            <p>Or use a template for the description:</p>
94            <label>Template:
95            <select id="ms-templates" name="template">
[17777]96                <option value=""{sel}>(Use given Description)</option>
97                {options}
[15153]98            </select>
99            <span class="hint">The description will be replaced with the template contents on submit.</span>
100            </label>
[17777]101        </div>"""
102    milestone_option_tmpl = u"""<option value="{templ}"{sel}>
103                    {templ}
104                </option>"""
105
[15438]106    preview_tmpl = u"""
107            <div id="mschange" class="ticketdraft" style="display: none">
108                <h3 id="ms-change-h3">Preview:</h3>
109                    <div class="notes-preview comment searchable">
110                    </div>
111            </div>
112    """
113    def __init__(self):
114        # Let our component handle the milestone stuff
115        if self.env.is_enabled(MilestoneAdminPanel):
116            self.env.disable_component(MilestoneAdminPanel)
[15153]117
[17778]118        self.is_trac_1_2 = self.env.trac_version[:3] == '1.2'
119
[15153]120    def render_admin_panel(self, req, cat, page, version):
121        req.perm.require('MILESTONE_VIEW')
122
123        templ = req.args.get('template', '')
124        if req.method == 'POST' and templ:
125            if req.args.get('add') and req.args.get('name'):
126                req.perm.require('MILESTONE_CREATE')
127                name = req.args.get('name')
128                try:
129                    mil = Milestone(self.env, name=name)
130                except ResourceNotFound:
131                    mil = Milestone(self.env)
132                    mil.name = name
133                    mil.description = self.get_description_from_template(req, templ, name)
134                    if req.args.get('duedate'):
135                        mil.due = parse_date(req.args.get('duedate'),
136                                             req.tz, 'datetime')
137                    mil.insert()
138                    add_notice(req, _(u'The milestone "%(name)s" has been '
139                                      u'added.', name=name))
140                    req.redirect(req.href.admin(cat, page))
141                else:
142                    if mil.name is None:
143                        raise TracError(_(u'Invalid milestone name.'))
144                    raise TracError(_(u'Milestone %(name)s already exists.',
145                                      name=name))
146        return super(MilestoneAdminPanel, self).render_admin_panel(req, cat, page, version)
147
148    def get_description_from_template(self, req, template, ms_name):
149        """Get template text from wiki and replace $MILESTONE with given milestone name
150
151        :param req: Request object
152        :param template: template name to be used. This is a wiki page name.
153        :param ms_name: milestone name chosen by the user
154
155        :return
156        """
157        template_page = WikiPage(self.env, self.MILESTONE_TEMPLATES_PREFIX+template)
158        if template_page and template_page.exists and \
[16623]159                'WIKI_VIEW' in req.perm(template_page.resource):
[15153]160            return Template(template_page.text).safe_substitute(MILESTONE=ms_name)
161        return u""
162
[16623]163    def _add_preview(self, req):
164        Chrome(self.env).add_auto_preview(req)
165        scr_data = {'ms_preview_renderer': req.href.wiki_render()}
[15438]166        add_script_data(req, scr_data)
167        add_script(req, 'mstemplate/js/ms_preview.js')
168        add_stylesheet(req, 'common/css/ticket.css')
169        add_stylesheet(req, 'mstemplate/css/ms_preview.css')
170
[15153]171    def get_milestone_templates(self, req):
172        """Get milestone templates from wiki. You need WIKI_VIEW oermission to use templates"""
173        prefix = self.MILESTONE_TEMPLATES_PREFIX
174        ws = WikiSystem(self.env)
175        templates = [template[len(prefix):]
176                    for template in ws.get_pages(prefix)
177                    if 'WIKI_VIEW' in req.perm('wiki', template)]
178        return templates
179
[17777]180    def create_admin_page_select_ctrl(self, templates):
181        """Create a select control to be added to 'Add milestone' page in the admin area.
[15153]182
[17777]183        :param templates: list of templates (wiki page names)
184        :return <div> tag holding a select control with label (unicode)
185        """
186        tmpl = self.admin_page_template
187        opt = ''
188        for item in templates:
189            opt += self.admin_option_tmpl.format(templ=item)
190        return tmpl.format(options=opt)
[15153]191
[17777]192    def create_milestone_page_select_ctrl(self, templates, cur_sel=None):
193        """Create a select control to be added to 'Add milestone' page or 'Edit milestone' page. USed when creating
194           milestones from the Roadmap page.
195
196        :param templates: list of templates (wiki page names)
197        :param cur_sel: template selected by the user or None if using description field. Note that this is always
198                        None for admin page
199        :return <div> tag holding a select control with label (unicode)
[15153]200        """
[17777]201        tmpl = self.milestone_page_template
202        opt = ''
203        for item in templates:
204            opt += self.milestone_option_tmpl.format(templ=item, sel=' selected="selected"' if cur_sel == item else '')
205        return tmpl.format(sel=' selected="selected"' if not cur_sel else '', options=opt)
[15153]206
207    # IRequestFilter methods
208
209    def pre_process_request(self, req, handler):
[16623]210        """Add the template contents as description when adding from the
211        roadmap page.
212        """
[15153]213        # Fill milestone description with contents of template.
214        if req.method == 'POST' and self._is_valid_request(req):
215            template = req.args.get('template')
216            if req.args.get('add') and template:
217                # The name of the milestone is given as a parameter to the template
218                req.args[u'description'] = self.get_description_from_template(req, template, req.args.get('name'))
219        return handler
220
221    @staticmethod
222    def _is_valid_request(req):
223        """Check request for correct path and valid form token"""
[16623]224        if req.path_info.startswith('/milestone') and \
225                req.args.get('__FORM_TOKEN') == req.form_token:
[15153]226            return True
227        return False
228
229    def post_process_request(self, req, template, data, content_type):
[17778]230        filter_list = []
231        if template and data:
232            self.filter_html(req, template, data, filter_list)
233            if filter_list:
234                add_script_data(req, {'ms_filter': filter_list})
235                add_script(req, 'mstemplate/js/ms_add_select.js')
236
[15153]237        return template, data, content_type
[15438]238
[17778]239    def filter_html(self, req, filename, data, filter_lst):
240        """Create filter information for Javascript. The filters may specify some html to be appended or prepended
241           to DOM elements using jQuery $().before() and $().after() statements. A separate script file takes
242           the filter data and executes jQuery code."""
243
244        path = req.path_info.split('/')
245        if filename == 'admin_milestones.html':
246            # Milestone creation from admin page
247            view = data.get('view')
248            if view == 'list':
249                templates = self.get_milestone_templates(req)
250                if templates:
251                    # xpath: //form[@id="addmilestone"]//div[@class="buttons"]
252                    xform = JTransformer('form#addmilestone div.buttons')
253                    filter_lst.append(xform.before(self.create_admin_page_select_ctrl(templates)))
254            elif view == 'detail':
255                if self.is_trac_1_2:
256                    # Add preview div
257                    self._add_preview(req)
258                    # xpath: //form[@id="edit"]//textarea
259                    xform = JTransformer('form#edit textarea')
260                    filter_lst.append(xform.after(self.preview_tmpl))
261        elif len(path) > 1 and path[1] == 'milestone':
262            templates = self.get_milestone_templates(req)
263            action = req.args.get('action')
264
265            if templates and action == 'new':
266                # Milestone creation from roadmap page
267                # xpath: //form[@id="edit"]//div[contains(@class, "description")]
268                xform = JTransformer('form#edit div[class*=description]')
269                filter_lst.append(xform.after(self.create_milestone_page_select_ctrl(templates)))
270                if self.is_trac_1_2:
271                    # Preview
272                    self._add_preview(req)
273                    filter_lst.append(xform.after(self.preview_tmpl))
274
275            elif templates and action == 'edit':
276                self._add_preview(req)
277                # xpath: //form[@id="edit"]//textarea
278                xform = JTransformer('form#edit textarea')
279                if req.method == "POST":
280                    # Milestone creation from roadmap page. Duplicate name redirected to edit page.
281                    filter_lst.append(xform.after(self.create_milestone_page_select_ctrl(templates,
282                                                                                         req.args.get('template', None)
283                                                                                         )))
284                if self.is_trac_1_2:
285                    # Preview area
286                    self._add_preview(req)
287                    filter_lst.append(xform.after(self.preview_tmpl))
288        return filter_lst
289
[15438]290    # ITemplateProvider methods
291
292    def get_templates_dirs(self):
293        return []
294
295    def get_htdocs_dirs(self):
296        from pkg_resources import resource_filename
[16172]297        return [('mstemplate', resource_filename(__name__, 'htdocs'))]
Note: See TracBrowser for help on using the repository browser.