| 1 | # |
|---|
| 2 | # Copyright (C) 2005-2006 Team5 |
|---|
| 3 | # Copyright (C) 2016 Cinc-th |
|---|
| 4 | # |
|---|
| 5 | # All rights reserved. |
|---|
| 6 | # |
|---|
| 7 | # This software is licensed as described in the file COPYING.txt, which |
|---|
| 8 | # you should have received as part of this distribution. |
|---|
| 9 | # |
|---|
| 10 | # Author: Team5 |
|---|
| 11 | # |
|---|
| 12 | # Provides functionality for main page |
|---|
| 13 | # Works with peerReviewMain.html |
|---|
| 14 | |
|---|
| 15 | import itertools |
|---|
| 16 | from trac.core import Component, implements |
|---|
| 17 | from trac.perm import IPermissionRequestor |
|---|
| 18 | from trac.resource import IResourceManager, Resource, ResourceNotFound |
|---|
| 19 | from trac.util import as_int, format_date |
|---|
| 20 | from trac.util.html import Markup, html as tag |
|---|
| 21 | from trac.util.translation import _ |
|---|
| 22 | from trac.web.chrome import INavigationContributor, ITemplateProvider, add_stylesheet, add_ctxtnav |
|---|
| 23 | from trac.web.main import IRequestHandler |
|---|
| 24 | from trac.wiki.formatter import format_to |
|---|
| 25 | from model import ReviewCommentModel, ReviewDataModel, ReviewFileModel, PeerReviewModel, PeerReviewerModel |
|---|
| 26 | from util import review_is_finished |
|---|
| 27 | |
|---|
| 28 | |
|---|
| 29 | def web_context_compat(req, resource=None, id=False, version=False, parent=False, |
|---|
| 30 | absurls=False): |
|---|
| 31 | """Create a rendering context from a request. |
|---|
| 32 | |
|---|
| 33 | The `perm` and `href` properties of the context will be initialized |
|---|
| 34 | from the corresponding properties of the request object. |
|---|
| 35 | |
|---|
| 36 | >>> from trac.test import Mock, MockPerm |
|---|
| 37 | >>> req = Mock(href=Mock(), perm=MockPerm()) |
|---|
| 38 | >>> context = web_context(req) |
|---|
| 39 | >>> context.href is req.href |
|---|
| 40 | True |
|---|
| 41 | >>> context.perm is req.perm |
|---|
| 42 | True |
|---|
| 43 | |
|---|
| 44 | :param req: the HTTP request object |
|---|
| 45 | :param resource: the `Resource` object or realm |
|---|
| 46 | :param id: the resource identifier |
|---|
| 47 | :param version: the resource version |
|---|
| 48 | :param absurls: whether URLs generated by the ``href`` object should |
|---|
| 49 | be absolute (including the protocol scheme and host |
|---|
| 50 | name) |
|---|
| 51 | :return: a new rendering context |
|---|
| 52 | :rtype: `RenderingContext` |
|---|
| 53 | |
|---|
| 54 | :since: version 1.0 |
|---|
| 55 | """ |
|---|
| 56 | from trac.mimeview.api import Context |
|---|
| 57 | if req: |
|---|
| 58 | href = req.abs_href if absurls else req.href |
|---|
| 59 | perm = req.perm |
|---|
| 60 | else: |
|---|
| 61 | href = None |
|---|
| 62 | perm = None |
|---|
| 63 | self = Context(Resource(resource, id=id, version=version, |
|---|
| 64 | parent=parent), href=href, perm=perm) |
|---|
| 65 | self.req = req |
|---|
| 66 | return self |
|---|
| 67 | |
|---|
| 68 | try: |
|---|
| 69 | from trac.web.chrome import web_context |
|---|
| 70 | except ImportError: |
|---|
| 71 | web_context = web_context_compat |
|---|
| 72 | |
|---|
| 73 | def add_ctxt_nav_items(req): |
|---|
| 74 | add_ctxtnav(req, _("My Code Reviews"), "peerReviewMain", title=_("My Code Reviews")) |
|---|
| 75 | add_ctxtnav(req, _("Create a Code Review"), "peerReviewNew", title=_("Create a Code review")) |
|---|
| 76 | add_ctxtnav(req, _("Report"), "peerreviewreport", title=_("Show Codereview Reports")) |
|---|
| 77 | add_ctxtnav(req, _("Search Code Reviews"), "peerReviewSearch", _("Search Code Reviews")) |
|---|
| 78 | |
|---|
| 79 | |
|---|
| 80 | class PeerReviewMain(Component): |
|---|
| 81 | """Show overview page for code reviews.""" |
|---|
| 82 | implements(INavigationContributor, IRequestHandler, ITemplateProvider, |
|---|
| 83 | IPermissionRequestor, IResourceManager) |
|---|
| 84 | |
|---|
| 85 | # INavigationContributor methods |
|---|
| 86 | |
|---|
| 87 | def get_active_navigation_item(self, req): |
|---|
| 88 | return 'peerReviewMain' |
|---|
| 89 | |
|---|
| 90 | def get_navigation_items(self, req): |
|---|
| 91 | if 'CODE_REVIEW_DEV' in req.perm: |
|---|
| 92 | yield ('mainnav', 'peerReviewMain', |
|---|
| 93 | Markup('<a href="%s">Peer Review</a>') % req.href.peerReviewMain()) |
|---|
| 94 | |
|---|
| 95 | # IPermissionRequestor methods |
|---|
| 96 | |
|---|
| 97 | def get_permission_actions(self): |
|---|
| 98 | return [ |
|---|
| 99 | ('CODE_REVIEW_VIEW',['PEERREVIEWFILE_VIEW', 'PEERREVIEW_VIEW']), # Allow viewing of realm so reports show |
|---|
| 100 | ('CODE_REVIEW_DEV', ['CODE_REVIEW_VIEW']), # results. |
|---|
| 101 | ('CODE_REVIEW_MGR', ['CODE_REVIEW_DEV', 'PEERREVIEWFILE_VIEW']) |
|---|
| 102 | ] |
|---|
| 103 | |
|---|
| 104 | # IRequestHandler methods |
|---|
| 105 | |
|---|
| 106 | def send_preview(self, req): |
|---|
| 107 | # Taken from WikiRender component in wiki/web_api.py |
|---|
| 108 | # Allow all POST requests (with a valid __FORM_TOKEN, ensuring that |
|---|
| 109 | # the client has at least some permission). Additionally, allow GET |
|---|
| 110 | # requests from TRAC_ADMIN for testing purposes. |
|---|
| 111 | if req.method != 'POST': |
|---|
| 112 | req.perm.require('TRAC_ADMIN') |
|---|
| 113 | realm = req.args.get('realm', 'wiki') |
|---|
| 114 | id = req.args.get('id') |
|---|
| 115 | version = as_int(req.args.get('version'), None) |
|---|
| 116 | text = req.args.get('text', '') |
|---|
| 117 | flavor = req.args.get('flavor') |
|---|
| 118 | options = {} |
|---|
| 119 | if 'escape_newlines' in req.args: |
|---|
| 120 | options['escape_newlines'] = bool(int(req.args['escape_newlines'] |
|---|
| 121 | or 0)) |
|---|
| 122 | if 'shorten' in req.args: |
|---|
| 123 | options['shorten'] = bool(int(req.args['shorten'] or 0)) |
|---|
| 124 | resource = Resource(realm, id=id, version=version) |
|---|
| 125 | context = web_context(req, resource) |
|---|
| 126 | rendered = format_to(self.env, flavor, context, text, **options) |
|---|
| 127 | req.send(rendered.encode('utf-8')) |
|---|
| 128 | |
|---|
| 129 | |
|---|
| 130 | def match_request(self, req): |
|---|
| 131 | return req.path_info == '/peerReviewMain' or req.path_info == "/preview_render" |
|---|
| 132 | |
|---|
| 133 | def process_request(self, req): |
|---|
| 134 | req.perm.require('CODE_REVIEW_DEV') |
|---|
| 135 | |
|---|
| 136 | if req.path_info == "/preview_render": |
|---|
| 137 | self.send_preview(req) |
|---|
| 138 | |
|---|
| 139 | data = {} |
|---|
| 140 | # test whether this user is a manager or not |
|---|
| 141 | if 'CODE_REVIEW_MGR' in req.perm: |
|---|
| 142 | data['manager'] = True |
|---|
| 143 | else: |
|---|
| 144 | data['manager'] = False |
|---|
| 145 | |
|---|
| 146 | # User requests an update |
|---|
| 147 | data['allassigned'] = req.args.get('allassigned') |
|---|
| 148 | data['allcreated'] = req.args.get('allcreated') |
|---|
| 149 | |
|---|
| 150 | r_tmpl = PeerReviewModel(self.env) |
|---|
| 151 | r_tmpl.clear_props() |
|---|
| 152 | if data['allcreated']: |
|---|
| 153 | all_reviews = list(r_tmpl.list_matching_objects()) |
|---|
| 154 | else: |
|---|
| 155 | all_reviews = [rev for rev in r_tmpl.list_matching_objects() if rev['status'] != "closed"] |
|---|
| 156 | |
|---|
| 157 | # We need this for displaying information about comments |
|---|
| 158 | comments = ReviewCommentModel.comments_by_file_id(self.env) |
|---|
| 159 | my_comment_data = ReviewDataModel.comments_for_owner(self.env, req.authname) |
|---|
| 160 | |
|---|
| 161 | # Add files |
|---|
| 162 | files = ReviewFileModel.file_dict_by_review(self.env) |
|---|
| 163 | |
|---|
| 164 | # fill the table of currently open reviews |
|---|
| 165 | myreviews = [] |
|---|
| 166 | assigned_to_me =[] |
|---|
| 167 | manager_reviews = [] |
|---|
| 168 | |
|---|
| 169 | for rev in all_reviews: |
|---|
| 170 | # Reviews created by me |
|---|
| 171 | if rev['owner'] == req.authname: |
|---|
| 172 | rev.date = format_date(rev['created']) |
|---|
| 173 | if rev['closed']: |
|---|
| 174 | rev.finish_date = format_date(rev['closed']) |
|---|
| 175 | else: |
|---|
| 176 | rev.finish_date = '' |
|---|
| 177 | rev.rev_files = files[rev['review_id']] |
|---|
| 178 | # Prepare number of comments for a review |
|---|
| 179 | rev.num_comments = 0 |
|---|
| 180 | for f in rev.rev_files: |
|---|
| 181 | if f['file_id'] in comments: |
|---|
| 182 | rev.num_comments += len(comments[f['file_id']]) |
|---|
| 183 | rev.num_notread = rev.num_comments - len([c_id for c_id, r, t, dat in my_comment_data if t == 'read' |
|---|
| 184 | and r == rev['review_id']]) |
|---|
| 185 | myreviews.append(rev) |
|---|
| 186 | |
|---|
| 187 | r_tmpl = PeerReviewerModel(self.env) |
|---|
| 188 | r_tmpl.clear_props() |
|---|
| 189 | r_tmpl['reviewer'] = req.authname |
|---|
| 190 | |
|---|
| 191 | if data['allassigned']: |
|---|
| 192 | # Don't filter list here |
|---|
| 193 | reviewer = list(r_tmpl.list_matching_objects()) |
|---|
| 194 | else: |
|---|
| 195 | reviewer = [rev for rev in r_tmpl.list_matching_objects() if rev['status'] != "reviewed"] |
|---|
| 196 | |
|---|
| 197 | # All reviews assigned to me |
|---|
| 198 | for item in reviewer: |
|---|
| 199 | rev = PeerReviewModel(self.env, item['review_id']) |
|---|
| 200 | if not review_is_finished(self.env.config, rev): |
|---|
| 201 | rev.reviewer = item |
|---|
| 202 | rev.date = format_date(rev['created']) |
|---|
| 203 | if rev['closed']: |
|---|
| 204 | rev.finish_date = format_date(rev['closed']) |
|---|
| 205 | else: |
|---|
| 206 | rev.finish_date = '' |
|---|
| 207 | rev.rev_files = files[rev['review_id']] |
|---|
| 208 | # Prepare number of comments for a review |
|---|
| 209 | rev.num_comments = 0 |
|---|
| 210 | for f in rev.rev_files: |
|---|
| 211 | if f['file_id'] in comments: |
|---|
| 212 | rev.num_comments += len(comments[f['file_id']]) |
|---|
| 213 | rev.num_notread = rev.num_comments - len([c_id for c_id, r, t, dat in my_comment_data if t == 'read' |
|---|
| 214 | and r == rev['review_id']]) |
|---|
| 215 | assigned_to_me.append(rev) |
|---|
| 216 | |
|---|
| 217 | data['myreviews'] = myreviews |
|---|
| 218 | data['manager_reviews'] = manager_reviews |
|---|
| 219 | data['assigned_reviews'] = assigned_to_me |
|---|
| 220 | data['cycle'] = itertools.cycle |
|---|
| 221 | |
|---|
| 222 | add_stylesheet(req, 'hw/css/peerreview.css') |
|---|
| 223 | add_ctxt_nav_items(req) |
|---|
| 224 | |
|---|
| 225 | return 'peerReviewMain.html', data, None |
|---|
| 226 | |
|---|
| 227 | # IResourceManager methods |
|---|
| 228 | |
|---|
| 229 | def get_resource_url(self, resource, href, **kwargs): |
|---|
| 230 | """Return the canonical URL for displaying the given resource. |
|---|
| 231 | |
|---|
| 232 | :param resource: a `Resource` |
|---|
| 233 | :param href: an `Href` used for creating the URL |
|---|
| 234 | |
|---|
| 235 | Note that if there's no special rule associated to this realm for |
|---|
| 236 | creating URLs (i.e. the standard convention of using realm/id applies), |
|---|
| 237 | then it's OK to not define this method. |
|---|
| 238 | """ |
|---|
| 239 | if resource.realm == 'peerreviewfile': |
|---|
| 240 | return href('peerReviewPerform', IDFile=resource.id) |
|---|
| 241 | elif resource.realm == 'peerreview': |
|---|
| 242 | return href('peerReviewView', Review=resource.id) |
|---|
| 243 | |
|---|
| 244 | return href('peerReviewMain') |
|---|
| 245 | |
|---|
| 246 | def get_resource_realms(self): |
|---|
| 247 | yield 'peerreview' |
|---|
| 248 | yield 'peerreviewfile' |
|---|
| 249 | |
|---|
| 250 | def get_resource_description(self, resource, format=None, context=None, |
|---|
| 251 | **kwargs): |
|---|
| 252 | desc = unicode(resource.id) |
|---|
| 253 | if resource.realm == 'peerreview': |
|---|
| 254 | if format == 'compact': |
|---|
| 255 | return 'review:%s' % resource.id # Will be used as id in reports when 'realm' is used |
|---|
| 256 | else: |
|---|
| 257 | return 'Review %s' % resource.id |
|---|
| 258 | elif resource.realm == 'peerreviewfile': |
|---|
| 259 | if format == 'compact': |
|---|
| 260 | return 'rfile:%s' % resource.id |
|---|
| 261 | else: |
|---|
| 262 | return 'ReviewFile %s' % resource.id |
|---|
| 263 | return "" |
|---|
| 264 | |
|---|
| 265 | |
|---|
| 266 | def resource_exists(self, resource): |
|---|
| 267 | db = self.env.get_read_db() |
|---|
| 268 | cursor = db.cursor() |
|---|
| 269 | if resource.realm == 'peerreview': |
|---|
| 270 | cursor.execute("SELECT * FROM peerreview WHERE review_id = %s", (resource.id,)) |
|---|
| 271 | if cursor.fetchone(): |
|---|
| 272 | return True |
|---|
| 273 | else: |
|---|
| 274 | return False |
|---|
| 275 | elif resource.realm == 'peerreviewfile': |
|---|
| 276 | cursor.execute("SELECT * FROM peerreviewfile WHERE file_id = %s", (resource.id,)) |
|---|
| 277 | if cursor.fetchone(): |
|---|
| 278 | return True |
|---|
| 279 | else: |
|---|
| 280 | return False |
|---|
| 281 | |
|---|
| 282 | raise ResourceNotFound('Resource %s not found.' % resource.realm) |
|---|
| 283 | |
|---|
| 284 | # ITemplateProvider methods |
|---|
| 285 | |
|---|
| 286 | def get_templates_dirs(self): |
|---|
| 287 | """Return the path of the directory containing the provided templates.""" |
|---|
| 288 | from pkg_resources import resource_filename |
|---|
| 289 | return [resource_filename(__name__, 'templates')] |
|---|
| 290 | |
|---|
| 291 | def get_htdocs_dirs(self): |
|---|
| 292 | from pkg_resources import resource_filename |
|---|
| 293 | return [('hw', resource_filename(__name__, 'htdocs'))] |
|---|