source: peerreviewplugin/trunk/codereview/model.py

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

PeerReviewPlugin: improvements to changeset reviews:

  • Allow to create another changeset review when old review was closed.
  • Show pin icon in lines with comments
  • Hide/show comments

Some minor improvements to presentation like titles for revision links.

Refs #14007

File size: 36.4 KB
RevLine 
[15165]1# -*- coding: utf-8 -*-
[15192]2#
[18049]3# Copyright (C) 2016-2021 Cinc
[15192]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#
[15285]11
[18280]12from collections import defaultdict, namedtuple
[15499]13from datetime import datetime
[15270]14from trac.core import Component, implements, TracError
[18280]15from trac.db import Table, Column, Index
[15270]16from trac.env import IEnvironmentSetupParticipant
[17430]17from trac.search.api import shorten_result
18from trac.util.datefmt import from_utimestamp, to_utimestamp, utc
[16616]19from trac.util.translation import N_, _
[18280]20from .compat import itervalues
[18249]21from .tracgenericclass.model import IConcreteClassProvider, AbstractVariableFieldsObject, \
[15270]22    need_db_create_for_realm, create_db_for_realm, need_db_upgrade_for_realm, upgrade_db_for_realm
23
[15165]24__author__ = 'Cinc'
25
[18280]26
[15333]27db_name_old = 'codereview_version'  # for database version 1
28db_name = 'peerreview_version'
[15499]29db_version = 2  # Don't change this one!
[15270]30
[15499]31datetime_now = datetime.now
[15333]32
[17430]33
[15270]34class PeerReviewModel(AbstractVariableFieldsObject):
35    # Fields that have no default, and must not be modified directly by the user
36    protected_fields = ('review_id', 'res_realm', 'state')
37
38    def __init__(self, env, id_=None, res_realm=None, state='new', db=None):
39        self.values = {}
[15328]40        self.env = env
[15270]41
42        self.values['review_id'] = id_
43        self.values['res_realm'] = 'peerreview'
[15285]44        # Set defaults
[15270]45        self.values['state'] = state
46        self.values['status'] = state
[15499]47        self.values['created'] = to_utimestamp(datetime_now(utc))
[15285]48        self.values['parent_id'] = 0
[15270]49
50        key = self.build_key_object()
51        AbstractVariableFieldsObject.__init__(self, env, 'peerreview', key, db)
52
53    def get_key_prop_names(self):
54        # Set the key used as ID when getting an object from the database
55        # If provided several ones they will all be used in the query:
56        #     SELECT foo FROM bar WHERE key1 = .. AND key2 = .. AND ...
57        return ['review_id']
58
[15296]59    def clear_props(self):
60        for key in self.values:
61            if key not in ['review_id', 'res_realm']:
62                self.values[key] = None
63
[15270]64    def create_instance(self, key):
[17431]65        """Create an instance which is identified by the values in dict 'key'
66
67        @param key: dict with key: identifier 'review_id', val: actual value from the database.
68
69        Note: while it's technically possible to have several identifiers uniquely describing an
70        object here, this class only use a single one.
71        """
[15296]72        return PeerReviewModel(self.env, key['review_id'], 'peerreview')
[15270]73
[15305]74    def change_status(self, new_status, author=None):
[17436]75        """Called from the change object listener to change state of review and connected files.
76
77        Note that files are only set to closed when the new status is one of
78
79        [peerreview]
80        terminal_review_states = ...
81
82        as set in trac.ini. For any other status the file status is set to 'new'.
83
84        @param new_status: new status for this review
85        @param author: user causing this change
86        @return:
87        """
[17266]88        finish_states = self.env.config.getlist("peerreview", "terminal_review_states")
89        if new_status in finish_states:
90            self['closed'] = to_utimestamp(datetime_now(utc))
91        else:
92            self['closed'] = None
[15305]93        self['status'] = new_status
94        self.save_changes(author=author)
[15281]95
[18287]96        # Handle changeset reviews
97
98        dm = ReviewDataModel(self.env)
99        dm['type'] = 'changeset'
100        dm['review_id'] = self['review_id']
101        rev_data = list(dm.list_matching_objects())
102        if rev_data:
103            changeset = rev_data[-1]
104            if self['status'] == 'closed':
105                # Mark changeset review as closed. 'data' will be something like: reponame:xxxxx:closed
106                changeset['data'] += u':closed'
107            else:
108                # User reopened the review
109                if changeset['data'].endswith(u':closed'):
110                    changeset['data'] = changeset['data'][:-7]
111            changeset.save_changes(author)
112
[15305]113        # Change the status of files attached to this review
114
115        r_tmpl = ReviewFileModel(self.env)
116        r_tmpl.clear_props()
117        r_tmpl['review_id'] = self['review_id']
118        all_files = r_tmpl.list_matching_objects()  # This is a generator
[15328]119        # We only mark files for terminal states
120        if new_status in finish_states:
[15334]121            status = new_status
[17265]122            self.env.log.debug("PeerReviewModel: changing status of attached files for review '#%s'to '%s'" %
[15305]123                               (self['review_id'], new_status))
124        else:
125            status = 'new'
[17265]126            self.env.log.debug("PeerReviewModel: changing status of attached files for review '#%s'to '%s'" %
[15305]127                               (self['review_id'], status))
128        for f in all_files:
129            f['status'] = status
130            f.save_changes(author, "Status of review '#%s' changed." % self['review_id'])
131
[15334]132    @classmethod
133    def reviews_by_period(cls, env, start_timestamp, end_timestamp):
[17436]134        """Used for getting timeline reviews.
[15305]135
[17436]136        Times are compared using '>=' and '<='.
137
138        @param env: Trac Environment object
139        @param start_timestamp: start time as a utimestamp
140        @param end_timestamp: end time as a utimestamp
141        @return: list of PeerReviewModels matching the times ordered by field 'created'
142        """
[15334]143        reviews = []
[17436]144        with env.db_query as db:
145            for row in db("SELECT review_id FROM peerreview WHERE created >= %s AND created <= %s ORDER BY created",
146                          (start_timestamp, end_timestamp)):
147                reviews.append(cls(env, row[0]))
[15334]148        return reviews
[15305]149
[17430]150    @classmethod
151    def select_all_reviews(cls, env):
152        with env.db_query as db:
[17431]153            for row in db("SELECT review_id FROM peerreview"):
[17430]154                yield cls(env, row[0])
[15334]155
[17430]156    def get_search_results(self, req, terms, filters):
157        results = {}
158        set_lst = []
159        review = PeerReviewModel(self.env)
160        if "peerreview:all" in terms:
161            # list all codereviews if no search term is given. This is the behaviour of the old
162            # codereview search page.
163            self.env.log.info("SEARCH terms: %s 2", terms)
164
[17431]165            for rev in PeerReviewModel.select_all_reviews(self.env):
[17430]166                title = u"Review #%s - %s: %s" % (rev['review_id'], rev['status'], rev['name'])
167                yield (req.href.peerreviewview(rev['review_id']),
168                       title,
169                       from_utimestamp(rev['created']),
170                       rev['owner'],
171                       shorten_result(rev['notes']))
172            return
173
174        for term in terms:
175            seen_reviews = []
176            for field in ('name', 'notes', 'owner', 'status'):
177                review.clear_props()
178                review[field] = "%" + term + "%"
179                res = review.list_matching_objects(exact_match=False)
180                for rev in res:
181                    seen_reviews.append(rev['review_id'])
182                    if rev['review_id'] not in results:
183                        results[rev['review_id']] = rev
184            set_lst.append(set(seen_reviews))
185
186        if results:
187            # Calculate common review_ids over all sets
188            res_set = set_lst[0]
189            for item in set_lst[1:]:
190                res_set &= item
191
192            for rev_id in res_set:
193                rev = results[rev_id]
194                title = u"Review #%s - %s: %s" % (rev['review_id'], rev['status'], rev['name'])
195                yield (req.href.peerreviewview(rev['review_id']),
196                       title,
197                       from_utimestamp(rev['created']),
198                       rev['owner'],
199                       shorten_result(rev['notes']))
200
201
[15270]202class PeerReviewerModel(AbstractVariableFieldsObject):
203    # Fields that have no default, and must not be modified directly by the user
204    protected_fields = ('reviewer_id', 'res_realm', 'state')
205
[15289]206    def __init__(self, env, id_=None, res_realm=None, state='new', db=None):
[15270]207        self.values = {}
208
[15289]209        self.values['reviewer_id'] = id_
[15270]210        self.values['res_realm'] = res_realm
211        self.values['state'] = state
212        self.values['status'] = state
213
214        key = self.build_key_object()
215        AbstractVariableFieldsObject.__init__(self, env, 'peerreviewer', key, db)
216
217    def get_key_prop_names(self):
218        return ['reviewer_id']
219
[15296]220    def clear_props(self):
221        for key in self.values:
222            if key not in ['reviewer_id', 'res_realm']:
223                self.values[key] = None
224
[15270]225    def create_instance(self, key):
[15296]226        return PeerReviewerModel(self.env, key['reviewer_id'], 'peerreviewer')
[15270]227
[17437]228    @staticmethod
229    def select_by_review_id(env, review_id):
230        """Get all reviewers associated with the review with the given id
231
232        @param env: Trac Environment object
233        @param review_id: review id as int
234        @return: a generator returning PeerReviewerModels
235        """
[15509]236        rm = PeerReviewerModel(env)
237        rm.clear_props()
238        rm['review_id'] = review_id
239        return rm.list_matching_objects()
[15281]240
[17438]241    @classmethod
242    def delete_by_review_id_and_name(cls, env, review_id, name):
243        """Delete the reviewer 'name' from the review eith id 'review_id'.
[15509]244
[17438]245
246        @param env: Trac Environment object
247        @param review_id: id of the review as an int
248        @param name: name of the reviewer.
249        @return: None
250        """
251        reviewer = cls(env)
252        reviewer['review_id'] = 1
253        reviewer['reviewer'] = name
254        res = list(reviewer.list_matching_objects())
255        if len(res) > 1:
256            raise ValueError("Found two reviewers with name '%s' for review '%s'." % (name, review_id))
257        res[0].delete()
258
259
[15281]260class ReviewFileModel(AbstractVariableFieldsObject):
261    # Fields that have no default, and must not be modified directly by the user
262    protected_fields = ('file_id', 'res_realm', 'state')
263
264    def __init__(self, env, id_=None, res_realm=None, state='new', db=None):
265        self.values = {}
266
[15305]267        if type(id_) is int:
268            id_ = str(id_)
[15281]269        self.values['file_id'] = id_
270        self.values['res_realm'] = res_realm
271        self.values['state'] = state
272        self.values['status'] = state
273
274        key = self.build_key_object()
275        AbstractVariableFieldsObject.__init__(self, env, 'peerreviewfile', key, db)
276
[15305]277    def clear_props(self):
278        for key in self.values:
279            if key not in ['file_id', 'res_realm']:
280                self.values[key] = None
281
[15281]282    def get_key_prop_names(self):
283        return ['file_id']
284
285    def create_instance(self, key):
[15296]286        return ReviewFileModel(self.env, key['file_id'], 'peerreviewfile')
[15281]287
[15385]288    @classmethod
[17435]289    def file_dict_by_review(cls, env):
290        """Return a dict with review_id as key (int) and a file list as value.
[15281]291
[17435]292        @return dict: key: review id as int, val: list of ReviewFileModels
293
294        Note that files with review_id == 0 are omitted here. These are files
295        which are members of file lists belonging to projects.
[15385]296        """
297        files_dict = defaultdict(list)
[17435]298        with env.db_query as db:
299            for row in db("SELECT file_id, review_id FROM peerreviewfile WHERE review_id != 0 ORDER BY review_id"):
300                file_ = cls(env, row[0])
301                files_dict[row[1]].append(file_)
[15385]302        return files_dict
303
[17435]304    @staticmethod
305    def delete_files_by_project_name(env, proj_name):
[15493]306        """Delete all file information belonging to project proj_name.
[15448]307
[15493]308        @param env: Trac environment object
[17435]309        @param proj_name: name of project. Used to filter by 'project' column
[15493]310        @return: None
[17435]311
312        It's possible to have a list of all files belonging to a project. Using this
313        list one may check which files are not yet reviewed.
314        These files are those in the table which have data in the 'project' column.
315        The review_id is set to 0.
[15493]316        """
[17435]317        with env.db_transaction as db:
318            db("DELETE FROM peerreviewfile WHERE project=%s", (proj_name,))
[15493]319
[17435]320    @staticmethod
321    def select_by_review(env, review_id):
322        """Get all file objects for a given review_id.
323        @param env: Trac Environment object
324        @param review_id: id of a review as int
[17436]325        @return: Returns a generator.
[17435]326
327        Note that review_id 0 is allowed here to query files belonging to
328        project file lists.
329        """
[15512]330        rf = ReviewFileModel(env)
331        rf.clear_props()
332        rf['review_id'] = review_id
333        return rf.list_matching_objects()
[15493]334
[15512]335
[15448]336class ReviewDataModel(AbstractVariableFieldsObject):
337    """Data model holding whatever you want to create relations for."""
338    # Fields that have no default, and must not be modified directly by the user
339    protected_fields = ('data_id', 'res_realm', 'state')
340
341    def __init__(self, env, id_=None, res_realm=None, state='new', db=None):
342        self.values = {}
343
344        self.values['data_id'] = id_
345        self.values['res_realm'] = res_realm
346        self.values['state'] = state
347
348        key = self.build_key_object()
349        AbstractVariableFieldsObject.__init__(self, env, 'peerreviewdata', key, db)
350
351    def get_key_prop_names(self):
352        return ['data_id']
353
354    def clear_props(self):
355        for key in self.values:
356            if key not in ['data_id', 'res_realm']:
357                self.values[key] = None
358
359    def create_instance(self, key):
360        return ReviewDataModel(self.env, key['data_id'], 'peerreviewdata')
361
362    @classmethod
363    def comments_for_file_and_owner(cls, env, file_id, owner):
364        """Return a list of data."""
365
[18049]366        with env.db_query as db:
367            cursor = db.cursor()
368            cursor.execute("SELECT comment_id, type, data FROM peerreviewdata "
369                           "WHERE file_id = %s AND owner = %s",
370                           (file_id, owner))
371            return cursor.fetchall()
[15448]372
373    @classmethod
374    def comments_for_owner(cls, env, owner):
375        """Return a list of comment data for owner.
376        """
[18049]377        with env.db_query as db:
378            cursor = db.cursor()
379            cursor.execute("SELECT comment_id, review_id, type, data FROM peerreviewdata "
380                           "WHERE owner = %s", (owner,))
381            return cursor.fetchall()
[15448]382
[15493]383    @classmethod
384    def all_file_project_data(cls, env):
385        """Return a dict with project name as key and a dict with project information as value."""
[18047]386        fileprojectname, datatype, data = range(3)
[15493]387        sql = """SELECT n.data AS name , r.type, r.data FROM peerreviewdata AS n
388                 JOIN peerreviewdata AS r ON r.data_key = n.data
389                 WHERE n.type = 'fileproject'"""
[18049]390        with env.db_query as db:
391            cursor = db.cursor()
392            cursor.execute(sql)
393            files_dict = defaultdict(dict)
394            for row in cursor:
395                files_dict[row[fileprojectname]][row[datatype]] = row[data]
396            return files_dict
[15493]397
398
[15448]399class ReviewCommentModel(AbstractVariableFieldsObject):
400    """Data model holding whatever you want to create relations for."""
401    # Fields that have no default, and must not be modified directly by the user
[15462]402    protected_fields = ('comment_id', 'res_realm', 'state')
[15448]403
404    def __init__(self, env, id_=None, res_realm=None, state='new', db=None):
405        self.values = {}
406
407        self.values['comment_id'] = id_
408        self.values['res_realm'] = res_realm
409        self.values['state'] = state
[15499]410        self.values['created'] = to_utimestamp(datetime_now(utc))
[17441]411        self.children = {}
[15448]412
413        key = self.build_key_object()
414        AbstractVariableFieldsObject.__init__(self, env, 'peerreviewcomment', key, db)
415
416    def get_key_prop_names(self):
417        return ['comment_id']
418
419    def clear_props(self):
420        for key in self.values:
421            if key not in ['comment_id', 'res_realm']:
422                self.values[key] = None
423
424    def create_instance(self, key):
[15462]425        return ReviewCommentModel(self.env, key['comment_id'], 'peerreviewcomment')
[15448]426
[17441]427    @staticmethod
[18263]428    def comment_ids_by_file_id(env):
[17435]429        """Return a dict with file_id as key and a comment id list as value.
[15448]430
[17435]431        @param env: Trac Environment object
432        @return dict with key: file id as int, val: list of comment ids for that file as int
433        """
[15448]434        the_dict = defaultdict(list)
[17441]435        for row in env.db_query("SELECT comment_id, file_id FROM peerreviewcomment"):
[15448]436            the_dict[row[1]].append(row[0])
437        return the_dict
438
[17443]439    @staticmethod
440    def select_by_file_id(env, file_id):
[17441]441        """Return all comments for the file specified by 'file_id'.
[15448]442
[17441]443        :param env: Trac Environment object
444        :param file_id: file id as int. All comments for this file are returned
[17443]445        :return: generator for ReviewCommentModels
[17441]446        """
[17443]447        rcm = ReviewCommentModel(env)
448        rcm.clear_props()
449        rcm['file_id'] = file_id
450        return rcm.list_matching_objects()
[17441]451
[18280]452    @staticmethod
453    def create_comment_tree(env, fileid, line):
454        """Create a comment tree for the given file and line number.
[17441]455
[18280]456        :param env: Trac environment object
457        :param fileid: id of a peerreviewfile
458        :param line: line number we wnat to get comments for
459        :return dict with key: comment id, val: comment data as a namedtuple
460                each comments 'children' dict is properly populated thus for each
461                comment we have a (sub)tree. Comments with parent_id = -1 are root
462                comments.
463        """
464        Comment = namedtuple('Comment', "children, comment_id, file_id, parent_id, line, author, comment, created")
465        tree = {}
466        for row in env.db_query("SELECT comment_id, file_id, parent_id, line_num, author, comment, created"
467                                " FROM peerreviewcomment WHERE file_id = %s"
468                                " AND line_num = %s"
469                                " ORDER by created", (fileid, line)):
470            comment = Comment({}, *row)
471            tree[comment.comment_id] = comment
472        for comment in itervalues(tree):
473            if comment.parent_id != -1 and comment.parent_id in tree:
474                tree[comment.parent_id].children[comment.comment_id] = comment
475        return tree
[17441]476
[18280]477
[15270]478class PeerReviewModelProvider(Component):
[15526]479    """This class provides the data model for the generic workflow plugin.
[15270]480
[15526]481    [[BR]]
[15270]482    The actual data model on the db is created starting from the
483    SCHEMA declaration below.
484    For each table, we specify whether to create also a '_custom' and
485    a '_change' table.
486
487    This class also provides the specification of the available fields
488    for each class, being them standard fields and the custom fields
489    specified in the trac.ini file.
490    The custom field specification follows the same syntax as for
491    Tickets.
492    Currently, only 'text' type of custom fields are supported.
493    """
494
495    implements(IConcreteClassProvider, IEnvironmentSetupParticipant)
496
497    current_db_version = 0
498
499    SCHEMA = {
500                'peerreview':
501                    {'table':
[15503]502                        Table('peerreview', key=('review_id'))[
[15270]503                              Column('review_id', auto_increment=True, type='int'),
504                              Column('owner'),
505                              Column('status'),
506                              Column('created', type='int'),
[15499]507                              Column('closed', type='int'),
[15270]508                              Column('name'),
509                              Column('notes'),
510                              Column('parent_id', type='int'),
[15271]511                              Column('project'),
[15503]512                              Column('keywords'),
513                              Index(['owner']),
514                              Index(['status'])],
[15270]515                     'has_custom': True,
516                     'has_change': True,
[15503]517                     'version': 6},
[15270]518                'peerreviewfile':
519                    {'table':
[15503]520                        Table('peerreviewfile', key=('file_id'))[
[15270]521                              Column('file_id', auto_increment=True, type='int'),
522                              Column('review_id', type='int'),
523                              Column('path'),
524                              Column('line_start', type='int'),
525                              Column('line_end', type='int'),
526                              Column('repo'),
527                              Column('revision'),
528                              Column('changerevision'),
529                              Column('hash'),
[15492]530                              Column('status'),
[15503]531                              Column('project'),
532                              Index(['hash']),
533                              Index(['review_id']),
534                              Index(['status']),
535                              Index(['project'])
536                        ],
[15270]537                     'has_custom': True,
538                     'has_change': True,
[15503]539                     'version': 5},
[15270]540                'peerreviewcomment':
541                    {'table':
[15503]542                        Table('peerreviewcomment', key=('comment_id'))[
[15270]543                              Column('comment_id', auto_increment=True, type='int'),
544                              Column('file_id', type='int'),
545                              Column('parent_id', type='int'),
546                              Column('line_num', type='int'),
547                              Column('author'),
548                              Column('comment'),
549                              Column('attachment_path'),
550                              Column('created', type='int'),
551                              Column('refs'),
552                              Column('type'),
[15503]553                              Column('status'),
554                              Index(['file_id']),
555                              Index(['author'])
556                        ],
[15270]557                     'has_custom': True,
558                     'has_change': True,
[15503]559                     'version': 6},
[15270]560                'peerreviewer':
561                    {'table':
[15503]562                        Table('peerreviewer', key=('reviewer_id'))[
[15272]563                              Column('reviewer_id', auto_increment=True, type='int'),
[15270]564                              Column('review_id', type='int'),
565                              Column('reviewer'),
566                              Column('status'),
[15503]567                              Column('vote', type='int'),
568                              Index(['reviewer']),
569                              Index(['review_id'])
570                        ],
[15270]571                     'has_custom': True,
572                     'has_change': True,
[15503]573                     'version': 5},
[15448]574                'peerreviewdata':
575                    {'table':
[15503]576                         Table('peerreviewdata', key=('data_id'))[
[15448]577                             Column('data_id', type='int'),
578                             Column('review_id', type='int'),
579                             Column('comment_id', type='int'),
580                             Column('file_id', type='int'),
581                             Column('reviewer_id', type='int'),
582                             Column('type'),
583                             Column('data'),
[15492]584                             Column('owner'),
[15503]585                             Column('data_key'),
586                             Index(['review_id']),
587                             Index(['comment_id']),
588                             Index(['file_id'])
589                         ],
[15448]590                     'has_custom': False,
591                     'has_change': False,
[15503]592                     'version': 3},
[15448]593                    }
[15270]594
595    FIELDS = {
596                'peerreview': [
597                    {'name': 'review_id', 'type': 'int', 'label': N_('Review ID')},
598                    {'name': 'owner', 'type': 'text', 'label': N_('Review owner')},
599                    {'name': 'status', 'type': 'text', 'label': N_('Review status')},
600                    {'name': 'created', 'type': 'int', 'label': N_('Review creation date')},
[15499]601                    {'name': 'closed', 'type': 'int', 'label': N_('Review closing date')},
[15270]602                    {'name': 'name', 'type': 'text', 'label': N_('Review name')},
603                    {'name': 'notes', 'type': 'text', 'label': N_('Review notes')},
604                    {'name': 'parent_id', 'type': 'int', 'label': N_('Review parent. 0 if not a followup review')},
[15271]605                    {'name': 'project', 'type': 'text', 'label': N_('Project')},
[15270]606                    {'name': 'keywords', 'type': 'text', 'label': N_('Review keywords')}
607                ],
608                'peerreviewfile': [
609                    {'name': 'file_id', 'type': 'int', 'label': N_('File ID')},
610                    {'name': 'review_id', 'type': 'int', 'label': N_('Review ID')},
611                    {'name': 'path', 'type': 'text', 'label': N_('File path')},
612                    {'name': 'line_start', 'type': 'int', 'label': N_('First line to review')},
613                    {'name': 'line_end', 'type': 'int', 'label': N_('Last line to review')},
614                    {'name': 'repo', 'type': 'text', 'label': N_('Repository')},
615                    {'name': 'revision', 'type': 'text', 'label': N_('Revision')},
616                    {'name': 'changerevision', 'type': 'text', 'label': N_('Revision of last change')},
617                    {'name': 'hash', 'type': 'text', 'label': N_('Hash of file content')},
[15492]618                    {'name': 'status', 'type': 'text', 'label': N_('File status')},
619                    {'name': 'project', 'type': 'text', 'label': N_('Project')},
[15270]620                ],
621                'peerreviewcomment': [
622                    {'name': 'comment_id', 'type': 'int', 'label': N_('Comment ID')},
623                    {'name': 'file_id', 'type': 'int', 'label': N_('File ID')},
624                    {'name': 'parent_id', 'type': 'int', 'label': N_('Parent comment')},
625                    {'name': 'line_num', 'type': 'int', 'label': N_('Line')},
626                    {'name': 'author', 'type': 'text', 'label': N_('Author')},
627                    {'name': 'comment', 'type': 'text', 'label': N_('Comment')},
628                    {'name': 'attachment_path', 'type': 'text', 'label': N_('Attachment')},
629                    {'name': 'created', 'type': 'int', 'label': N_('Comment creation date')},
630                    {'name': 'status', 'type': 'text', 'label': N_('Comment status')}
631                ],
632                'peerreviewer': [
[15272]633                    {'name': 'reviewer_id', 'type': 'int', 'label': N_('ID')},
[15270]634                    {'name': 'review_id', 'type': 'int', 'label': N_('Review ID')},
635                    {'name': 'reviewer', 'type': 'text', 'label': N_('Reviewer')},
636                    {'name': 'status', 'type': 'text', 'label': N_('Review status')},
637                    {'name': 'vote', 'type': 'int', 'label': N_('Vote')},
[15448]638                ],
639                'peerreviewdata': [
640                    {'name': 'data_id', 'type': 'int', 'label': N_('ID')},
641                    {'name': 'review_id', 'type': 'int', 'label': N_('Review ID')},
642                    {'name': 'comment_id', 'type': 'int', 'label': N_('Comment ID')},
643                    {'name': 'file_id', 'type': 'int', 'label': N_('File ID')},
644                    {'name': 'reviewer_id', 'type': 'int', 'label': N_('Reviewer ID')},
645                    {'name': 'type', 'type': 'text', 'label': N_('Type')},
646                    {'name': 'data', 'type': 'text', 'label': N_('Data')},
647                    {'name': 'owner', 'type': 'text', 'label': N_('Owner')},
[15493]648                    {'name': 'data_key', 'type': 'text', 'label': N_('Key for data')},
[15448]649                ],
[15270]650            }
651
652    METADATA = {
653                'peerreview': {
654                        'label': "Review",
655                        'searchable': True,
656                        'has_custom': True,
[15273]657                        'has_change': True
[15270]658                    },
659                'peerreviewfile': {
660                        'label': "ReviewFile",
661                        'searchable': False,
[15273]662                        'has_custom': True,
663                        'has_change': True
[15270]664                    },
665                'peerreviewcomment': {
666                    'label': "ReviewComment",
[15273]667                    'searchable': True,
668                    'has_custom': True,
669                    'has_change': True
[15270]670                },
671                'peerreviewer': {
672                    'label': "Reviewer",
[15273]673                    'searchable': False,
[15270]674                    'has_custom': True,
[15273]675                    'has_change': True
[15270]676                },
[15448]677                'peerreviewdata': {
678                    'label': "Data",
679                    'searchable': True,
680                    'has_custom': False,
681                    'has_change': False
682                },
[15270]683    }
684
[17430]685    # IConcreteClassProvider methods
[15270]686
687    def get_realms(self):
[16451]688        yield 'peerreview'
689        yield 'peerreviewer'
[15270]690
691    def get_data_models(self):
692        return self.SCHEMA
693
694    def get_fields(self):
695        return self.FIELDS
696
697    def get_metadata(self):
698        return self.METADATA
699
700    def create_instance(self, realm, key=None):
701        obj = None
702
703        if realm == 'peerreview':
704            if key is not None:
705                obj = PeerReviewModel(self.env, key, realm)
706            else:
707                obj = PeerReviewModel(self.env)
708        elif realm == 'peerreviewer':
709            if key is not None:
710                obj = PeerReviewerModel(self.env, key, realm)
711            else:
712                obj = PeerReviewerModel(self.env)
713        return obj
714
715    def check_permission(self, req, realm, key_str=None, operation='set', name=None, value=None):
716        pass
717
718    # IEnvironmentSetupParticipant methods
719    def environment_created(self):
720        self.current_db_version = 0
721        self.upgrade_environment()
722
[18049]723    def environment_needs_upgrade(self, db_=None):
724        with self.env.db_query as db:
725            self.current_db_version = self._get_version(db.cursor())
[15524]726
[18049]727            if self.current_db_version < db_version:
728                self.log.info("PeerReview plugin database schema version is %d, should be %d",
729                              self.current_db_version, db_version)
730                return True
[15270]731
[18049]732            for realm in self.SCHEMA:
733                realm_metadata = self.SCHEMA[realm]
734                if need_db_create_for_realm(self.env, realm, realm_metadata, db) or \
735                    need_db_upgrade_for_realm(self.env, realm, realm_metadata, db):
736                    return True
[15270]737
738        return False
739
740    def upgrade_environment(self, db=None):
[15503]741        # Create or update db. We are going step by step through all database versions.
[15270]742
[15557]743        if self.current_db_version != 0 and self.current_db_version < 6:
744            raise TracError("Upgrade for database version %s not supported. Raise a ticket for "
[15576]745                            "PeerReviewPlugin for a fix."
[15557]746                            % self.current_db_version)
[15270]747
[15503]748        self.upgrade_tracgeneric()
[15273]749
[15503]750    def upgrade_tracgeneric(self):
751        """Upgrade for versions > 2 using the TracGenericClass mechanism."""
[18049]752        with self.env.db_transaction as db:
[15270]753            for realm in self.SCHEMA:
754                realm_metadata = self.SCHEMA[realm]
755
756                if need_db_create_for_realm(self.env, realm, realm_metadata, db):
757                    create_db_for_realm(self.env, realm, realm_metadata, db)
[15557]758                    self.add_workflows()
[15270]759
760                elif need_db_upgrade_for_realm(self.env, realm, realm_metadata, db):
761                    upgrade_db_for_realm(self.env, 'codereview.upgrades', realm, realm_metadata, db)
762
[15557]763    def add_workflows(self):
764
765        env = self.env
766        # Add default workflow for peerreview
767
768        wf_data = [['approve', 'reviewed -> approved'],
769                   ['approve.name', 'Approve the review'],
770                   ['close', 'new, reviewed, in-review -> closed'],
771                   ['close.name', 'Close review'],
772                   ['disapprove', 'reviewed -> disapproved'],
773                   ['disapprove.name', 'Deny this review'],
774                   ['reopen', 'closed, reviewed, approved, disapproved -> new'],
775                   ['reopen.permissions', 'CODE_REVIEW_MGR'],
776                   ['review-done', 'in-review -> reviewed'],
777                   ['review-done.name', 'Mark as reviewed'],
778                   ['reviewing', 'new -> in-review'],
779                   ['reviewing.default', '5'],
780                   ['reviewing.name', 'Start review'],
[17430]781                   ['change_owner', '* -> *'],
782                   ['change_owner.name', 'Change Owner to'],
783                   ['change_owner.operations', 'set_review_owner'],
784                   ['change_owner.permissions', 'CODE_REVIEW_MGR'],
785                   ['change_owner.default', '-1'],
[15557]786                   ]
787        wf_section = 'peerreview-resource_workflow'
788
789        if wf_section not in env.config.sections():
790            env.log.info("Adding default workflow for 'peerreview' to config.")
791            for item in wf_data:
792                env.config.set(wf_section, item[0], item[1])
793            env.config.save()
794
795        # Add default workflow for peerreviewer
796
797        wf_data = [['reviewing', 'new -> in-review'],
798                   ['reviewing.name', 'Start review'],
799                   ['review_done', 'in-review -> reviewed'],
800                   ['review_done.name', 'Mark review as done.'],
801                   ['reopen', 'in-review, reviewed -> new'],
802                   ['reopen.name', "Reset review state to 'new'"],
803                   ]
804        wf_section = 'peerreviewer-resource_workflow'
805
806        if wf_section not in env.config.sections():
807            env.log.info("Adding default workflow for 'peerreviewer' to config.")
808            for item in wf_data:
809                env.config.set(wf_section, item[0], item[1])
810            env.config.save()
811
[15270]812    def _get_version(self, cursor):
[15576]813        cursor.execute("SELECT value FROM system WHERE name = %s", (db_name,))
[15270]814        value = cursor.fetchone()
815        val = int(value[0]) if value else 0
816        return val
817
[17430]818
[15205]819def get_users(env):
820    users = []
[17441]821    with env.db_query as db:
822        cursor = db.cursor()
823        cursor.execute("""SELECT DISTINCT p1.username FROM permission AS p1
824                          LEFT JOIN permission AS p2 ON p1.action = p2.username
825                          WHERE p2.action IN ('CODE_REVIEW_DEV', 'CODE_REVIEW_MGR')
826                          OR p1.action IN ('CODE_REVIEW_DEV', 'CODE_REVIEW_MGR', 'TRAC_ADMIN')
827                          """)
[15205]828        for row in cursor:
[17441]829            users.append(row[0])
830        if users:
831            # Filter groups from the results. We should probably do this using the group provider component
832            cursor.execute("""Select DISTINCT p3.action FROM permission AS p3
833                              JOIN permission p4 ON p3.action = p4.username""")
834            groups = []
835            for row in cursor:
836                groups.append(row[0])
837            groups.append('authenticated')
838            users = list(set(users)-set(groups))
[15205]839    return sorted(users)
840
841
[17441]842# Deprecated use ReviewCommentModel instead
[15195]843class Comment(object):
844
845    def __init__(self, env, file_id=None):
[15242]846        self.env = env
[15195]847        self._init_from_row((None,)*8)
848
849    def _init_from_row(self, row):
850        comment_id, file_id, parent_id, line_num, author, comment, attachment_path, created = row
851        self.comment_id = comment_id
852        self.file_id = file_id
853        self.parent_id = parent_id
854        self.line_num = line_num
855        self.author = author
856        self.comment = comment
857        self.attachment_path = attachment_path
858        self.created = created
859
860    @classmethod
861    def select_by_file_id(cls, env, file_id):
[18049]862        with env.db_query as db:
863            cursor = db.cursor()
864            cursor.execute("SELECT comment_id, file_id, parent_id, line_num, author, comment, attachment_path, created FROM "
865                           "peerreviewcomment WHERE file_id=%s ORDER BY line_num", (file_id,))
866            comments = []
867            for row in cursor:
868                c = cls(env)
869                c._init_from_row(row)
870                comments.append(c)
[17430]871        return comments
Note: See TracBrowser for help on using the repository browser.