source: peerreviewplugin/trunk/codereview/admin.py

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

PeerReviewPlugin: use display_rev() to render short revisions in more places. Esp. useful when using git repositories.

File size: 15.7 KB
RevLine 
[15161]1# -*- coding: utf-8 -*-
[15493]2#
3# Copyright (C) 2016 Cinc
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING.txt, which
7# you should have received as part of this distribution.
8#
9# Author: Cinc
10#
[15161]11
[17412]12from collections import namedtuple
[15493]13from trac.admin import IAdminPanelProvider
[17277]14from trac.config import ConfigSection
[15493]15from trac.core import Component, implements
[17414]16from trac.mimeview.api import Mimeview
[16616]17from trac.util.translation import _
[18276]18from trac.versioncontrol.api import RepositoryManager
[18242]19from trac.web.chrome import add_link, add_notice, add_script, add_script_data, add_stylesheet, add_warning, Chrome
[15493]20from .model import ReviewDataModel, ReviewFileModel
21from .repo import insert_project_files, repo_path_exists
22
[15161]23__author__ = 'Cinc'
[15493]24__license__ = "BSD"
25
[17414]26
27def get_prj_file_list(self, prj_name):
28    with self.env.db_query as db:
29        FileData = namedtuple('FileData', ['file_id', 'path', 'repo', 'hash', 'rev', 'changerev'])
30        files = [[FileData(*item), ''] for item in db("""SELECT f.file_id, f.path,
31                                               f.repo, f.hash, f.revision, f.changerevision
32                                               FROM peerreviewfile f
33                                               WHERE f.project = %s ORDER BY f.path
34                                               """, (prj_name,))]
35        approved_hashes = [item[0] for item in db("SELECT a.hash FROM peerreviewfile AS a WHERE status = 'approved'")]
36        for item in files:
37            if item[0].hash in approved_hashes:
38                item[1] = 'Approved'
39        return files
40
41
[15493]42class PeerReviewFileAdmin(Component):
[15494]43    """Admin panel to specify files belonging to a project.
44
[15526]45    [[BR]]
[15494]46    You may define a project identifier and a root folder from the repository holding all the files
47    of a project using this admin panel. When saving the information all the files in the folder hierarchy are hashed
48    and file name, revision, hash and project name are inserted in the database.
49
50    Using the file information it is possible to create reports (see TracReports for more information) like which
51    files may need a review and more.
52    """
[15493]53    implements(IAdminPanelProvider)
54
[17277]55    external_map_section = ConfigSection('peerreview:externals',
56        """It is possible to create a list of project files to check against files approved during a code review.
57        A root directory may be selected which contains the source files. While traversing the tree {{{svn:esternal}}}
58        information is taken into account and all linked directories are also traversed.
59       
60        It is possible to have virtual repositories in Trac pointing to different parts of a bigger Subversion
61        repository. To make sure externals are properly mapped path information may be provided in the section
62        {{{[peerreview:externals]}}} of the TracIni. Note that Subversion 1.5 server root relative URLs supported
63        (like {{{/foo/bar/src}}}) but not other relative URLs.
64       
65        Example:
66        {{{
67        #!ini
68        [peerreview:externals]
69        1 = /svn/repos1/src_branch1/foo           /src_branch1/foo  Repo-Src1                 
70        2 = /svn/repos1/src_branch2/bar           /src_branch2/bar  Repo-Src2
71        3 = /svn/repos1/src_branch3/baz           /src_branch3/baz  Repo-Src3
72        4 = http://server/svn/repos1/src_branch3  /src_branch3  Repo-Src3
73        }}}
74       
75        With the above, the
76        `/svn/repos1/src_branch1/foo` external will
77        be mapped to `/src_branch1/foo` in the repository {{{Repo-Src1}}}.
78       
79        You only have to provide the common path prefix here. The remainder of the external path will automatically
80        appended thus {{{/svn/repos1/src_branch1/foo/dir1/dir2/dir3}}} becomes {{{/src_branch1/foo/dir1/dir2/dir3}}}.
81        """)
82
[15493]83    # IAdminPanelProvider methods
84
[17277]85    def __init__(self):
86        self._externals_map = {}
87
[15493]88    def get_admin_panels(self, req):
89        if 'CODE_REVIEW_DEV' in req.perm:
90            yield ('codereview', 'Code review', 'projectfiles', 'Project Files')
91
92    def render_admin_panel(self, req, cat, page, path_info):
[15590]93
[15493]94        def remove_project_info(rem_name):
95            # Remove project name info
96            rev_data = ReviewDataModel(self.env)
97            rev_data.clear_props()
98            rev_data['data'] = rem_name
99            rev_data['data_key'] = 'name'
100            for item in rev_data.list_matching_objects():
101                item.delete()
[15590]102            # Remove info about project like rootfolder, extensions, revision, repo
[15493]103            rev_data = ReviewDataModel(self.env)
104            rev_data.clear_props()
105            rev_data['data_key'] = rem_name
106            for item in rev_data.list_matching_objects():
107                item.delete()
108            ReviewFileModel.delete_files_by_project_name(self.env, rem_name)
109
[15590]110        def add_project_info():
111            def _insert_project_info(type_, key_, val):
112                rev_data = ReviewDataModel(self.env)
113                rev_data['type'] = type_
114                rev_data['data_key'] = key_
115                rev_data['data'] = val
116                rev_data.insert()
117            _insert_project_info('fileproject', 'name', name)
118            _insert_project_info('rootfolder', name, rootfolder)
[17280]119            _insert_project_info('excludeext', name, exts)
[17281]120            _insert_project_info('excludepath', name, exclpath)
[17280]121            _insert_project_info('includeext', name, incl)
[15590]122            _insert_project_info('repo', name, reponame)
123            _insert_project_info('revision', name, rev)
[17404]124            _insert_project_info('follow_externals', name, follow_externals)
[17410]125            # Create option for DynamicVariables plugin
126            # Note: we need to get the updated project data here
127            prj_lst = sorted([key for key in ReviewDataModel.all_file_project_data(self.env)])
128            if prj_lst:
129                self.config.set('dynvars', 'project_files.options', '|'.join(prj_lst))
130                self.config.save()
[15590]131
[15493]132        def create_ext_list(ext_str):
133            """Create a list of extensions from a string.
134
135            Double ',', trailing ',' and empty extensions are filtered out. Extensions not starting with '.'
136            are ignored.
137
138            @return: unfiltered extension list, filtered extension list
139            """
140            if not ext_str:
141                return [], []
[17277]142            # filter trailing ',', double ','and empty exts
143            ext_list = [ext.strip() for ext in ext_str.split(',') if ext.strip()]
144            return ext_list, [ext.lower() for ext in ext_list if ext[0] == '.']
[15493]145
[17281]146        def create_path_list(path_str):
147            """Create a list of paths from a string.
148
149            Double ',', trailing ',' and empty extensions are filtered out. Paths not starting with '/'
150            are ignored.
151
152            @return: unfiltered list, filtered list
153            """
154            if not path_str:
155                return [], []
156            # filter trailing ',', double ','and empty exts
157            ext_list = [ext.strip() for ext in path_str.split(',') if ext.strip()]
158            return ext_list, [ext for ext in ext_list if ext[0] == '/']
159
[15493]160        req.perm.require('CODE_REVIEW_DEV')
161
162        name = req.args.get('projectname') or path_info
163        rootfolder = req.args.get('rootfolder')
[15585]164        reponame = req.args.get('reponame', '')
165        rev = req.args.get('rev', None)
[17280]166        exts = req.args.get('excludeext', '')
167        incl = req.args.get('includeext', '')
[17281]168        exclpath = req.args.get('excludepath', '')
[17277]169        follow_externals = req.args.get('follow_ext', False)
[18253]170        sel = req.args.getlist('sel')  # For removal
[15493]171
172        all_proj = ReviewDataModel.all_file_project_data(self.env)
173
174        if req.method=='POST':
[17282]175            def _do_redirect(action):
176                parms = {'projectname': name,
177                         'rootfolder': rootfolder,
178                         'excludeext': exts,
179                         'includeext': incl,
180                         'excludepath': exclpath,
181                         'repo': reponame,
182                         'rev': rev,
183                         'error': 1
184                         }
185                if action == 'add':
186                    req.redirect(req.href.admin(cat, page, parms))
187                elif action == 'save':
188                    req.redirect(req.href.admin(cat, page, path_info, parms))
189
190            def check_parameters(action):
191                if not repo_path_exists(self.env, rootfolder, reponame):
192                    add_warning(req, _("The given root folder %s can't be found in the repository or it is a file."),
193                                       rootfolder)
194                    _do_redirect(action)
195                if len(ext_list) != len(ext_filtered):
196                    add_warning(req, _("Some extensions in exclude list are not valid: %s"), exts)
197                    _do_redirect(action)
198                if len(incl_list) != len(incl_filtered):
199                    add_warning(req, _("Some extensions in include list are not valid: %s"), incl)
200                    _do_redirect(action)
201                if len(path_lst) != len(path_lst_filtered):
202                    add_warning(req, _("Some entries in the exclude path list are not valid."))
203                    _do_redirect(action)
204
205            ext_list, ext_filtered = create_ext_list(exts)
206            incl_list, incl_filtered = create_ext_list(incl)
207            path_lst, path_lst_filtered = create_path_list(exclpath)
208
[15493]209            if req.args.get('add'):
[17282]210                action = 'add'
[15590]211                if not name:
212                    add_warning(req, _("You need to specify a project name."))
[17282]213                    _do_redirect(action)
[15590]214                if name in all_proj:
215                    add_warning(req, _("The project identifier already exists."))
[17282]216                    _do_redirect(action)
217                check_parameters(action)  # This redirects to the page on parameter error
[15590]218                add_project_info()
[17280]219                errors, num_files = insert_project_files(self, rootfolder, name, ext_filtered, incl_filtered,
[17281]220                                                         path_lst_filtered,
[17407]221                                                         follow_externals, rev=rev, repo_name=reponame)
[17282]222                add_notice(req, _("The project has been added. %s files belonging to the project %s have been added "
223                                  "to the database"), num_files, name)
[15594]224                for err in errors:
225                    add_warning(req, err)
[15493]226            elif req.args.get('save'):
[17282]227                action = 'save'
[15590]228                if not req.args.get('projectname'):
229                    add_warning(req, _("No project name given. The old name was inserted again."))
230                    add_warning(req, _("No changes have been saved."))
[17282]231                    _do_redirect(action)
[15493]232                if name != path_info:
233                    if name in all_proj:
234                        add_warning(req, _("The project identifier already exists."))
[17282]235                        _do_redirect(action)
236                check_parameters(action)  # This redirects to the page on parameter error
[17281]237
[15493]238                # Handle change. We remove all data for old name and recreate it using the new one
239                remove_project_info(path_info)
[15590]240                add_project_info()
[17280]241                errors, num_files = insert_project_files(self, rootfolder, name, ext_filtered, incl_filtered,
[17281]242                                                         path_lst_filtered,
[17407]243                                                         follow_externals, rev=rev, repo_name=reponame)
[17282]244                add_notice(req, _("Your changes have been saved. %s files belonging to the project %s have been added "
245                                  "to the database"), num_files, name)
[15594]246                for err in errors:
247                    add_warning(req, err)
[15493]248            elif req.args.get('remove'):
249                for rem_name in sel:
250                    remove_project_info(rem_name)
251
252            req.redirect(req.href.admin(cat, page))
[15585]253
[15493]254        all_proj_lst = [[key, value] for key, value in all_proj.items()]
[18276]255        for prj in all_proj_lst:
256            repos = RepositoryManager(self.env).get_repository(prj[1]['repo'])  # This should be 'reponame' but...
257            prj[1]['display_rev'] = repos.display_rev
[15493]258        data = {'view': 'detail' if path_info else 'list',
259                'projects': sorted(all_proj_lst, key=lambda item: item[0]),
[15585]260                'projectname': name,
[15493]261        }
262        if(path_info):
[17412]263            # Details page or file list
[15493]264            data['view_project'] = path_info
265            view_proj = all_proj[path_info]
[15589]266            # With V3.1 the following was added to the saved information for multi repo support.
267            # It isn't available for old projects.
[17282]268            view_proj.setdefault('repo', '')
269            view_proj.setdefault('revision', '')
270            # With V3.2 includeext and excludepath were added.
271            # These aren't available for old projects.
272            view_proj.setdefault('includeext', '')
273            view_proj.setdefault('excludepath', '')
[17280]274            # Legacy support. The name changed in V3.2
275            try:
[17281]276                excl_ext = view_proj['excludeext']
[17280]277            except KeyError:
[17281]278                excl_ext = view_proj['extensions']
[17404]279            view_proj.setdefault('follow_externals', False)
[15493]280            data.update({
281                'rootfolder': rootfolder or view_proj['rootfolder'],
[17281]282                'excludeext': exts or excl_ext,
[17282]283                'excludepath': exclpath or view_proj['excludepath'],  #exclpath or excl_path,
284                'includeext': incl or view_proj['includeext'],  #incl or incl_ext,
[15585]285                'reponame': reponame or view_proj['repo'],
286                'revision': rev or view_proj['revision'],
[17404]287                'follow_externals': follow_externals or view_proj['follow_externals']
[15493]288            })
[18276]289            repos = RepositoryManager(self.env).get_repository(data['reponame'])
290            data['display_rev'] = repos.display_rev
[17412]291            if req.args.get('filelist'):
292                data['view'] = 'filelist'
[17414]293                data['files'] = get_prj_file_list(self, path_info)
294
295                # For downloading in docx format
296                conversions = Mimeview(self.env).get_supported_conversions('text/x-trac-reviewfilelist')
297                for key, name, ext, mime_in, mime_out, q, c in conversions:
298                    conversion_href = req.href("peerreview", format=key, filelist=path_info)
299                    add_link(req, 'alternate', conversion_href, name, mime_out)
300
[15493]301        else:
302            data.update({
303                'rootfolder': rootfolder,
[17280]304                'excludeext': exts,
[17281]305                'excludepath': exclpath,
[17280]306                'includeext': incl,
[15585]307                'reponame': reponame,
308                'revision': rev
[15493]309            })
[17404]310
[15585]311        add_stylesheet(req, 'common/css/browser.css')
312        add_stylesheet(req, 'hw/css/admin_file.css')
[17448]313        add_script_data(req, {'repo_browser': req.href.adminrepobrowser(data['rootfolder'],
314                                                                        repo=data['reponame'],
315                                                                        rev=data['revision']),
[15590]316                              'show_repo_idx': path_info == None if 'error' not in req.args else False}
[15587]317                        )
[15585]318        add_script(req, 'hw/js/admin_files.js')
[18242]319
320        if hasattr(Chrome, 'jenv'):
321            return 'peeradmin_files_jinja.html', data
322        else:
323            return 'admin_files.html', data
Note: See TracBrowser for help on using the repository browser.