Review Board

beta

Improve json usefulness for post-commit hook

Updated 4 months, 1 week ago

Jonathan Ellis Reviewers
reviewboard
None Review Board SVN
the main point of this change is to allow a staff user to specify submit_as=a-different-user when creating a review request so that user will be the one notified of reviews, etc.  this enables using a post-commit hook to auto-create reviews for all or part of a repository.  there are some side concerns related to this:
 
 * basedir must be allowed to be empty when creating diffs against a svn repository as opposed to a working copy.  that is what the diffviewer change does; for the web interface the functionality is identical, but the json manually rips out the basedir when necessary, allowing a svn repo diff to be treated as "absolute."
 * added is_visible_to and is_mutable_by methods to the Request models to encapsulate the notion that "staff can always view and modify requests"
 * moved sending of mail into the models to avoid duplication of code in json and view
 * json new_diff includes traceback when something unexpected goes wrong
 * add get_changeset to svn to allow update_from_changenum for that scm

 

Diff revision 1 (Latest)

  1. /trunk/reviewboard/diffviewer/forms.py: 3 changes [ 1 2 3 ]
  2. /trunk/reviewboard/reviews/models.py: 5 changes [ 1 2 3 4 5 ]
  3. /trunk/reviewboard/reviews/views.py: 4 changes [ 1 2 3 4 ]
  4. /trunk/reviewboard/scmtools/svn.py: 4 changes [ 1 2 3 4 ]
  5. /trunk/reviewboard/webapi/json.py: 21 changes [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ]
/trunk/reviewboard/diffviewer/forms.py
Revision 1137 New Change
38
38
39
        if len(files) == 0:
39
        if len(files) == 0:
40
            raise EmptyDiffError(_("The diff file is empty"))
40
            raise EmptyDiffError(_("The diff file is empty"))
41
41
42
        # Check that we can actually get all these files.
42
        # Check that we can actually get all these files.
43
        if tool.get_diffs_use_absolute_paths():
43
        if 'basedir' in self.fields:
44
            basedir = ''
45
        else:
46
            basedir = smart_unicode(self.cleaned_data['basedir'])
44
            basedir = smart_unicode(self.cleaned_data['basedir'])
45
        else:
46
            basedir = ''
47
47
48
        for f in files:
48
        for f in files:
49
            f2, revision = tool.parse_diff_revision(f.origFile, f.origInfo)
49
            f2, revision = tool.parse_diff_revision(f.origFile, f.origInfo)
50
            filename = os.path.join(basedir, f2)
50
            filename = os.path.join(basedir, f2)
51
51
/trunk/reviewboard/reviews/models.py
Revision 1137 New Change
1
import os
1
import os
2
import re
2
import re
3
from datetime import datetime
3
from datetime import datetime
4
4
5
from django.conf import settings
5
from django.contrib.auth.models import User
6
from django.contrib.auth.models import User
6
from django.db import models
7
from django.db import models
7
from django.db.models import Q, permalink
8
from django.db.models import Q, permalink
8
from django.utils.html import escape
9
from django.utils.html import escape
9
from django.utils.safestring import mark_safe
10
from django.utils.safestring import mark_safe
14
15
15
from reviewboard.diffviewer.models import DiffSet, DiffSetHistory, FileDiff
16
from reviewboard.diffviewer.models import DiffSet, DiffSetHistory, FileDiff
16
from reviewboard.scmtools.models import Repository
17
from reviewboard.scmtools.models import Repository
17
from reviewboard.utils.fields import ModificationTimestampField
18
from reviewboard.utils.fields import ModificationTimestampField
18
from reviewboard.utils.templatetags.htmlutils import crop_image, thumbnail
19
from reviewboard.utils.templatetags.htmlutils import crop_image, thumbnail
20
from reviewboard.reviews.email import mail_review, mail_review_request, \
21
                                      mail_reply
19
22
20
23
21
class InvalidChangeNumberError(Exception):
24
class InvalidChangeNumberError(Exception):
22
    def __init__(self):
25
    def __init__(self):
23
        Exception.__init__(self, None)
26
        Exception.__init__(self, None)
