Exemplo n.º 1
0
    def _update_review(self,
                       request,
                       review,
                       public=None,
                       extra_fields={},
                       *args,
                       **kwargs):
        """Common function to update fields on a draft review."""
        if not self.has_modify_permissions(request, review):
            # Can't modify published reviews or those not belonging
            # to the user.
            return self._no_access_error(request.user)

        if 'ship_it' in kwargs:
            review.ship_it = kwargs['ship_it']

        self.set_text_fields(review, 'body_top', **kwargs)
        self.set_text_fields(review, 'body_bottom', **kwargs)

        self.import_extra_data(review, review.extra_data, extra_fields)

        review.save()

        if public:
            try:
                review.publish(user=request.user)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(e.msg)

        return 200, {
            self.item_result_key: review,
        }
Exemplo n.º 2
0
    def _update_review(self, request, review, public=None, extra_fields={},
                       *args, **kwargs):
        """Common function to update fields on a draft review."""
        if not self.has_modify_permissions(request, review):
            # Can't modify published reviews or those not belonging
            # to the user.
            return self._no_access_error(request.user)

        if 'ship_it' in kwargs:
            review.ship_it = kwargs['ship_it']

        self.set_text_fields(review, 'body_top', **kwargs)
        self.set_text_fields(review, 'body_bottom', **kwargs)

        self.import_extra_data(review, review.extra_data, extra_fields)

        review.save()

        if public:
            try:
                review.publish(user=request.user)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(e.msg)

        return 200, {
            self.item_result_key: review,
        }
Exemplo n.º 3
0
    def _update_reply(self,
                      request,
                      reply,
                      public=None,
                      extra_fields={},
                      *args,
                      **kwargs):
        """Common function to update fields on a draft reply."""
        if not self.has_modify_permissions(request, reply):
            # Can't modify published replies or those not belonging
            # to the user.
            return self._no_access_error(request.user)

        old_rich_text = reply.rich_text

        for field in ('body_top', 'body_bottom'):
            value = kwargs.get(field, None)

            if value is not None:
                setattr(reply, field, value.strip())

                if value == '':
                    reply_to = None
                else:
                    reply_to = reply.base_reply_to

                setattr(reply, '%s_reply_to' % field, reply_to)

        if 'text_type' in kwargs:
            reply.rich_text = (kwargs['text_type'] == self.TEXT_TYPE_MARKDOWN)

        self.normalize_markdown_fields(reply, ['body_top', 'body_bottom'],
                                       old_rich_text, **kwargs)

        self._import_extra_data(reply.extra_data, extra_fields)

        if public:
            try:
                reply.publish(user=request.user)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(e.msg)

        else:
            reply.save()

        return 200, {
            self.item_result_key: reply,
        }, {
            'Last-Modified': self.get_last_modified(request, reply),
        }
Exemplo n.º 4
0
    def _update_reply(self,
                      request,
                      reply,
                      public=None,
                      trivial=False,
                      extra_fields={},
                      *args,
                      **kwargs):
        """Common function to update fields on a draft reply."""
        if not self.has_modify_permissions(request, reply):
            # Can't modify published replies or those not belonging
            # to the user.
            return self.get_no_access_error(request)

        for field in ('body_top', 'body_bottom'):
            value = kwargs.get(field, None)

            if value is not None:
                if value == '':
                    reply_to = None
                else:
                    reply_to = reply.base_reply_to

                setattr(reply, '%s_reply_to' % field, reply_to)

        self.set_text_fields(reply, 'body_top', **kwargs)
        self.set_text_fields(reply, 'body_bottom', **kwargs)

        try:
            self.import_extra_data(reply, reply.extra_data, extra_fields)
        except ImportExtraDataError as e:
            return e.error_payload

        if public:
            try:
                reply.publish(user=request.user, trivial=trivial)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(six.text_type(e))

        else:
            reply.save()

        return 200, {
            self.item_result_key: reply,
        }, {
            'Last-Modified': self.get_last_modified(request, reply),
        }
Exemplo n.º 5
0
    def _update_reply(self, request, reply, public=None, extra_fields={},
                      *args, **kwargs):
        """Common function to update fields on a draft reply."""
        if not self.has_modify_permissions(request, reply):
            # Can't modify published replies or those not belonging
            # to the user.
            return self._no_access_error(request.user)

        old_rich_text = reply.rich_text

        for field in ('body_top', 'body_bottom'):
            value = kwargs.get(field, None)

            if value is not None:
                setattr(reply, field, value.strip())

                if value == '':
                    reply_to = None
                else:
                    reply_to = reply.base_reply_to

                setattr(reply, '%s_reply_to' % field, reply_to)

        if 'text_type' in kwargs:
            reply.rich_text = (kwargs['text_type'] == self.TEXT_TYPE_MARKDOWN)

        self.normalize_markdown_fields(reply, ['body_top', 'body_bottom'],
                                       old_rich_text, **kwargs)

        self._import_extra_data(reply.extra_data, extra_fields)

        if public:
            try:
                reply.publish(user=request.user)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(e.msg)

        else:
            reply.save()

        return 200, {
            self.item_result_key: reply,
        }, {
            'Last-Modified': self.get_last_modified(request, reply),
        }
Exemplo n.º 6
0
    def _update_review(self,
                       request,
                       review,
                       public=None,
                       extra_fields={},
                       *args,
                       **kwargs):
        """Common function to update fields on a draft review."""
        if not self.has_modify_permissions(request, review):
            # Can't modify published reviews or those not belonging
            # to the user.
            return self._no_access_error(request.user)

        old_rich_text = review.rich_text

        for field in ('ship_it', 'body_top', 'body_bottom'):
            value = kwargs.get(field, None)

            if value is not None:
                if isinstance(value, six.string_types):
                    value = value.strip()

                setattr(review, field, value)

        if 'text_type' in kwargs:
            review.rich_text = \
                (kwargs['text_type'] == self.TEXT_TYPE_MARKDOWN)

        self.normalize_markdown_fields(review, ['body_top', 'body_bottom'],
                                       old_rich_text, **kwargs)

        self._import_extra_data(review.extra_data, extra_fields)

        review.save()

        if public:
            try:
                review.publish(user=request.user)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(e.msg)

        return 200, {
            self.item_result_key: review,
        }
Exemplo n.º 7
0
    def _update_reply(self, request, reply, public=None, trivial=False,
                      extra_fields={}, *args, **kwargs):
        """Common function to update fields on a draft reply."""
        if not self.has_modify_permissions(request, reply):
            # Can't modify published replies or those not belonging
            # to the user.
            return self.get_no_access_error(request)

        for field in ('body_top', 'body_bottom'):
            value = kwargs.get(field, None)

            if value is not None:
                if value == '':
                    reply_to = None
                else:
                    reply_to = reply.base_reply_to

                setattr(reply, '%s_reply_to' % field, reply_to)

        self.set_text_fields(reply, 'body_top', **kwargs)
        self.set_text_fields(reply, 'body_bottom', **kwargs)

        try:
            self.import_extra_data(reply, reply.extra_data, extra_fields)
        except ImportExtraDataError as e:
            return e.error_payload

        if public:
            try:
                reply.publish(user=request.user, trivial=trivial)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(six.text_type(e))

        else:
            reply.save()

        return 200, {
            self.item_result_key: reply,
        }, {
            'Last-Modified': self.get_last_modified(request, reply),
        }
