| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | |
|---|
| 3 | from string import Template |
|---|
| 4 | from trac.admin import IAdminPanelProvider |
|---|
| 5 | from trac.config import ListOption |
|---|
| 6 | from trac.core import Component, implements |
|---|
| 7 | from trac.mimeview.api import IContentConverter, Mimeview |
|---|
| 8 | from trac.util.translation import _ |
|---|
| 9 | from trac.web.api import IRequestHandler |
|---|
| 10 | from trac.web.chrome import add_notice, add_warning |
|---|
| 11 | from model import PeerReviewModel, ReviewDataModel |
|---|
| 12 | |
|---|
| 13 | try: |
|---|
| 14 | from docx import Document |
|---|
| 15 | from docx_export import create_docx_for_review |
|---|
| 16 | docx_support = True |
|---|
| 17 | except ImportError: |
|---|
| 18 | docx_support = False |
|---|
| 19 | |
|---|
| 20 | |
|---|
| 21 | __author__ = 'Cinc' |
|---|
| 22 | __copyright__ = "Copyright 2016" |
|---|
| 23 | __license__ = "BSD" |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | def escape_chars(txt): |
|---|
| 27 | repl = {u'ä': u'ae', u'ü': u'ue', u'ö': u'oe', |
|---|
| 28 | u'ß': u'ss', u'(': u'', u')': u'', |
|---|
| 29 | u'Ä': u'Ae', u'Ü': u'Ue', u'Ö': u'Oe'} |
|---|
| 30 | for key in repl: |
|---|
| 31 | txt = txt.replace(key, repl[key]) |
|---|
| 32 | return txt |
|---|
| 33 | |
|---|
| 34 | |
|---|
| 35 | class PeerReviewDocx(Component): |
|---|
| 36 | """Export reviews as a document in Word 2010 format (docx). |
|---|
| 37 | |
|---|
| 38 | [[BR]] |
|---|
| 39 | == Overview |
|---|
| 40 | When enabled a download link (''Download in other formats'') for a Word 2010 document is added to |
|---|
| 41 | review pages. |
|---|
| 42 | The document holds all the information from the review page and the file content of each file. |
|---|
| 43 | File comments are printed inline. |
|---|
| 44 | |
|---|
| 45 | It is possible to provide a default template document for new environments by providing the path |
|---|
| 46 | in ''trac.ini'': |
|---|
| 47 | [[TracIni(peerreview, review.docx)]] |
|---|
| 48 | |
|---|
| 49 | The path must be readable by Trac. It will be used only on first start to populate the database and is |
|---|
| 50 | meant to make automated deploying easier. |
|---|
| 51 | You may use the admin page to change it later on. |
|---|
| 52 | |
|---|
| 53 | == Template document format |
|---|
| 54 | Markers are used to signify the position where to add information to the document. |
|---|
| 55 | |
|---|
| 56 | The following is added to predefined tables: |
|---|
| 57 | * Review info |
|---|
| 58 | * Reviewer info |
|---|
| 59 | * File info |
|---|
| 60 | |
|---|
| 61 | File contents with inline comments is added as text. |
|---|
| 62 | |
|---|
| 63 | === Review info table |
|---|
| 64 | The table must have the following format. |
|---|
| 65 | |
|---|
| 66 | ||= Name =|| $REVIEWNAME$ || |
|---|
| 67 | ||= Status =|| $STATUS$ || |
|---|
| 68 | ||= ID =|| $ID$ || |
|---|
| 69 | ||= Project =|| $PROJECT$ || |
|---|
| 70 | ||= Author =|| $AUTHOR$ || |
|---|
| 71 | ||= Date =|| $DATE$ || |
|---|
| 72 | ||= Followup from =|| $FOLLOWUP$ || |
|---|
| 73 | [[BR]] |
|---|
| 74 | Any formatting will be preserved. Note that the order of rows is not important. You may also omit |
|---|
| 75 | rows. |
|---|
| 76 | |
|---|
| 77 | === Reviewer info table |
|---|
| 78 | The table must have the following format. |
|---|
| 79 | |
|---|
| 80 | || $REVIEWER$ || || |
|---|
| 81 | |
|---|
| 82 | You may add a header row: |
|---|
| 83 | |
|---|
| 84 | ||= Reviewer =||= Status =|| |
|---|
| 85 | || $REVIEWER$ || || |
|---|
| 86 | [[BR]] |
|---|
| 87 | Formatting for headers will be preserved. Note that a predefined text style is used for the information |
|---|
| 88 | added. |
|---|
| 89 | |
|---|
| 90 | === File info table |
|---|
| 91 | |
|---|
| 92 | ||= ID =||= Path =||= Hash =||= Revision =||= Comments =||= Status =|| |
|---|
| 93 | || || $FILEPATH$ || || || || || |
|---|
| 94 | [[BR]] |
|---|
| 95 | === File content marker |
|---|
| 96 | File content is added at the position marked by the paragraph ''$FILECONTENT$''. |
|---|
| 97 | |
|---|
| 98 | For each file a heading ''Heading 2'' with the file path is added. |
|---|
| 99 | |
|---|
| 100 | === Defining styles |
|---|
| 101 | The plugin uses different paragraph styles when adding file contents with inline comments. If the styles are not |
|---|
| 102 | yet defined in the template document they will be added using some defaults. You may use your own style definitions |
|---|
| 103 | by defining a style in the document. |
|---|
| 104 | |
|---|
| 105 | The following styles are used: |
|---|
| 106 | * ''Code'': for printing file contents |
|---|
| 107 | * ''Reviewcomment'': for comments printed inline |
|---|
| 108 | * ''Reviewcommentinfo'': information like author, date about an inline comment |
|---|
| 109 | |
|---|
| 110 | == Prerequisite |
|---|
| 111 | The python package ''python-docx'' (https://python-docx.readthedocs.org/en/latest/index.html) must |
|---|
| 112 | be installed. If it isn't available the feature will be silently disabled. |
|---|
| 113 | """ |
|---|
| 114 | implements(IAdminPanelProvider, IContentConverter, IRequestHandler,) |
|---|
| 115 | |
|---|
| 116 | ListOption('peerreview', 'review.docx', doc=u"Path to template document in ''docx'' format used for generating " |
|---|
| 117 | u"review documents.") |
|---|
| 118 | def __init__(self): |
|---|
| 119 | if not docx_support: |
|---|
| 120 | self.env.log.info("PeerReviewPlugin: python-docx is not installed. Review report creation as docx is not " |
|---|
| 121 | "available.") |
|---|
| 122 | else: |
|---|
| 123 | # Create default database entries |
|---|
| 124 | defaults = ['reviewreport.title', 'reviewreport.subject', 'reviewreport.template'] |
|---|
| 125 | rdm = ReviewDataModel(self.env) |
|---|
| 126 | rdm.clear_props() |
|---|
| 127 | rdm['type'] = "reviewreport.%" |
|---|
| 128 | keys = [item['type'] for item in rdm.list_matching_objects(False)] |
|---|
| 129 | for d in defaults: |
|---|
| 130 | if d not in keys: |
|---|
| 131 | if d == 'reviewreport.template': |
|---|
| 132 | # Admins may set this value in trac.ini to specify a default which will be used on first |
|---|
| 133 | # start. |
|---|
| 134 | data = self.env.config.get('peerreview', 'review.docx', '') |
|---|
| 135 | else: |
|---|
| 136 | data = u"" |
|---|
| 137 | rdm = ReviewDataModel(self.env) |
|---|
| 138 | rdm['type'] = d |
|---|
| 139 | rdm['data'] = data |
|---|
| 140 | rdm.insert() |
|---|
| 141 | self.env.log.info("PeerReviewPlugin: added '%s' with value '%s' to 'peerreviewdata' table", |
|---|
| 142 | d, data) |
|---|
| 143 | |
|---|
| 144 | # IAdminPanelProvider methods |
|---|
| 145 | |
|---|
| 146 | def get_admin_panels(self, req): |
|---|
| 147 | if docx_support and 'CODE_REVIEW_MGR' in req.perm: |
|---|
| 148 | yield ('codereview', 'Code review', 'reviewreport', 'Review Report') |
|---|
| 149 | |
|---|
| 150 | def render_admin_panel(self, req, cat, page, path_info): |
|---|
| 151 | req.perm.require('CODE_REVIEW_MGR') |
|---|
| 152 | |
|---|
| 153 | report_data = self.get_report_defaults() |
|---|
| 154 | |
|---|
| 155 | if req.method=='POST': |
|---|
| 156 | save = req.args.get('save', '') |
|---|
| 157 | if save: |
|---|
| 158 | report_data['reviewreport.title']['data'] = req.args.get('title', u'') |
|---|
| 159 | report_data['reviewreport.title'].save_changes() |
|---|
| 160 | report_data['reviewreport.subject']['data'] = req.args.get('subject', u'') |
|---|
| 161 | report_data['reviewreport.subject'].save_changes() |
|---|
| 162 | report_data['reviewreport.template']['data'] = req.args.get('template', u'') |
|---|
| 163 | report_data['reviewreport.template'].save_changes() |
|---|
| 164 | add_notice(req, _("Your changes have been saved.")) |
|---|
| 165 | req.redirect(req.href.admin(cat, page)) |
|---|
| 166 | |
|---|
| 167 | data = {'title': report_data['reviewreport.title']['data'], |
|---|
| 168 | 'subject': report_data['reviewreport.subject']['data'], |
|---|
| 169 | 'template': report_data['reviewreport.template']['data']} |
|---|
| 170 | return 'admin_review_report.html', data |
|---|
| 171 | |
|---|
| 172 | def get_report_defaults(self): |
|---|
| 173 | """ |
|---|
| 174 | @return: dict with default values. Key: one of [reviewreport.title, reviewreport.subject], value: unicode |
|---|
| 175 | """ |
|---|
| 176 | rdm = ReviewDataModel(self.env) |
|---|
| 177 | rdm.clear_props() |
|---|
| 178 | rdm['type'] = "reviewreport%" |
|---|
| 179 | d = {} |
|---|
| 180 | for item in rdm.list_matching_objects(False): |
|---|
| 181 | d[item['type']] = item |
|---|
| 182 | return d |
|---|
| 183 | |
|---|
| 184 | # IRequestHandler methods |
|---|
| 185 | |
|---|
| 186 | def match_request(self, req): |
|---|
| 187 | if not docx_support: |
|---|
| 188 | return False |
|---|
| 189 | return req.path_info == '/peerreview' |
|---|
| 190 | |
|---|
| 191 | def process_request(self, req): |
|---|
| 192 | |
|---|
| 193 | format_arg = req.args.get('format') |
|---|
| 194 | review_id = req.args.get('reviewid', None) |
|---|
| 195 | referrer=req.get_header("Referer") |
|---|
| 196 | if review_id and format_arg == 'docx': |
|---|
| 197 | review = PeerReviewModel(self.env, review_id) |
|---|
| 198 | if review: |
|---|
| 199 | def proj_name(): |
|---|
| 200 | return review['project'] + u'_' if review['project'] and review['project'].upper() != 'MC000000' \ |
|---|
| 201 | else u'' |
|---|
| 202 | def review_name(): |
|---|
| 203 | return escape_chars(review['name'].replace(' ', '_')) |
|---|
| 204 | doc_name = u"%sSRC-REV_%s_Review_%s_V1.0" % (proj_name(), review_name(), review_id) |
|---|
| 205 | else: |
|---|
| 206 | doc_name = u"Review %s" % review_id |
|---|
| 207 | content_info = {'review_id': review_id, |
|---|
| 208 | 'review': review} |
|---|
| 209 | Mimeview(self.env).send_converted(req, |
|---|
| 210 | 'text/x-trac-peerreview', |
|---|
| 211 | content_info, format_arg, doc_name) |
|---|
| 212 | |
|---|
| 213 | self.env.log.info("PeerReviewPlugin: Export of Review data in format 'docx' failed because of missing " |
|---|
| 214 | "parameters. Review id is '%s'. Format is '%s'.", review_id, format_arg) |
|---|
| 215 | req.redirect(referrer) |
|---|
| 216 | |
|---|
| 217 | # IContentConverter methods |
|---|
| 218 | |
|---|
| 219 | def get_supported_conversions(self): |
|---|
| 220 | if docx_support: |
|---|
| 221 | yield ('docx', 'MS-Word 2010', 'docx', 'text/x-trac-peerreview', |
|---|
| 222 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 4) |
|---|
| 223 | |
|---|
| 224 | def convert_content(self, req, mimetype, content, key): |
|---|
| 225 | """ |
|---|
| 226 | @param content: This is the review id |
|---|
| 227 | """ |
|---|
| 228 | if mimetype == 'text/x-trac-peerreview': |
|---|
| 229 | report_data = self.get_report_defaults() |
|---|
| 230 | template = report_data['reviewreport.template']['data'] |
|---|
| 231 | review = content['review'] |
|---|
| 232 | # Data for title and subject templates |
|---|
| 233 | tdata = {'reviewid': content['review_id'], |
|---|
| 234 | 'review_name': review['name'], |
|---|
| 235 | 'review_name_escaped': escape_chars(review['name'])} |
|---|
| 236 | |
|---|
| 237 | info = {'review_id': content['review_id'], |
|---|
| 238 | 'review': review, |
|---|
| 239 | 'author': review['owner'], |
|---|
| 240 | 'title': Template(report_data['reviewreport.title']['data']).safe_substitute(tdata), |
|---|
| 241 | 'subject': Template(report_data['reviewreport.subject']['data']).safe_substitute(tdata)} |
|---|
| 242 | data = create_docx_for_review(self.env, info, template) |
|---|
| 243 | return data, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' |
|---|
| 244 | |
|---|
| 245 | return None # This will cause a Trac error displayed to the user |
|---|