| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | |
|---|
| 3 | __author__ = 'Cinc' |
|---|
| 4 | |
|---|
| 5 | from trac.core import implements, TracError |
|---|
| 6 | from trac.resource import ResourceNotFound |
|---|
| 7 | from trac.ticket.admin import MilestoneAdminPanel |
|---|
| 8 | from trac.ticket.model import Milestone |
|---|
| 9 | from trac.util.datefmt import parse_date |
|---|
| 10 | from trac.util.translation import _ |
|---|
| 11 | from trac.web.api import IRequestFilter |
|---|
| 12 | from trac.web.chrome import add_notice, add_script, add_script_data, add_stylesheet, \ |
|---|
| 13 | ITemplateProvider, ITemplateStreamFilter |
|---|
| 14 | from trac.wiki import WikiSystem, WikiPage |
|---|
| 15 | from genshi.filters import Transformer |
|---|
| 16 | from genshi.template.markup import MarkupTemplate |
|---|
| 17 | from string import Template |
|---|
| 18 | |
|---|
| 19 | |
|---|
| 20 | class MilestoneTemplatePlugin(MilestoneAdminPanel): |
|---|
| 21 | """Use templates when creating milestones. |
|---|
| 22 | |
|---|
| 23 | Any wiki page with a name starting with ''!MilestoneTemplates/'' can be used as a template. This works in a |
|---|
| 24 | similar way as wiki templates work (see PageTemplates). |
|---|
| 25 | |
|---|
| 26 | Examples for milestone templates: |
|---|
| 27 | |
|---|
| 28 | * !MilestoneTemplates/MsTemplate |
|---|
| 29 | * !MilestoneTemplates/MyMilestoneTemplate |
|---|
| 30 | |
|---|
| 31 | Milestone template may use the variable ''$MILESTONE'' for inserting the chosen milestone name into the |
|---|
| 32 | description. This may be useful if the template contains a TracQuery with milestone parameter. |
|---|
| 33 | |
|---|
| 34 | {{{ |
|---|
| 35 | [[TicketQuery(max=5,status=closed,milestone=$MILESTONE,format=table,col=resolution|summary|milestone)]] |
|---|
| 36 | }}} |
|---|
| 37 | """ |
|---|
| 38 | |
|---|
| 39 | implements(ITemplateStreamFilter, IRequestFilter, ITemplateProvider) |
|---|
| 40 | |
|---|
| 41 | MILESTONE_TEMPLATES_PREFIX = u'MilestoneTemplates/' |
|---|
| 42 | |
|---|
| 43 | admin_page_template = u""" |
|---|
| 44 | <div xmlns:py="http://genshi.edgewall.org/" class="field"> |
|---|
| 45 | <label>Template: |
|---|
| 46 | <select id="ms-templates" name="template"> |
|---|
| 47 | <option value="">(blank)</option> |
|---|
| 48 | <option py:for="tmpl in templates" value="$tmpl"> |
|---|
| 49 | ${tmpl} |
|---|
| 50 | </option> |
|---|
| 51 | </select> |
|---|
| 52 | <span class="hint">For Milestone description</span> |
|---|
| 53 | </label> |
|---|
| 54 | </div> |
|---|
| 55 | """ |
|---|
| 56 | edit_page_template = u""" |
|---|
| 57 | <div xmlns:py="http://genshi.edgewall.org/" class="field"> |
|---|
| 58 | <p>Or use a template for the description:</p> |
|---|
| 59 | <label>Template: |
|---|
| 60 | <select id="ms-templates" name="template"> |
|---|
| 61 | <option value="" selected="${sel == None or None}">(Use given Description)</option> |
|---|
| 62 | <option py:for="tmpl in templates" value="$tmpl" selected="${sel == tmpl or None}"> |
|---|
| 63 | ${tmpl} |
|---|
| 64 | </option> |
|---|
| 65 | </select> |
|---|
| 66 | <span class="hint">The description will be replaced with the template contents on submit.</span> |
|---|
| 67 | </label> |
|---|
| 68 | </div> |
|---|
| 69 | """ |
|---|
| 70 | preview_tmpl = u""" |
|---|
| 71 | <div id="mschange" class="ticketdraft" style="display: none"> |
|---|
| 72 | <h3 id="ms-change-h3">Preview:</h3> |
|---|
| 73 | <div class="notes-preview comment searchable"> |
|---|
| 74 | </div> |
|---|
| 75 | </div> |
|---|
| 76 | """ |
|---|
| 77 | def __init__(self): |
|---|
| 78 | # Let our component handle the milestone stuff |
|---|
| 79 | if self.env.is_enabled(MilestoneAdminPanel): |
|---|
| 80 | self.env.disable_component(MilestoneAdminPanel) |
|---|
| 81 | |
|---|
| 82 | def render_admin_panel(self, req, cat, page, version): |
|---|
| 83 | req.perm.require('MILESTONE_VIEW') |
|---|
| 84 | |
|---|
| 85 | templ = req.args.get('template', '') |
|---|
| 86 | if req.method == 'POST' and templ: |
|---|
| 87 | if req.args.get('add') and req.args.get('name'): |
|---|
| 88 | req.perm.require('MILESTONE_CREATE') |
|---|
| 89 | name = req.args.get('name') |
|---|
| 90 | try: |
|---|
| 91 | mil = Milestone(self.env, name=name) |
|---|
| 92 | except ResourceNotFound: |
|---|
| 93 | mil = Milestone(self.env) |
|---|
| 94 | mil.name = name |
|---|
| 95 | mil.description = self.get_description_from_template(req, templ, name) |
|---|
| 96 | if req.args.get('duedate'): |
|---|
| 97 | mil.due = parse_date(req.args.get('duedate'), |
|---|
| 98 | req.tz, 'datetime') |
|---|
| 99 | mil.insert() |
|---|
| 100 | add_notice(req, _(u'The milestone "%(name)s" has been ' |
|---|
| 101 | u'added.', name=name)) |
|---|
| 102 | req.redirect(req.href.admin(cat, page)) |
|---|
| 103 | else: |
|---|
| 104 | if mil.name is None: |
|---|
| 105 | raise TracError(_(u'Invalid milestone name.')) |
|---|
| 106 | raise TracError(_(u'Milestone %(name)s already exists.', |
|---|
| 107 | name=name)) |
|---|
| 108 | return super(MilestoneAdminPanel, self).render_admin_panel(req, cat, page, version) |
|---|
| 109 | |
|---|
| 110 | def get_description_from_template(self, req, template, ms_name): |
|---|
| 111 | """Get template text from wiki and replace $MILESTONE with given milestone name |
|---|
| 112 | |
|---|
| 113 | :param req: Request object |
|---|
| 114 | :param template: template name to be used. This is a wiki page name. |
|---|
| 115 | :param ms_name: milestone name chosen by the user |
|---|
| 116 | |
|---|
| 117 | :return |
|---|
| 118 | """ |
|---|
| 119 | template_page = WikiPage(self.env, self.MILESTONE_TEMPLATES_PREFIX+template) |
|---|
| 120 | if template_page and template_page.exists and \ |
|---|
| 121 | 'WIKI_VIEW' in req.perm(template_page.resource): |
|---|
| 122 | return Template(template_page.text).safe_substitute(MILESTONE=ms_name) |
|---|
| 123 | return u"" |
|---|
| 124 | |
|---|
| 125 | # ITemplateStreamFilter methods |
|---|
| 126 | |
|---|
| 127 | def filter_stream(self, req, method, filename, stream, data): |
|---|
| 128 | path = req.path_info.split('/') |
|---|
| 129 | if filename == 'admin_milestones.html': |
|---|
| 130 | # Milestone creation from admin page |
|---|
| 131 | if data: |
|---|
| 132 | if data.get('view') == 'list': |
|---|
| 133 | templates = self.get_milestone_templates(req) |
|---|
| 134 | if templates: |
|---|
| 135 | filter_ = Transformer('//form[@id="addmilestone"]//div[@class="buttons"]') |
|---|
| 136 | stream = stream | filter_.before(self.create_templ_select_ctrl(templates, self.admin_page_template)) |
|---|
| 137 | elif data.get('view') == 'detail': |
|---|
| 138 | # Add preview div |
|---|
| 139 | tmpl = MarkupTemplate(self.preview_tmpl) |
|---|
| 140 | self._add_preview(req, req.base_path+'/wiki_render') |
|---|
| 141 | filter_ = Transformer('//form[@id="modifymilestone"]//div[@class="buttons"]') |
|---|
| 142 | stream = stream | filter_.before(tmpl.generate()) |
|---|
| 143 | elif req.args.get('action') == 'new' and len(path) > 1 and path[1] == 'milestone': |
|---|
| 144 | # Milestone creation from roadmap page |
|---|
| 145 | templates = self.get_milestone_templates(req) |
|---|
| 146 | self._add_preview(req, 'wiki_render') |
|---|
| 147 | filter_ = Transformer('//form[@id="edit"]//p') |
|---|
| 148 | if templates: |
|---|
| 149 | stream = stream | filter_.after(self.create_templ_select_ctrl(templates, self.edit_page_template)) |
|---|
| 150 | tmpl = MarkupTemplate(self.preview_tmpl) |
|---|
| 151 | stream = stream | filter_.after(tmpl.generate()) |
|---|
| 152 | elif filename == 'milestone_edit.html': |
|---|
| 153 | self._add_preview(req, req.base_path+'/wiki_render') |
|---|
| 154 | filter_ = Transformer('//form[@id="edit"]//p') |
|---|
| 155 | if req.method == "POST": |
|---|
| 156 | # Milestone creation from roadmap page. Duplicate name redirected to edit page. |
|---|
| 157 | templates = self.get_milestone_templates(req) |
|---|
| 158 | if templates: |
|---|
| 159 | stream = stream | filter_.after(self.create_templ_select_ctrl(templates, self.edit_page_template, |
|---|
| 160 | req.args.get('template', None))) |
|---|
| 161 | tmpl = MarkupTemplate(self.preview_tmpl) |
|---|
| 162 | stream = stream | filter_.after(tmpl.generate()) |
|---|
| 163 | |
|---|
| 164 | return stream |
|---|
| 165 | |
|---|
| 166 | def _add_preview(self, req, render_url): |
|---|
| 167 | scr_data = {'auto_preview_timeout': self.env.config.get('trac', 'auto_preview_timeout', '2.0'), |
|---|
| 168 | 'form_token': req.form_token, |
|---|
| 169 | 'ms_preview_renderer': render_url} |
|---|
| 170 | add_script_data(req, scr_data) |
|---|
| 171 | add_script(req, 'common/js/auto_preview.js') |
|---|
| 172 | add_script(req, 'mstemplate/js/ms_preview.js') |
|---|
| 173 | add_stylesheet(req, 'common/css/ticket.css') |
|---|
| 174 | add_stylesheet(req, 'mstemplate/css/ms_preview.css') |
|---|
| 175 | |
|---|
| 176 | def get_milestone_templates(self, req): |
|---|
| 177 | """Get milestone templates from wiki. You need WIKI_VIEW oermission to use templates""" |
|---|
| 178 | prefix = self.MILESTONE_TEMPLATES_PREFIX |
|---|
| 179 | ws = WikiSystem(self.env) |
|---|
| 180 | templates = [template[len(prefix):] |
|---|
| 181 | for template in ws.get_pages(prefix) |
|---|
| 182 | if 'WIKI_VIEW' in req.perm('wiki', template)] |
|---|
| 183 | return templates |
|---|
| 184 | |
|---|
| 185 | def create_templ_select_ctrl(self, templates, tmpl, cur_sel=None): |
|---|
| 186 | """Create a selct control to be added to add milestone page or edit milestone page. |
|---|
| 187 | |
|---|
| 188 | :param templates: list of templates (wikipage names) |
|---|
| 189 | :param tmpl: Genshi template to be used for creating the select control |
|---|
| 190 | :param cur_sel: tempalte selected by the user of None if using description |
|---|
| 191 | |
|---|
| 192 | :return <div> tag holding a select control with label |
|---|
| 193 | """ |
|---|
| 194 | sel = MarkupTemplate(tmpl) |
|---|
| 195 | return sel.generate(templates=templates, sel=cur_sel) |
|---|
| 196 | |
|---|
| 197 | # IRequestFilter methods |
|---|
| 198 | |
|---|
| 199 | # IRequestFilter is used to add the template contents as description when adding from the roadmap page |
|---|
| 200 | def pre_process_request(self, req, handler): |
|---|
| 201 | |
|---|
| 202 | # Fill milestone description with contents of template. |
|---|
| 203 | if req.method == 'POST' and self._is_valid_request(req): |
|---|
| 204 | template = req.args.get('template') |
|---|
| 205 | if req.args.get('add') and template: |
|---|
| 206 | # The name of the milestone is given as a parameter to the template |
|---|
| 207 | req.args[u'description'] = self.get_description_from_template(req, template, req.args.get('name')) |
|---|
| 208 | return handler |
|---|
| 209 | |
|---|
| 210 | @staticmethod |
|---|
| 211 | def _is_valid_request(req): |
|---|
| 212 | """Check request for correct path and valid form token""" |
|---|
| 213 | if req.path_info.startswith('/milestone') and req.args.get('__FORM_TOKEN') == req.form_token: |
|---|
| 214 | return True |
|---|
| 215 | return False |
|---|
| 216 | |
|---|
| 217 | def post_process_request(self, req, template, data, content_type): |
|---|
| 218 | return template, data, content_type |
|---|
| 219 | |
|---|
| 220 | # ITemplateProvider methods |
|---|
| 221 | |
|---|
| 222 | def get_templates_dirs(self): |
|---|
| 223 | return [] |
|---|
| 224 | |
|---|
| 225 | def get_htdocs_dirs(self): |
|---|
| 226 | from pkg_resources import resource_filename |
|---|
| 227 | return [('mstemplate', resource_filename(__name__, 'htdocs'))] |
|---|