Exemplo n.º 8
0
    def _update_review(self, request, review, public=None, extra_fields={},
                       *args, **kwargs):
        """Common function to update fields on a draft review."""
        if not self.has_modify_permissions(request, review):
            # Can't modify published reviews or those not belonging
            # to the user.
            return self._no_access_error(request.user)

        old_rich_text = review.rich_text

        for field in ('ship_it', 'body_top', 'body_bottom'):
            value = kwargs.get(field, None)

            if value is not None:
                if isinstance(value, six.string_types):
                    value = value.strip()

                setattr(review, field, value)

        if 'text_type' in kwargs:
            review.rich_text = \
                (kwargs['text_type'] == self.TEXT_TYPE_MARKDOWN)

        self.normalize_markdown_fields(review, ['body_top', 'body_bottom'],
                                       old_rich_text, **kwargs)

        self._import_extra_data(review.extra_data, extra_fields)

        review.save()

        if public:
            try:
                review.publish(user=request.user)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(e.msg)

        return 200, {
            self.item_result_key: review,
        }
Exemplo n.º 9
0
    def update_review(self,
                      request,
                      review,
                      public=None,
                      publish_to_submitter_only=False,
                      extra_fields={},
                      ship_it=None,
                      *args,
                      **kwargs):
        """Common function to update fields on a draft review."""
        if not self.has_modify_permissions(request, review):
            # Can't modify published reviews or those not belonging
            # to the user.
            return self.get_no_access_error(request)

        if ship_it is not None:
            review.ship_it = ship_it

        self.set_text_fields(review, 'body_top', **kwargs)
        self.set_text_fields(review, 'body_bottom', **kwargs)

        self.import_extra_data(review, review.extra_data, extra_fields)

        review.save()

        if public:
            try:
                review.publish(user=request.user,
                               to_submitter_only=publish_to_submitter_only,
                               request=request)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(six.text_type(e))

        return 200, {
            self.item_result_key: review,
        }
Exemplo n.º 10
0
    def update(self,
               request,
               always_save=False,
               local_site_name=None,
               update_from_commit_id=False,
               trivial=None,
               publish_as_owner=False,
               extra_fields={},
               *args,
               **kwargs):
        """Updates a draft of a review request.

        This will update the draft with the newly provided data.

        Most of the fields correspond to fields in the review request, but
        there is one special one, ``public``. When ``public`` is set to true,
        the draft will be published, moving the new content to the
        review request itself, making it public, and sending out a notification
        (such as an e-mail) if configured on the server. The current draft will
        then be deleted.

        Extra data can be stored later lookup. See
        :ref:`webapi2.0-extra-data` for more information.
        """
        try:
            review_request = resources.review_request.get_object(
                request, local_site_name=local_site_name, *args, **kwargs)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if kwargs.get('commit_id') == '':
            kwargs['commit_id'] = None

        commit_id = kwargs.get('commit_id', None)

        try:
            draft = self.prepare_draft(request, review_request)
        except PermissionDenied:
            return self.get_no_access_error(request)

        if (commit_id and commit_id != review_request.commit_id
                and commit_id != draft.commit_id):
            # Check to make sure the new commit ID isn't being used already
            # in another review request or draft.
            repository = review_request.repository

            existing_review_request = ReviewRequest.objects.filter(
                commit_id=commit_id, repository=repository)

            if (existing_review_request
                    and existing_review_request != review_request):
                return COMMIT_ID_ALREADY_EXISTS

            existing_draft = ReviewRequestDraft.objects.filter(
                commit_id=commit_id, review_request__repository=repository)

            if existing_draft and existing_draft != draft:
                return COMMIT_ID_ALREADY_EXISTS

        modified_objects = []
        invalid_fields = {}

        for field_name, field_info in six.iteritems(self.fields):
            if (field_info.get('mutable', True)
                    and kwargs.get(field_name, None) is not None):
                field_result, field_modified_objects, invalid = \
                    self._set_draft_field_data(draft, field_name,
                                               kwargs[field_name],
                                               local_site_name, request)

                if invalid:
                    invalid_fields[field_name] = invalid
                elif field_modified_objects:
                    modified_objects += field_modified_objects

        if commit_id and update_from_commit_id:
            try:
                draft.update_from_commit_id(commit_id)
            except InvalidChangeNumberError:
                return INVALID_CHANGE_NUMBER

        if draft.changedesc_id:
            changedesc = draft.changedesc
            modified_objects.append(draft.changedesc)

            self.set_text_fields(changedesc,
                                 'changedescription',
                                 text_model_field='text',
                                 rich_text_field_name='rich_text',
                                 **kwargs)

        self.set_text_fields(draft, 'description', **kwargs)
        self.set_text_fields(draft, 'testing_done', **kwargs)

        for field_cls in get_review_request_fields():
            if (not issubclass(field_cls, BuiltinFieldMixin)
                    and getattr(field_cls, 'enable_markdown', False)):
                self.set_extra_data_text_fields(draft, field_cls.field_id,
                                                extra_fields, **kwargs)

        try:
            self.import_extra_data(draft, draft.extra_data, extra_fields)
        except ImportExtraDataError as e:
            return e.error_payload

        if always_save or not invalid_fields:
            for obj in set(modified_objects):
                obj.save()

            draft.save()

        if invalid_fields:
            return INVALID_FORM_DATA, {
                'fields': invalid_fields,
                self.item_result_key: draft,
            }

        if request.POST.get('public', False):
            if not review_request.public and not draft.changedesc_id:
                # This is a new review request. Publish this on behalf of the
                # owner of the review request, rather than the current user,
                # regardless of the original publish_as_owner in the request.
                # This allows a review request previously created with
                # submit-as= to be published by that user instead of the
                # logged in user.
                publish_as_owner = True

            if publish_as_owner:
                publish_user = review_request.owner
            else:
                # Default to posting as the logged in user.
                publish_user = request.user

            try:
                review_request.publish(user=publish_user, trivial=trivial)
            except NotModifiedError:
                return NOTHING_TO_PUBLISH
            except PublishError as e:
                return PUBLISH_ERROR.with_message(six.text_type(e))

        return 200, {
            self.item_result_key: draft,
        }
