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)
- /trunk/reviewboard/diffviewer/forms.py: 3 changes [ 1 2 3 ]
- /trunk/reviewboard/reviews/models.py: 5 changes [ 1 2 3 4 5 ]
- /trunk/reviewboard/reviews/views.py: 4 changes [ 1 2 3 4 ]
- /trunk/reviewboard/scmtools/svn.py: 4 changes [ 1 2 3 4 ]
- /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 | ||
| ... | 37 lines hidden [Expand] | ||
| 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 | ||
| ... | 48 lines hidden [Expand] | ||
| /trunk/reviewboard/reviews/models.py | |||
|---|---|---|---|
| Revision 1137 | New Change | ||
| 1 |
|
1 |
|
| 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 |
| ... | 4 lines hidden [Expand] | ||
| 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) |
| ... | 367 lines hidden [Expand] | ||
| 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') |
| ... | 221 lines hidden [Expand] | ||
| 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',) |
| ... | 209 lines hidden [Expand] | ||
| 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. |
| ... | 20 lines hidden [Expand] | ||
| /trunk/reviewboard/reviews/views.py | |||
|---|---|---|---|
| Revision 1137 | New Change | ||
| ... | 326 lines hidden [Expand] | ||
| 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. |
| ... | 9 lines hidden [Expand] | ||
| 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 | ||
| ... | 211 lines hidden [Expand] | ||
| /trunk/reviewboard/scmtools/svn.py | |||
|---|---|---|---|
| Revision 1137 | New Change | ||
| 1 |
|
1 |
|
| 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 | ||
| ... | 26 lines hidden [Expand] | ||
| 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 |
| ... | 84 lines hidden [Expand] | ||
| /trunk/reviewboard/webapi/json.py | |||
|---|---|---|---|
| Revision 1137 | New Change | ||
| ... | 16 lines hidden [Expand] | ||
| 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, \ |
| ... | 23 lines hidden [Expand] | ||
| 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 " + |
| ... | 325 lines hidden [Expand] | ||
| 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}) |
| ... | 9 lines hidden [Expand] | ||
| 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) |
| ... | 131 lines hidden [Expand] | ||
| 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) |
| ... | 6 lines hidden [Expand] | ||
| 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: |
| ... | 8 lines hidden [Expand] | ||
| 616 | return user |
621 | return user |
| 617 | return None |
622 | return None |