391
            # If this is not a pending review request now, delete any
394
            # If this is not a pending review request now, delete any
392
            # and all ReviewRequestVisit objects.
395
            # and all ReviewRequestVisit objects.
393
            self.visits.all().delete()
396
            self.visits.all().delete()
394
397
395
        super(ReviewRequest, self).save()
398
        super(ReviewRequest, self).save()
399
        
400
    def is_visible_to(self, user):
401
        return self.public or self.is_mutable_by(user)
402
403
    def is_mutable_by(self, user):
404
        return self.submitter == user or user.is_staff
396
405
397
    class Admin:
406
    class Admin:
398
        list_display = ('summary', 'submitter', 'status', 'public', \
407
        list_display = ('summary', 'submitter', 'status', 'public', \
399
                        'last_updated')
408
                        'last_updated')
400
        list_filter = ('public', 'status', 'time_added', 'last_updated')
409
        list_filter = ('public', 'status', 'time_added', 'last_updated')
622
        else:
631
        else:
623
            changes['diff'] = False
632
            changes['diff'] = False
624
633
625
        request.save()
634
        request.save()
626
635
636
        if settings.SEND_REVIEW_MAIL and changes:
637
            mail_review_request(request.submitter, request, changes)
638
627
        return changes
639
        return changes
628
640
629
    class Admin:
641
    class Admin:
630
        list_display = ('summary', 'submitter', 'last_updated')
642
        list_display = ('summary', 'submitter', 'last_updated')
631
        list_filter = ('last_updated',)
643
        list_filter = ('last_updated',)
841
853
842
        for comment in self.screenshot_comments.all():
854
        for comment in self.screenshot_comments.all():
843
            comment.timetamp = self.timestamp
855
            comment.timetamp = self.timestamp
844
            comment.save()
856
            comment.save()
845
857
858
        if settings.SEND_REVIEW_MAIL:
859
            if self.is_reply():
860
                mail_reply(self.user, self)
861
            else:
862
                mail_review(self.user, self)
863
846
    def delete(self):
864
    def delete(self):
847
        """
865
        """
848
        Deletes this review.
866
        Deletes this review.
849
867
850
        This will enforce that all contained comments are also deleted.
868
        This will enforce that all contained comments are also deleted.
/trunk/reviewboard/reviews/views.py
Revision 1137 New Change
327
    Publishes a new review request or the changes on a draft for a review
327
    Publishes a new review request or the changes on a draft for a review
328
    request.
328
    request.
329
    """
329
    """
330
    review_request = get_object_or_404(ReviewRequest, pk=review_request_id)
330
    review_request = get_object_or_404(ReviewRequest, pk=review_request_id)
331
331
332
    if review_request.submitter == request.user:
332
    if review_request.is_mutable_by(request.user):
333
        if not review_request.target_groups and \
333
        if not review_request.target_groups and \
334
           not review_request.target_people:
334
           not review_request.target_people:
335
            pass # FIXME show an error
335
            # FIXME error nicely
336
            raise Exception('cannot publish with no reviewers set') 
336
337
337
        try:
338
        try:
338
            draft = review_request.reviewrequestdraft_set.get()
339
            draft = review_request.reviewrequestdraft_set.get()
339
340
340
            # This will in turn save the review request, so we'll be done.
341
            # This will in turn save the review request, so we'll be done.
350
            # The draft didn't exist, so we must save the review request
351
            # The draft didn't exist, so we must save the review request
351
            # ourselves.
352
            # ourselves.
352
            review_request.public = True
353
            review_request.public = True
353
            review_request.save()
354
            review_request.save()
354
355
355
        if settings.SEND_REVIEW_MAIL:
356
            mail_review_request(request.user, review_request)
357
358
        return HttpResponseRedirect(review_request.get_absolute_url())
356
        return HttpResponseRedirect(review_request.get_absolute_url())
359
    else:
357
    else:
360
        raise HttpResponseForbidden() # XXX Error out
358
        raise HttpResponseForbidden() # XXX Error out
361
359
362
360
/trunk/reviewboard/scmtools/svn.py
Revision 1137 New Change
1
import re
1
import re
2
2
3
try:
3
try:
4
    from pysvn import ClientError, Revision, opt_revision_kind
