source: peerreviewplugin/trunk/codereview/peerreview_docx.py

Last change on this file was 18249, checked in by Cinc-th, 2 years ago

PeerReviewPlugin: fix imports and create cmp() function for Python 3.

Refs #14005

File size: 15.1 KB
Line 
1# -*- coding: utf-8 -*-
2
3import os
4from .admin import get_prj_file_list
5from string import Template
6from trac.admin import IAdminPanelProvider
7from trac.config import PathOption
8from trac.core import Component, implements
9from trac.mimeview.api import IContentConverter, Mimeview
10from trac.util.translation import _
11from trac.web.api import IRequestHandler
12from trac.web.chrome import add_notice, add_warning, Chrome
13from .model import PeerReviewModel, ReviewDataModel
14
15try:
16    from docx import Document
17    from docx_export import create_docx_for_filelist, create_docx_for_review
18    docx_support = True
19except ImportError:
20    docx_support = False
21
22
23__author__ = 'Cinc'
24__copyright__ = "Copyright 2016"
25__license__ = "BSD"
26
27
28def escape_chars(txt):
29    repl = {u'ä': u'ae', u'ü': u'ue', u'ö': u'oe',
30            u'ß': u'ss', u'(': u'', u')': u'',
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
37class PeerReviewDocx(Component):
38    """Export reviews as a document in Word 2010 format (docx).
39
40    [[BR]]
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
47    It is possible to provide default template documents for new environments by providing the path
48    in ''trac.ini'':
49    {{{#!ini
50    [peerreview]
51    review.docx = path/to/report/template.docx
52    filelist.docx = path/to/filelist/template.docx
53    }}}
54    The path must be readable by Trac. It will be used only on first start to populate the database and is
55    meant to make automated deployment easier.
56    You may use the admin page to change it later on.
57
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
61    == Template document format for reports
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$ ||
80    ||= Followup from =|| $FOLLOWUP$ ||
81    [[BR]]
82    Any formatting will be preserved. Note that the order of rows is not important. You may also omit
83    rows.
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
100    ||= ID =||= Path =||= Hash =||= Revision =||= Comments =||= Status =||
101    ||  || $FILEPATH$ ||  ||  ||  ||  ||
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    """
122    implements(IAdminPanelProvider, IContentConverter, IRequestHandler,)
123
124    PathOption('peerreview', 'review.docx', doc=u"Path to template document in ''docx'' format used for generating "
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
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.")
135        else:
136            # Create default database entries
137            defaults = ['reviewreport.title', 'reviewreport.subject', 'reviewreport.template']
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:
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.
147                        data = self.env.config.get('peerreview', 'review.docx', '')
148                    else:
149                        data = u""
150                    rdm = ReviewDataModel(self.env)
151                    rdm['type'] = d
152                    rdm['data'] = data
153                    rdm.insert()
154                    self.env.log.info("PeerReviewPlugin: added '%s' with value '%s' to 'peerreviewdata' table",
155                                      d, data)
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)
175
176    def _get_template_path(self, template, default_tmpl='review_report.docx'):
177        if not os.path.exists(template):
178            self.log.info(u"Report template '%s' does not exist.", template)
179            template = os.path.join(self.config.getpath('inherit', 'templates_dir', ''), default_tmpl)
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.')
183            template = os.path.join(self.env.templates_dir, default_tmpl)
184            if not os.path.exists(template):
185                template = 'No template found'
186        return template
187
188    # IAdminPanelProvider methods
189
190    def get_admin_panels(self, req):
191        if docx_support and 'CODE_REVIEW_MGR' in req.perm:
192            yield ('codereview', 'Code review', 'reporttemplates', 'Report Templates')
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()
198        filelist_data = self.get_filelist_defaults()
199
200        if req.method=='POST':
201            if req.args.get('save', ''):
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()
206                report_data['reviewreport.template']['data'] = req.args.get('template', u'')
207                report_data['reviewreport.template'].save_changes()
208                add_notice(req, _("Your changes have been saved."))
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."))
213            req.redirect(req.href.admin(cat, page))
214
215        data = {'title': report_data['reviewreport.title']['data'],
216                'subject': report_data['reviewreport.subject']['data'],
217                'template': report_data['reviewreport.template']['data'],
218                'template_valid': os.path.exists(report_data['reviewreport.template']['data']),
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                }
225        if hasattr(Chrome, 'jenv'):
226            return 'peeradmin_review_report_jinja.html', data
227        else:
228            return 'admin_review_report.html', data
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
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
254    # IRequestHandler methods
255
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)
265        filelist = req.args.get('filelist', None)
266        referrer=req.get_header("Referer")
267        if review_id and format_arg == 'docx':
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(' ', '_'))
275                doc_name = u"%sSRC-REV_%s_Review_%s_V1.0" % (proj_name(), review_name(), review_id)
276            else:
277                doc_name = u"Review %s" % review_id
278            content_info = {'review_id': review_id,
279                            'review': review}
280            Mimeview(self.env).send_converted(req,
281                                              'text/x-trac-peerreview',
282                                              content_info, format_arg, doc_name)
283
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
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)
300            yield ('docx', 'MS-Word 2010', 'docx', 'text/x-trac-reviewfilelist',
301                   'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 4)
302
303    def convert_content(self, req, mimetype, content, key):
304        """
305        @param content: dict holding information about the review, see request processing code for more information
306        """
307        if mimetype == 'text/x-trac-peerreview':
308            report_data = self.get_report_defaults()
309            template = self._get_template_path(report_data['reviewreport.template']['data'] or '')
310
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'],
320                    'title': Template(report_data['reviewreport.title']['data']).safe_substitute(tdata),
321                    'subject': Template(report_data['reviewreport.subject']['data']).safe_substitute(tdata)}
322            data = create_docx_for_review(self.env, info, template)
323            return data, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
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'
334
335        return None # This will cause a Trac error displayed to the user
Note: See TracBrowser for help on using the repository browser.