source: peerreviewplugin/tags/0.12/3.1/codereview/model.py

Last change on this file was 17266, checked in by Cinc-th, 5 years ago

PeerReviewPlugin: record timestamp when finishing a review. The status considered as a terminal status are closed, approved, disapproved (unless configuration was changed). The finishing date is shown in the UI.

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