source: peerreviewplugin/trunk/codereview/peerReviewMain.py

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

PeerReviewPlugin: use /peerreviewfile/xx instead of /peerreviewperform?IDFile=xx as url for review files.

File size: 14.0 KB
RevLine 
[13497]1#
2# Copyright (C) 2005-2006 Team5
[18053]3# Copyright (C) 2016-2021 Cinc
[15230]4#
[13497]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#
[717]10# Author: Team5
11#
12# Provides functionality for main page
[18246]13# Works with peerreview_main.html
[717]14
[13497]15import itertools
[15172]16from trac.core import Component, implements
[717]17from trac.perm import IPermissionRequestor
[17403]18from trac.resource import get_resource_url, IResourceManager, resource_exists, Resource, ResourceNotFound
[18248]19from trac.util import as_int
20from trac.util.datefmt import format_date, to_datetime, user_time
[16616]21from trac.util.html import Markup, html as tag
22from trac.util.translation import _
[18278]23from trac.versioncontrol.api import RepositoryManager
[18242]24from trac.web.chrome import add_ctxtnav, add_stylesheet, Chrome,\
25    INavigationContributor, ITemplateProvider, web_context
[13497]26from trac.web.main import IRequestHandler
[17403]27from trac.wiki.api import IWikiSyntaxProvider
[15382]28from trac.wiki.formatter import format_to
[18249]29from .model import ReviewCommentModel, ReviewDataModel, ReviewFileModel, PeerReviewModel, PeerReviewerModel
30from .util import review_is_finished
[13497]31
[717]32
[15172]33def add_ctxt_nav_items(req):
[18261]34    add_ctxtnav(req, _("My Code Reviews"), req.href.peerreviewmain(), title=_("My Code Reviews"))
35    add_ctxtnav(req, _("Create a Code Review"), req.href.peerreviewnew(), title=_("Create a Code review"))
[17430]36    add_ctxtnav(req, _("Report"), req.href.peerreviewreport(), title=_("Show Codereview Reports"))
[18261]37    add_ctxtnav(req, _("Search Code Reviews"), req.href.peerreviewsearch(), _("Search Code Reviews"))
[15172]38
39
[15304]40class PeerReviewMain(Component):
[17403]41    """Main component for code reviews providing basic features. Show overview page for code reviews.
[15230]42
[17403]43    [[BR]]
44    === Permissions
45    There are three additional permissions for code reviews:
46    * CODE_REVIEW_VIEW
47    * CODE_REVIEW_DEV
48    * CODE_REVIEW_MGR
49
50    === Wiki syntax
51    Two new trac links are available with this plugin:
52    * {{{review:<xxx>}}} with <xxx> being a review id
53    * {{{rfile:<xxx>}}} with <xxx> being a file id
54
55    These links open the review page or file page.
56    """
57    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
58               IResourceManager, ITemplateProvider, IWikiSyntaxProvider)
59
[717]60    # INavigationContributor methods
[15230]61
[717]62    def get_active_navigation_item(self, req):
[18261]63        return 'peerreviewmain'
[15230]64
[717]65    def get_navigation_items(self, req):
[15164]66        if 'CODE_REVIEW_DEV' in req.perm:
[18261]67            yield ('mainnav', 'peerreviewmain',
68                   Markup('<a href="%s">Codereview</a>') % req.href.peerreviewmain())
[717]69
70    # IPermissionRequestor methods
[15230]71
[717]72    def get_permission_actions(self):
[15487]73        return [
[15498]74            ('CODE_REVIEW_VIEW',['PEERREVIEWFILE_VIEW', 'PEERREVIEW_VIEW']),  # Allow viewing of realm so reports show
75            ('CODE_REVIEW_DEV', ['CODE_REVIEW_VIEW']),                        # results.
[15487]76            ('CODE_REVIEW_MGR', ['CODE_REVIEW_DEV', 'PEERREVIEWFILE_VIEW'])
77        ]
[717]78
[13497]79    # IRequestHandler methods
[15230]80
[15382]81    def send_preview(self, req):
82        # Taken from WikiRender component in wiki/web_api.py
83        # Allow all POST requests (with a valid __FORM_TOKEN, ensuring that
84        # the client has at least some permission). Additionally, allow GET
85        # requests from TRAC_ADMIN for testing purposes.
86        if req.method != 'POST':
87            req.perm.require('TRAC_ADMIN')
88        realm = req.args.get('realm', 'wiki')
89        id = req.args.get('id')
90        version = as_int(req.args.get('version'), None)
91        text = req.args.get('text', '')
92        flavor = req.args.get('flavor')
93        options = {}
94        if 'escape_newlines' in req.args:
[17441]95            options['escape_newlines'] = bool(int(req.args['escape_newlines'] or 0))
[15382]96        if 'shorten' in req.args:
97            options['shorten'] = bool(int(req.args['shorten'] or 0))
98        resource = Resource(realm, id=id, version=version)
99        context = web_context(req, resource)
100        rendered = format_to(self.env, flavor, context, text, **options)
101        req.send(rendered.encode('utf-8'))
102
103
[13497]104    def match_request(self, req):
[18261]105        if req.path_info == '/peerreviewmain' or req.path_info == "/preview_render":
106            return True
107        elif req.path_info == '/peerReviewMain':
108            self.env.log.info("Legacy URL 'peerReviewMain' called from: %s", req.get_header('Referer'))
109            return True
110        return False
[13497]111
[717]112    def process_request(self, req):
[15168]113        req.perm.require('CODE_REVIEW_DEV')
[3544]114
[15382]115        if req.path_info == "/preview_render":
116            self.send_preview(req)
117
[3544]118        data = {}
[717]119        # test whether this user is a manager or not
[15160]120        if 'CODE_REVIEW_MGR' in req.perm:
[15168]121            data['manager'] = True
[717]122        else:
[15168]123            data['manager'] = False
[717]124
[15527]125        # User requests an update
[15316]126        data['allassigned'] = req.args.get('allassigned')
127        data['allcreated'] = req.args.get('allcreated')
128
[15296]129        r_tmpl = PeerReviewModel(self.env)
130        r_tmpl.clear_props()
[15316]131        if data['allcreated']:
132            all_reviews = list(r_tmpl.list_matching_objects())
133        else:
134            all_reviews = [rev for rev in r_tmpl.list_matching_objects() if rev['status'] != "closed"]
[15296]135
[15448]136        # We need this for displaying information about comments
[18263]137        comments = ReviewCommentModel.comment_ids_by_file_id(self.env)
[15448]138        my_comment_data = ReviewDataModel.comments_for_owner(self.env, req.authname)
139
[15385]140        # Add files
141        files = ReviewFileModel.file_dict_by_review(self.env)
142
[717]143        # fill the table of currently open reviews
[15166]144        myreviews = []
[15168]145        assigned_to_me =[]
146        manager_reviews = []
[15296]147
[15166]148        for rev in all_reviews:
[15168]149            # Reviews created by me
[15296]150            if rev['owner'] == req.authname:
[18248]151                rev.date = user_time(req, format_date, to_datetime(rev['created']))
[17266]152                if rev['closed']:
[18248]153                    rev.finish_date = user_time(req, format_date, to_datetime(rev['closed']))
[17266]154                else:
155                    rev.finish_date = ''
[18278]156                rm = RepositoryManager(self.env)
157                for file in files[rev['review_id']]:
158                    repos = rm.get_repository(file['repo'])
159                    file['short_rev'] = repos.display_rev(file['revision'])
[15385]160                rev.rev_files = files[rev['review_id']]
[15448]161                # Prepare number of comments for a review
162                rev.num_comments = 0
163                for f in rev.rev_files:
164                    if f['file_id'] in comments:
165                        rev.num_comments += len(comments[f['file_id']])
166                rev.num_notread = rev.num_comments - len([c_id for c_id, r, t, dat in my_comment_data if t == 'read'
167                                                          and r == rev['review_id']])
[15166]168                myreviews.append(rev)
[15168]169
[15304]170        r_tmpl = PeerReviewerModel(self.env)
171        r_tmpl.clear_props()
172        r_tmpl['reviewer'] = req.authname
173
[15316]174        if data['allassigned']:
175            # Don't filter list here
176            reviewer = list(r_tmpl.list_matching_objects())
177        else:
178            reviewer = [rev for rev in r_tmpl.list_matching_objects() if rev['status'] != "reviewed"]
[15315]179
[15168]180        # All reviews assigned to me
[15296]181        for item in reviewer:
182            rev = PeerReviewModel(self.env, item['review_id'])
[15328]183            if not review_is_finished(self.env.config, rev):
[15448]184                rev.reviewer = item
[18248]185                rev.date = user_time(req, format_date, to_datetime(rev['created']))
[17266]186                if rev['closed']:
[18248]187                    rev.finish_date = user_time(req, format_date, to_datetime(rev['closed']))
[17266]188                else:
189                    rev.finish_date = ''
[15385]190                rev.rev_files = files[rev['review_id']]
[15448]191                # Prepare number of comments for a review
192                rev.num_comments = 0
193                for f in rev.rev_files:
194                    if f['file_id'] in comments:
195                        rev.num_comments += len(comments[f['file_id']])
196                rev.num_notread = rev.num_comments - len([c_id for c_id, r, t, dat in my_comment_data if t == 'read'
197                                                          and r == rev['review_id']])
[15168]198                assigned_to_me.append(rev)
[717]199
[15166]200        data['myreviews'] = myreviews
[15168]201        data['manager_reviews'] = manager_reviews
202        data['assigned_reviews'] = assigned_to_me
203        data['cycle'] = itertools.cycle
[15166]204
[18254]205        add_stylesheet(req, 'common/css/browser.css')
[15316]206        add_stylesheet(req, 'hw/css/peerreview.css')
[15172]207        add_ctxt_nav_items(req)
[15316]208
[18242]209        if hasattr(Chrome, 'jenv'):
210            return 'peerreview_main_jinja.html', data
211        else:
[18246]212            return 'peerreview_main.html', data, None
[3544]213
[15487]214    # IResourceManager methods
215
216    def get_resource_url(self, resource, href, **kwargs):
217        """Return the canonical URL for displaying the given resource.
218
219        :param resource: a `Resource`
220        :param href: an `Href` used for creating the URL
221
222        Note that if there's no special rule associated to this realm for
223        creating URLs (i.e. the standard convention of using realm/id applies),
224        then it's OK to not define this method.
225        """
[15498]226        if resource.realm == 'peerreviewfile':
[18282]227            return href('peerreviewfile', resource.id)
[15498]228        elif resource.realm == 'peerreview':
[17429]229            return href.peerreviewview(resource.id)
[15487]230
[18261]231        return href('peerreviewmain')
[15498]232
[15487]233    def get_resource_realms(self):
[15498]234        yield 'peerreview'
[15487]235        yield 'peerreviewfile'
236
237    def get_resource_description(self, resource, format=None, context=None,
238                                 **kwargs):
[15498]239        if resource.realm == 'peerreview':
240            if format == 'compact':
241                return 'review:%s' % resource.id  # Will be used as id in reports when 'realm' is used
242            else:
243                return 'Review %s' % resource.id
244        elif resource.realm == 'peerreviewfile':
245            if format == 'compact':
246                return 'rfile:%s' % resource.id
247            else:
248                return 'ReviewFile %s' % resource.id
249        return ""
[15487]250
251    def resource_exists(self, resource):
[17439]252        with self.env.db_query as db:
253            cursor = db.cursor()
254            if resource.realm == 'peerreview':
255                cursor.execute("SELECT * FROM peerreview WHERE review_id = %s", (resource.id,))
256                if cursor.fetchone():
257                    return True
258                else:
259                    return False
260            elif resource.realm == 'peerreviewfile':
261                # Only files associated with a review are real peerreviewfiles
262                cursor.execute("SELECT * FROM peerreviewfile WHERE file_id = %s AND review_id != 0", (resource.id,))
263                if cursor.fetchone():
264                    return True
265                else:
266                    return False
267
[15498]268        raise ResourceNotFound('Resource %s not found.' % resource.realm)
269
[15170]270    # ITemplateProvider methods
[15230]271
[15170]272    def get_templates_dirs(self):
[15230]273        """Return the path of the directory containing the provided templates."""
[15170]274        from pkg_resources import resource_filename
275        return [resource_filename(__name__, 'templates')]
276
277    def get_htdocs_dirs(self):
278        from pkg_resources import resource_filename
[17403]279        return [('hw', resource_filename(__name__, 'htdocs'))]
280
281    # IWikiSyntaxProvider
282
283    def get_link_resolvers(self):
284        return [('review', self._format_review_link),
285                ('rfile', self._format_file_link)]
286
287    def get_wiki_syntax(self):
288        return []
289
[18275]290    status_map = {'approved': tag.span(u" \u2713", class_='approved'),
291                  'disapproved': tag.span(u" \u2717", class_='disapproved')}
292
[17403]293    def _format_review_link(self, formatter, ns, target, label):
294        res = Resource('peerreview', target)
295        if resource_exists(self.env, res):
296            review = PeerReviewModel(self.env, target)
[18260]297            if review['status'] == 'closed':
298                cls = 'peer-wiki closed'
[17403]299            else:
[18260]300                cls = 'peer-wiki'
[18275]301
[18260]302            try:
[18275]303                span = self.status_map[review['status']]
[18260]304            except KeyError:
305                span = ''
[17403]306
[18260]307            return tag.a([label, span],
[17403]308                         href=get_resource_url(self.env, res, formatter.href),
309                         title=_(u"Review #%s (%s)") % (target, review['status']),
310                         class_=cls
311                        )
312
313        return tag.span(label + '?',
314                        title=_(u"Review #%s doesn't exist") % target,
315                        class_='missing')
316
317    def _format_file_link(self, formatter, ns, target, label):
318        def rfile_is_finished(config, rfile):
319            """A finished review may only be reopened by a manager or admisnistrator
320
321            :param config: Trac config object
322            :param rfile: review file object
323
324            :return True if review is in one of the terminal states
325            """
326            finish_states = config.getlist("peerreview", "terminal_review_states")
327            return rfile['status'] in finish_states
328
329        res = Resource('peerreviewfile', target)
330        if resource_exists(self.env, res):
331            rfile = ReviewFileModel(self.env, target)
[18275]332            if rfile['status'] == 'closed':
333                cls = 'peer-wiki closed'
[17403]334            else:
[18275]335                cls = 'peer-wiki'
[17403]336
[18275]337            try:
338                span = self.status_map[rfile['status']]
339            except KeyError:
340                span = ''
341
342            return tag.a([label, span],
[17403]343                         href=get_resource_url(self.env, res, formatter.href),
344                         title=_(u"File #%s (%s)") % (target, rfile['status']),
345                         class_=cls
346                         )
347
348        return tag.span(label + '?',
349                        title=_(u"File #%s doesn't exist") % target,
350                        class_='missing')
Note: See TracBrowser for help on using the repository browser.