Exemplo n.º 11
0
    def update(self,
               request,
               local_site_name=None,
               branch=None,
               bugs_closed=None,
               changedescription=None,
               commit_id=None,
               depends_on=None,
               submitter=None,
               summary=None,
               target_groups=None,
               target_people=None,
               update_from_commit_id=False,
               trivial=None,
               publish_as_owner=False,
               extra_fields={},
               *args,
               **kwargs):
        """Updates a draft of a review request.

        This will update the draft with the newly provided data.

        Most of the fields correspond to fields in the review request, but
        there is one special one, ``public``. When ``public`` is set to true,
        the draft will be published, moving the new content to the
        review request itself, making it public, and sending out a notification
        (such as an e-mail) if configured on the server. The current draft will
        then be deleted.

        Extra data can be stored later lookup. See
        :ref:`webapi2.0-extra-data` for more information.
        """
        try:
            review_request = resources.review_request.get_object(
                request, local_site_name=local_site_name, *args, **kwargs)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if not review_request.is_mutable_by(request.user):
            return self.get_no_access_error(request)

        draft = review_request.get_draft()

        # Before we update anything, sanitize the commit ID, see if it
        # changed, and make sure that the the new value isn't owned by
        # another review request or draft.
        if commit_id == '':
            commit_id = None

        if (commit_id and commit_id != review_request.commit_id
                and (draft is None or commit_id != draft.commit_id)):
            # The commit ID has changed, so now we check for other usages of
            # this ID.
            repository = review_request.repository

            existing_review_request_ids = (ReviewRequest.objects.filter(
                commit_id=commit_id,
                repository=repository).values_list('pk', flat=True))

            if (existing_review_request_ids
                    and review_request.pk not in existing_review_request_ids):
                # Another review request is using this ID. Error out.
                return COMMIT_ID_ALREADY_EXISTS

            existing_draft_ids = (ReviewRequestDraft.objects.filter(
                commit_id=commit_id,
                review_request__repository=repository).values_list('pk',
                                                                   flat=True))

            if (existing_draft_ids
                    and (draft is None or draft.pk not in existing_draft_ids)):
                # Another review request draft is using this ID. Error out.
                return COMMIT_ID_ALREADY_EXISTS

        # Now that we've completed our initial accessibility and conflict
        # checks, we can start checking for changes to individual fields.
        #
        # We'll keep track of state pertaining to the fields we want to
        # set/save, and any errors we hit. For setting/saving, these's two
        # types of things we're tracking: The new field values (which will be
        # applied to the objects or Many-To-Many relations) and a list of
        # field names to set when calling `save(update_fields=...)`. The
        # former implies the latter. The latter only needs to be directly
        # set if the fields are modified directly by another function.
        new_draft_values = {}
        new_changedesc_values = {}
        draft_update_fields = set()
        changedesc_update_fields = set()
        invalid_fields = {}

        if draft is None:
            draft = ReviewRequestDraft.create(review_request=review_request)

        # Check for a new value for branch.
        if branch is not None:
            new_draft_values['branch'] = branch

        # Check for a new value for bugs_closed:
        if bugs_closed is not None:
            new_draft_values['bugs_closed'] = \
                ','.join(self._parse_bug_list(bugs_closed))

        # Check for a new value for changedescription.
        if changedescription is not None and draft.changedesc_id is None:
            invalid_fields['changedescription'] = [
                'Change descriptions cannot be used for drafts of '
                'new review requests'
            ]

        # Check for a new value for commit_id.
        if commit_id is not None:
            new_draft_values['commit_id'] = commit_id

            if update_from_commit_id:
                try:
                    draft_update_fields.update(
                        draft.update_from_commit_id(commit_id))
                except InvalidChangeNumberError:
                    return INVALID_CHANGE_NUMBER

        # Check for a new value for depends_on.
        if depends_on is not None:
            found_deps, missing_dep_ids = self._find_depends_on(
                dep_ids=self._parse_value_list(depends_on), request=request)

            if missing_dep_ids:
                invalid_fields['depends_on'] = missing_dep_ids
            else:
                new_draft_values['depends_on'] = found_deps

        # Check for a new value for submitter.
        if submitter is not None:
            # While we only allow for one submitter, we'll try to parse a
            # possible list of values. This allows us to provide a suitable
            # error message if users try to set more than one submitter
            # (which people do try, in practice).
            found_users, missing_usernames = self._find_users(
                usernames=self._parse_value_list(submitter), request=request)

            if len(found_users) + len(missing_usernames) > 1:
                invalid_fields['submitter'] = [
                    'Only one user can be set as the owner of a review '
                    'request'
                ]
            elif missing_usernames:
                assert len(missing_usernames) == 1
                invalid_fields['submitter'] = missing_usernames
            elif found_users:
                assert len(found_users) == 1
                new_draft_values['owner'] = found_users[0]
            else:
                invalid_fields['submitter'] = [
                    'The owner of a review request cannot be blank'
                ]

        # Check for a new value for summary.
        if summary is not None:
            if '\n' in summary:
                invalid_fields['summary'] = [
                    "The summary can't contain a newline"
                ]
            else:
                new_draft_values['summary'] = summary

        # Check for a new value for target_groups.
        if target_groups is not None:
            found_groups, missing_group_names = self._find_review_groups(
                group_names=self._parse_value_list(target_groups),
                request=request)

            if missing_group_names:
                invalid_fields['target_groups'] = missing_group_names
            else:
                new_draft_values['target_groups'] = found_groups

        # Check for a new value for target_people.
        if target_people is not None:
            found_users, missing_usernames = self._find_users(
                usernames=self._parse_value_list(target_people),
                request=request)

            if missing_usernames:
                invalid_fields['target_people'] = missing_usernames
            else:
                new_draft_values['target_people'] = found_users

        # See if we've caught any invalid values. If so, we can error out
        # immediately before we update anything else.
        if invalid_fields:
            return INVALID_FORM_DATA, {
                'fields': invalid_fields,
                self.item_result_key: draft,
            }

        # Start applying any rich text processing to any text fields on the
        # ChangeDescription and draft. We'll track any fields that get set
        # for later saving.
        #
        # NOTE: If any text fields or text type fields are ever made
        #       parameters of this method, then their values will need to be
        #       passed directly to set_text_fields() calls below.
        if draft.changedesc_id:
            changedesc_update_fields.update(
                self.set_text_fields(obj=draft.changedesc,
                                     text_field='changedescription',
                                     text_model_field='text',
                                     rich_text_field_name='rich_text',
                                     changedescription=changedescription,
                                     **kwargs))

        for text_field in ('description', 'testing_done'):
            draft_update_fields.update(
                self.set_text_fields(obj=draft,
                                     text_field=text_field,
                                     **kwargs))

        # Go through the list of Markdown-enabled custom fields and apply
        # any rich text processing. These will go in extra_data, so we'll
        # want to make sure that's tracked for saving.
        for field_cls in get_review_request_fields():
            if (not issubclass(field_cls, BuiltinFieldMixin)
                    and getattr(field_cls, 'enable_markdown', False)):
                modified_fields = self.set_extra_data_text_fields(
                    obj=draft,
                    text_field=field_cls.field_id,
                    extra_fields=extra_fields,
                    **kwargs)

                if modified_fields:
                    draft_update_fields.add('extra_data')

        # See if the caller has set or patched extra_data values. For
        # compatibility, we're going to do this after processing the rich
        # text fields ine extra_data above.
        try:
            if self.import_extra_data(draft, draft.extra_data, extra_fields):
                # Track extra_data for saving.
                draft_update_fields.add('extra_data')
        except ImportExtraDataError as e:
            return e.error_payload

        # Everything checks out. We can now begin the process of applying any
        # field changes and then save objects.
        #
        # We'll start by making an initial pass on the objects we need to
        # either care about. This optimistically lets us avoid a lookup on the
        # ChangeDescription, if it's not being modified.
        to_apply = []

        if draft_update_fields or new_draft_values:
            # If there's any changes made at all to the draft, make sure we
            # allow last_updated to be computed and saved.
            if draft_update_fields or new_draft_values:
                draft_update_fields.add('last_updated')

            to_apply.append((draft, draft_update_fields, new_draft_values))

        if changedesc_update_fields or new_changedesc_values:
            to_apply.append((draft.changedesc, changedesc_update_fields,
                             new_changedesc_values))

        for obj, update_fields, new_values in to_apply:
            new_m2m_values = {}

            # We may have a mixture of field values and Many-To-Many
            # relation values, which we want to set only after the object
            # is saved. Start by setting any field values, and store the
            # M2M values for after.
            for key, value in six.iteritems(new_values):
                field = obj._meta.get_field(key)

                if isinstance(field, ManyToManyField):
                    # Save this until after the object is saved.
                    new_m2m_values[key] = value
                else:
                    # We can set this one now, and mark it for saving.
                    setattr(obj, key, value)
                    update_fields.add(key)

            if update_fields:
                obj.save(update_fields=sorted(update_fields))

            # Now we can set any values on M2M fields.
            #
            # Each entry will have zero or more values. We'll be
            # setting to the list of values, which will fully replace
            # the stored entries in the database.
            for key, values in six.iteritems(new_m2m_values):
                setattr(obj, key, values)

        # Next, check if the draft is set to be published.
        if request.POST.get('public', False):
            if not review_request.public and not draft.changedesc_id:
                # This is a new review request. Publish this on behalf of the
                # owner of the review request, rather than the current user,
                # regardless of the original publish_as_owner in the request.
                # This allows a review request previously created with
                # submit-as= to be published by that user instead of the
                # logged in user.
                publish_as_owner = True

            if publish_as_owner:
                publish_user = review_request.owner
            else:
                # Default to posting as the logged in user.
                publish_user = request.user

            try:
                review_request.publish(user=publish_user, trivial=trivial)
            except NotModifiedError:
                return NOTHING_TO_PUBLISH
            except PublishError as e:
                return PUBLISH_ERROR.with_message(six.text_type(e))

        return 200, {
            self.item_result_key: draft,
        }