4
    from pysvn import ClientError, Revision, opt_revision_kind
5
except ImportError:
5
except ImportError:
6
    pass
6
    pass
7
7
8
from reviewboard.diffviewer.parser import DiffParser
8
from reviewboard.diffviewer.parser import DiffParser
9
from reviewboard.scmtools.core import \
9
from reviewboard.scmtools.core import \
10
    SCMError, FileNotFoundError, SCMTool, HEAD, PRE_CREATION, UNKNOWN
10
    ChangeSet, SCMError, FileNotFoundError, SCMTool, HEAD, PRE_CREATION, UNKNOWN
11
11
12
class SVNTool(SCMTool):
12
class SVNTool(SCMTool):
13
    def __init__(self, repository):
13
    def __init__(self, repository):
14
        self.repopath = repository.path
14
        self.repopath = repository.path
15
        if self.repopath[-1] == '/':
15
        if self.repopath[-1] == '/':
16
            self.repopath = self.repopath[:-1]
16
            self.repopath = self.repopath[:-1]
17
17
18
        SCMTool.__init__(self, repository)
18
        SCMTool.__init__(self, repository)
19
19
20
        import pysvn
20
        import pysvn
21
        self.client = pysvn.Client()
21
        self.client = pysvn.Client()
22
        def callback_ssl(trust_dict):
23
            return True, trust_dict['failures'], False
24
        self.client.callback_ssl_server_trust_prompt = callback_ssl
22
        if repository.username:
25
        if repository.username:
23
            self.client.set_default_username(str(repository.username))
26
            self.client.set_default_username(str(repository.username))
24
        if repository.password:
27
        if repository.password:
25
            self.client.set_default_password(str(repository.password))
28
            self.client.set_default_password(str(repository.password))
26
29
53
                                   self.__normalize_revision(revision))
56
                                   self.__normalize_revision(revision))
54
        except ClientError, e:
57
        except ClientError, e:
55
            stre = str(e)
58
            stre = str(e)
56
            if 'File not found' in stre:
59
            if 'File not found' in stre:
57
                raise FileNotFoundError(path, revision, str(e))
60
                raise FileNotFoundError(path, revision, str(e))
58
            elif 'callback_ssl_server_trust_prompt required' in stre:
59
                raise SCMError(
60
                    'HTTPS certificate not accepted.  Please ensure that ' +
61
                    'the proper certificate exists in ~/.subversion/auth ' +
62
                    'for the user that reviewboard is running as.')
63
            else:
61
            else:
64
                raise SCMError(e)
62
                raise SCMError(e)
65
63
66
64
65
    def get_changeset(self, changesetid):
66
        r = self.__normalize_revision(changesetid)
67
        log = self.client.log(self.repopath, r, r, True)[0]
68
        cs = ChangeSet()
69
        cs.changenum = changesetid
70
        cs.summary = log['message']
71
        def pathinfo(path):
72
            return path['action'] + ' ' + path['path']
73
        cs.description = '\n'.join(pathinfo(path) for path in log['changed_paths'])
74
        cs.username = log['author']
75
        return cs
76
        
77
67
    def parse_diff_revision(self, file_str, revision_str):
78
    def parse_diff_revision(self, file_str, revision_str):
68
        if revision_str == "(working copy)":
79
        if revision_str == "(working copy)":
69
            return file_str, HEAD
80
            return file_str, HEAD
70
81
71
        # Binary diffs don't provide revision information, so we set a fake
82
        # Binary diffs don't provide revision information, so we set a fake
/trunk/reviewboard/webapi/json.py
Revision 1137 New Change
17
from djblets.util.misc import get_object_or_none
17
from djblets.util.misc import get_object_or_none
18
18
19
from reviewboard.accounts.models import Profile
19
from reviewboard.accounts.models import Profile
20
from reviewboard.diffviewer.forms import UploadDiffForm, EmptyDiffError
20
from reviewboard.diffviewer.forms import UploadDiffForm, EmptyDiffError
21
from reviewboard.diffviewer.models import FileDiff, DiffSet
21
from reviewboard.diffviewer.models import FileDiff, DiffSet
22
from reviewboard.reviews.email import mail_review, mail_review_request, \
23
                                      mail_reply
