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,
        }
    def publish(self, review_request=None, user=None,
                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 assocated ReviewRequest object will be used if one isn't
        passed in.

        The keys that may be saved in 'fields_changed' in the
        ChangeDescription are:

           *  'summary'
           *  'description'
           *  'testing_done'
           *  'bugs_closed'
           *  'depends_on'
           *  'branch'
           *  'target_groups'
           *  'target_people'
           *  'screenshots'
           *  'screenshot_captions'
           *  'diff'

        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 user:
            user = review_request.submitter

        if not self.changedesc and review_request.public:
            self.changedesc = ChangeDescription()

        def update_list(a, b, name, record_changes=True, name_field=None):
            aset = set([x.id for x in a.all()])
            bset = set([x.id for x in b.all()])

            if aset.symmetric_difference(bset):
                if record_changes and self.changedesc:
                    self.changedesc.record_field_change(name, a.all(), b.all(),
                                                        name_field)

                a.clear()
                for item in b.all():
                    a.add(item)

        for field_cls in get_review_request_fields():
            field = field_cls(review_request)

            if field.can_record_change_entry:
                old_value = field.load_value(review_request)
                new_value = field.load_value(self)

                if field.has_value_changed(old_value, new_value):
                    field.save_value(new_value)

                    if self.changedesc:
                        field.record_change_entry(self.changedesc,
                                                  old_value, new_value)

        # Screenshots are a bit special.  The list of associated screenshots
        # can change, but so can captions within each screenshot.
        screenshots = list(self.screenshots.all())
        caption_changes = {}

        for s in review_request.screenshots.all():
            if s in screenshots and s.caption != s.draft_caption:
                caption_changes[s.id] = {
                    'old': (s.caption,),
                    'new': (s.draft_caption,),
                }

                s.caption = s.draft_caption
                s.save(update_fields=['caption'])

        # Now scan through again and set the caption correctly for newly-added
        # screenshots by copying the draft_caption over. We don't need to
        # include this in the changedescs here because it's a new screenshot,
        # and update_list will record the newly-added item.
        for s in screenshots:
            if s.caption != s.draft_caption:
                s.caption = s.draft_caption
                s.save(update_fields=['caption'])

        if caption_changes and self.changedesc:
            self.changedesc.fields_changed['screenshot_captions'] = \
                caption_changes

        update_list(review_request.screenshots, self.screenshots,
                    'screenshots', name_field="caption")

        # There's no change notification required for this field.
        review_request.inactive_screenshots = self.inactive_screenshots.all()

        # Files are treated like screenshots. The list of files can
        # change, but so can captions within each file.
        files = list(self.file_attachments.all())
        caption_changes = {}

        for f in review_request.file_attachments.all():
            if f in files and f.caption != f.draft_caption:
                caption_changes[f.id] = {
                    'old': (f.caption,),
                    'new': (f.draft_caption,),
                }

                f.caption = f.draft_caption
                f.save(update_fields=['caption'])

        # Now scan through again and set the caption correctly for newly-added
        # files by copying the draft_caption over. We don't need to include
        # this in the changedescs here because it's a new screenshot, and
        # update_list will record the newly-added item.
        for f in files:
            if f.caption != f.draft_caption:
                f.caption = f.draft_caption
                f.save(update_fields=['caption'])

        if caption_changes and self.changedesc:
            self.changedesc.fields_changed['file_captions'] = caption_changes

        update_list(review_request.file_attachments, self.file_attachments,
                    'files', name_field="display_name")

        # There's no change notification required for this field.
        review_request.inactive_file_attachments = \
            self.inactive_file_attachments.all()

        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 self.changedesc:
            self.changedesc.timestamp = timezone.now()
            self.changedesc.rich_text = self.rich_text
            self.changedesc.public = True
            self.changedesc.save()
            review_request.changedescs.add(self.changedesc)

        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,
                                          changedesc=self.changedesc)

        return self.changedesc
    def copy_fields_to_request(self, review_request):
        """Copies the draft information to the review request and updates the
        draft's change description.
        """
        def update_list(a, b, name, record_changes=True, name_field=None):
            aset = set([x.id for x in a.all()])
            bset = set([x.id for x in b.all()])

            if aset.symmetric_difference(bset):
                if record_changes and self.changedesc:
                    self.changedesc.record_field_change(name, a.all(), b.all(),
                                                        name_field)

                a.clear()
                for item in b.all():
                    a.add(item)

        for field_cls in get_review_request_fields():
            field = field_cls(review_request)

            if field.can_record_change_entry:
                old_value = field.load_value(review_request)
                new_value = field.load_value(self)

                if field.has_value_changed(old_value, new_value):
                    field.propagate_data(self)

                    if self.changedesc:
                        field.record_change_entry(self.changedesc,
                                                  old_value, new_value)

        # Screenshots are a bit special.  The list of associated screenshots
        # can change, but so can captions within each screenshot.
        if (review_request.screenshots_count > 0 or
            self.screenshots_count > 0):
            screenshots = list(self.screenshots.all())
            caption_changes = {}

            for s in review_request.screenshots.all():
                if s in screenshots and s.caption != s.draft_caption:
                    caption_changes[s.id] = {
                        'old': (s.caption,),
                        'new': (s.draft_caption,),
                    }

                    s.caption = s.draft_caption
                    s.save(update_fields=['caption'])

            # Now scan through again and set the caption correctly for
            # newly-added screenshots by copying the draft_caption over. We
            # don't need to include this in the changedescs here because it's a
            # new screenshot, and update_list will record the newly-added item.
            for s in screenshots:
                if s.caption != s.draft_caption:
                    s.caption = s.draft_caption
                    s.save(update_fields=['caption'])

            if caption_changes and self.changedesc:
                self.changedesc.fields_changed['screenshot_captions'] = \
                    caption_changes

            update_list(review_request.screenshots,
                        self.screenshots,
                        name='screenshots',
                        name_field="caption")

        if (review_request.inactive_screenshots_count > 0 or
            self.inactive_screenshots_count > 0):
            # There's no change notification required for this field.
            review_request.inactive_screenshots = \
                self.inactive_screenshots.all()

        # Files are treated like screenshots. The list of files can
        # change, but so can captions within each file.
        if (review_request.file_attachments_count > 0 or
            self.file_attachments_count > 0):
            files = list(self.file_attachments.all())
            caption_changes = {}

            for f in review_request.file_attachments.all():
                if f in files and f.caption != f.draft_caption:
                    caption_changes[f.id] = {
                        'old': (f.caption,),
                        'new': (f.draft_caption,),
                    }

                    f.caption = f.draft_caption
                    f.save(update_fields=['caption'])

            # Now scan through again and set the caption correctly for
            # newly-added files by copying the draft_caption over. We don't
            # need to include this in the changedescs here because it's a new
            # screenshot, and update_list will record the newly-added item.
            for f in files:
                if f.caption != f.draft_caption:
                    f.caption = f.draft_caption
                    f.save(update_fields=['caption'])

            if caption_changes and self.changedesc:
                self.changedesc.fields_changed['file_captions'] = \
                    caption_changes

            update_list(review_request.file_attachments,
                        self.file_attachments,
                        name='files',
                        name_field="display_name")

        if (review_request.inactive_file_attachments_count > 0 or
            self.inactive_file_attachments_count > 0):
            # There's no change notification required for this field.
            review_request.inactive_file_attachments = \
                self.inactive_file_attachments.all()
    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,
        }
