def close(self, type, user=None, description=None, rich_text=False): """Closes the review request. The type must be one of SUBMITTED or DISCARDED. """ if (user and not self.is_mutable_by(user) and not user.has_perm("reviews.can_change_status", self.local_site)): raise PermissionError if type not in [self.SUBMITTED, self.DISCARDED]: raise AttributeError("%s is not a valid close type" % type) draft = get_object_or_none(self.draft) if self.status != type: if (draft is not None and not self.public and type == self.DISCARDED): # Copy over the draft information if this is a private discard. draft.copy_fields_to_request(self) # TODO: Use the user's default for rich_text. changedesc = ChangeDescription(public=True, text=description or "", rich_text=rich_text or False) status_field = get_review_request_field('status')(self) status_field.record_change_entry(changedesc, self.status, type) changedesc.save() self.changedescs.add(changedesc) if type == self.SUBMITTED: if not self.public: raise PublishError("The draft must be public first.") else: self.commit_id = None self.status = type self.save(update_counts=True) review_request_closed.send(sender=self.__class__, user=user, review_request=self, type=type) else: # Update submission description. changedesc = self.changedescs.filter(public=True).latest() changedesc.timestamp = timezone.now() changedesc.text = description or "" changedesc.rich_text = rich_text changedesc.save() # Needed to renew last-update. self.save() # Delete the associated draft review request. if draft is not None: draft.delete()
def on_review_request_publishing(user, review_request_draft, **kwargs): # There have been strange cases (all local, and during development), where # when attempting to publish a review request, this handler will fail # because the draft does not exist. This is a really strange case, and not # one we expect to happen in production. However, since we've seen it # locally, we handle it here, and log. if not review_request_draft: logger.error('Strangely, there was no review request draft on the ' 'review request we were attempting to publish.') return # If the review request draft has a new DiffSet we will only allow # publishing if that DiffSet has been verified. It is important to # do this for every review request, not just pushed ones, because # we can't trust the storage mechanism which indicates it was pushed. # TODO: This will be fixed when we transition away from extra_data. if review_request_draft.diffset: try: DiffSetVerification.objects.get( diffset=review_request_draft.diffset) except DiffSetVerification.DoesNotExist: logger.error( 'An attempt was made by User %s to publish an unverified ' 'DiffSet with id %s', user.id, review_request_draft.diffset.id) raise PublishError( 'This review request draft contained a manually uploaded ' 'diff, which is prohibited. Please push to the review server ' 'to create review requests. If you believe you received this ' 'message in error, please file a bug.') review_request = review_request_draft.get_review_request() commit_data = fetch_commit_data(review_request) # skip review requests that were not pushed if not is_pushed(review_request, commit_data=commit_data): return if not is_parent(review_request, commit_data): # Send a signal asking for approval to publish this review request. # We only want to publish this commit request if we are in the middle # of publishing the parent. If the parent is publishing it will be # listening for this signal to approve it. approvals = commit_request_publishing.send_robust( sender=review_request, user=user, review_request_draft=review_request_draft) for receiver, approved in approvals: if approved: break else: # This publish is not approved by the parent review request. raise CommitPublishProhibited() # The reviewid passed through p2rb is, for Mozilla's instance anyway, # bz://<bug id>/<irc nick>. reviewid = commit_data.draft_extra_data.get(IDENTIFIER_KEY, None) m = REVIEWID_RE.match(reviewid) if not m: raise InvalidBugIdError('<unknown>') bug_id = m.group(1) try: bug_id = int(bug_id) except (TypeError, ValueError): raise InvalidBugIdError(bug_id) siteconfig = SiteConfiguration.objects.get_current() using_bugzilla = (siteconfig.settings.get("auth_backend", "builtin") == "bugzilla") if using_bugzilla: commit_data = fetch_commit_data(review_request_draft) publish_as_id = commit_data.draft_extra_data.get(PUBLISH_AS_KEY) if publish_as_id: u = User.objects.get(id=publish_as_id) b = Bugzilla(get_bugzilla_api_key(u)) else: b = Bugzilla(get_bugzilla_api_key(user)) try: if b.is_bug_confidential(bug_id): raise ConfidentialBugError except BugzillaError as e: # Special cases: # 100: Invalid Bug Alias # 101: Bug does not exist if e.fault_code and (e.fault_code == 100 or e.fault_code == 101): raise InvalidBugIdError(bug_id) raise # Note that the bug ID has already been set when the review was created. # If this is a squashed/parent review request, automatically publish all # relevant children. if is_parent(review_request, commit_data): unpublished_rids = map( int, json.loads(commit_data.extra_data[UNPUBLISHED_KEY])) discard_on_publish_rids = map( int, json.loads(commit_data.extra_data[DISCARD_ON_PUBLISH_KEY])) child_rrs = list(gen_child_rrs(review_request_draft)) # Create or update Bugzilla attachments for each draft commit. This # is done before the children are published to ensure that MozReview # doesn't get into a strange state if communication with Bugzilla is # broken or attachment creation otherwise fails. The Bugzilla # attachments will then, of course, be in a weird state, but that # should be fixed by the next successful publish. if using_bugzilla: children_to_post = [] children_to_obsolete = [] for child in child_rrs: child_draft = child.get_draft(user=user) if child_draft: if child.id in discard_on_publish_rids: children_to_obsolete.append(child) children_to_post.append((child_draft, child)) if children_to_post or children_to_obsolete: update_bugzilla_attachments(b, bug_id, children_to_post, children_to_obsolete) # Publish draft commits. This will already include items that are in # unpublished_rids, so we'll remove anything we publish out of # unpublished_rids. for child in child_rrs: if child.get_draft(user=user) or not child.public: def approve_publish(sender, user, review_request_draft, **kwargs): return child is sender # Setup the parent signal handler to approve the publish # and then publish the child. commit_request_publishing.connect(approve_publish, sender=child, weak=False) try: child.publish(user=user) except NotModifiedError: # As we create empty drafts as part of allowing reviewer # delegation, delete these empty drafts instead of # throwing an error. child.get_draft(user=user).delete() finally: commit_request_publishing.disconnect( receiver=approve_publish, sender=child, weak=False) if child.id in unpublished_rids: unpublished_rids.remove(child.id) # The remaining unpubished_rids need to be closed as discarded because # they have never been published, and they will appear in the user's # dashboard unless closed. for child in gen_rrs_by_rids(unpublished_rids): child.close(ReviewRequest.DISCARDED, user=user, description=NEVER_USED_DESCRIPTION) # We also close the discard_on_publish review requests because, well, # we don't need them anymore. We use a slightly different message # though. for child in gen_rrs_by_rids(discard_on_publish_rids): child.close(ReviewRequest.DISCARDED, user=user, description=OBSOLETE_DESCRIPTION) commit_data.extra_data[UNPUBLISHED_KEY] = '[]' commit_data.extra_data[DISCARD_ON_PUBLISH_KEY] = '[]' # Copy any drafted CommitData from draft_extra_data to extra_data. for key in DRAFTED_COMMIT_DATA_KEYS: if key in commit_data.draft_extra_data: commit_data.extra_data[key] = commit_data.draft_extra_data[key] commit_data.save(update_fields=['extra_data']) review_request.save()
def close(self, close_type=None, user=None, description=None, rich_text=False, **kwargs): """Closes the review request. Args: close_type (unicode): How the close occurs. This should be one of :py:attr:`SUBMITTED` or :py:attr:`DISCARDED`. user (django.contrib.auth.models.User): The user who is closing the review request. description (unicode): An optional description that indicates why the review request was closed. rich_text (bool): Indicates whether or not that the description is rich text. Raises: ValueError: The provided close type is not a valid value. PermissionError: The user does not have permission to close the review request. TypeError: Keyword arguments were supplied to the function. .. versionchanged:: 3.0 The ``type`` argument is deprecated: ``close_type`` should be used instead. This method raises :py:exc:`ValueError` instead of :py:exc:`AttributeError` when the ``close_type`` has an incorrect value. """ if close_type is None: try: close_type = kwargs.pop('type') except KeyError: raise AttributeError('close_type must be provided') warnings.warn( 'The "type" argument was deprecated in Review Board 3.0 and ' 'will be removed in a future version. Use "close_type" ' 'instead.') if kwargs: raise TypeError('close() does not accept keyword arguments.') if (user and not self.is_mutable_by(user) and not user.has_perm( "reviews.can_change_status", self.local_site)): raise PermissionError if close_type not in [self.SUBMITTED, self.DISCARDED]: raise ValueError("%s is not a valid close type" % type) review_request_closing.send(sender=type(self), user=user, review_request=self, close_type=close_type, type=deprecated_signal_argument( signal_name='review_request_closing', old_name='type', new_name='close_type', value=close_type), description=description, rich_text=rich_text) draft = get_object_or_none(self.draft) if self.status != close_type: if (draft is not None and not self.public and close_type == self.DISCARDED): # Copy over the draft information if this is a private discard. draft.copy_fields_to_request(self) # TODO: Use the user's default for rich_text. changedesc = ChangeDescription(public=True, text=description or "", rich_text=rich_text or False, user=user or self.submitter) status_field = get_review_request_field('status')(self) status_field.record_change_entry(changedesc, self.status, close_type) changedesc.save() self.changedescs.add(changedesc) if close_type == self.SUBMITTED: if not self.public: raise PublishError("The draft must be public first.") else: self.commit_id = None self.status = close_type self.save(update_counts=True) review_request_closed.send(sender=type(self), user=user, review_request=self, close_type=close_type, type=deprecated_signal_argument( signal_name='review_request_closed', old_name='type', new_name='close_type', value=close_type), description=description, rich_text=rich_text) else: # Update submission description. changedesc = self.changedescs.filter(public=True).latest() changedesc.timestamp = timezone.now() changedesc.text = description or "" changedesc.rich_text = rich_text changedesc.save() # Needed to renew last-update. self.save() # Delete the associated draft review request. if draft is not None: draft.delete()
def publish(self, review_request=None, user=None, trivial=False, send_notification=True, validate_fields=True, timestamp=None): """Publish this draft. This is an internal method. Programmatic publishes should use :py:meth:`reviewboard.reviews.models.review_request.ReviewRequest.publish` instead. This updates and returns the draft's ChangeDescription, which contains the changed fields. This is used by the e-mail template to tell people what's new and interesting. The keys that may be saved in ``fields_changed`` in the ChangeDescription are: * ``submitter`` * ``summary`` * ``description`` * ``testing_done`` * ``bugs_closed`` * ``depends_on`` * ``branch`` * ``target_groups`` * ``target_people`` * ``screenshots`` * ``screenshot_captions`` * ``diff`` * Any custom field IDs Each field in 'fields_changed' represents a changed field. This will save fields in the standard formats as defined by the 'ChangeDescription' documentation, with the exception of the 'screenshot_captions' and 'diff' fields. For the 'screenshot_captions' field, the value will be a dictionary of screenshot ID/dict pairs with the following fields: * ``old``: The old value of the field * ``new``: The new value of the field For the ``diff`` field, there is only ever an ``added`` field, containing the ID of the new diffset. Args: review_request (reviewboard.reviews.models.review_request. ReviewRequest, optional): The review request associated with this diff. If not provided, it will be looked up. user (django.contrib.auth.models.User, optional): The user publishing the draft. If not provided, this defaults to the review request submitter. trivial (bool, optional): Whether or not this is a trivial publish. Trivial publishes do not result in e-mail notifications. send_notification (bool, optional): Whether or not this will emit the :py:data:`reviewboard.reviews.signals.review_request_published` signal. This parameter is intended for internal use **only**. validate_fields (bool, optional): Whether or not the fields should be validated. This should only be ``False`` in the case of programmatic publishes, e.g., from close as submitted hooks. timestamp (datetime.datetime, optional): The datetime that should be used for all timestamps for objects published (:py:class:`~reviewboard.diffviewer.models.diff_set.DiffSet`, :py:class:`~reviewboard.changedescs.models.ChangeDescription`) over the course of the method. Returns: reviewboard.changedescs.models.ChangeDescription: The change description that results from this publish (if any). If this is an initial publish, there will be no change description (and this function will return ``None``). """ if timestamp is None: timestamp = timezone.now() if not review_request: review_request = self.review_request if not self.changedesc and review_request.public: self.changedesc = ChangeDescription() if not user: if self.changedesc: user = self.changedesc.get_user(self) else: user = review_request.submitter self.copy_fields_to_request(review_request) # If no changes were made, raise exception and do not save if self.changedesc and not self.changedesc.has_modified_fields(): raise NotModifiedError() if validate_fields: if not (self.target_groups.exists() or self.target_people.exists()): raise PublishError( ugettext('There must be at least one reviewer before this ' 'review request can be published.')) if not review_request.summary.strip(): raise PublishError( ugettext('The draft must have a summary.')) if not review_request.description.strip(): raise PublishError( ugettext('The draft must have a description.')) if self.diffset: self.diffset.history = review_request.diffset_history self.diffset.timestamp = timestamp self.diffset.save(update_fields=('history', 'timestamp')) if self.changedesc: self.changedesc.user = user self.changedesc.timestamp = timestamp self.changedesc.public = True self.changedesc.save() review_request.changedescs.add(self.changedesc) review_request.description_rich_text = self.description_rich_text review_request.testing_done_rich_text = self.testing_done_rich_text review_request.rich_text = self.rich_text review_request.save() if send_notification: review_request_published.send(sender=type(review_request), user=user, review_request=review_request, trivial=trivial, changedesc=self.changedesc) return self.changedesc
def publish(self, review_request=None, user=None, trivial=False, send_notification=True): """Publishes this draft. This updates and returns the draft's ChangeDescription, which contains the changed fields. This is used by the e-mail template to tell people what's new and interesting. The draft's associated ReviewRequest object will be used if one isn't passed in. The keys that may be saved in ``fields_changed`` in the ChangeDescription are: * ``submitter`` * ``summary`` * ``description`` * ``testing_done`` * ``bugs_closed`` * ``depends_on`` * ``branch`` * ``target_groups`` * ``target_people`` * ``screenshots`` * ``screenshot_captions`` * ``diff`` * Any custom field IDs Each field in 'fields_changed' represents a changed field. This will save fields in the standard formats as defined by the 'ChangeDescription' documentation, with the exception of the 'screenshot_captions' and 'diff' fields. For the 'screenshot_captions' field, the value will be a dictionary of screenshot ID/dict pairs with the following fields: * ``old``: The old value of the field * ``new``: The new value of the field For the ``diff`` field, there is only ever an ``added`` field, containing the ID of the new diffset. The ``send_notification`` parameter is intended for internal use only, and is there to prevent duplicate notifications when being called by ReviewRequest.publish. """ if not review_request: review_request = self.review_request if not self.changedesc and review_request.public: self.changedesc = ChangeDescription() if not user: if self.changedesc: user = self.changedesc.get_user(self) else: user = review_request.submitter self.copy_fields_to_request(review_request) if self.diffset: self.diffset.history = review_request.diffset_history self.diffset.save(update_fields=['history']) # If no changes were made, raise exception and do not save if self.changedesc and not self.changedesc.has_modified_fields(): raise NotModifiedError() if not (self.target_groups.exists() or self.target_people.exists()): raise PublishError( ugettext('There must be at least one reviewer before this ' 'review request can be published.')) if not review_request.summary.strip(): raise PublishError(ugettext('The draft must have a summary.')) if not review_request.description.strip(): raise PublishError(ugettext('The draft must have a description.')) if self.changedesc: self.changedesc.user = user self.changedesc.timestamp = timezone.now() self.changedesc.public = True self.changedesc.save() review_request.changedescs.add(self.changedesc) review_request.description_rich_text = self.description_rich_text review_request.testing_done_rich_text = self.testing_done_rich_text review_request.rich_text = self.rich_text review_request.save() if send_notification: review_request_published.send(sender=review_request.__class__, user=user, review_request=review_request, trivial=trivial, changedesc=self.changedesc) return self.changedesc
def __init__(self): PublishError.__init__(self, 'Exactly one bug ID must be provided.')
def __init__(self): PublishError.__init__(self, 'This bug is confidential; please attach ' 'the patch directly to the bug.')
def __init__(self): PublishError.__init__(self, '"Ship it" reviews on parent review ' 'requests are not allowed. Please review ' 'individual commits.')
def _transform_errors(*args, **kwargs): try: return func(*args, **kwargs) except BugzillaError as e: raise PublishError('Bugzilla error: %s' % e.msg)
def __init__(self): PublishError.__init__( self, 'This bug is confidential; please attach ' 'the patch directly to the bug.')
def __init__(self, bug_id): PublishError.__init__(self, 'Invalid bug ID "%s".' % bug_id)
def __init__(self): PublishError.__init__(self, 'Publishing commit review requests is ' 'prohibited, please publish parent.')
def __init__(self, username): PublishError.__init__(self, 'Chosen reviewer (%s) is assigned to a ' 'non-existing Bugzilla user. If you believe you ' 'received this message in error, please ask for ' 'help on IRC#mozreview.' % username)