Exemplo n.º 12
0
    def update(self,
               request,
               local_site_name=None,
               branch=None,
               bugs_closed=None,
               changedescription=None,
               commit_id=None,
               depends_on=None,
               submitter=None,
               summary=None,
               target_groups=None,
               target_people=None,
               update_from_commit_id=False,
               trivial=None,
               publish_as_owner=False,
               extra_fields={},
               *args,
               **kwargs):
        """Updates a draft of a review request.

        This will update the draft with the newly provided data.

        Most of the fields correspond to fields in the review request, but
        there is one special one, ``public``. When ``public`` is set to true,
        the draft will be published, moving the new content to the
        review request itself, making it public, and sending out a notification
        (such as an e-mail) if configured on the server. The current draft will
        then be deleted.

        Extra data can be stored later lookup. See
        :ref:`webapi2.0-extra-data` for more information.
        """
        try:
            review_request = resources.review_request.get_object(
                request, local_site_name=local_site_name, *args, **kwargs)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if not review_request.is_mutable_by(request.user):
            return self.get_no_access_error(request)

        draft = review_request.get_draft()

        # Before we update anything, sanitize the commit ID, see if it
        # changed, and make sure that the the new value isn't owned by
        # another review request or draft.
        if commit_id == '':
            commit_id = None

        if (commit_id and
            commit_id != review_request.commit_id and
            (draft is None or commit_id != draft.commit_id)):
            # The commit ID has changed, so now we check for other usages of
            # this ID.
            repository = review_request.repository

            existing_review_request_ids = (
                ReviewRequest.objects
                .filter(commit_id=commit_id,
                        repository=repository)
                .values_list('pk', flat=True)
            )

            if (existing_review_request_ids and
                review_request.pk not in existing_review_request_ids):
                # Another review request is using this ID. Error out.
                return COMMIT_ID_ALREADY_EXISTS

            existing_draft_ids = (
                ReviewRequestDraft.objects
                .filter(commit_id=commit_id,
                        review_request__repository=repository)
                .values_list('pk', flat=True)
            )

            if (existing_draft_ids and
                (draft is None or draft.pk not in existing_draft_ids)):
                # Another review request draft is using this ID. Error out.
                return COMMIT_ID_ALREADY_EXISTS

        # Now that we've completed our initial accessibility and conflict
        # checks, we can start checking for changes to individual fields.
        #
        # We'll keep track of state pertaining to the fields we want to
        # set/save, and any errors we hit. For setting/saving, these's two
        # types of things we're tracking: The new field values (which will be
        # applied to the objects or Many-To-Many relations) and a list of
        # field names to set when calling `save(update_fields=...)`. The
        # former implies the latter. The latter only needs to be directly
        # set if the fields are modified directly by another function.
        new_draft_values = {}
        new_changedesc_values = {}
        draft_update_fields = set()
        changedesc_update_fields = set()
        invalid_fields = {}

        if draft is None:
            draft = ReviewRequestDraft.create(review_request=review_request)

        # Check for a new value for branch.
        if branch is not None:
            new_draft_values['branch'] = branch

        # Check for a new value for bugs_closed:
        if bugs_closed is not None:
            new_draft_values['bugs_closed'] = \
                ','.join(self._parse_bug_list(bugs_closed))

        # Check for a new value for changedescription.
        if changedescription is not None and draft.changedesc_id is None:
            invalid_fields['changedescription'] = [
                'Change descriptions cannot be used for drafts of '
                'new review requests'
            ]

        # Check for a new value for commit_id.
        if commit_id is not None:
            new_draft_values['commit_id'] = commit_id

            if update_from_commit_id:
                try:
                    draft_update_fields.update(
                        draft.update_from_commit_id(commit_id))
                except InvalidChangeNumberError:
                    return INVALID_CHANGE_NUMBER

        # Check for a new value for depends_on.
        if depends_on is not None:
            found_deps, missing_dep_ids = self._find_depends_on(
                dep_ids=self._parse_value_list(depends_on),
                request=request)

            if missing_dep_ids:
                invalid_fields['depends_on'] = missing_dep_ids
            else:
                new_draft_values['depends_on'] = found_deps

        # Check for a new value for submitter.
        if submitter is not None:
            # While we only allow for one submitter, we'll try to parse a
            # possible list of values. This allows us to provide a suitable
            # error message if users try to set more than one submitter
            # (which people do try, in practice).
            found_users, missing_usernames = self._find_users(
                usernames=self._parse_value_list(submitter),
                request=request)

            if len(found_users) + len(missing_usernames) > 1:
                invalid_fields['submitter'] = [
                    'Only one user can be set as the owner of a review '
                    'request'
                ]
            elif missing_usernames:
                assert len(missing_usernames) == 1
                invalid_fields['submitter'] = missing_usernames
            elif found_users:
                assert len(found_users) == 1
                new_draft_values['owner'] = found_users[0]
            else:
                invalid_fields['submitter'] = [
                    'The owner of a review request cannot be blank'
                ]

        # Check for a new value for summary.
        if summary is not None:
            if '\n' in summary:
                invalid_fields['summary'] = [
                    "The summary can't contain a newline"
                ]
            else:
                new_draft_values['summary'] = summary

        # Check for a new value for target_groups.
        if target_groups is not None:
            found_groups, missing_group_names = self._find_review_groups(
                group_names=self._parse_value_list(target_groups),
                request=request)

            if missing_group_names:
                invalid_fields['target_groups'] = missing_group_names
            else:
                new_draft_values['target_groups'] = found_groups

        # Check for a new value for target_people.
        if target_people is not None:
            found_users, missing_usernames = self._find_users(
                usernames=self._parse_value_list(target_people),
                request=request)

            if missing_usernames:
                invalid_fields['target_people'] = missing_usernames
            else:
                new_draft_values['target_people'] = found_users

        # See if we've caught any invalid values. If so, we can error out
        # immediately before we update anything else.
        if invalid_fields:
            return INVALID_FORM_DATA, {
                'fields': invalid_fields,
                self.item_result_key: draft,
            }

        # Start applying any rich text processing to any text fields on the
        # ChangeDescription and draft. We'll track any fields that get set
        # for later saving.
        #
        # NOTE: If any text fields or text type fields are ever made
        #       parameters of this method, then their values will need to be
        #       passed directly to set_text_fields() calls below.
        if draft.changedesc_id:
            changedesc_update_fields.update(
                self.set_text_fields(obj=draft.changedesc,
                                     text_field='changedescription',
                                     text_model_field='text',
                                     rich_text_field_name='rich_text',
                                     changedescription=changedescription,
                                     **kwargs))

        for text_field in ('description', 'testing_done'):
            draft_update_fields.update(self.set_text_fields(
                obj=draft,
                text_field=text_field,
                **kwargs))

        # Go through the list of Markdown-enabled custom fields and apply
        # any rich text processing. These will go in extra_data, so we'll
        # want to make sure that's tracked for saving.
        for field_cls in get_review_request_fields():
            if (not issubclass(field_cls, BuiltinFieldMixin) and
                getattr(field_cls, 'enable_markdown', False)):
                modified_fields = self.set_extra_data_text_fields(
                    obj=draft,
                    text_field=field_cls.field_id,
                    extra_fields=extra_fields,
                    **kwargs)

                if modified_fields:
                    draft_update_fields.add('extra_data')

        # See if the caller has set or patched extra_data values. For
        # compatibility, we're going to do this after processing the rich
        # text fields ine extra_data above.
        try:
            if self.import_extra_data(draft, draft.extra_data, extra_fields):
                # Track extra_data for saving.
                draft_update_fields.add('extra_data')
        except ImportExtraDataError as e:
            return e.error_payload

        # Everything checks out. We can now begin the process of applying any
        # field changes and then save objects.
        #
        # We'll start by making an initial pass on the objects we need to
        # either care about. This optimistically lets us avoid a lookup on the
        # ChangeDescription, if it's not being modified.
        to_apply = []

        if draft_update_fields or new_draft_values:
            # If there's any changes made at all to the draft, make sure we
            # allow last_updated to be computed and saved.
            if draft_update_fields or new_draft_values:
                draft_update_fields.add('last_updated')

            to_apply.append((draft, draft_update_fields, new_draft_values))

        if changedesc_update_fields or new_changedesc_values:
            to_apply.append((draft.changedesc, changedesc_update_fields,
                             new_changedesc_values))

        for obj, update_fields, new_values in to_apply:
            new_m2m_values = {}

            # We may have a mixture of field values and Many-To-Many
            # relation values, which we want to set only after the object
            # is saved. Start by setting any field values, and store the
            # M2M values for after.
            for key, value in six.iteritems(new_values):
                field = obj._meta.get_field(key)

                if isinstance(field, ManyToManyField):
                    # Save this until after the object is saved.
                    new_m2m_values[key] = value
                else:
                    # We can set this one now, and mark it for saving.
                    setattr(obj, key, value)
                    update_fields.add(key)

            if update_fields:
                obj.save(update_fields=sorted(update_fields))

            # Now we can set any values on M2M fields.
            #
            # Each entry will have zero or more values. We'll be
            # setting to the list of values, which will fully replace
            # the stored entries in the database.
            for key, values in six.iteritems(new_m2m_values):
                setattr(obj, key, values)

        # Next, check if the draft is set to be published.
        if request.POST.get('public', False):
            if not review_request.public and not draft.changedesc_id:
                # This is a new review request. Publish this on behalf of the
                # owner of the review request, rather than the current user,
                # regardless of the original publish_as_owner in the request.
                # This allows a review request previously created with
                # submit-as= to be published by that user instead of the
                # logged in user.
                publish_as_owner = True

            if publish_as_owner:
                publish_user = review_request.owner
            else:
                # Default to posting as the logged in user.
                publish_user = request.user

            try:
                review_request.publish(user=publish_user, trivial=trivial)
            except NotModifiedError:
                return NOTHING_TO_PUBLISH
            except PublishError as e:
                return PUBLISH_ERROR.with_message(six.text_type(e))

        return 200, {
            self.item_result_key: draft,
        }
