| [15463] | 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | |
|---|
| [17411] | 3 | import os |
|---|
| [18249] | 4 | from .admin import get_prj_file_list |
|---|
| [15465] | 5 | from string import Template |
|---|
| 6 | from trac.admin import IAdminPanelProvider |
|---|
| [17411] | 7 | from trac.config import PathOption |
|---|
| [15463] | 8 | from trac.core import Component, implements |
|---|
| 9 | from trac.mimeview.api import IContentConverter, Mimeview |
|---|
| [16616] | 10 | from trac.util.translation import _ |
|---|
| [15506] | 11 | from trac.web.api import IRequestHandler |
|---|
| [18242] | 12 | from trac.web.chrome import add_notice, add_warning, Chrome |
|---|
| [18249] | 13 | from .model import PeerReviewModel, ReviewDataModel |
|---|
| [15465] | 14 | |
|---|
| [15463] | 15 | try: |
|---|
| 16 | from docx import Document |
|---|
| [17414] | 17 | from docx_export import create_docx_for_filelist, create_docx_for_review |
|---|
| [15463] | 18 | docx_support = True |
|---|
| 19 | except ImportError: |
|---|
| 20 | docx_support = False |
|---|
| 21 | |
|---|
| 22 | |
|---|
| 23 | __author__ = 'Cinc' |
|---|
| 24 | __copyright__ = "Copyright 2016" |
|---|
| 25 | __license__ = "BSD" |
|---|
| 26 | |
|---|
| 27 | |
|---|
| [15691] | 28 | def escape_chars(txt): |
|---|
| 29 | repl = {u'ä': u'ae', u'ü': u'ue', u'ö': u'oe', |
|---|
| [15701] | 30 | u'ß': u'ss', u'(': u'', u')': u'', |
|---|
| [15691] | 31 | u'Ä': u'Ae', u'Ü': u'Ue', u'Ö': u'Oe'} |
|---|
| 32 | for key in repl: |
|---|
| 33 | txt = txt.replace(key, repl[key]) |
|---|
| 34 | return txt |
|---|
| 35 | |
|---|
| 36 | |
|---|
| [15463] | 37 | class PeerReviewDocx(Component): |
|---|
| [15464] | 38 | """Export reviews as a document in Word 2010 format (docx). |
|---|
| [15463] | 39 | |
|---|
| [15526] | 40 | [[BR]] |
|---|
| [15464] | 41 | == Overview |
|---|
| 42 | When enabled a download link (''Download in other formats'') for a Word 2010 document is added to |
|---|
| 43 | review pages. |
|---|
| 44 | The document holds all the information from the review page and the file content of each file. |
|---|
| 45 | File comments are printed inline. |
|---|
| 46 | |
|---|
| [17413] | 47 | It is possible to provide default template documents for new environments by providing the path |
|---|
| [15477] | 48 | in ''trac.ini'': |
|---|
| [17413] | 49 | {{{#!ini |
|---|
| 50 | [peerreview] |
|---|
| 51 | review.docx = path/to/report/template.docx |
|---|
| 52 | filelist.docx = path/to/filelist/template.docx |
|---|
| 53 | }}} |
|---|
| [15477] | 54 | The path must be readable by Trac. It will be used only on first start to populate the database and is |
|---|
| [17411] | 55 | meant to make automated deployment easier. |
|---|
| [15477] | 56 | You may use the admin page to change it later on. |
|---|
| [15464] | 57 | |
|---|
| [17411] | 58 | When no path is found in the database when trying to export a report the standard ''templates'' directory of the |
|---|
| 59 | environment is used. |
|---|
| 60 | |
|---|
| [17413] | 61 | == Template document format for reports |
|---|
| [15464] | 62 | Markers are used to signify the position where to add information to the document. |
|---|
| 63 | |
|---|
| 64 | The following is added to predefined tables: |
|---|
| 65 | * Review info |
|---|
| 66 | * Reviewer info |
|---|
| 67 | * File info |
|---|
| 68 | |
|---|
| 69 | File contents with inline comments is added as text. |
|---|
| 70 | |
|---|
| 71 | === Review info table |
|---|
| 72 | The table must have the following format. |
|---|
| 73 | |
|---|
| 74 | ||= Name =|| $REVIEWNAME$ || |
|---|
| 75 | ||= Status =|| $STATUS$ || |
|---|
| 76 | ||= ID =|| $ID$ || |
|---|
| 77 | ||= Project =|| $PROJECT$ || |
|---|
| 78 | ||= Author =|| $AUTHOR$ || |
|---|
| 79 | ||= Date =|| $DATE$ || |
|---|
| [15475] | 80 | ||= Followup from =|| $FOLLOWUP$ || |
|---|
| [15464] | 81 | [[BR]] |
|---|
| [15475] | 82 | Any formatting will be preserved. Note that the order of rows is not important. You may also omit |
|---|
| 83 | rows. |
|---|
| [15464] | 84 | |
|---|
| 85 | === Reviewer info table |
|---|
| 86 | The table must have the following format. |
|---|
| 87 | |
|---|
| 88 | || $REVIEWER$ || || |
|---|
| 89 | |
|---|
| 90 | You may add a header row: |
|---|
| 91 | |
|---|
| 92 | ||= Reviewer =||= Status =|| |
|---|
| 93 | || $REVIEWER$ || || |
|---|
| 94 | [[BR]] |
|---|
| 95 | Formatting for headers will be preserved. Note that a predefined text style is used for the information |
|---|
| 96 | added. |
|---|
| 97 | |
|---|
| 98 | === File info table |
|---|
| 99 | |
|---|
| [15475] | 100 | ||= ID =||= Path =||= Hash =||= Revision =||= Comments =||= Status =|| |
|---|
| 101 | || || $FILEPATH$ || || || || || |
|---|
| [15464] | 102 | [[BR]] |
|---|
| 103 | === File content marker |
|---|
| 104 | File content is added at the position marked by the paragraph ''$FILECONTENT$''. |
|---|
| 105 | |
|---|
| 106 | For each file a heading ''Heading 2'' with the file path is added. |
|---|
| 107 | |
|---|
| 108 | === Defining styles |
|---|
| 109 | The plugin uses different paragraph styles when adding file contents with inline comments. If the styles are not |
|---|
| 110 | yet defined in the template document they will be added using some defaults. You may use your own style definitions |
|---|
| 111 | by defining a style in the document. |
|---|
| 112 | |
|---|
| 113 | The following styles are used: |
|---|
| 114 | * ''Code'': for printing file contents |
|---|
| 115 | * ''Reviewcomment'': for comments printed inline |
|---|
| 116 | * ''Reviewcommentinfo'': information like author, date about an inline comment |
|---|
| 117 | |
|---|
| 118 | == Prerequisite |
|---|
| 119 | The python package ''python-docx'' (https://python-docx.readthedocs.org/en/latest/index.html) must |
|---|
| 120 | be installed. If it isn't available the feature will be silently disabled. |
|---|
| 121 | """ |
|---|
| [15465] | 122 | implements(IAdminPanelProvider, IContentConverter, IRequestHandler,) |
|---|
| [15463] | 123 | |
|---|
| [17411] | 124 | PathOption('peerreview', 'review.docx', doc=u"Path to template document in ''docx'' format used for generating " |
|---|
| [17413] | 125 | u"review documents. '''Note:''' this setting is only used during " |
|---|
| 126 | u"first startup to make automated deployment easier.") |
|---|
| 127 | PathOption('peerreview', 'filelist.docx', doc=u"Path to template document in ''docx'' format used for generating " |
|---|
| 128 | u" file list documents. '''Note:''' this setting is only used during " |
|---|
| 129 | u"first startup to make automated deployment easier.") |
|---|
| 130 | |
|---|
| [15464] | 131 | def __init__(self): |
|---|
| 132 | if not docx_support: |
|---|
| 133 | self.env.log.info("PeerReviewPlugin: python-docx is not installed. Review report creation as docx is not " |
|---|
| 134 | "available.") |
|---|
| [15465] | 135 | else: |
|---|
| 136 | # Create default database entries |
|---|
| [15477] | 137 | defaults = ['reviewreport.title', 'reviewreport.subject', 'reviewreport.template'] |
|---|
| [15465] | 138 | rdm = ReviewDataModel(self.env) |
|---|
| 139 | rdm.clear_props() |
|---|
| 140 | rdm['type'] = "reviewreport.%" |
|---|
| 141 | keys = [item['type'] for item in rdm.list_matching_objects(False)] |
|---|
| 142 | for d in defaults: |
|---|
| 143 | if d not in keys: |
|---|
| [15477] | 144 | if d == 'reviewreport.template': |
|---|
| 145 | # Admins may set this value in trac.ini to specify a default which will be used on first |
|---|
| 146 | # start. |
|---|
| [15536] | 147 | data = self.env.config.get('peerreview', 'review.docx', '') |
|---|
| [15477] | 148 | else: |
|---|
| 149 | data = u"" |
|---|
| [15465] | 150 | rdm = ReviewDataModel(self.env) |
|---|
| 151 | rdm['type'] = d |
|---|
| [15477] | 152 | rdm['data'] = data |
|---|
| [15465] | 153 | rdm.insert() |
|---|
| [15477] | 154 | self.env.log.info("PeerReviewPlugin: added '%s' with value '%s' to 'peerreviewdata' table", |
|---|
| 155 | d, data) |
|---|
| [17413] | 156 | defaults = ['filelist.template'] |
|---|
| 157 | rdm = ReviewDataModel(self.env) |
|---|
| 158 | rdm.clear_props() |
|---|
| 159 | rdm['type'] = "filelist.%" |
|---|
| 160 | keys = [item['type'] for item in rdm.list_matching_objects(False)] |
|---|
| 161 | for d in defaults: |
|---|
| 162 | if d not in keys: |
|---|
| 163 | if d == 'filelist.template': |
|---|
| 164 | # Admins may set this value in trac.ini to specify a default which will be used on first |
|---|
| 165 | # start. |
|---|
| 166 | data = self.env.config.get('peerreview', 'filelist.docx', '') |
|---|
| 167 | else: |
|---|
| 168 | data = u"" |
|---|
| 169 | rdm = ReviewDataModel(self.env) |
|---|
| 170 | rdm['type'] = d |
|---|
| 171 | rdm['data'] = data |
|---|
| 172 | rdm.insert() |
|---|
| 173 | self.env.log.info("PeerReviewPlugin: added '%s' with value '%s' to 'peerreviewdata' table", |
|---|
| 174 | d, data) |
|---|
| [15465] | 175 | |
|---|
| [17413] | 176 | def _get_template_path(self, template, default_tmpl='review_report.docx'): |
|---|
| [17411] | 177 | if not os.path.exists(template): |
|---|
| 178 | self.log.info(u"Report template '%s' does not exist.", template) |
|---|
| [17413] | 179 | template = os.path.join(self.config.getpath('inherit', 'templates_dir', ''), default_tmpl) |
|---|
| [17411] | 180 | template = os.path.abspath(template) |
|---|
| 181 | if not os.path.exists(template): |
|---|
| 182 | self.log.info('No inherited templates directory. Using default templates directory.') |
|---|
| [17413] | 183 | template = os.path.join(self.env.templates_dir, default_tmpl) |
|---|
| [17411] | 184 | if not os.path.exists(template): |
|---|
| 185 | template = 'No template found' |
|---|
| 186 | return template |
|---|
| 187 | |
|---|
| [15465] | 188 | # IAdminPanelProvider methods |
|---|
| 189 | |
|---|
| 190 | def get_admin_panels(self, req): |
|---|
| [15480] | 191 | if docx_support and 'CODE_REVIEW_MGR' in req.perm: |
|---|
| [17413] | 192 | yield ('codereview', 'Code review', 'reporttemplates', 'Report Templates') |
|---|
| [15465] | 193 | |
|---|
| 194 | def render_admin_panel(self, req, cat, page, path_info): |
|---|
| 195 | req.perm.require('CODE_REVIEW_MGR') |
|---|
| 196 | |
|---|
| 197 | report_data = self.get_report_defaults() |
|---|
| [17413] | 198 | filelist_data = self.get_filelist_defaults() |
|---|
| [15465] | 199 | |
|---|
| 200 | if req.method=='POST': |
|---|
| [17413] | 201 | if req.args.get('save', ''): |
|---|
| [15465] | 202 | report_data['reviewreport.title']['data'] = req.args.get('title', u'') |
|---|
| 203 | report_data['reviewreport.title'].save_changes() |
|---|
| 204 | report_data['reviewreport.subject']['data'] = req.args.get('subject', u'') |
|---|
| 205 | report_data['reviewreport.subject'].save_changes() |
|---|
| [15477] | 206 | report_data['reviewreport.template']['data'] = req.args.get('template', u'') |
|---|
| 207 | report_data['reviewreport.template'].save_changes() |
|---|
| [15465] | 208 | add_notice(req, _("Your changes have been saved.")) |
|---|
| [17413] | 209 | elif req.args.get('save_filelist', ''): |
|---|
| 210 | filelist_data['filelist.template']['data'] = req.args.get('template', u'') |
|---|
| 211 | filelist_data['filelist.template'].save_changes() |
|---|
| 212 | add_notice(req, _("Your changes have been saved.")) |
|---|
| [15465] | 213 | req.redirect(req.href.admin(cat, page)) |
|---|
| 214 | |
|---|
| 215 | data = {'title': report_data['reviewreport.title']['data'], |
|---|
| [15477] | 216 | 'subject': report_data['reviewreport.subject']['data'], |
|---|
| [17411] | 217 | 'template': report_data['reviewreport.template']['data'], |
|---|
| 218 | 'template_valid': os.path.exists(report_data['reviewreport.template']['data']), |
|---|
| [17413] | 219 | 'template_default': self._get_template_path(report_data['reviewreport.template']['data'] or ''), |
|---|
| 220 | 'filelist_template': filelist_data['filelist.template']['data'], |
|---|
| 221 | 'filelist_template_valid': os.path.exists(filelist_data['filelist.template']['data']), |
|---|
| 222 | 'filelist_default': self._get_template_path(filelist_data['filelist.template']['data'] or '', |
|---|
| 223 | 'review_filelist.docx') |
|---|
| 224 | } |
|---|
| [18242] | 225 | if hasattr(Chrome, 'jenv'): |
|---|
| 226 | return 'peeradmin_review_report_jinja.html', data |
|---|
| 227 | else: |
|---|
| 228 | return 'admin_review_report.html', data |
|---|
| [15465] | 229 | |
|---|
| 230 | def get_report_defaults(self): |
|---|
| 231 | """ |
|---|
| 232 | @return: dict with default values. Key: one of [reviewreport.title, reviewreport.subject], value: unicode |
|---|
| 233 | """ |
|---|
| 234 | rdm = ReviewDataModel(self.env) |
|---|
| 235 | rdm.clear_props() |
|---|
| 236 | rdm['type'] = "reviewreport%" |
|---|
| 237 | d = {} |
|---|
| 238 | for item in rdm.list_matching_objects(False): |
|---|
| 239 | d[item['type']] = item |
|---|
| 240 | return d |
|---|
| 241 | |
|---|
| [17413] | 242 | def get_filelist_defaults(self): |
|---|
| 243 | """ |
|---|
| 244 | @return: dict with default values. Key: one of [reviewreport.title, reviewreport.subject], value: unicode |
|---|
| 245 | """ |
|---|
| 246 | rdm = ReviewDataModel(self.env) |
|---|
| 247 | rdm.clear_props() |
|---|
| 248 | rdm['type'] = "filelist%" |
|---|
| 249 | d = {} |
|---|
| 250 | for item in rdm.list_matching_objects(False): |
|---|
| 251 | d[item['type']] = item |
|---|
| 252 | return d |
|---|
| 253 | |
|---|
| [15463] | 254 | # IRequestHandler methods |
|---|
| [15465] | 255 | |
|---|
| [15463] | 256 | def match_request(self, req): |
|---|
| 257 | if not docx_support: |
|---|
| 258 | return False |
|---|
| 259 | return req.path_info == '/peerreview' |
|---|
| 260 | |
|---|
| 261 | def process_request(self, req): |
|---|
| 262 | |
|---|
| 263 | format_arg = req.args.get('format') |
|---|
| 264 | review_id = req.args.get('reviewid', None) |
|---|
| [17414] | 265 | filelist = req.args.get('filelist', None) |
|---|
| [15463] | 266 | referrer=req.get_header("Referer") |
|---|
| 267 | if review_id and format_arg == 'docx': |
|---|
| [15691] | 268 | review = PeerReviewModel(self.env, review_id) |
|---|
| 269 | if review: |
|---|
| 270 | def proj_name(): |
|---|
| 271 | return review['project'] + u'_' if review['project'] and review['project'].upper() != 'MC000000' \ |
|---|
| 272 | else u'' |
|---|
| 273 | def review_name(): |
|---|
| 274 | return escape_chars(review['name'].replace(' ', '_')) |
|---|
| [15692] | 275 | doc_name = u"%sSRC-REV_%s_Review_%s_V1.0" % (proj_name(), review_name(), review_id) |
|---|
| [15691] | 276 | else: |
|---|
| 277 | doc_name = u"Review %s" % review_id |
|---|
| 278 | content_info = {'review_id': review_id, |
|---|
| 279 | 'review': review} |
|---|
| [15463] | 280 | Mimeview(self.env).send_converted(req, |
|---|
| 281 | 'text/x-trac-peerreview', |
|---|
| [15691] | 282 | content_info, format_arg, doc_name) |
|---|
| [15463] | 283 | |
|---|
| [17414] | 284 | elif filelist and format_arg == 'docx': |
|---|
| 285 | doc_name = u"Filelist_%s" % filelist |
|---|
| 286 | Mimeview(self.env).send_converted(req, |
|---|
| 287 | 'text/x-trac-reviewfilelist', |
|---|
| 288 | {'filelist': filelist}, format_arg, doc_name) |
|---|
| 289 | |
|---|
| [15463] | 290 | self.env.log.info("PeerReviewPlugin: Export of Review data in format 'docx' failed because of missing " |
|---|
| 291 | "parameters. Review id is '%s'. Format is '%s'.", review_id, format_arg) |
|---|
| 292 | req.redirect(referrer) |
|---|
| 293 | |
|---|
| 294 | # IContentConverter methods |
|---|
| 295 | |
|---|
| 296 | def get_supported_conversions(self): |
|---|
| 297 | if docx_support: |
|---|
| 298 | yield ('docx', 'MS-Word 2010', 'docx', 'text/x-trac-peerreview', |
|---|
| 299 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 4) |
|---|
| [17414] | 300 | yield ('docx', 'MS-Word 2010', 'docx', 'text/x-trac-reviewfilelist', |
|---|
| 301 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 4) |
|---|
| [15463] | 302 | |
|---|
| 303 | def convert_content(self, req, mimetype, content, key): |
|---|
| 304 | """ |
|---|
| [17411] | 305 | @param content: dict holding information about the review, see request processing code for more information |
|---|
| [15463] | 306 | """ |
|---|
| 307 | if mimetype == 'text/x-trac-peerreview': |
|---|
| [15465] | 308 | report_data = self.get_report_defaults() |
|---|
| [17413] | 309 | template = self._get_template_path(report_data['reviewreport.template']['data'] or '') |
|---|
| [17411] | 310 | |
|---|
| [15691] | 311 | review = content['review'] |
|---|
| 312 | # Data for title and subject templates |
|---|
| 313 | tdata = {'reviewid': content['review_id'], |
|---|
| 314 | 'review_name': review['name'], |
|---|
| 315 | 'review_name_escaped': escape_chars(review['name'])} |
|---|
| 316 | |
|---|
| 317 | info = {'review_id': content['review_id'], |
|---|
| 318 | 'review': review, |
|---|
| 319 | 'author': review['owner'], |
|---|
| [15695] | 320 | 'title': Template(report_data['reviewreport.title']['data']).safe_substitute(tdata), |
|---|
| 321 | 'subject': Template(report_data['reviewreport.subject']['data']).safe_substitute(tdata)} |
|---|
| [15465] | 322 | data = create_docx_for_review(self.env, info, template) |
|---|
| [15463] | 323 | return data, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' |
|---|
| [17414] | 324 | if mimetype == 'text/x-trac-reviewfilelist': |
|---|
| 325 | filelist_data = self.get_filelist_defaults() |
|---|
| 326 | template = self._get_template_path(filelist_data['filelist.template']['data'] or '', 'review_filelist.docx') |
|---|
| 327 | files = get_prj_file_list(self, content['filelist']) |
|---|
| 328 | info = {'files': files, |
|---|
| 329 | 'author': req.authname, |
|---|
| 330 | 'titel': 'File List', |
|---|
| 331 | 'subject': content['filelist']} |
|---|
| 332 | data = create_docx_for_filelist(self.env, info, template) |
|---|
| 333 | return data, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' |
|---|
| [15463] | 334 | |
|---|
| 335 | return None # This will cause a Trac error displayed to the user |
|---|