def test_get_with_site(self): """Testing the GET review-requests/<id>/changes/ API with access to a local site """ review_request = self.create_review_request(publish=True, with_local_site=True) self._login_user(local_site=True) now = timezone.now() change1 = ChangeDescription(public=True, timestamp=now) change1.record_field_change('summary', 'foo', 'bar') change1.save() review_request.changedescs.add(change1) change2 = ChangeDescription(public=True, timestamp=now + timedelta(seconds=1)) change2.record_field_change('description', 'foo', 'bar') change2.save() review_request.changedescs.add(change2) rsp = self.api_get(get_change_list_url(review_request, self.local_site_name), expected_mimetype=change_list_mimetype) self.assertEqual(rsp['stat'], 'ok') self.assertEqual(len(rsp['changes']), 2) self.assertEqual(rsp['changes'][0]['id'], change2.pk) self.assertEqual(rsp['changes'][1]['id'], change1.pk)
def reopen(self, user=None): """Reopens the review request for review.""" from reviewboard.reviews.models.review_request_draft import \ ReviewRequestDraft if (user and not self.is_mutable_by(user) and not user.has_perm("reviews.can_change_status", self.local_site)): raise PermissionError if self.status != self.PENDING_REVIEW: changedesc = ChangeDescription() changedesc.record_field_change('status', self.status, self.PENDING_REVIEW) if self.status == self.DISCARDED: # A draft is needed if reopening a discarded review request. self.public = False changedesc.save() draft = ReviewRequestDraft.create(self) draft.changedesc = changedesc draft.save() else: changedesc.public = True changedesc.save() self.changedescs.add(changedesc) self.status = self.PENDING_REVIEW self.save(update_counts=True) review_request_reopened.send(sender=self.__class__, user=user, review_request=self)
def test_record_list_mismatch_type(self): """Testing ChangeDescription.record_field_change with mismatched types """ changedesc = ChangeDescription() self.assertRaises(ValueError, changedesc.record_field_change, "test", 123, True)
def close(self, type, user=None, description=None, rich_text=False): """Closes the review request. The type must be one of SUBMITTED or DISCARDED. """ if (user and not self.is_mutable_by(user) and not user.has_perm("reviews.can_change_status", self.local_site)): raise PermissionError if type not in [self.SUBMITTED, self.DISCARDED]: raise AttributeError("%s is not a valid close type" % type) draft = get_object_or_none(self.draft) if self.status != type: if (draft is not None and not self.public and type == self.DISCARDED): # Copy over the draft information if this is a private discard. draft.copy_fields_to_request(self) # TODO: Use the user's default for rich_text. changedesc = ChangeDescription(public=True, text=description or "", rich_text=rich_text or False) status_field = get_review_request_field('status')(self) status_field.record_change_entry(changedesc, self.status, type) changedesc.save() self.changedescs.add(changedesc) if type == self.SUBMITTED: if not self.public: raise PublishError("The draft must be public first.") else: self.commit_id = None self.status = type self.save(update_counts=True) review_request_closed.send(sender=self.__class__, user=user, review_request=self, type=type) else: # Update submission description. changedesc = self.changedescs.filter(public=True).latest() changedesc.timestamp = timezone.now() changedesc.text = description or "" changedesc.rich_text = rich_text changedesc.save() # Needed to renew last-update. self.save() # Delete the associated draft review request. if draft is not None: draft.delete()
def test_get_change_not_modified(self): """Testing the GET review-requests/<id>/changes/<id>/ API with Not Modified response""" review_request = self.create_review_request() changedesc = ChangeDescription(public=True) changedesc.save() review_request.changedescs.add(changedesc) self._testHttpCaching(get_change_item_url(changedesc), check_last_modified=True)
def test_is_new_for_user_with_owner(self): """Testing ChangeDescription.is_new_for_user with owner""" user = User.objects.create(username='******') changedesc = ChangeDescription( user=user, timestamp=datetime(2017, 9, 7, 15, 27, 0)) self.assertFalse(changedesc.is_new_for_user( user=user, last_visited=datetime(2017, 9, 7, 16, 0, 0)))
def test_determine_user_for_review_request(self): """Testing ChangeDescription.get_user for change descriptions for review requests """ review_request = self.create_review_request(publish=True) doc = review_request.submitter grumpy = User.objects.get(username='******') change1 = ChangeDescription() change1.record_field_change('foo', ['bar'], ['baz']) change1.save() review_request.changedescs.add(change1) change2 = ChangeDescription() change2.record_field_change('submitter', doc, grumpy, 'username') change2.save() review_request.changedescs.add(change2) change3 = ChangeDescription() change3.record_field_change('foo', ['bar'], ['baz']) change3.save() review_request.changedescs.add(change3) change4 = ChangeDescription() change4.record_field_change('submitter', grumpy, doc, 'username') change4.save() review_request.changedescs.add(change4) self.assertIsNone(change1.user) self.assertIsNone(change2.user) self.assertIsNone(change3.user) self.assertIsNone(change4.user) self.assertEqual(change1.get_user(review_request), doc) self.assertEqual(change2.get_user(review_request), doc) self.assertEqual(change3.get_user(review_request), grumpy) self.assertEqual(change4.get_user(review_request), grumpy) self.assertEqual(change1.user, doc) self.assertEqual(change2.user, doc) self.assertEqual(change3.user, grumpy) self.assertEqual(change4.user, grumpy)
def close(self, type, user=None, description=None, rich_text=False): """Closes the review request. The type must be one of SUBMITTED or DISCARDED. """ if (user and not self.is_mutable_by(user) and not user.has_perm( "reviews.can_change_status", self.local_site)): raise PermissionError if type not in [self.SUBMITTED, self.DISCARDED]: raise AttributeError("%s is not a valid close type" % type) if self.status != type: changedesc = ChangeDescription(public=True, text=description or "", rich_text=rich_text) changedesc.record_field_change('status', self.status, type) changedesc.save() self.changedescs.add(changedesc) if type == self.SUBMITTED: self.public = True else: self.commit_id = None self.status = type self.save(update_counts=True) review_request_closed.send(sender=self.__class__, user=user, review_request=self, type=type) else: # Update submission description. changedesc = self.changedescs.filter(public=True).latest() changedesc.timestamp = timezone.now() changedesc.text = description or "" changedesc.save() # Needed to renew last-update. self.save() try: draft = self.draft.get() except ObjectDoesNotExist: pass else: draft.delete()
def test_get_with_site_no_access(self): """Testing the GET review-requests/<id>/changes/<id>/ API without access to a local site """ review_request = self.create_review_request(publish=True, with_local_site=True) now = timezone.now() change = ChangeDescription(public=True, timestamp=now) change.record_field_change('summary', 'foo', 'bar') change.save() review_request.changedescs.add(change) rsp = self.api_get(get_change_item_url(change, self.local_site_name), expected_status=403) self.assertEqual(rsp['stat'], 'fail') self.assertEqual(rsp['err']['code'], PERMISSION_DENIED.code)
def testRecordString(self): """Testing record_field_change with a string value""" old_value = "abc" new_value = "def" changedesc = ChangeDescription() changedesc.record_field_change("test", old_value, new_value) self.assertTrue("test" in changedesc.fields_changed) self.assertTrue("old" in changedesc.fields_changed["test"]) self.assertTrue("new" in changedesc.fields_changed["test"]) self.assertTrue("added" not in changedesc.fields_changed["test"]) self.assertTrue("removed" not in changedesc.fields_changed["test"]) self.assertEqual(changedesc.fields_changed["test"]["old"], (old_value,)) self.assertEqual(changedesc.fields_changed["test"]["new"], (new_value,))
def test_record_string(self): """Testing ChangeDescription.record_field_change with a string value""" old_value = "abc" new_value = "def" changedesc = ChangeDescription() changedesc.record_field_change("test", old_value, new_value) self.assertIn("test", changedesc.fields_changed) self.assertIn("old", changedesc.fields_changed["test"]) self.assertIn("new", changedesc.fields_changed["test"]) self.assertNotIn("added", changedesc.fields_changed["test"]) self.assertNotIn("removed", changedesc.fields_changed["test"]) self.assertEqual(changedesc.fields_changed["test"]["old"], (old_value,)) self.assertEqual(changedesc.fields_changed["test"]["new"], (new_value,))
def reopen(self, user=None): """Reopens the review request for review.""" from reviewboard.reviews.models.review_request_draft import \ ReviewRequestDraft if (user and not self.is_mutable_by(user) and not user.has_perm( "reviews.can_change_status", self.local_site)): raise PermissionError old_status = self.status old_public = self.public if old_status != self.PENDING_REVIEW: # The reopening signal is only fired when actually making a status # change since the main consumers (extensions) probably only care # about changes. review_request_reopening.send(sender=self.__class__, user=user, review_request=self) changedesc = ChangeDescription(user=user or self.submitter) status_field = get_review_request_field('status')(self) status_field.record_change_entry(changedesc, old_status, self.PENDING_REVIEW) if old_status == self.DISCARDED: # A draft is needed if reopening a discarded review request. self.public = False changedesc.save() draft = ReviewRequestDraft.create(self) draft.changedesc = changedesc draft.save() else: changedesc.public = True changedesc.save() self.changedescs.add(changedesc) self.status = self.PENDING_REVIEW self.save(update_counts=True) review_request_reopened.send(sender=self.__class__, user=user, review_request=self, old_status=old_status, old_public=old_public)
def test_get_with_site(self): """Testing the GET review-requests/<id>/changes/<id>/ API with access to a local site """ review_request = self.create_review_request(publish=True, with_local_site=True) self._login_user(local_site=True) now = timezone.now() change = ChangeDescription(public=True, timestamp=now) change.record_field_change('summary', 'foo', 'bar') change.save() review_request.changedescs.add(change) rsp = self.api_get(get_change_item_url(change, self.local_site_name), expected_mimetype=change_item_mimetype) self.assertEqual(rsp['stat'], 'ok') self.assertEqual(rsp['change']['id'], change.pk)
def testRecordList(self): """Testing record_field_change with a list value""" old_value = [1, 2, 3] new_value = [2, 3, 4] changedesc = ChangeDescription() changedesc.record_field_change("test", old_value, new_value) self.assertTrue("test" in changedesc.fields_changed) self.assertTrue("old" in changedesc.fields_changed["test"]) self.assertTrue("new" in changedesc.fields_changed["test"]) self.assertTrue("added" in changedesc.fields_changed["test"]) self.assertTrue("removed" in changedesc.fields_changed["test"]) self.assertEqual(changedesc.fields_changed["test"]["old"], [(i,) for i in old_value]) self.assertEqual(changedesc.fields_changed["test"]["new"], [(i,) for i in new_value]) self.assertEqual(changedesc.fields_changed["test"]["added"], [(4,)]) self.assertEqual(changedesc.fields_changed["test"]["removed"], [(1,)])
def test_is_new_for_user_with_non_owner(self): """Testing ChangeDescription.is_new_for_user with non-owner""" user1 = User.objects.create_user(username='******', email='*****@*****.**') user2 = User.objects.create_user(username='******', email='*****@*****.**') changedesc = ChangeDescription( user=user1, timestamp=datetime(2017, 9, 7, 15, 27, 0)) self.assertTrue(changedesc.is_new_for_user( user=user2, last_visited=datetime(2017, 9, 7, 10, 0, 0))) self.assertFalse(changedesc.is_new_for_user( user=user2, last_visited=datetime(2017, 9, 7, 16, 0, 0))) self.assertFalse(changedesc.is_new_for_user( user=user2, last_visited=datetime(2017, 9, 7, 15, 27, 0)))
def test_record_object_list_name_field(self): """Testing ChangeDescription.record_field_change with an object list (using name_field) """ class DummyObject(object): def __init__(self, id): self.id = id self.text = "Object %s" % id def get_absolute_url(self): return "http://localhost/%s" % self.id objs = [DummyObject(i) for i in range(4)] old_value = [objs[0], objs[1], objs[2]] new_value = [objs[1], objs[2], objs[3]] changedesc = ChangeDescription() changedesc.record_field_change("test", old_value, new_value, "text") self.assertIn("test", changedesc.fields_changed) self.assertIn("old", changedesc.fields_changed["test"]) self.assertIn("new", changedesc.fields_changed["test"]) self.assertIn("added", changedesc.fields_changed["test"]) self.assertIn("removed", changedesc.fields_changed["test"]) self.assertEqual( set(changedesc.fields_changed["test"]["old"]), set([(obj.text, obj.get_absolute_url(), obj.id) for obj in old_value])) self.assertEqual( set(changedesc.fields_changed["test"]["new"]), set([(obj.text, obj.get_absolute_url(), obj.id) for obj in new_value])) self.assertEqual( set(changedesc.fields_changed["test"]["added"]), set([(new_value[2].text, new_value[2].get_absolute_url(), new_value[2].id)])) self.assertEqual( set(changedesc.fields_changed["test"]["removed"]), set([(old_value[0].text, old_value[0].get_absolute_url(), old_value[0].id)]))
def save_autolandrequest_id(self, fieldname, rr, autoland_request_id): # TODO: this method is only required while we are using change # descriptions to render autoland results. Once Bug 1176330 is # fixed this code can be removed. # There's possibly a race condition here with multiple web-heads. If # two requests come in at the same time to this endpoint, the request # that saves their value first here will get overwritten by the second # but the first request will have their changedescription come below # the second. In that case you'd have the "most recent" try build stats # appearing at the top be for a changedescription that has a different # try build below it (Super rare, not a big deal really). old_request_id = rr.extra_data.get(fieldname, None) rr.extra_data[fieldname] = autoland_request_id rr.save() # In order to display the fact that a build was kicked off in the UI, # we construct a change description that our TryField can render. changedesc = ChangeDescription(public=True, text='', rich_text=False) changedesc.record_field_change(fieldname, old_request_id, autoland_request_id) changedesc.save() rr.changedescs.add(changedesc)
def publish(self, review_request=None, user=None, trivial=False, send_notification=True): """Publishes this draft. This updates and returns the draft's ChangeDescription, which contains the changed fields. This is used by the e-mail template to tell people what's new and interesting. The draft's associated ReviewRequest object will be used if one isn't passed in. The keys that may be saved in ``fields_changed`` in the ChangeDescription are: * ``submitter`` * ``summary`` * ``description`` * ``testing_done`` * ``bugs_closed`` * ``depends_on`` * ``branch`` * ``target_groups`` * ``target_people`` * ``screenshots`` * ``screenshot_captions`` * ``diff`` * Any custom field IDs Each field in 'fields_changed' represents a changed field. This will save fields in the standard formats as defined by the 'ChangeDescription' documentation, with the exception of the 'screenshot_captions' and 'diff' fields. For the 'screenshot_captions' field, the value will be a dictionary of screenshot ID/dict pairs with the following fields: * ``old``: The old value of the field * ``new``: The new value of the field For the ``diff`` field, there is only ever an ``added`` field, containing the ID of the new diffset. The ``send_notification`` parameter is intended for internal use only, and is there to prevent duplicate notifications when being called by ReviewRequest.publish. """ if not review_request: review_request = self.review_request if not self.changedesc and review_request.public: self.changedesc = ChangeDescription() if not user: if self.changedesc: user = self.changedesc.get_user(self) else: user = review_request.submitter self.copy_fields_to_request(review_request) if self.diffset: self.diffset.history = review_request.diffset_history self.diffset.save(update_fields=['history']) # If no changes were made, raise exception and do not save if self.changedesc and not self.changedesc.has_modified_fields(): raise NotModifiedError() if self.changedesc: self.changedesc.user = user self.changedesc.timestamp = timezone.now() self.changedesc.public = True self.changedesc.save() review_request.changedescs.add(self.changedesc) review_request.description_rich_text = self.description_rich_text review_request.testing_done_rich_text = self.testing_done_rich_text review_request.rich_text = self.rich_text review_request.save() if send_notification: review_request_published.send(sender=review_request.__class__, user=user, review_request=review_request, trivial=trivial, changedesc=self.changedesc) return self.changedesc
def close(self, close_type=None, user=None, description=None, rich_text=False, **kwargs): """Closes the review request. Args: close_type (unicode): How the close occurs. This should be one of :py:attr:`SUBMITTED` or :py:attr:`DISCARDED`. user (django.contrib.auth.models.User): The user who is closing the review request. description (unicode): An optional description that indicates why the review request was closed. rich_text (bool): Indicates whether or not that the description is rich text. Raises: ValueError: The provided close type is not a valid value. PermissionError: The user does not have permission to close the review request. TypeError: Keyword arguments were supplied to the function. .. versionchanged:: 3.0 The ``type`` argument is deprecated: ``close_type`` should be used instead. This method raises :py:exc:`ValueError` instead of :py:exc:`AttributeError` when the ``close_type`` has an incorrect value. """ if close_type is None: try: close_type = kwargs.pop('type') except KeyError: raise AttributeError('close_type must be provided') warnings.warn( 'The "type" argument was deprecated in Review Board 3.0 and ' 'will be removed in a future version. Use "close_type" ' 'instead.') if kwargs: raise TypeError('close() does not accept keyword arguments.') if (user and not self.is_mutable_by(user) and not user.has_perm( "reviews.can_change_status", self.local_site)): raise PermissionError if close_type not in [self.SUBMITTED, self.DISCARDED]: raise ValueError("%s is not a valid close type" % type) review_request_closing.send(sender=type(self), user=user, review_request=self, close_type=close_type, type=deprecated_signal_argument( signal_name='review_request_closing', old_name='type', new_name='close_type', value=close_type), description=description, rich_text=rich_text) draft = get_object_or_none(self.draft) if self.status != close_type: if (draft is not None and not self.public and close_type == self.DISCARDED): # Copy over the draft information if this is a private discard. draft.copy_fields_to_request(self) # TODO: Use the user's default for rich_text. changedesc = ChangeDescription(public=True, text=description or "", rich_text=rich_text or False, user=user or self.submitter) status_field = get_review_request_field('status')(self) status_field.record_change_entry(changedesc, self.status, close_type) changedesc.save() self.changedescs.add(changedesc) if close_type == self.SUBMITTED: if not self.public: raise PublishError("The draft must be public first.") else: self.commit_id = None self.status = close_type self.save(update_counts=True) review_request_closed.send(sender=type(self), user=user, review_request=self, close_type=close_type, type=deprecated_signal_argument( signal_name='review_request_closed', old_name='type', new_name='close_type', value=close_type), description=description, rich_text=rich_text) else: # Update submission description. changedesc = self.changedescs.filter(public=True).latest() changedesc.timestamp = timezone.now() changedesc.text = description or "" changedesc.rich_text = rich_text changedesc.save() # Needed to renew last-update. self.save() # Delete the associated draft review request. if draft is not None: draft.delete()
def publish(self, review_request=None, user=None, trivial=False, send_notification=True, validate_fields=True, timestamp=None): """Publish this draft. This is an internal method. Programmatic publishes should use :py:meth:`reviewboard.reviews.models.review_request.ReviewRequest.publish` instead. This updates and returns the draft's ChangeDescription, which contains the changed fields. This is used by the e-mail template to tell people what's new and interesting. The keys that may be saved in ``fields_changed`` in the ChangeDescription are: * ``submitter`` * ``summary`` * ``description`` * ``testing_done`` * ``bugs_closed`` * ``depends_on`` * ``branch`` * ``target_groups`` * ``target_people`` * ``screenshots`` * ``screenshot_captions`` * ``diff`` * Any custom field IDs Each field in 'fields_changed' represents a changed field. This will save fields in the standard formats as defined by the 'ChangeDescription' documentation, with the exception of the 'screenshot_captions' and 'diff' fields. For the 'screenshot_captions' field, the value will be a dictionary of screenshot ID/dict pairs with the following fields: * ``old``: The old value of the field * ``new``: The new value of the field For the ``diff`` field, there is only ever an ``added`` field, containing the ID of the new diffset. Args: review_request (reviewboard.reviews.models.review_request. ReviewRequest, optional): The review request associated with this diff. If not provided, it will be looked up. user (django.contrib.auth.models.User, optional): The user publishing the draft. If not provided, this defaults to the review request submitter. trivial (bool, optional): Whether or not this is a trivial publish. Trivial publishes do not result in e-mail notifications. send_notification (bool, optional): Whether or not this will emit the :py:data:`reviewboard.reviews.signals.review_request_published` signal. This parameter is intended for internal use **only**. validate_fields (bool, optional): Whether or not the fields should be validated. This should only be ``False`` in the case of programmatic publishes, e.g., from close as submitted hooks. timestamp (datetime.datetime, optional): The datetime that should be used for all timestamps for objects published (:py:class:`~reviewboard.diffviewer.models.diff_set.DiffSet`, :py:class:`~reviewboard.changedescs.models.ChangeDescription`) over the course of the method. Returns: reviewboard.changedescs.models.ChangeDescription: The change description that results from this publish (if any). If this is an initial publish, there will be no change description (and this function will return ``None``). """ if timestamp is None: timestamp = timezone.now() if not review_request: review_request = self.review_request if not self.changedesc and review_request.public: self.changedesc = ChangeDescription() if not user: if self.changedesc: user = self.changedesc.get_user(self) else: user = review_request.submitter self.copy_fields_to_request(review_request) # If no changes were made, raise exception and do not save if self.changedesc and not self.changedesc.has_modified_fields(): raise NotModifiedError() if validate_fields: if not (self.target_groups.exists() or self.target_people.exists()): raise PublishError( ugettext('There must be at least one reviewer before this ' 'review request can be published.')) if not review_request.summary.strip(): raise PublishError( ugettext('The draft must have a summary.')) if not review_request.description.strip(): raise PublishError( ugettext('The draft must have a description.')) if self.diffset: self.diffset.history = review_request.diffset_history self.diffset.timestamp = timestamp self.diffset.save(update_fields=('history', 'timestamp')) if self.changedesc: self.changedesc.user = user self.changedesc.timestamp = timestamp self.changedesc.public = True self.changedesc.save() review_request.changedescs.add(self.changedesc) review_request.description_rich_text = self.description_rich_text review_request.testing_done_rich_text = self.testing_done_rich_text review_request.rich_text = self.rich_text review_request.save() if send_notification: review_request_published.send(sender=type(review_request), user=user, review_request=review_request, trivial=trivial, changedesc=self.changedesc) return self.changedesc
def publish(self, review_request=None, user=None, 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