Exemplo n.º 13
0
    def create(self, request, parent_request_id, reviewers, *args, **kwargs):
        try:
            parent_rr = ReviewRequest.objects.get(pk=parent_request_id)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if not (parent_rr.is_accessible_by(request.user)
                or parent_rr.is_mutable_by(request.user)):
            return PERMISSION_DENIED

        if not is_parent(parent_rr):
            return NOT_PARENT

        # Validate and expand the new reviewer list.

        bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
        child_reviewers = json.loads(reviewers)
        invalid_reviewers = []
        for child_rrid in child_reviewers:
            users = []
            for username in child_reviewers[child_rrid]:
                try:
                    users.append(bugzilla.get_user_from_irc_nick(username))
                except User.DoesNotExist:
                    invalid_reviewers.append(username)
            child_reviewers[child_rrid] = users

        if invalid_reviewers:
            # Because this isn't called through Review Board's built-in
            # backbone system, it's dramatically simpler to return just the
            # intended error message instead of categorising the errors by
            # field.
            if len(invalid_reviewers) == 1:
                return INVALID_FORM_DATA.with_message(
                    "The reviewer '%s' was not found" % invalid_reviewers[0])
            else:
                return INVALID_FORM_DATA.with_message(
                    "The reviewers '%s' were not found" %
                    "', '".join(invalid_reviewers))

        # Review Board only supports the submitter updating a review
        # request.  In order for this to work, we publish these changes
        # in Review Board under the review submitter's account, and
        # set an extra_data field which instructs our bugzilla
        # connector to use this request's user when adjusting flags.
        #
        # Updating the review request requires creating a draft and
        # publishing it, so we have to be careful to not overwrite
        # existing drafts.

        try:
            with transaction.atomic():
                for rr in itertools.chain([parent_rr],
                                          gen_child_rrs(parent_rr)):
                    if rr.get_draft() is not None:
                        return REVIEW_REQUEST_UPDATE_NOT_ALLOWED.with_message(
                            "Unable to update reviewers as the review "
                            "request has pending changes (the patch author "
                            "has a draft)")

                try:
                    for child_rr in gen_child_rrs(parent_rr):
                        if str(child_rr.id) in child_reviewers:
                            if not child_rr.is_accessible_by(request.user):
                                return PERMISSION_DENIED.with_message(
                                    "You do not have permission to update "
                                    "reviewers on review request %s" %
                                    child_rr.id)

                            draft = ReviewRequestDraft.create(child_rr)
                            draft.target_people.clear()
                            for user in child_reviewers[str(child_rr.id)]:
                                draft.target_people.add(user)

                    set_publish_as(parent_rr, request.user)
                    parent_rr_draft = ReviewRequestDraft.create(parent_rr)
                    update_parent_rr_reviewers(parent_rr_draft)
                    parent_rr.publish(user=parent_rr.submitter)
                finally:
                    clear_publish_as(parent_rr)

        except PublishError as e:
            logging.error("failed to update reviewers on %s: %s" %
                          (parent_rr.id, str(e)))
            return PUBLISH_ERROR.with_message(str(e))

        return 200, {}