24
from reviewboard.reviews.forms import UploadScreenshotForm
22
from reviewboard.reviews.forms import UploadScreenshotForm
25
from reviewboard.reviews.models import ChangeNumberInUseError, \
23
from reviewboard.reviews.models import ChangeNumberInUseError, \
26
                                       InvalidChangeNumberError, \
24
                                       InvalidChangeNumberError, \
27
                                       ReviewRequest, Review, Group, Comment, \
25
                                       ReviewRequest, Review, Group, Comment, \
28
                                       ReviewRequestDraft, Screenshot, \
26
                                       ReviewRequestDraft, Screenshot, \
52
INVALID_ATTRIBUTE         = JsonError(102, "Invalid attribute")
50
INVALID_ATTRIBUTE         = JsonError(102, "Invalid attribute")
53
NOT_LOGGED_IN             = JsonError(103, "You are not logged in")
51
NOT_LOGGED_IN             = JsonError(103, "You are not logged in")
54
LOGIN_FAILED              = JsonError(104, "The username or password was " +
52
LOGIN_FAILED              = JsonError(104, "The username or password was " +
55
                                           "not correct")
53
                                           "not correct")
56
INVALID_FORM_DATA         = JsonError(105, "One or more fields had errors")
54
INVALID_FORM_DATA         = JsonError(105, "One or more fields had errors")
55
INVALID_USER              = JsonError(106, "User does not exist")
57
56
58
UNSPECIFIED_DIFF_REVISION = JsonError(200, "Diff revision not specified")
57
UNSPECIFIED_DIFF_REVISION = JsonError(200, "Diff revision not specified")
59
INVALID_DIFF_REVISION     = JsonError(201, "Invalid diff revision")
58
INVALID_DIFF_REVISION     = JsonError(201, "Invalid diff revision")
60
INVALID_ACTION            = JsonError(202, "Invalid action specified")
59
INVALID_ACTION            = JsonError(202, "Invalid action specified")
61
INVALID_CHANGE_NUMBER     = JsonError(203, "The change number specified " +
60
INVALID_CHANGE_NUMBER     = JsonError(203, "The change number specified " +
387
@require_POST
386
@require_POST
388
def new_review_request(request):
387
def new_review_request(request):
389
    try:
388
    try:
390
        repository_path = request.POST.get('repository_path',
389
        repository_path = request.POST.get('repository_path',
391
                                           settings.DEFAULT_REPOSITORY_PATH)
390
                                           settings.DEFAULT_REPOSITORY_PATH)
392
        repository_id = request.POST.get('repository_id', None)
391
        repository_id = request.POST.get('repository_id')
392
        submit_as = request.POST.get('submit_as')
393
        if submit_as:
394
            if not request.user.is_staff:
395
                return JsonResponseError(request, PERMISSION_DENIED)
396
            try:
397
                user = User.objects.filter(username=submit_as)[0]
398
            except IndexError:
399
                return JsonResponseError(request, INVALID_USER)
400
        else:
401
            user = request.user
393
402
394
        if repository_path == None and repository_id == None:
403
        if repository_path == None and repository_id == None:
395
            return JsonResponseError(request, MISSING_REPOSITORY)
404
            return JsonResponseError(request, MISSING_REPOSITORY)
396
405
397
        repository = Repository.objects.get(
406
        repository = Repository.objects.get(
398
            Q(path=repository_path) |
407
            Q(path=repository_path) |
399
            Q(mirror_path=repository_path))
408
            Q(mirror_path=repository_path))
400
409
401
        review_request = ReviewRequest.objects.create(
410
        review_request = ReviewRequest.objects.create(
402
            request.user, repository, request.POST.get('changenum', None))
411
            user, repository, request.POST.get('changenum', None))
403
412
404
        return JsonResponse(request, {'review_request': review_request})
413
        return JsonResponse(request, {'review_request': review_request})
405
    except Repository.DoesNotExist, e:
414
    except Repository.DoesNotExist, e:
406
        return JsonResponseError(request, INVALID_REPOSITORY,
415
        return JsonResponseError(request, INVALID_REPOSITORY,
407
                                 {'repository_path': repository_path})
416
                                 {'repository_path': repository_path})
417
    """
426
    """
418
    Returns the review request with the specified ID.
427
    Returns the review request with the specified ID.
419
    """
428
    """
420
    review_request = get_object_or_404(ReviewRequest, pk=review_request_id)
429
    review_request = get_object_or_404(ReviewRequest, pk=review_request_id)
421
430
422
    if not review_request.public and review_request.submitter != request.user:
431
    if not review_request.is_visible_to(request.user):
423
        return JsonResponseError(request, PERMISSION_DENIED)
432
        return JsonResponseError(request, PERMISSION_DENIED)
424
433
425
    return JsonResponse(request, {'review_request': review_request})
434
    return JsonResponse(request, {'review_request': review_request})
426
435
427
436
428
@json_login_required
437
@json_login_required
429
def review_request_by_changenum(request, repository_id, changenum):
438
def review_request_by_changenum(request, repository_id, changenum):
430
    try:
439
    try:
431
        review_request = ReviewRequest.objects.get(changenum=changenum,
440
        review_request = ReviewRequest.objects.get(changenum=changenum,
432
                                                   repository=repository_id)
441
                                                   repository=repository_id)
433
442
434
        if not review_request.public and \
443
        if not review_request.is_visible_to(request.user):
435
           review_request.submitter != request.user:
436
            return JsonResponseError(request, PERMISSION_DENIED)
444
            return JsonResponseError(request, PERMISSION_DENIED)
437
445
438
        return JsonResponse(request, {'review_request': review_request})
446
        return JsonResponse(request, {'review_request': review_request})
439
    except ReviewRequest.DoesNotExist:
447
    except ReviewRequest.DoesNotExist:
440
        return JsonResponseError(request, INVALID_CHANGE_NUMBER)
448
        return JsonResponseError(request, INVALID_CHANGE_NUMBER)
572
    try:
580
    try:
573
        draft = ReviewRequestDraft.objects.get(review_request=review_request)
581
        draft = ReviewRequestDraft.objects.get(review_request=review_request)
574
    except ReviewRequestDraft.DoesNotExist:
582
    except ReviewRequestDraft.DoesNotExist:
575
        return JsonResponseError(request, DOES_NOT_EXIST)
583
        return JsonResponseError(request, DOES_NOT_EXIST)
576
584
577
    if review_request.submitter != request.user:
585
    if not review_request.is_mutable_by(request.user):
578
        return JsonResponseError(request, PERMISSION_DENIED)
586
        return JsonResponseError(request, PERMISSION_DENIED)
579
587
580
    draft.delete()
588
    draft.delete()
581
589
582
    return JsonResponse(request)
590
    return JsonResponse(request)
589
        draft = ReviewRequestDraft.objects.get(review_request=review_request_id)
597
        draft = ReviewRequestDraft.objects.get(review_request=review_request_id)
590
        review_request = draft.review_request
598
        review_request = draft.review_request
591
    except ReviewRequestDraft.DoesNotExist:
599
    except ReviewRequestDraft.DoesNotExist:
592
        return JsonResponseError(request, DOES_NOT_EXIST)
600
        return JsonResponseError(request, DOES_NOT_EXIST)
593
601
594
    if review_request.submitter != request.user:
602
    if not review_request.is_mutable_by(request.user):
595
        return JsonResponseError(request, PERMISSION_DENIED)
603
        return JsonResponseError(request, PERMISSION_DENIED)
596
604
597
    changes = draft.save_draft()
605
    draft.save_draft()
598
    draft.delete()
606
    draft.delete()
599
607
600
    if settings.SEND_REVIEW_MAIL and changes:
601
        mail_review_request(request.user, review_request, changes)
602
603
    return JsonResponse(request)
608
    return JsonResponse(request)
604
609
605
610
606
def find_user(username):
611
def find_user(username):
607
    try:
612
    try:
616
                return user
621
                return user
617
    return None
622
    return None