Exemple #5
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,
        }
Exemple #6
0
    def copy_fields_to_request(self, review_request):
        """Copies the draft information to the review request and updates the
        draft's change description.
        """
        def update_list(a, b, name, record_changes=True, name_field=None):
            aset = set([x.id for x in a.all()])
            bset = set([x.id for x in b.all()])

            if aset.symmetric_difference(bset):
                if record_changes and self.changedesc:
                    self.changedesc.record_field_change(
                        name, a.all(), b.all(), name_field)

                a.clear()
                for item in b.all():
                    a.add(item)

        for field_cls in get_review_request_fields():
            field = field_cls(review_request)

            if field.can_record_change_entry:
                old_value = field.load_value(review_request)
                new_value = field.load_value(self)

                if field.has_value_changed(old_value, new_value):
                    field.propagate_data(self)

                    if self.changedesc:
                        field.record_change_entry(self.changedesc, old_value,
                                                  new_value)

        # Screenshots are a bit special.  The list of associated screenshots
        # can change, but so can captions within each screenshot.
        screenshots = list(self.screenshots.all())
        caption_changes = {}

        for s in review_request.screenshots.all():
            if s in screenshots and s.caption != s.draft_caption:
                caption_changes[s.id] = {
                    'old': (s.caption, ),
                    'new': (s.draft_caption, ),
                }

                s.caption = s.draft_caption
                s.save(update_fields=['caption'])

        # Now scan through again and set the caption correctly for newly-added
        # screenshots by copying the draft_caption over. We don't need to
        # include this in the changedescs here because it's a new screenshot,
        # and update_list will record the newly-added item.
        for s in screenshots:
            if s.caption != s.draft_caption:
                s.caption = s.draft_caption
                s.save(update_fields=['caption'])

        if caption_changes and self.changedesc:
            self.changedesc.fields_changed['screenshot_captions'] = \
                caption_changes

        update_list(review_request.screenshots,
                    self.screenshots,
                    'screenshots',
                    name_field="caption")

        # There's no change notification required for this field.
        review_request.inactive_screenshots = self.inactive_screenshots.all()

        # Files are treated like screenshots. The list of files can
        # change, but so can captions within each file.
        files = list(self.file_attachments.all())
        caption_changes = {}

        for f in review_request.file_attachments.all():
            if f in files and f.caption != f.draft_caption:
                caption_changes[f.id] = {
                    'old': (f.caption, ),
                    'new': (f.draft_caption, ),
                }

                f.caption = f.draft_caption
                f.save(update_fields=['caption'])

        # Now scan through again and set the caption correctly for newly-added
        # files by copying the draft_caption over. We don't need to include
        # this in the changedescs here because it's a new screenshot, and
        # update_list will record the newly-added item.
        for f in files:
            if f.caption != f.draft_caption:
                f.caption = f.draft_caption
                f.save(update_fields=['caption'])

        if caption_changes and self.changedesc:
            self.changedesc.fields_changed['file_captions'] = caption_changes

        update_list(review_request.file_attachments,
                    self.file_attachments,
                    'files',
                    name_field="display_name")

        # There's no change notification required for this field.
        review_request.inactive_file_attachments = \
            self.inactive_file_attachments.all()
    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,
        }
    def publish(self, review_request=None, user=None, 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 assocated ReviewRequest object will be used if one isn't
        passed in.

        The keys that may be saved in 'fields_changed' in the
        ChangeDescription are:

           *  'summary'
           *  'description'
           *  'testing_done'
           *  'bugs_closed'
           *  'depends_on'
           *  'branch'
           *  'target_groups'
           *  'target_people'
           *  'screenshots'
           *  'screenshot_captions'
           *  'diff'

        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 user:
            user = review_request.submitter

        if not self.changedesc and review_request.public:
            self.changedesc = ChangeDescription()

        def update_list(a, b, name, record_changes=True, name_field=None):
            aset = set([x.id for x in a.all()])
            bset = set([x.id for x in b.all()])

            if aset.symmetric_difference(bset):
                if record_changes and self.changedesc:
                    self.changedesc.record_field_change(
                        name, a.all(), b.all(), name_field)

                a.clear()
                for item in b.all():
                    a.add(item)

        for field_cls in get_review_request_fields():
            field = field_cls(review_request)

            if field.can_record_change_entry:
                old_value = field.load_value(review_request)
                new_value = field.load_value(self)

                if field.has_value_changed(old_value, new_value):
                    field.save_value(new_value)

                    if self.changedesc:
                        field.record_change_entry(self.changedesc, old_value,
                                                  new_value)

        # Screenshots are a bit special.  The list of associated screenshots
        # can change, but so can captions within each screenshot.
        screenshots = list(self.screenshots.all())
        caption_changes = {}

        for s in review_request.screenshots.all():
            if s in screenshots and s.caption != s.draft_caption:
                caption_changes[s.id] = {
                    'old': (s.caption, ),
                    'new': (s.draft_caption, ),
                }

                s.caption = s.draft_caption
                s.save(update_fields=['caption'])

        # Now scan through again and set the caption correctly for newly-added
        # screenshots by copying the draft_caption over. We don't need to
        # include this in the changedescs here because it's a new screenshot,
        # and update_list will record the newly-added item.
        for s in screenshots:
            if s.caption != s.draft_caption:
                s.caption = s.draft_caption
                s.save(update_fields=['caption'])

        if caption_changes and self.changedesc:
            self.changedesc.fields_changed['screenshot_captions'] = \
                caption_changes

        update_list(review_request.screenshots,
                    self.screenshots,
                    'screenshots',
                    name_field="caption")

        # There's no change notification required for this field.
        review_request.inactive_screenshots = self.inactive_screenshots.all()

        # Files are treated like screenshots. The list of files can
        # change, but so can captions within each file.
        files = list(self.file_attachments.all())
        caption_changes = {}

        for f in review_request.file_attachments.all():
            if f in files and f.caption != f.draft_caption:
                caption_changes[f.id] = {
                    'old': (f.caption, ),
                    'new': (f.draft_caption, ),
                }

                f.caption = f.draft_caption
                f.save(update_fields=['caption'])

        # Now scan through again and set the caption correctly for newly-added
        # files by copying the draft_caption over. We don't need to include
        # this in the changedescs here because it's a new screenshot, and
        # update_list will record the newly-added item.
        for f in files:
            if f.caption != f.draft_caption:
                f.caption = f.draft_caption
                f.save(update_fields=['caption'])

        if caption_changes and self.changedesc:
            self.changedesc.fields_changed['file_captions'] = caption_changes

        update_list(review_request.file_attachments,
                    self.file_attachments,
                    'files',
                    name_field="display_name")

        # There's no change notification required for this field.
        review_request.inactive_file_attachments = \
            self.inactive_file_attachments.all()

        if self.diffset:
            self.diffset.history = review_request.diffset_history
            self.diffset.save(update_fields=['history'])

        if self.changedesc:
            self.changedesc.timestamp = timezone.now()
            self.changedesc.rich_text = self.rich_text
            self.changedesc.public = True
            self.changedesc.save()
            review_request.changedescs.add(self.changedesc)

        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,
                                          changedesc=self.changedesc)

        return self.changedesc
Exemple #9
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,
        }