Exemplo n.º 14
0
    def update(self,
               request,
               always_save=False,
               local_site_name=None,
               update_from_commit_id=False,
               extra_fields={},
               *args,
               **kwargs):
        """Updates a draft of a review request.

        This will update the draft with the newly provided data.

        Most of the fields correspond to fields in the review request, but
        there is one special one, ``public``. When ``public`` is set to true,
        the draft will be published, moving the new content to the
        review request itself, making it public, and sending out a notification
        (such as an e-mail) if configured on the server. The current draft will
        then be deleted.

        Extra data can be stored on the review request for later lookup by
        passing ``extra_data.key_name=value``. The ``key_name`` and ``value``
        can be any valid strings. Passing a blank ``value`` will remove the
        key. The ``extra_data.`` prefix is required.
        """
        try:
            review_request = resources.review_request.get_object(
                request, local_site_name=local_site_name, *args, **kwargs)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if kwargs.get('commit_id') == '':
            kwargs['commit_id'] = None

        commit_id = kwargs.get('commit_id', None)

        try:
            draft = self.prepare_draft(request, review_request)
        except PermissionDenied:
            return self._no_access_error(request.user)

        if (commit_id and commit_id != review_request.commit_id
                and commit_id != draft.commit_id):
            # Check to make sure the new commit ID isn't being used already
            # in another review request or draft.
            repository = review_request.repository

            existing_review_request = ReviewRequest.objects.filter(
                commit_id=commit_id, repository=repository)

            if (existing_review_request
                    and existing_review_request != review_request):
                return COMMIT_ID_ALREADY_EXISTS

            existing_draft = ReviewRequestDraft.objects.filter(
                commit_id=commit_id, review_request__repository=repository)

            if existing_draft and existing_draft != draft:
                return COMMIT_ID_ALREADY_EXISTS

        modified_objects = []
        invalid_fields = {}

        for field_name, field_info in six.iteritems(self.fields):
            if (field_info.get('mutable', True)
                    and kwargs.get(field_name, None) is not None):
                field_result, field_modified_objects, invalid = \
                    self._set_draft_field_data(draft, field_name,
                                               kwargs[field_name],
                                               local_site_name, request)

                if invalid:
                    invalid_fields[field_name] = invalid
                elif field_modified_objects:
                    modified_objects += field_modified_objects

        if commit_id and update_from_commit_id:
            try:
                draft.update_from_commit_id(commit_id)
            except InvalidChangeNumberError:
                return INVALID_CHANGE_NUMBER

        if draft.changedesc_id:
            changedesc = draft.changedesc
            modified_objects.append(draft.changedesc)

            self.set_text_fields(changedesc,
                                 'changedescription',
                                 text_model_field='text',
                                 rich_text_field_name='rich_text',
                                 **kwargs)

        self.set_text_fields(draft, 'description', **kwargs)
        self.set_text_fields(draft, 'testing_done', **kwargs)

        for field_cls in get_review_request_fields():
            if (not issubclass(field_cls, BuiltinFieldMixin)
                    and getattr(field_cls, 'enable_markdown', False)):
                self.set_extra_data_text_fields(draft, field_cls.field_id,
                                                extra_fields, **kwargs)

        self.import_extra_data(draft, draft.extra_data, extra_fields)

        if always_save or not invalid_fields:
            for obj in set(modified_objects):
                obj.save()

            draft.save()

        if invalid_fields:
            return INVALID_FORM_DATA, {
                'fields': invalid_fields,
                self.item_result_key: draft,
            }

        if request.POST.get('public', False):
            try:
                review_request.publish(user=request.user)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(e.msg)
            except NotModifiedError:
                return NOTHING_TO_PUBLISH

        return 200, {
            self.item_result_key: draft,
        }
Exemplo n.º 15
0
    def update(self,
               request,
               always_save=False,
               local_site_name=None,
               update_from_commit_id=False,
               extra_fields={},
               *args,
               **kwargs):
        """Updates a draft of a review request.

        This will update the draft with the newly provided data.

        Most of the fields correspond to fields in the review request, but
        there is one special one, ``public``. When ``public`` is set to true,
        the draft will be published, moving the new content to the
        review request itself, making it public, and sending out a notification
        (such as an e-mail) if configured on the server. The current draft will
        then be deleted.

        If ``text_type`` is provided and changed from the original value,
        then the ``changedescription``, ``description`` and ``testing_done``
        fields will be set to be interpreted according to the new type.

        When setting to ``markdown`` and not specifying any new text, the
        existing text will be escaped so as not to be unintentionally
        interpreted as Markdown.

        When setting to ``plain``, and new text is not provided, the existing
        text will be unescaped.

        Extra data can be stored on the review request for later lookup by
        passing ``extra_data.key_name=value``. The ``key_name`` and ``value``
        can be any valid strings. Passing a blank ``value`` will remove the
        key. The ``extra_data.`` prefix is required.
        """
        try:
            review_request = resources.review_request.get_object(
                request, local_site_name=local_site_name, *args, **kwargs)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        try:
            draft = self.prepare_draft(request, review_request)
        except PermissionDenied:
            return self._no_access_error(request.user)

        modified_objects = []
        invalid_fields = {}

        old_rich_text = draft.rich_text
        old_changedesc_rich_text = (draft.changedesc_id is not None
                                    and draft.changedesc.rich_text)

        for field_name, field_info in six.iteritems(self.fields):
            if (field_info.get('mutable', True)
                    and kwargs.get(field_name, None) is not None):
                field_result, field_modified_objects, invalid = \
                    self._set_draft_field_data(draft, field_name,
                                               kwargs[field_name],
                                               local_site_name, request)

                if invalid:
                    invalid_fields[field_name] = invalid
                elif field_modified_objects:
                    modified_objects += field_modified_objects

        commit_id = kwargs.get('commit_id', None)

        if commit_id and update_from_commit_id:
            try:
                draft.update_from_commit_id(commit_id)
            except InvalidChangeNumberError:
                return INVALID_CHANGE_NUMBER

        if draft.changedesc_id:
            changedesc = draft.changedesc
            modified_objects.append(draft.changedesc)

            if 'text_type' in kwargs:
                changedesc.rich_text = \
                    (kwargs['text_type'] == self.TEXT_TYPE_MARKDOWN)

            self.normalize_markdown_fields(changedesc, ['changedescription'],
                                           old_changedesc_rich_text,
                                           model_field_map={
                                               'changedescription': 'text',
                                           },
                                           **kwargs)

        self.normalize_markdown_fields(draft, ['description', 'testing_done'],
                                       old_rich_text, **kwargs)

        self._import_extra_data(draft.extra_data, extra_fields)

        if always_save or not invalid_fields:
            for obj in set(modified_objects):
                obj.save()

            draft.save()

        if invalid_fields:
            return INVALID_FORM_DATA, {
                'fields': invalid_fields,
                self.item_result_key: draft,
            }

        if request.POST.get('public', False):
            try:
                review_request.publish(user=request.user)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(e.msg)

        return 200, {
            self.item_result_key: draft,
        }
