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, }
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 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
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, }