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
Line 
1#
2# Copyright (C) 2005-2006 Team5
3# Copyright (C) 2016-2021 Cinc
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 peerreview_main.html
14
15import itertools
16from trac.core import Component, implements
17from trac.perm import IPermissionRequestor
18from trac.resource import get_resource_url, IResourceManager, resource_exists, Resource, ResourceNotFound
19from trac.util import as_int
20from trac.util.datefmt import format_date, to_datetime, user_time
21from trac.util.html import Markup, html as tag
22from trac.util.translation import _
23from trac.versioncontrol.api import RepositoryManager
24from trac.web.chrome import add_ctxtnav, add_stylesheet, Chrome,\
25    INavigationContributor, ITemplateProvider, web_context
26from trac.web.main import IRequestHandler
27from trac.wiki.api import IWikiSyntaxProvider
28from trac.wiki.formatter import format_to
29from .model import ReviewCommentModel, ReviewDataModel, ReviewFileModel, PeerReviewModel, PeerReviewerModel
30from .util import review_is_finished
31
32
33def add_ctxt_nav_items(req):
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"))
36    add_ctxtnav(req, _("Report"), req.href.peerreviewreport(), title=_("Show Codereview Reports"))
37    add_ctxtnav(req, _("Search Code Reviews"), req.href.peerreviewsearch(), _("Search Code Reviews"))
38
39
40class PeerReviewMain(Component):
41    """Main component for code reviews providing basic features. Show overview page for code reviews.
42
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
60    # INavigationContributor methods
61
62    def get_active_navigation_item(self, req):
63        return 'peerreviewmain'
64
65    def get_navigation_items(self, req):
66        if 'CODE_REVIEW_DEV' in req.perm:
67            yield ('mainnav', 'peerreviewmain',
68                   Markup('<a href="%s">Codereview</a>') % req.href.peerreviewmain())
69
70    # IPermissionRequestor methods
71
72    def get_permission_actions(self):
73        return [
74            ('CODE_REVIEW_VIEW',['PEERREVIEWFILE_VIEW', 'PEERREVIEW_VIEW']),  # Allow viewing of realm so reports show
75            ('CODE_REVIEW_DEV', ['CODE_REVIEW_VIEW']),                        # results.
76            ('CODE_REVIEW_MGR', ['CODE_REVIEW_DEV', 'PEERREVIEWFILE_VIEW'])
77        ]
78
79    # IRequestHandler methods
80
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:
95            options['escape_newlines'] = bool(int(req.args['escape_newlines'] or 0))
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
104    def match_request(self, req):
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
111
112    def process_request(self, req):
113        req.perm.require('CODE_REVIEW_DEV')
114
115        if req.path_info == "/preview_render":
116            self.send_preview(req)
117
118        data = {}
119        # test whether this user is a manager or not
120        if 'CODE_REVIEW_MGR' in req.perm:
121            data['manager'] = True
122        else:
123            data['manager'] = False
124
125        # User requests an update
126        data['allassigned'] = req.args.get('allassigned')
127        data['allcreated'] = req.args.get('allcreated')
128
129        r_tmpl = PeerReviewModel(self.env)
130        r_tmpl.clear_props()
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"]
135
136        # We need this for displaying information about comments
137        comments = ReviewCommentModel.comment_ids_by_file_id(self.env)
138        my_comment_data = ReviewDataModel.comments_for_owner(self.env, req.authname)
139
140        # Add files
141        files = ReviewFileModel.file_dict_by_review(self.env)
142
143        # fill the table of currently open reviews
144        myreviews = []
145        assigned_to_me =[]
146        manager_reviews = []
147
148        for rev in all_reviews:
149            # Reviews created by me
150            if rev['owner'] == req.authname:
151                rev.date = user_time(req, format_date, to_datetime(rev['created']))
152                if rev['closed']:
153                    rev.finish_date = user_time(req, format_date, to_datetime(rev['closed']))
154                else:
155                    rev.finish_date = ''
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'])
160                rev.rev_files = files[rev['review_id']]
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']])
168                myreviews.append(rev)
169
170        r_tmpl = PeerReviewerModel(self.env)
171        r_tmpl.clear_props()
172        r_tmpl['reviewer'] = req.authname
173
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"]
179
180        # All reviews assigned to me
181        for item in reviewer:
182            rev = PeerReviewModel(self.env, item['review_id'])
183            if not review_is_finished(self.env.config, rev):
184                rev.reviewer = item
185                rev.date = user_time(req, format_date, to_datetime(rev['created']))
186                if rev['closed']:
187                    rev.finish_date = user_time(req, format_date, to_datetime(rev['closed']))
188                else:
189                    rev.finish_date = ''
190                rev.rev_files = files[rev['review_id']]
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']])
198                assigned_to_me.append(rev)
199
200        data['myreviews'] = myreviews
201        data['manager_reviews'] = manager_reviews
202        data['assigned_reviews'] = assigned_to_me
203        data['cycle'] = itertools.cycle
204
205        add_stylesheet(req, 'common/css/browser.css')
206        add_stylesheet(req, 'hw/css/peerreview.css')
207        add_ctxt_nav_items(req)
208
209        if hasattr(Chrome, 'jenv'):
210            return 'peerreview_main_jinja.html', data
211        else:
212            return 'peerreview_main.html', data, None
213
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        """
226        if resource.realm == 'peerreviewfile':
227            return href('peerreviewfile', resource.id)
228        elif resource.realm == 'peerreview':
229            return href.peerreviewview(resource.id)
230
231        return href('peerreviewmain')
232
233    def get_resource_realms(self):
234        yield 'peerreview'
235        yield 'peerreviewfile'
236
237    def get_resource_description(self, resource, format=None, context=None,
238                                 **kwargs):
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 ""
250
251    def resource_exists(self, resource):
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
268        raise ResourceNotFound('Resource %s not found.' % resource.realm)
269
270    # ITemplateProvider methods
271
272    def get_templates_dirs(self):
273        """Return the path of the directory containing the provided templates."""
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
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
290    status_map = {'approved': tag.span(u" \u2713", class_='approved'),
291                  'disapproved': tag.span(u" \u2717", class_='disapproved')}
292
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)
297            if review['status'] == 'closed':
298                cls = 'peer-wiki closed'
299            else:
300                cls = 'peer-wiki'
301
302            try:
303                span = self.status_map[review['status']]
304            except KeyError:
305                span = ''
306
307            return tag.a([label, span],
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)
332            if rfile['status'] == 'closed':
333                cls = 'peer-wiki closed'
334            else:
335                cls = 'peer-wiki'
336
337            try:
338                span = self.status_map[rfile['status']]
339            except KeyError:
340                span = ''
341
342            return tag.a([label, span],
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.