Exemplo n.º 16
0
    def update(self, request, always_save=False, local_site_name=None,
               update_from_commit_id=False, trivial=None,
               extra_fields={}, *args, **kwargs):
        """Updates a draft of a review request.

        This will update the draft with the newly provided data.

        Most of the fields correspond to fields in the review request, but
        there is one special one, ``public``. When ``public`` is set to true,
        the draft will be published, moving the new content to the
        review request itself, making it public, and sending out a notification
        (such as an e-mail) if configured on the server. The current draft will
        then be deleted.

        Extra data can be stored later lookup. See
        :ref:`webapi2.0-extra-data` for more information.
        """
        try:
            review_request = resources.review_request.get_object(
                request, local_site_name=local_site_name, *args, **kwargs)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if kwargs.get('commit_id') == '':
            kwargs['commit_id'] = None

        commit_id = kwargs.get('commit_id', None)

        try:
            draft = self.prepare_draft(request, review_request)
        except PermissionDenied:
            return self.get_no_access_error(request)

        if (commit_id and commit_id != review_request.commit_id and
            commit_id != draft.commit_id):
            # Check to make sure the new commit ID isn't being used already
            # in another review request or draft.
            repository = review_request.repository

            existing_review_request = ReviewRequest.objects.filter(
                commit_id=commit_id,
                repository=repository)

            if (existing_review_request and
                existing_review_request != review_request):
                return COMMIT_ID_ALREADY_EXISTS

            existing_draft = ReviewRequestDraft.objects.filter(
                commit_id=commit_id,
                review_request__repository=repository)

            if existing_draft and existing_draft != draft:
                return COMMIT_ID_ALREADY_EXISTS

        modified_objects = []
        invalid_fields = {}

        for field_name, field_info in six.iteritems(self.fields):
            if (field_info.get('mutable', True) and
                kwargs.get(field_name, None) is not None):
                field_result, field_modified_objects, invalid = \
                    self._set_draft_field_data(draft, field_name,
                                               kwargs[field_name],
                                               local_site_name, request)

                if invalid:
                    invalid_fields[field_name] = invalid
                elif field_modified_objects:
                    modified_objects += field_modified_objects

        if commit_id and update_from_commit_id:
            try:
                draft.update_from_commit_id(commit_id)
            except InvalidChangeNumberError:
                return INVALID_CHANGE_NUMBER

        if draft.changedesc_id:
            changedesc = draft.changedesc
            modified_objects.append(draft.changedesc)

            self.set_text_fields(changedesc, 'changedescription',
                                 text_model_field='text',
                                 rich_text_field_name='rich_text',
                                 **kwargs)

        self.set_text_fields(draft, 'description', **kwargs)
        self.set_text_fields(draft, 'testing_done', **kwargs)

        for field_cls in get_review_request_fields():
            if (not issubclass(field_cls, BuiltinFieldMixin) and
                getattr(field_cls, 'enable_markdown', False)):
                self.set_extra_data_text_fields(draft, field_cls.field_id,
                                                extra_fields, **kwargs)

        try:
            self.import_extra_data(draft, draft.extra_data, extra_fields)
        except ImportExtraDataError as e:
            return e.error_payload

        if always_save or not invalid_fields:
            for obj in set(modified_objects):
                obj.save()

            draft.save()

        if invalid_fields:
            return INVALID_FORM_DATA, {
                'fields': invalid_fields,
                self.item_result_key: draft,
            }

        if request.POST.get('public', False):
            try:
                review_request.publish(user=request.user, trivial=trivial)
            except NotModifiedError:
                return NOTHING_TO_PUBLISH
            except PublishError as e:
                return PUBLISH_ERROR.with_message(six.text_type(e))

        return 200, {
            self.item_result_key: draft,
        }
Exemplo n.º 17
0
    def update_review(self, request, review, public=None,
                      publish_to_owner_only=False, extra_fields={},
                      ship_it=None, **kwargs):
        """Update an existing review based on the requested data.

        This will modify a review, setting new fields requested by the
        caller.

        Args:
            request (django.http.HttpRequest):
                The HTTP request from the client.

            review (reviewboard.reviews.models.review.Review):
                The review being modified.

            public (bool, optional):
                Whether the review is being made public for the first
                time.

            publish_to_owner_only (bool, optional):
                Whether an e-mail for the published review should only be
                sent to the owner of the review request. This is ignored if
                ``public`` is not ``True``.

            extra_fields (dict, optional):
                Extra fields from the request not otherwise handled by the
                API resource. Any ``extra_data`` modifications from this will
                be applied to the comment.

            ship_it (bool, optional):
                The new Ship It state for the review.

            **kwargs (dict):
                Keyword arguments representing additional fields handled by
                the API resource.

        Returns:
            tuple or djblets.webapi.errors.WebAPIError:
            Either a successful payload containing the review, or an error
            payload.
        """
        if not self.has_modify_permissions(request, review):
            # Can't modify published reviews or those not belonging
            # to the user.
            return self.get_no_access_error(request)

        if ship_it is not None:
            review.ship_it = ship_it

        self.set_text_fields(review, 'body_top', **kwargs)
        self.set_text_fields(review, 'body_bottom', **kwargs)

        try:
            self.import_extra_data(review, review.extra_data, extra_fields)
        except ImportExtraDataError as e:
            return e.error_payload

        review.save()

        if public:
            try:
                review.publish(user=request.user,
                               to_owner_only=publish_to_owner_only,
                               request=request)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(six.text_type(e))

        return 200, {
            self.item_result_key: review,
        }
Exemplo n.º 18
0
    def update(self, request, always_save=False, local_site_name=None,
               update_from_commit_id=False, extra_fields={}, *args, **kwargs):
        """Updates a draft of a review request.

        This will update the draft with the newly provided data.

        Most of the fields correspond to fields in the review request, but
        there is one special one, ``public``. When ``public`` is set to true,
        the draft will be published, moving the new content to the
        review request itself, making it public, and sending out a notification
        (such as an e-mail) if configured on the server. The current draft will
        then be deleted.

        If ``text_type`` is provided and changed from the original value,
        then the ``changedescription``, ``description`` and ``testing_done``
        fields will be set to be interpreted according to the new type.

        When setting to ``markdown`` and not specifying any new text, the
        existing text will be escaped so as not to be unintentionally
        interpreted as Markdown.

        When setting to ``plain``, and new text is not provided, the existing
        text will be unescaped.

        Extra data can be stored on the review request for later lookup by
        passing ``extra_data.key_name=value``. The ``key_name`` and ``value``
        can be any valid strings. Passing a blank ``value`` will remove the
        key. The ``extra_data.`` prefix is required.
        """
        try:
            review_request = resources.review_request.get_object(
                request, local_site_name=local_site_name, *args, **kwargs)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if kwargs.get('commit_id') == '':
            kwargs['commit_id'] = None

        commit_id = kwargs.get('commit_id', None)

        try:
            draft = self.prepare_draft(request, review_request)
        except PermissionDenied:
            return self._no_access_error(request.user)

        if (commit_id and commit_id != review_request.commit_id and
            commit_id != draft.commit_id):
            # Check to make sure the new commit ID isn't being used already
            # in another review request or draft.
            repository = review_request.repository

            existing_review_request = ReviewRequest.objects.filter(
                commit_id=commit_id,
                repository=repository)

            if (existing_review_request and
                existing_review_request != review_request):
                return COMMIT_ID_ALREADY_EXISTS

            existing_draft = ReviewRequestDraft.objects.filter(
                commit_id=commit_id,
                review_request__repository=repository)

            if existing_draft and existing_draft != draft:
                return COMMIT_ID_ALREADY_EXISTS

        modified_objects = []
        invalid_fields = {}

        old_rich_text = draft.rich_text
        old_changedesc_rich_text = (draft.changedesc_id is not None and
                                    draft.changedesc.rich_text)

        for field_name, field_info in six.iteritems(self.fields):
            if (field_info.get('mutable', True) and
                kwargs.get(field_name, None) is not None):
                field_result, field_modified_objects, invalid = \
                    self._set_draft_field_data(draft, field_name,
                                               kwargs[field_name],
                                               local_site_name, request)

                if invalid:
                    invalid_fields[field_name] = invalid
                elif field_modified_objects:
                    modified_objects += field_modified_objects

        if commit_id and update_from_commit_id:
            try:
                draft.update_from_commit_id(commit_id)
            except InvalidChangeNumberError:
                return INVALID_CHANGE_NUMBER

        if draft.changedesc_id:
            changedesc = draft.changedesc
            modified_objects.append(draft.changedesc)

            if 'text_type' in kwargs:
                changedesc.rich_text = \
                    (kwargs['text_type'] == self.TEXT_TYPE_MARKDOWN)

            self.normalize_markdown_fields(changedesc, ['changedescription'],
                                           old_changedesc_rich_text,
                                           model_field_map={
                                               'changedescription': 'text',
                                           },
                                           **kwargs)

        self.normalize_markdown_fields(draft, ['description', 'testing_done'],
                                       old_rich_text, **kwargs)

        self._import_extra_data(draft.extra_data, extra_fields)

        if always_save or not invalid_fields:
            for obj in set(modified_objects):
                obj.save()

            draft.save()

        if invalid_fields:
            return INVALID_FORM_DATA, {
                'fields': invalid_fields,
                self.item_result_key: draft,
            }

        if request.POST.get('public', False):
            try:
                review_request.publish(user=request.user)
            except PublishError as e:
                return PUBLISH_ERROR.with_message(e.msg)
            except NotModifiedError:
                return NOTHING_TO_PUBLISH

        return 200, {
            self.item_result_key: draft,
        }
    def create(self, request, parent_request_id, reviewers, *args, **kwargs):
        try:
            parent_rr = ReviewRequest.objects.get(pk=parent_request_id)
        except ReviewRequest.DoesNotExist:
            return DOES_NOT_EXIST

        if not (parent_rr.is_accessible_by(request.user)
                or parent_rr.is_mutable_by(request.user)):
            return PERMISSION_DENIED

        if not is_parent(parent_rr):
            return NOT_PARENT

        # Validate and expand the new reviewer list.

        bugzilla = Bugzilla(get_bugzilla_api_key(request.user))
        child_reviewers = json.loads(reviewers)
        invalid_reviewers = []
        for child_rrid in child_reviewers:
            users = []
            for username in child_reviewers[child_rrid]:
                try:
                    users.append(bugzilla.get_user_from_irc_nick(username))
                except User.DoesNotExist:
                    invalid_reviewers.append(username)
            child_reviewers[child_rrid] = users

        if invalid_reviewers:
            # Because this isn't called through Review Board's built-in
            # backbone system, it's dramatically simpler to return just the
            # intended error message instead of categorising the errors by
            # field.
            if len(invalid_reviewers) == 1:
                return INVALID_FORM_DATA.with_message(
                    "The reviewer '%s' was not found" % invalid_reviewers[0])
            else:
                return INVALID_FORM_DATA.with_message(
                    "The reviewers '%s' were not found"
                    % "', '".join(invalid_reviewers))

        # Review Board only supports the submitter updating a review
        # request.  In order for this to work, we publish these changes
        # in Review Board under the review submitter's account, and
        # set an extra_data field which instructs our bugzilla
        # connector to use this request's user when adjusting flags.
        #
        # Updating the review request requires creating a draft and
        # publishing it, so we have to be careful to not overwrite
        # existing drafts.

        try:
            with transaction.atomic():
                for rr in itertools.chain([parent_rr],
                                          gen_child_rrs(parent_rr)):
                    if rr.get_draft() is not None:
                        return REVIEW_REQUEST_UPDATE_NOT_ALLOWED.with_message(
                            "Unable to update reviewers as the review "
                            "request has pending changes (the patch author "
                            "has a draft)")

                try:
                    for child_rr in gen_child_rrs(parent_rr):
                        if str(child_rr.id) in child_reviewers:
                            if not child_rr.is_accessible_by(request.user):
                                return PERMISSION_DENIED.with_message(
                                    "You do not have permission to update "
                                    "reviewers on review request %s"
                                    % child_rr.id)

                            draft = ReviewRequestDraft.create(child_rr)
                            draft.target_people.clear()
                            for user in child_reviewers[str(child_rr.id)]:
                                draft.target_people.add(user)

                    set_publish_as(parent_rr, request.user)
                    parent_rr_draft = ReviewRequestDraft.create(parent_rr)
                    update_parent_rr_reviewers(parent_rr_draft)
                    parent_rr.publish(user=parent_rr.submitter)
                finally:
                    clear_publish_as(parent_rr)

        except PublishError as e:
                logging.error("failed to update reviewers on %s: %s"
                              % (parent_rr.id, str(e)))
                return PUBLISH_ERROR.with_message(str(e))

        return 200, {}