def test_post_no_history_allowed(self): """Testing the POST <URL> API for a review request created without history support """ repository = self.create_repository(tool_name='Git') review_request = self.create_review_request( repository=repository, submitter=self.user, create_with_history=False) ReviewRequestDraft.create(review_request) diffset = self.create_diffset(review_request, draft=True) diff = SimpleUploadedFile('diff', self._DEFAULT_DIFF_CONTENTS, content_type='text/x-patch') with override_feature_checks(self.override_features): rsp = self.api_post( get_draft_diffcommit_list_url(review_request, diffset.revision), dict(self._DEFAULT_POST_DATA, **{ 'diff': diff, }), expected_status=400) self.assertEqual(rsp['stat'], 'fail') self.assertEqual(rsp['err']['code'], INVALID_ATTRIBUTE.code) self.assertEqual( rsp['reason'], 'This review request was not created with support for multiple ' 'commits.\n\n' 'Use the draft_diff resource to upload diffs instead. See the ' 'draft_diff link on the parent resource for the URL.')
def test_get_or_create_user_auth_backend(self): """Testing the PUT review-requests/<id>/draft/ API with AuthBackend.get_or_create_user failure """ class SandboxAuthBackend(AuthBackend): backend_id = 'test-id' name = 'test' def get_or_create_user(self, username, request=None, password=None): raise Exception backend = SandboxAuthBackend() self.spy_on(auth.get_backends, call_fake=lambda: [backend]) # The first spy messes with permissions, this lets it through self.spy_on(ReviewRequest.is_mutable_by, call_fake=lambda x, y: True) self.spy_on(backend.get_or_create_user) review_request = self.create_review_request( submitter=self.user) ReviewRequestDraft.create(review_request) rsp = self.api_put( get_review_request_draft_url(review_request, None), { 'target_people': 'Target', }, expected_status=400) self.assertEqual(rsp['stat'], 'fail') self.assertTrue(backend.get_or_create_user.called)
def test_create_with_existing_new_draft(self): """Testing ReviewRequestDraft.create with existing draft""" review_request = self.create_review_request( publish=True, bugs_closed='1,20,300', commit_id='abc123', description_rich_text=True, rich_text=True, testing_done_rich_text=True, extra_data={ 'key': { 'values': [1, 2, 3], }, 'mybool': True, }) # Create the first draft. orig_draft = ReviewRequestDraft.create(review_request) self.assertIsNotNone(orig_draft.changedesc) # Try to create it again. draft = ReviewRequestDraft.create(review_request) self.assertIsNotNone(draft.changedesc) self.assertEqual(orig_draft, draft) self.assertEqual(orig_draft.changedesc, draft.changedesc)
def setup_basic_delete_test(self, user, with_local_site, local_site_name): review_request = self.create_review_request( with_local_site=with_local_site, submitter=user, publish=True) ReviewRequestDraft.create(review_request) return (get_review_request_draft_url(review_request, local_site_name), [review_request])
def test_outgoing_requests(self): """Testing counters with creating outgoing review requests""" # The review request was already created self._check_counters(total_outgoing=1, pending_outgoing=1) ReviewRequestDraft.create(self.review_request) self.review_request.publish(self.user) self._check_counters(total_outgoing=1, pending_outgoing=1, starred_public=1)
def test_put_publish(self): """Testing the PUT review-requests/<id>/draft/?public=1 API""" self.siteconfig.set("mail_send_review_mail", True) self.siteconfig.save() review_request = self.create_review_request(submitter=self.user, publish=True) draft = ReviewRequestDraft.create(review_request) draft.summary = "My Summary" draft.description = "My Description" draft.testing_done = "My Testing Done" draft.branch = "My Branch" draft.target_people.add(User.objects.get(username="******")) draft.save() mail.outbox = [] rsp = self.apiPut( get_review_request_draft_url(review_request), {"public": True}, expected_mimetype=review_request_draft_item_mimetype, ) self.assertEqual(rsp["stat"], "ok") review_request = ReviewRequest.objects.get(pk=review_request.id) self.assertEqual(review_request.summary, "My Summary") self.assertEqual(review_request.description, "My Description") self.assertEqual(review_request.testing_done, "My Testing Done") self.assertEqual(review_request.branch, "My Branch") self.assertTrue(review_request.public) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, "Re: Review Request %s: My Summary" % review_request.pk) self.assertValidRecipients(["doc", "grumpy"])
def _test_put_with_text_type_escaping_unspecified_fields(self, text_type, text, expected_text): self.assertIn(text_type, ("markdown", "plain")) rich_text = text_type == "markdown" description = "`This` is the **description**" review_request = self.create_review_request(submitter=self.user, publish=True) review_request.rich_text = not rich_text review_request.description = text review_request.testing_done = text review_request.save() draft = ReviewRequestDraft.create(review_request) draft.changedesc.text = text draft.changedesc.save() rsp = self.api_put( get_review_request_draft_url(review_request), {"text_type": text_type, "description": description}, expected_mimetype=review_request_draft_item_mimetype, ) self.assertEqual(rsp["stat"], "ok") draft_rsp = rsp["draft"] self.assertEqual(draft_rsp["text_type"], text_type) self.assertEqual(draft_rsp["changedescription"], expected_text) self.assertEqual(draft_rsp["description"], description) self.assertEqual(draft_rsp["testing_done"], expected_text) draft = ReviewRequestDraft.objects.get(pk=rsp["draft"]["id"]) self.compare_item(draft_rsp, draft)
def test_put_without_text_type_and_escaping_provided_fields(self): """Testing the PUT review-requests/<id>/draft/ API without changing text_type and with escaping provided fields """ review_request = self.create_review_request(submitter=self.user, publish=True) review_request.rich_text = True review_request.save() draft = ReviewRequestDraft.create(review_request) self.assertTrue(draft.rich_text) self.assertTrue(draft.changedesc.rich_text) rsp = self.api_put( get_review_request_draft_url(review_request), { "description": "This is **Description**", "testing_done": "This is **Testing Done**", "changedescription": "This is **Change Description**", }, expected_mimetype=review_request_draft_item_mimetype, ) self.assertEqual(rsp["stat"], "ok") draft_rsp = rsp["draft"] self.assertEqual(draft_rsp["text_type"], "markdown") self.assertEqual(draft_rsp["description"], "This is \*\*Description\*\*") self.assertEqual(draft_rsp["testing_done"], "This is \*\*Testing Done\*\*") self.assertEqual(draft_rsp["changedescription"], "This is \*\*Change Description\*\*") draft = ReviewRequestDraft.objects.get(pk=rsp["draft"]["id"]) self.compare_item(draft_rsp, draft)
def _test_put_with_rich_text_escaping_unspecified_fields( self, rich_text, text, expected_text): description = '`This` is the **description**' review_request = self.create_review_request(submitter=self.user, publish=True) review_request.rich_text = not rich_text review_request.description = text review_request.testing_done = text review_request.save() draft = ReviewRequestDraft.create(review_request) draft.changedesc.text = text draft.changedesc.save() rsp = self.apiPut( get_review_request_draft_url(review_request), { 'rich_text': rich_text, 'description': description, }, expected_mimetype=review_request_draft_item_mimetype) self.assertEqual(rsp['stat'], 'ok') draft_rsp = rsp['draft'] self.assertEqual(draft_rsp['rich_text'], rich_text) self.assertEqual(draft_rsp['changedescription'], expected_text) self.assertEqual(draft_rsp['description'], description) self.assertEqual(draft_rsp['testing_done'], expected_text) draft = ReviewRequestDraft.objects.get(pk=rsp['draft']['id']) self.compare_item(draft_rsp, draft)
def create_diffset(self, review_request=None, revision=1, repository=None, draft=False, name='diffset'): """Creates a DiffSet for testing. The DiffSet defaults to revision 1. This can be overriden by the caller. DiffSets generally are tied to a ReviewRequest, but it's optional. """ if review_request: repository = review_request.repository diffset = DiffSet.objects.create( name=name, revision=revision, repository=repository) if review_request: if draft: review_request_draft = \ ReviewRequestDraft.create(review_request) review_request_draft.diffset = diffset review_request_draft.save() else: review_request.diffset_history.diffsets.add(diffset) return diffset
def _test_put_with_text_type_escaping_all_fields( self, text_type, text, expected_text): self.assertIn(text_type, ('markdown', 'plain')) rich_text = (text_type == 'markdown') review_request = self.create_review_request(submitter=self.user, publish=True) review_request.rich_text = not rich_text review_request.description = text review_request.testing_done = text review_request.save() draft = ReviewRequestDraft.create(review_request) draft.changedesc.text = text draft.changedesc.save() rsp = self.api_put( get_review_request_draft_url(review_request), { 'text_type': text_type, }, expected_mimetype=review_request_draft_item_mimetype) self.assertEqual(rsp['stat'], 'ok') draft_rsp = rsp['draft'] self.assertEqual(draft_rsp['text_type'], text_type) self.assertEqual(draft_rsp['changedescription'], expected_text) self.assertEqual(draft_rsp['description'], expected_text) self.assertEqual(draft_rsp['testing_done'], expected_text) draft = ReviewRequestDraft.objects.get(pk=rsp['draft']['id']) self.compare_item(draft_rsp, draft)
def create_screenshot(self, review_request, caption='My caption', draft=False, active=True, **kwargs): """Create a Screenshot for testing. The screenshot is tied to the given :py:class:`~reviewboard.reviews.models.review_request.ReviewRequest`. It's populated with default data that can be overridden by the caller. Args: review_request (reviewboard.reviews.models.review_request. ReviewRequest): The review request that ultimately owns the screenshot. caption (unicode, optional): The caption to use for the screenshot. draft (bool or reviewboard.reviews.models.review_request_draft. ReviewRequestDraft): A draft to associate the screenshot with. This can also be a boolean, for legacy reasons, which will attempt to look up or create a draft for the review request. active (bool): Whether this screenshot is considered active (not deleted). **kwargs (dict): Additional fields to set on the screenshot. Returns: reviewboard.reviews.models.screenshot.Screenshot: The resulting screenshot. """ screenshot = Screenshot(caption=caption, **kwargs) filename = os.path.join(settings.STATIC_ROOT, 'rb', 'images', 'logo.png') with open(filename, 'r') as f: screenshot.image.save(filename, File(f), save=True) if draft: if isinstance(draft, ReviewRequestDraft): review_request_draft = draft else: review_request_draft = \ ReviewRequestDraft.create(review_request) if active: screenshots = review_request_draft.screenshots else: screenshots = review_request_draft.inactive_screenshots else: if active: screenshots = review_request.screenshots else: screenshots = review_request.inactive_screenshots screenshots.add(screenshot) return screenshot
def test_discard_unpublished_public(self): """Testing ReviewRequest.close with public requests on discard to ensure changes from draft are not copied over """ review_request = self.create_review_request( publish=False, public=True) self.assertTrue(review_request.public) self.assertNotEqual(review_request.status, ReviewRequest.DISCARDED) draft = ReviewRequestDraft.create(review_request) summary = 'Test summary' description = 'Test description' testing_done = 'Test testing done' draft.summary = summary draft.description = description draft.testing_done = testing_done draft.save() review_request.close(ReviewRequest.DISCARDED) self.assertNotEqual(review_request.summary, summary) self.assertNotEqual(review_request.description, description) self.assertNotEqual(review_request.testing_done, testing_done)
def create(self, file, review_request, filediff=None): caption = self.cleaned_data['caption'] or file.name mimetype = file.content_type or self._guess_mimetype(file) filename = '%s__%s' % (uuid4(), file.name) attachment_kwargs = { 'caption': '', 'draft_caption': caption, 'orig_filename': os.path.basename(file.name), 'mimetype': mimetype, } if filediff: file_attachment = FileAttachment.objects.create_from_filediff( filediff, save=False, **attachment_kwargs) else: file_attachment = FileAttachment(**attachment_kwargs) file_attachment.file.save(filename, file, save=True) draft = ReviewRequestDraft.create(review_request) draft.file_attachments.add(file_attachment) draft.save() return file_attachment
def test_update_from_committed_change_with_rich_text_reset(self): """Testing ReviewRequestDraft.update_from_commit_id resets rich text fields """ def get_change(repository, commit_to_get): commit = Commit( message='* This is a summary\n\n* This is a description.') diff_filename = os.path.join(self.testdata_dir, 'git_readme.diff') with open(diff_filename, 'r') as f: commit.diff = f.read() return commit def get_file_exists(repository, path, revision, base_commit_id=None, request=None): return (path, revision) in [('/readme', 'd6613f5')] review_request = ReviewRequest.objects.create(self.user, self.repository) draft = ReviewRequestDraft.create(review_request) self.spy_on(draft.repository.get_change, call_fake=get_change) self.spy_on(draft.repository.get_file_exists, call_fake=get_file_exists) draft.description_rich_text = True draft.update_from_commit_id('4') self.assertEqual(draft.summary, '* This is a summary') self.assertEqual(draft.description, '* This is a description.') self.assertFalse(draft.description_rich_text) self.assertFalse(review_request.description_rich_text)
def create(self, file, review_request, filediff=None): caption = self.cleaned_data['caption'] or file.name if (not file.content_type or file.content_type == 'application/octet-stream'): # We can't rely on the browser for the file type here, so # attempt to guess it. mimetype = self._guess_mimetype(file) else: mimetype = file.content_type filename = '%s__%s' % (uuid4(), file.name) attachment_kwargs = { 'caption': '', 'draft_caption': caption, 'orig_filename': os.path.basename(file.name), 'mimetype': mimetype, } if filediff: file_attachment = FileAttachment.objects.create_from_filediff( filediff, save=False, **attachment_kwargs) else: file_attachment = FileAttachment(**attachment_kwargs) file_attachment.file.save(filename, file, save=True) draft = ReviewRequestDraft.create(review_request) draft.file_attachments.add(file_attachment) draft.save() return file_attachment
def test_limited_recipients_other_fields(self): """Testing that recipient limiting only happens when adding reviewers """ review_request = self.create_review_request( summary='My test review request', public=True) review_request.email_message_id = "junk" review_request.target_people.add(User.objects.get(username='******')) review_request.save() draft = ReviewRequestDraft.create(review_request) draft.summary = 'Changed summary' draft.target_people.add(User.objects.get(username='******')) draft.publish(user=review_request.submitter) from_email = get_email_address_for_user(review_request.submitter) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].from_email, self.sender) self.assertEqual(mail.outbox[0].extra_headers['From'], from_email) self.assertEqual(mail.outbox[0].subject, 'Re: Review Request %s: Changed summary' % review_request.pk) self.assertValidRecipients([review_request.submitter.username, 'dopey', 'grumpy']) message = mail.outbox[0].message() self.assertEqual(message['Sender'], self._get_sender(review_request.submitter))
def test_recipients_with_muted_review_requests(self): """Testing e-mail recipients when users mute a review request""" dopey = User.objects.get(username='******') admin = User.objects.get(username='******') group = Group.objects.create(name='group') group.users.add(admin) group.save() review_request = self.create_review_request( summary='My test review request', public=True) review_request.target_people.add(dopey) review_request.target_people.add(User.objects.get(username='******')) review_request.target_groups.add(group) review_request.save() visit = self.create_visit(review_request, ReviewRequestVisit.MUTED, dopey) visit.save() visit = self.create_visit(review_request, ReviewRequestVisit.MUTED, admin) visit.save() draft = ReviewRequestDraft.create(review_request) draft.summary = 'Summary changed' draft.publish(user=review_request.submitter) self.assertEqual(len(mail.outbox), 1) self.assertValidRecipients(['doc', 'grumpy'])
def test_create_with_new_draft_and_custom_changedesc(self): """Testing ReviewRequestDraft.create with new draft and custom ChangeDescription """ review_request = self.create_review_request( publish=True, bugs_closed='1,20,300', commit_id='abc123', description_rich_text=True, rich_text=True, testing_done_rich_text=True, extra_data={ 'key': { 'values': [1, 2, 3], }, 'mybool': True, }) # Create the draft. changedesc = ChangeDescription.objects.create() orig_draft = ReviewRequestDraft.create(review_request, changedesc=changedesc) self.assertEqual(orig_draft.changedesc_id, changedesc.pk) self.assertEqual(ChangeDescription.objects.count(), 1) # Reload to be sure. draft = ReviewRequestDraft.objects.get(pk=orig_draft.pk) self.assertEqual(orig_draft, draft) self.assertEqual(draft.changedesc, changedesc)
def test_add_reviewer_review_request_email(self): """Testing limited e-mail recipients when adding a reviewer to an existing review request """ review_request = self.create_review_request( summary='My test review request', public=True) review_request.email_message_id = "junk" review_request.target_people.add(User.objects.get(username='******')) review_request.save() draft = ReviewRequestDraft.create(review_request) draft.target_people.add(User.objects.get(username='******')) draft.publish(user=review_request.submitter) from_email = get_email_address_for_user(review_request.submitter) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].from_email, self.sender) self.assertEqual(mail.outbox[0].extra_headers['From'], from_email) self.assertEqual(mail.outbox[0].subject, 'Re: Review Request %s: My test review request' % review_request.pk) # The only included users should be the submitter and 'grumpy' (not # 'dopey', since he was already included on the review request earlier) self.assertValidRecipients([review_request.submitter.username, 'grumpy']) message = mail.outbox[0].message() self.assertEqual(message['Sender'], self._get_sender(review_request.submitter))
def test_update_from_committed_change_with_rich_text_reset(self): """Testing ReviewRequestDraft.update_from_commit_id resets rich text fields """ def get_change(repository, commit_to_get): commit = Commit( message='* This is a summary\n\n* This is a description.', diff=self.DEFAULT_GIT_README_DIFF) return commit review_request = ReviewRequest.objects.create(self.user, self.repository) draft = ReviewRequestDraft.create(review_request) self.spy_on(draft.repository.get_change, call_fake=get_change) self.spy_on(draft.repository.get_file_exists) draft.description_rich_text = True draft.update_from_commit_id('4') self.assertFalse(draft.repository.get_file_exists.called) self.assertEqual(draft.summary, '* This is a summary') self.assertEqual(draft.description, '* This is a description.') self.assertFalse(draft.description_rich_text) self.assertFalse(review_request.description_rich_text)
def test_put_with_commit_id_and_used_in_draft(self): """Testing the PUT review-requests/<id>/draft/ API with commit_id used in another review request draft """ commit_id = 'abc123' existing_review_request = self.create_review_request( submitter=self.user, publish=True) existing_draft = ReviewRequestDraft.create(existing_review_request) existing_draft.commit_id = commit_id existing_draft.save() review_request = self.create_review_request(submitter=self.user, publish=True) self.api_put( get_review_request_draft_url(review_request), { 'commit_id': commit_id, }, expected_status=409) review_request = ReviewRequest.objects.get(pk=review_request.pk) self.assertIsNone(review_request.commit_id, None)
def create_file_attachment(self, review_request, orig_filename='filename.png', caption='My Caption', draft=False, **kwargs): """Creates a FileAttachment for testing. The FileAttachment is tied to the given ReviewRequest. It's populated with default data that can be overridden by the caller. """ file_attachment = FileAttachment( caption=caption, orig_filename=orig_filename, mimetype='image/png', **kwargs) filename = os.path.join(settings.STATIC_ROOT, 'rb', 'images', 'trophy.png') with open(filename, 'r') as f: file_attachment.file.save(filename, File(f), save=True) if draft: review_request_draft = ReviewRequestDraft.create(review_request) review_request_draft.file_attachments.add(file_attachment) else: review_request.file_attachments.add(file_attachment) return file_attachment
def test_create_with_site_and_commit_id_conflicts_draft(self): """Testing ReviewRequest.objects.create with LocalSite and commit ID that conflicts with a draft """ user = User.objects.get(username='******') local_site = LocalSite.objects.create(name='test') repository = self.create_repository() # This one should be fine. existing_review_request = ReviewRequest.objects.create( user, repository, local_site=local_site) existing_draft = ReviewRequestDraft.create(existing_review_request) existing_draft.commit_id = '123' existing_draft.save() self.assertEqual(local_site.review_requests.count(), 1) # This one will yell. with self.assertRaises(ChangeNumberInUseError): ReviewRequest.objects.create( user, repository, commit_id='123', local_site=local_site) # Make sure that entry doesn't exist in the database. self.assertEqual(local_site.review_requests.count(), 1)
def test_put_with_no_changes(self): """Testing the PUT review-requests/<id>/draft/ API with no changes made to the fields """ review_request = self.create_review_request(submitter=self.user, publish=True) ReviewRequestDraft.create(review_request) rsp = self.api_put( get_review_request_draft_url(review_request), {'public': True}, expected_status=NOTHING_TO_PUBLISH.http_status) self.assertEqual(rsp['stat'], 'fail') self.assertEqual(rsp['err']['code'], NOTHING_TO_PUBLISH.code)
def test_populate_counters(self): """Testing counters when populated from a fresh upgrade or clear""" # The review request was already created draft = ReviewRequestDraft.create(self.review_request) draft.target_groups.add(self.group) draft.target_people.add(self.user) self.review_request.publish(self.user) self._check_counters(total_outgoing=1, pending_outgoing=1, total_incoming=1, direct_incoming=1, starred_public=1, group_incoming=1) LocalSiteProfile.objects.update( direct_incoming_request_count=None, total_incoming_request_count=None, pending_outgoing_request_count=None, total_outgoing_request_count=None, starred_public_request_count=None) Group.objects.update(incoming_request_count=None) self._check_counters(total_outgoing=1, pending_outgoing=1, total_incoming=1, direct_incoming=1, starred_public=1, group_incoming=1)
def test_remove_person_and_fail_publish(self): """Testing counters when removing a person reviewer and then failing to publish the draft """ self.test_add_person() draft = ReviewRequestDraft.create(self.review_request) draft.target_people.remove(self.user) self._check_counters(total_outgoing=1, pending_outgoing=1, direct_incoming=1, total_incoming=1, starred_public=1) self.spy_on(ReviewRequestDraft.publish, call_fake=self._raise_publish_error) with self.assertRaises(NotModifiedError): self.review_request.publish(self.user) self._check_counters(total_outgoing=1, pending_outgoing=1, direct_incoming=1, total_incoming=1, starred_public=1)
def setUp(self): super(SandboxTests, self).setUp() register_ui(InitReviewUI) register_ui(SandboxReviewUI) register_ui(ConflictFreeReviewUI) self.factory = RequestFactory() filename = os.path.join(settings.STATIC_ROOT, 'rb', 'images', 'trophy.png') with open(filename, 'r') as f: self.file = SimpleUploadedFile(f.name, f.read(), content_type='image/png') self.user = User.objects.get(username='******') self.review_request = ReviewRequest.objects.create(self.user, None) self.file_attachment1 = FileAttachment.objects.create( mimetype='image/jpg', file=self.file) self.file_attachment2 = FileAttachment.objects.create( mimetype='image/png', file=self.file) self.file_attachment3 = FileAttachment.objects.create( mimetype='image/gif', file=self.file) self.review_request.file_attachments.add(self.file_attachment1) self.review_request.file_attachments.add(self.file_attachment2) self.review_request.file_attachments.add(self.file_attachment3) self.draft = ReviewRequestDraft.create(self.review_request)
def create_file_attachment(self, review_request, orig_filename='filename.png', caption='My Caption', draft=False, active=True, **kwargs): """Creates a FileAttachment for testing. The FileAttachment is tied to the given ReviewRequest. It's populated with default data that can be overridden by the caller. """ file_attachment = self._create_base_file_attachment( caption=caption, orig_filename=orig_filename, **kwargs) if draft: review_request_draft = ReviewRequestDraft.create(review_request) if active: attachments = review_request_draft.file_attachments else: attachments = review_request_draft.inactive_file_attachments else: if active: attachments = review_request.file_attachments else: attachments = review_request.inactive_file_attachments attachments.add(file_attachment) return file_attachment
def create_screenshot(self, review_request, caption='My caption', draft=False, active=True): """Creates a Screenshot for testing. The Screenshot is tied to the given ReviewRequest. It's populated with default data that can be overridden by the caller. """ screenshot = Screenshot(caption=caption) filename = os.path.join(settings.STATIC_ROOT, 'rb', 'images', 'logo.png') with open(filename, 'r') as f: screenshot.image.save(filename, File(f), save=True) if draft: review_request_draft = ReviewRequestDraft.create(review_request) if active: screenshots = review_request_draft.screenshots else: screenshots = review_request_draft.inactive_screenshots else: if active: screenshots = review_request.screenshots else: screenshots = review_request.inactive_screenshots screenshots.add(screenshot) return screenshot
def test_put_without_rich_text_and_escaping_provided_fields(self): """Testing the PUT review-requests/<id>/draft/ API without changing rich_text and with escaping provided fields """ review_request = self.create_review_request(submitter=self.user, publish=True) review_request.rich_text = True review_request.save() draft = ReviewRequestDraft.create(review_request) self.assertTrue(draft.rich_text) self.assertTrue(draft.changedesc.rich_text) rsp = self.apiPut( get_review_request_draft_url(review_request), { 'description': 'This is **Description**', 'testing_done': 'This is **Testing Done**', 'changedescription': 'This is **Change Description**', }, expected_mimetype=review_request_draft_item_mimetype) self.assertEqual(rsp['stat'], 'ok') draft_rsp = rsp['draft'] self.assertTrue(draft_rsp['rich_text']) self.assertEqual(draft_rsp['description'], 'This is \*\*Description\*\*') self.assertEqual(draft_rsp['testing_done'], 'This is \*\*Testing Done\*\*') self.assertEqual(draft_rsp['changedescription'], 'This is \*\*Change Description\*\*') draft = ReviewRequestDraft.objects.get(pk=rsp['draft']['id']) self.compare_item(draft_rsp, draft)
def test_remove_group(self): """Testing counters when removing a group reviewer""" self.test_add_group() draft = ReviewRequestDraft.create(self.review_request) draft.target_groups.remove(self.group) # There must be at least one target_group or target_people draft.target_people = [self.user] self._check_counters(total_outgoing=1, pending_outgoing=1, total_incoming=1, direct_incoming=0, group_incoming=1, starred_public=1) self.review_request.publish(self.user) self._check_counters(total_outgoing=1, pending_outgoing=1, direct_incoming=1, total_incoming=1, starred_public=1)
def test_put_publish(self): """Testing the PUT review-requests/<id>/draft/?public=1 API""" self.siteconfig.set('mail_send_review_mail', True) self.siteconfig.save() review_request = self.create_review_request(submitter=self.user, publish=True) draft = ReviewRequestDraft.create(review_request) draft.summary = 'My Summary' draft.description = 'My Description' draft.testing_done = 'My Testing Done' draft.branch = 'My Branch' draft.target_people.add(User.objects.get(username='******')) draft.save() mail.outbox = [] rsp = self.apiPut( get_review_request_draft_url(review_request), {'public': True}, expected_mimetype=review_request_draft_item_mimetype) self.assertEqual(rsp['stat'], 'ok') review_request = ReviewRequest.objects.get(pk=review_request.id) self.assertEqual(review_request.summary, "My Summary") self.assertEqual(review_request.description, "My Description") self.assertEqual(review_request.testing_done, "My Testing Done") self.assertEqual(review_request.branch, "My Branch") self.assertTrue(review_request.public) self.assertEqual(len(mail.outbox), 1) self.assertEqual( mail.outbox[0].subject, "Re: Review Request %s: My Summary" % review_request.pk) self.assertValidRecipients(["doc", "grumpy"])
def test_create_with_new_draft(self): """Testing ReviewRequestDraft.create with new draft""" user1 = User.objects.create(username='******') user2 = User.objects.create(username='******') group1 = self.create_review_group(name='group1') group2 = self.create_review_group(name='group2') dep_review_request_1 = self.create_review_request(publish=True) dep_review_request_2 = self.create_review_request(publish=True) review_request = self.create_review_request( publish=True, bugs_closed='1,20,300', commit_id='abc123', description_rich_text=True, depends_on=[dep_review_request_1, dep_review_request_2], rich_text=True, target_groups=[group1, group2], target_people=[user1, user2], testing_done_rich_text=True, extra_data={ 'key': { 'values': [1, 2, 3], }, 'mybool': True, }) active_file_attachment_1 = self.create_file_attachment(review_request) active_file_attachment_2 = self.create_file_attachment(review_request) inactive_file_attachment = self.create_file_attachment(review_request, active=False) active_screenshot_1 = self.create_screenshot(review_request) active_screenshot_2 = self.create_screenshot(review_request) inactive_screenshot = self.create_screenshot(review_request, active=False) # Create the draft. draft = ReviewRequestDraft.create(review_request) # Make sure all the fields are the same. self.assertEqual(draft.branch, review_request.branch) self.assertEqual(draft.bugs_closed, review_request.bugs_closed) self.assertEqual(draft.commit_id, review_request.commit_id) self.assertEqual(draft.description, review_request.description) self.assertEqual(draft.description_rich_text, review_request.description_rich_text) self.assertEqual(draft.extra_data, review_request.extra_data) self.assertEqual(draft.rich_text, review_request.rich_text) self.assertEqual(draft.summary, review_request.summary) self.assertEqual(draft.testing_done, review_request.testing_done) self.assertEqual(draft.testing_done_rich_text, review_request.testing_done_rich_text) self.assertEqual(list(draft.depends_on.order_by('pk')), [dep_review_request_1, dep_review_request_2]) self.assertEqual(list(draft.target_groups.all()), [group1, group2]) self.assertEqual(list(draft.target_people.all()), [user1, user2]) self.assertEqual(list(draft.file_attachments.all()), [active_file_attachment_1, active_file_attachment_2]) self.assertEqual(list(draft.inactive_file_attachments.all()), [inactive_file_attachment]) self.assertEqual(list(draft.screenshots.all()), [active_screenshot_1, active_screenshot_2]) self.assertEqual(list(draft.inactive_screenshots.all()), [inactive_screenshot]) self.assertIsNotNone(draft.changedesc)
def prepare_draft(self, request, review_request): """Creates a draft, if the user has permission to.""" if not review_request.is_mutable_by(request.user): raise PermissionDenied return ReviewRequestDraft.create(review_request)
def create(self, request, parent_request_id, reviewers, *args, **kwargs): try: parent_rr = ReviewRequest.objects.get(pk=parent_request_id) except ReviewRequest.DoesNotExist: return DOES_NOT_EXIST if not (parent_rr.is_accessible_by(request.user) or parent_rr.is_mutable_by(request.user)): return PERMISSION_DENIED if not is_parent(parent_rr): return NOT_PARENT # Validate and expand the new reviewer list. bugzilla = Bugzilla(get_bugzilla_api_key(request.user)) child_reviewers = json.loads(reviewers) invalid_reviewers = [] for child_rrid in child_reviewers: users = [] for username in child_reviewers[child_rrid]: try: users.append(bugzilla.get_user_from_irc_nick(username)) except User.DoesNotExist: invalid_reviewers.append(username) child_reviewers[child_rrid] = users if invalid_reviewers: # Because this isn't called through Review Board's built-in # backbone system, it's dramatically simpler to return just the # intended error message instead of categorising the errors by # field. if len(invalid_reviewers) == 1: return INVALID_FORM_DATA.with_message( "The reviewer '%s' was not found" % invalid_reviewers[0]) else: return INVALID_FORM_DATA.with_message( "The reviewers '%s' were not found" % "', '".join(invalid_reviewers)) # Review Board only supports the submitter updating a review # request. In order for this to work, we publish these changes # in Review Board under the review submitter's account, and # set an extra_data field which instructs our bugzilla # connector to use this request's user when adjusting flags. # # Updating the review request requires creating a draft and # publishing it, so we have to be careful to not overwrite # existing drafts. try: with transaction.atomic(): for rr in itertools.chain([parent_rr], gen_child_rrs(parent_rr)): if rr.get_draft() is not None: return REVIEW_REQUEST_UPDATE_NOT_ALLOWED.with_message( "Unable to update reviewers as the review " "request has pending changes (the patch author " "has a draft)") try: for child_rr in gen_child_rrs(parent_rr): if str(child_rr.id) in child_reviewers: if not child_rr.is_accessible_by(request.user): return PERMISSION_DENIED.with_message( "You do not have permission to update " "reviewers on review request %s" % child_rr.id) draft = ReviewRequestDraft.create(child_rr) draft.target_people.clear() for user in child_reviewers[str(child_rr.id)]: draft.target_people.add(user) set_publish_as(parent_rr, request.user) parent_rr_draft = ReviewRequestDraft.create(parent_rr) update_parent_rr_reviewers(parent_rr_draft) parent_rr.publish(user=parent_rr.submitter) finally: clear_publish_as(parent_rr) except PublishError as e: logging.error("failed to update reviewers on %s: %s" % (parent_rr.id, str(e))) return PUBLISH_ERROR.with_message(str(e)) return 200, {}
def update_review_request(local_site, request, privileged_user, reviewer_cache, rr, commit): """Synchronize the state of a review request with a commit. Updates the commit message, refreshes the diff, etc. """ try: draft = rr.draft.get() except ReviewRequestDraft.DoesNotExist: draft = ReviewRequestDraft.create(rr) draft.summary = commit['message'].splitlines()[0] draft.description = commit['message'] draft.bugs_closed = commit['bug'] commit_data = fetch_commit_data(draft) commit_data.draft_extra_data.update({ AUTHOR_KEY: commit['author'], COMMIT_ID_KEY: commit['id'], FIRST_PUBLIC_ANCESTOR_KEY: commit['first_public_ancestor'], }) commit_data.save(update_fields=['draft_extra_data']) reviewer_users, unrecognized_reviewers = \ resolve_reviewers(reviewer_cache, commit.get('reviewers', [])) requal_reviewer_users, unrecognized_requal_reviewers = \ resolve_reviewers(reviewer_cache, commit.get('requal_reviewers', [])) warnings = [] for reviewer in unrecognized_reviewers | unrecognized_requal_reviewers: warnings.append('unrecognized reviewer: %s' % reviewer) logger.info('unrecognized reviewer: %s' % reviewer) if requal_reviewer_users: pr = previous_reviewers(rr) for user in requal_reviewer_users: if not pr.get(user.username, False): warnings.append('commit message for %s has r=%s but they ' 'have not granted a ship-it. review will be ' 'requested on your behalf' % (commit['id'][:12], user.username)) reviewer_users |= requal_reviewer_users # Carry over from last time unless commit message overrules. if reviewer_users: draft.target_people.clear() for user in sorted(reviewer_users): draft.target_people.add(user) logger.debug('adding reviewer %s to #%d' % (user.username, rr.id)) try: diffset = DiffSet.objects.create_from_data( repository=rr.repository, diff_file_name='diff', diff_file_contents=commit['diff_b64'].encode('ascii').decode( 'base64'), parent_diff_file_name='diff', parent_diff_file_contents=None, diffset_history=None, basedir='', request=request, base_commit_id=commit.get('base_commit_id'), save=True, ) update_diffset_history(rr, diffset) diffset.save() DiffSetVerification(diffset=diffset).save( authorized_user=privileged_user, force_insert=True) except Exception: logger.exception('error processing diff') raise DiffProcessingException() update_review_request_draft_diffset(rr, diffset, draft=draft) return draft, warnings
def create_file_attachment(self, review_request, orig_filename='filename.png', caption='My Caption', draft=False, active=True, **kwargs): """Create a FileAttachment for testing. The attachment is tied to the given :py:class:`~reviewboard.reviews.models.review_request.ReviewRequest`. It's populated with default data that can be overridden by the caller. Args: review_request (reviewboard.reviews.models.review_request. ReviewRequest): The review request that ultimately owns the file attachment. orig_filename (unicode, optional): The filename to use for the file attachment. caption (unicode, optional): The caption to use for the file attachment. draft (bool or reviewboard.reviews.models.review_request_draft. ReviewRequestDraft): A draft to associate the attachment with. This can also be a boolean, for legacy reasons, which will attempt to look up or create a draft for the review request. active (bool): Whether this attachment is considered active (not deleted). **kwargs (dict): Additional fields to set on the attachment. Returns: reviewboard.attachments.models.FileAttachment: The resulting file attachment. """ file_attachment = self._create_base_file_attachment( caption=caption, orig_filename=orig_filename, **kwargs) if draft: if isinstance(draft, ReviewRequestDraft): review_request_draft = draft else: review_request_draft = \ ReviewRequestDraft.create(review_request) if active: attachments = review_request_draft.file_attachments else: attachments = review_request_draft.inactive_file_attachments else: if active: attachments = review_request.file_attachments else: attachments = review_request.inactive_file_attachments attachments.add(file_attachment) return file_attachment
def test_sidebar(self): """Testing dashboard sidebar""" self.client.login(username='******', password='******') user = User.objects.get(username='******') profile = user.get_profile() # Create all the test data. devgroup = self.create_review_group(name='devgroup') devgroup.users.add(user) privgroup = self.create_review_group(name='privgroup') privgroup.users.add(user) review_request = self.create_review_request(submitter=user, publish=True) review_request = self.create_review_request(submitter='grumpy') draft = ReviewRequestDraft.create(review_request) draft.target_people.add(user) review_request.publish(review_request.submitter) review_request = self.create_review_request(submitter='grumpy') draft = ReviewRequestDraft.create(review_request) draft.target_groups.add(devgroup) review_request.publish(review_request.submitter) review_request = self.create_review_request(submitter='grumpy') draft = ReviewRequestDraft.create(review_request) draft.target_groups.add(privgroup) review_request.publish(review_request.submitter) profile.star_review_request(review_request) # Now load the dashboard and get the sidebar items. response = self.client.get('/dashboard/') self.assertEqual(response.status_code, 200) sidebar_items = \ self._get_context_var(response, 'datagrid').sidebar_items self.assertEqual(len(sidebar_items), 3) # Test the "Overview" section. section = sidebar_items[0] self.assertEqual(six.text_type(section.label), 'Overview') # Test the "Outgoing" section. section = sidebar_items[1] self.assertEqual(six.text_type(section.label), 'Outgoing') self.assertEqual(len(section.items), 2) self.assertEqual(six.text_type(section.items[0].label), 'All') self.assertEqual(section.items[0].count, 1) self.assertEqual(six.text_type(section.items[1].label), 'Open') self.assertEqual(section.items[1].count, 1) # Test the "Incoming" section. section = sidebar_items[2] self.assertEqual(six.text_type(section.label), 'Incoming') self.assertEqual(len(section.items), 5) self.assertEqual(six.text_type(section.items[0].label), 'Open') self.assertEqual(section.items[0].count, 3) self.assertEqual(six.text_type(section.items[1].label), 'To Me') self.assertEqual(section.items[1].count, 1) self.assertEqual(six.text_type(section.items[2].label), 'Starred') self.assertEqual(section.items[2].count, 1) self.assertEqual(six.text_type(section.items[3].label), 'devgroup') self.assertEqual(section.items[3].count, 1) self.assertEqual(six.text_type(section.items[4].label), 'privgroup') self.assertEqual(section.items[4].count, 1)
def check_put_result(self, user, item_rsp, draft, review_request): draft = ReviewRequestDraft.create(review_request) self.compare_item(item_rsp, draft)
def create(self, user, repository, commit_id=None, local_site=None, create_from_commit_id=False): """ Creates a new review request, optionally filling in fields based off a commit ID. """ from reviewboard.reviews.models import ReviewRequestDraft if commit_id: # Try both the new commit_id and old changenum versions try: review_request = self.get(commit_id=commit_id, repository=repository) raise ChangeNumberInUseError(review_request) except ObjectDoesNotExist: pass try: draft = ReviewRequestDraft.objects.get( commit_id=commit_id, review_request__repository=repository) raise ChangeNumberInUseError(draft.review_request) except ObjectDoesNotExist: pass try: review_request = self.get(changenum=int(commit_id), repository=repository) raise ChangeNumberInUseError(review_request) except (ObjectDoesNotExist, TypeError, ValueError): pass diffset_history = DiffSetHistory() diffset_history.save() review_request = self.model(submitter=user, status='P', public=False, repository=repository, diffset_history=diffset_history, local_site=local_site) if commit_id and not create_from_commit_id: review_request.commit_id = commit_id review_request.validate_unique() review_request.save() if commit_id and create_from_commit_id: try: draft = ReviewRequestDraft.objects.create( review_request=review_request) draft.update_from_commit_id(commit_id) draft.save() draft.add_default_reviewers() except Exception as e: review_request.commit_id = commit_id review_request.save(update_fields=['commit_id']) logging.error( 'Unable to update new review request from ' 'commit ID %s: %s', commit_id, e, exc_info=1) if local_site: # We want to atomically set the local_id to be a monotonically # increasing ID unique to the local_site. This isn't really # possible in django's DB layer, so we have to drop back to pure # SQL and then reload the model. from reviewboard.reviews.models import ReviewRequest db = router.db_for_write(ReviewRequest) cursor = connections[db].cursor() cursor.execute( 'UPDATE %(table)s SET' ' local_id = COALESCE(' ' (SELECT MAX(local_id) from' ' (SELECT local_id FROM %(table)s' ' WHERE local_site_id = %(local_site_id)s) as x' ' ) + 1,' ' 1),' ' local_site_id = %(local_site_id)s' ' WHERE %(table)s.id = %(id)s' % { 'table': ReviewRequest._meta.db_table, 'local_site_id': local_site.pk, 'id': review_request.pk, }) transaction.commit() review_request = ReviewRequest.objects.get(pk=review_request.pk) # Ensure that a draft exists, so that users will be prompted to publish # the new review request. ReviewRequestDraft.create(review_request) return review_request
def setup_basic_get_test(self, user, with_local_site=False, local_site_name=None, update_review_request=False, update_diffset=False, update_review=False, update_reply=False, publish_user=None): """Setup a basic HTTP GET test. Args: user (django.contrib.auth.models.User): The user that will be used to retrieve the resource. They will also be the submitter/author of all objects (although not necessary the publisher). with_local_site (boolean, optional): Whether or not a LocalSite-specific resource will be used. local_site_name (unicode, optional): The name of the LocalSite to use. update_review_request (boolean, optional): Whether or not the review request should be updated after publishing. update_diffset (boolean, optional): Whether or not the review request should be updated with a diff after publishing. update_review (boolean, optional): Whether or not the review request should be reviewed after publishing. update_reply (boolean, optional): Whther or not to create a reply. This implies ``update_review``. publish_user (django.contrib.auth.models.User, optional): The user that will trigger the update (i.e., they will publish all requested objects). If not provided, ``user`` will be used. Returns: tuple: A 3-tuple of: * The URL to request. * The expected mimetype. * The review request to use for comparison. """ update_review = update_review or update_reply publish_user = publish_user or user review_request = self.create_review_request( create_repository=True, submitter=user, with_local_site=with_local_site, target_people=[user]) review_request.publish(user=publish_user) if update_review_request: draft = ReviewRequestDraft.create(review_request) draft.summary = '%s updated' % review_request.summary draft.save() review_request.publish(user=publish_user) if update_diffset: self.create_diffset(review_request=review_request, draft=True) review_request.publish(user=publish_user) if update_review: review = self.create_review(review_request=review_request, user=user, publish=False) review.publish(user=publish_user) if update_reply: reply = self.create_reply(review, user=user) reply.publish(user=publish_user) return ( get_review_request_last_update_url(local_site_name=local_site_name, review_request=review_request), review_request_last_update_mimetype, review_request, )
def _get_draft(self): """Convenience function for getting a new draft to work with.""" review_request = self.create_review_request(publish=True) return ReviewRequestDraft.create(review_request)
def update_review_request(local_site, request, privileged_user, reviewer_cache, rr, commit, create_commit_msg_filediff): """Synchronize the state of a review request with a commit. Updates the commit message, refreshes the diff, etc. """ try: draft = rr.draft.get() except ReviewRequestDraft.DoesNotExist: draft = ReviewRequestDraft.create(rr) draft.summary = commit['message'].splitlines()[0] draft.description = commit['message'] draft.bugs_closed = commit['bug'] commit_data = fetch_commit_data(draft) reviewer_users, unrecognized_reviewers = \ resolve_reviewers(reviewer_cache, commit.get('reviewers', [])) requal_reviewer_users, unrecognized_requal_reviewers = \ resolve_reviewers(reviewer_cache, commit.get('requal_reviewers', [])) warnings = [] for reviewer in unrecognized_reviewers | unrecognized_requal_reviewers: warnings.append('unrecognized reviewer: %s' % reviewer) logger.info('unrecognized reviewer: %s' % reviewer) if requal_reviewer_users: pr = previous_reviewers(rr) for user in requal_reviewer_users: if not pr.get(user.username, False): warnings.append('commit message for %s has r=%s but they ' 'have not granted a ship-it. review will be ' 'requested on your behalf' % (commit['id'][:12], user.username)) reviewer_users |= requal_reviewer_users # Commit message FileDiff creation. base_commit_id = commit.get('base_commit_id') commit_message_filediff = None if create_commit_msg_filediff: # Prepare commit message data commit_message_name = commit_data.draft_extra_data.get( COMMIT_MSG_FILENAME_KEY, 'commit-message-%s' % base_commit_id[0:5]) commit_message_lines = commit['message'].split('\n') commit_message_diff = COMMIT_MSG_DIFF_FORMAT % { 'source_filename': commit_message_name, 'target_filename': commit_message_name, 'num_lines': len(commit_message_lines), 'diff': '%s\n' % '\n'.join(['+%s' % l for l in commit_message_lines]) } commit_data.extra_data[COMMIT_MSG_FILENAME_KEY] = commit_message_name # Commit message FileDiff has to be displayed as the first one. # Therefore it needs to be created before other FileDiffs in # the DiffSet. # FileDiff object has a required DiffSet field. Because target DiffSet # is created along with other FileDiffs, there is a need to # create a temporary one. # Later in the code temporary DiffSet is replaced in the commit # message FileDiff with the target one. temp_diffset = get_temp_diffset(rr.repository) commit_message_filediff = FileDiff.objects.create( diffset=temp_diffset, source_file=commit_message_name, dest_file=commit_message_name, source_revision=PRE_CREATION, dest_detail='', parent_diff='', binary=False, status='M', diff=commit_message_diff) # Carry over from last time unless commit message overrules. if reviewer_users: draft.target_people.clear() for user in sorted(reviewer_users): draft.target_people.add(user) logger.debug('adding reviewer %s to #%d' % (user.username, rr.id)) try: diffset = DiffSet.objects.create_from_data( repository=rr.repository, diff_file_name='diff', diff_file_contents=commit['diff_b64'].encode('ascii').decode( 'base64'), parent_diff_file_name='diff', parent_diff_file_contents=None, diffset_history=None, basedir='', request=request, base_commit_id=base_commit_id, save=True, ) update_diffset_history(rr, diffset) diffset.save() DiffSetVerification(diffset=diffset).save( authorized_user=privileged_user, force_insert=True) except Exception: logger.exception('error processing diff') raise DiffProcessingException() # Now that the proper DiffSet has been created, re-assign # the commit message FileDiff we created to the new DiffSet. if commit_message_filediff: commit_message_filediff.diffset = diffset commit_message_filediff.save() commit_msg_filediff_ids = json.loads( commit_data.draft_extra_data.get(COMMIT_MSG_FILEDIFF_IDS_KEY, '{}')) commit_msg_filediff_ids[str( diffset.revision)] = commit_message_filediff.pk # Store commit message FileDiffs ids in extra_data commit_data.draft_extra_data[COMMIT_MSG_FILEDIFF_IDS_KEY] = json.dumps( commit_msg_filediff_ids) commit_data.draft_extra_data.update({ AUTHOR_KEY: commit['author'], COMMIT_ID_KEY: commit['id'], FIRST_PUBLIC_ANCESTOR_KEY: commit['first_public_ancestor'], }) commit_data.save(update_fields=['draft_extra_data', 'extra_data']) update_review_request_draft_diffset(rr, diffset, draft=draft) return draft, warnings
def create(self, filediff=None): """Create a FileAttachment based on this form. Args: filediff (reviewboard.diffviewer.models.filediff.FileDiff, optional): The optional diff to attach this file to (for use when this file represents a binary file within the diff). Returns: reviewboard.attachments.models.FileAttachment: The new file attachment model. """ file_obj = self.files['path'] caption = self.cleaned_data['caption'] or file_obj.name mimetype = get_uploaded_file_mimetype(file_obj) filename = get_unique_filename(file_obj.name) if self.cleaned_data['attachment_history'] is None: # This is a new file: create a new FileAttachmentHistory for it attachment_history = FileAttachmentHistory() attachment_revision = 1 attachment_history.display_position = \ FileAttachmentHistory.compute_next_display_position( self.review_request) attachment_history.save() self.review_request.file_attachment_histories.add( attachment_history) else: attachment_history = self.cleaned_data['attachment_history'] try: latest = attachment_history.file_attachments.latest() except FileAttachment.DoesNotExist: latest = None if latest is None: # This should theoretically never happen, but who knows. attachment_revision = 1 elif latest.review_request.exists(): # This is a new update in the draft. attachment_revision = latest.attachment_revision + 1 else: # The most recent revision is part of the same draft. Delete it # and replace with the newly uploaded file. attachment_revision = latest.attachment_revision latest.delete() attachment_kwargs = { 'attachment_history': attachment_history, 'attachment_revision': attachment_revision, 'caption': '', 'draft_caption': caption, 'orig_filename': os.path.basename(file_obj.name), 'mimetype': mimetype, } if filediff: file_attachment = FileAttachment.objects.create_from_filediff( filediff, save=False, **attachment_kwargs) else: file_attachment = FileAttachment(**attachment_kwargs) file_attachment.file.save(filename, file_obj, save=True) draft = ReviewRequestDraft.create(self.review_request) draft.file_attachments.add(file_attachment) draft.save() return file_attachment
def create(self, user, repository, commit_id=None, local_site=None, create_from_commit_id=False, create_with_history=False): """Create a new review request. Args: user (django.contrib.auth.models.User): The user creating the review request. They will be tracked as the submitter. repository (reviewboard.scmtools.Models.Repository): The repository, if any, the review request is associated with. If ``None``, diffs cannot be added to the review request. commit_id (unicode, optional): An optional commit ID. local_site (reviewboard.site.models.LocalSite, optional): An optional LocalSite to associate the review request with. create_from_commit_id (bool, optional): Whether or not the given ``commit_id`` should be used to pre-populate the review request data. If ``True``, the given ``repository`` will be used to do so. create_with_history (bool, optional): Whether or not the created review request will support attaching multiple commits per diff revision. If ``False``, it will not be possible to use the :py:class:`~reviewboard.webapi.resources.diff.DiffResource` to upload diffs; the :py:class:`~reviewboard.webapi.resources.DiffCommitResource` must be used instead. Returns: reviewboard.reviews.models.review_request.ReviewRequest: The created review request. Raises: reviewboard.scmtools.errors.ChangeNumberInUseError: The commit ID is already in use by another review request. ValueError: An invalid value was passed for an argument. """ from reviewboard.reviews.models import ReviewRequestDraft if commit_id: # Try both the new commit_id and old changenum versions try: review_request = self.get(commit_id=commit_id, repository=repository) raise ChangeNumberInUseError(review_request) except ObjectDoesNotExist: pass try: draft = ReviewRequestDraft.objects.get( commit_id=commit_id, review_request__repository=repository) raise ChangeNumberInUseError(draft.review_request) except ObjectDoesNotExist: pass try: review_request = self.get(changenum=int(commit_id), repository=repository) raise ChangeNumberInUseError(review_request) except (ObjectDoesNotExist, TypeError, ValueError): pass if create_with_history: if repository is None: raise ValueError('create_with_history requires a repository.') elif create_from_commit_id: raise ValueError( 'create_from_commit_id and create_with_history cannot ' 'both be set to True.') elif not repository.scmtool_class.supports_history: raise ValueError( 'This repository does not support review requests created ' 'with history.') # Create the review request. We're not going to actually save this # until we're confident we have all the data we need. review_request = self.model(submitter=user, status='P', public=False, repository=repository, diffset_history=DiffSetHistory(), local_site=local_site) review_request.created_with_history = create_with_history if commit_id: review_request.commit = commit_id review_request.validate_unique() draft = None if commit_id and create_from_commit_id: try: draft = ReviewRequestDraft(review_request=review_request) draft.update_from_commit_id(commit_id) except Exception as e: logging.exception( 'Unable to update new review request from ' 'commit ID %s on repository ID=%s: %s', commit_id, repository.pk, e) raise # Now that we've guaranteed we have everything needed for this review # request, we can save all related objects and re-attach (since the # "None" IDs are cached). review_request.diffset_history.save() review_request.diffset_history = review_request.diffset_history review_request.save() if draft: draft.review_request = review_request draft.save() draft.add_default_reviewers() if local_site: # We want to atomically set the local_id to be a monotonically # increasing ID unique to the local_site. This isn't really # possible in django's DB layer, so we have to drop back to pure # SQL and then reload the model. from reviewboard.reviews.models import ReviewRequest with transaction.atomic(): # TODO: Use the cursor as a context manager when we move over # to Django 1.7+. db = router.db_for_write(ReviewRequest) cursor = connections[db].cursor() cursor.execute( 'UPDATE %(table)s SET' ' local_id = COALESCE(' ' (SELECT MAX(local_id) from' ' (SELECT local_id FROM %(table)s' ' WHERE local_site_id = %(local_site_id)s) as x' ' ) + 1,' ' 1),' ' local_site_id = %(local_site_id)s' ' WHERE %(table)s.id = %(id)s' % { 'table': ReviewRequest._meta.db_table, 'local_site_id': local_site.pk, 'id': review_request.pk, }) cursor.close() review_request.local_id = (ReviewRequest.objects.filter( pk=review_request.pk).values_list('local_id', flat=True)[0]) # Ensure that a draft exists, so that users will be prompted to publish # the new review request. ReviewRequestDraft.create(review_request) return review_request
def create(self, user, repository, commit_id=None, local_site=None, create_from_commit_id=False): """ Creates a new review request, optionally filling in fields based off a commit ID. """ from reviewboard.reviews.models import ReviewRequestDraft if commit_id: # Try both the new commit_id and old changenum versions try: review_request = self.get(commit_id=commit_id, repository=repository) raise ChangeNumberInUseError(review_request) except ObjectDoesNotExist: pass try: draft = ReviewRequestDraft.objects.get( commit_id=commit_id, review_request__repository=repository) raise ChangeNumberInUseError(draft.review_request) except ObjectDoesNotExist: pass try: review_request = self.get(changenum=int(commit_id), repository=repository) raise ChangeNumberInUseError(review_request) except (ObjectDoesNotExist, TypeError, ValueError): pass # Create the review request. We're not going to actually save this # until we're confident we have all the data we need. review_request = self.model( submitter=user, status='P', public=False, repository=repository, diffset_history=DiffSetHistory(), local_site=local_site) if commit_id: review_request.commit = commit_id review_request.validate_unique() draft = None if commit_id and create_from_commit_id: try: draft = ReviewRequestDraft(review_request=review_request) draft.update_from_commit_id(commit_id) except Exception as e: logging.exception('Unable to update new review request from ' 'commit ID %s on repository ID=%s: %s', commit_id, repository.pk, e) raise # Now that we've guaranteed we have everything needed for this review # request, we can save all related objects and re-attach (since the # "None" IDs are cached). review_request.diffset_history.save() review_request.diffset_history = review_request.diffset_history review_request.save() if draft: draft.review_request = review_request draft.save() draft.add_default_reviewers() if local_site: # We want to atomically set the local_id to be a monotonically # increasing ID unique to the local_site. This isn't really # possible in django's DB layer, so we have to drop back to pure # SQL and then reload the model. from reviewboard.reviews.models import ReviewRequest with transaction.atomic(): # TODO: Use the cursor as a context manager when we move over # to Django 1.7+. db = router.db_for_write(ReviewRequest) cursor = connections[db].cursor() cursor.execute( 'UPDATE %(table)s SET' ' local_id = COALESCE(' ' (SELECT MAX(local_id) from' ' (SELECT local_id FROM %(table)s' ' WHERE local_site_id = %(local_site_id)s) as x' ' ) + 1,' ' 1),' ' local_site_id = %(local_site_id)s' ' WHERE %(table)s.id = %(id)s' % { 'table': ReviewRequest._meta.db_table, 'local_site_id': local_site.pk, 'id': review_request.pk, }) cursor.close() review_request.local_id = ( ReviewRequest.objects.filter(pk=review_request.pk) .values_list('local_id', flat=True)[0] ) # Ensure that a draft exists, so that users will be prompted to publish # the new review request. ReviewRequestDraft.create(review_request) return review_request
def test_get(self): """Testing the GET review-requests/<id>/changes/<id>/ API""" def write_fields(obj, index): for field, data in six.iteritems(test_data): value = data[index] if isinstance(value, list) and field not in model_fields: value = ','.join(value) if field == 'diff': field = 'diffset' setattr(obj, field, value) changedesc_text = 'Change description text' user1, user2 = User.objects.all()[:2] group1 = Group.objects.create(name='group1') group2 = Group.objects.create(name='group2') repository = self.create_repository() diff1 = self.create_diffset(revision=1, repository=repository) diff2 = self.create_diffset(revision=2, repository=repository) old_screenshot_caption = 'old screenshot' new_screenshot_caption = 'new screenshot' screenshot1 = Screenshot.objects.create() screenshot2 = Screenshot.objects.create() screenshot3 = Screenshot.objects.create(caption=old_screenshot_caption) for screenshot in [screenshot1, screenshot2, screenshot3]: with open(self._getTrophyFilename(), 'r') as f: screenshot.image.save('foo.png', File(f), save=True) test_data = { 'summary': ('old summary', 'new summary', None, None), 'description': ('old description', 'new description', None, None), 'testing_done': ('old testing done', 'new testing done', None, None), 'branch': ('old branch', 'new branch', None, None), 'bugs_closed': (['1', '2', '3'], ['2', '3', '4'], ['1'], ['4']), 'target_people': ([user1], [user2], [user1], [user2]), 'target_groups': ([group1], [group2], [group1], [group2]), 'screenshots': ([screenshot1, screenshot3], [screenshot2, screenshot3], [screenshot1], [screenshot2]), 'diff': (diff1, diff2, None, diff2), } model_fields = ('target_people', 'target_groups', 'screenshots', 'diff') # Set the initial data on the review request. r = self.create_review_request(submitter=self.user) write_fields(r, 0) r.publish(self.user) # Create some draft data that will end up in the change description. draft = ReviewRequestDraft.create(r) write_fields(draft, 1) # Special-case screenshots draft.inactive_screenshots = test_data['screenshots'][2] screenshot3.draft_caption = new_screenshot_caption screenshot3.save() draft.changedesc.text = changedesc_text draft.changedesc.save() draft.save() r.publish(self.user) # Sanity check the ChangeDescription self.assertEqual(r.changedescs.count(), 1) change = r.changedescs.get() self.assertEqual(change.text, changedesc_text) for field, data in six.iteritems(test_data): old, new, removed, added = data field_data = change.fields_changed[field] if field == 'diff': # Diff fields are special. They only have "added". self.assertEqual(len(field_data['added']), 1) self.assertEqual(field_data['added'][0][2], added.pk) elif field in model_fields: self.assertEqual([item[2] for item in field_data['old']], [obj.pk for obj in old]) self.assertEqual([item[2] for item in field_data['new']], [obj.pk for obj in new]) self.assertEqual([item[2] for item in field_data['removed']], [obj.pk for obj in removed]) self.assertEqual([item[2] for item in field_data['added']], [obj.pk for obj in added]) elif isinstance(old, list): self.assertEqual(field_data['old'], [[value] for value in old]) self.assertEqual(field_data['new'], [[value] for value in new]) self.assertEqual(field_data['removed'], [[value] for value in removed]) self.assertEqual(field_data['added'], [[value] for value in added]) else: self.assertEqual(field_data['old'], [old]) self.assertEqual(field_data['new'], [new]) self.assertNotIn('removed', field_data) self.assertNotIn('added', field_data) self.assertIn('screenshot_captions', change.fields_changed) field_data = change.fields_changed['screenshot_captions'] screenshot_id = six.text_type(screenshot3.pk) self.assertIn(screenshot_id, field_data) self.assertIn('old', field_data[screenshot_id]) self.assertIn('new', field_data[screenshot_id]) self.assertEqual(field_data[screenshot_id]['old'][0], old_screenshot_caption) self.assertEqual(field_data[screenshot_id]['new'][0], new_screenshot_caption) # Now confirm with the API rsp = self.api_get(get_change_list_url(r), expected_mimetype=change_list_mimetype) self.assertEqual(rsp['stat'], 'ok') self.assertEqual(len(rsp['changes']), 1) self.assertEqual(rsp['changes'][0]['id'], change.pk) rsp = self.api_get(rsp['changes'][0]['links']['self']['href'], expected_mimetype=change_item_mimetype) self.assertEqual(rsp['stat'], 'ok') self.assertEqual(rsp['change']['text'], changedesc_text) fields_changed = rsp['change']['fields_changed'] for field, data in six.iteritems(test_data): old, new, removed, added = data self.assertIn(field, fields_changed) field_data = fields_changed[field] if field == 'diff': self.assertIn('added', field_data) self.assertEqual(field_data['added']['id'], added.pk) elif field in model_fields: self.assertIn('old', field_data) self.assertIn('new', field_data) self.assertIn('added', field_data) self.assertIn('removed', field_data) self.assertEqual([item['id'] for item in field_data['old']], [obj.pk for obj in old]) self.assertEqual([item['id'] for item in field_data['new']], [obj.pk for obj in new]) self.assertEqual( [item['id'] for item in field_data['removed']], [obj.pk for obj in removed]) self.assertEqual([item['id'] for item in field_data['added']], [obj.pk for obj in added]) else: self.assertIn('old', field_data) self.assertIn('new', field_data) self.assertEqual(field_data['old'], old) self.assertEqual(field_data['new'], new) if isinstance(old, list): self.assertIn('added', field_data) self.assertIn('removed', field_data) self.assertEqual(field_data['added'], added) self.assertEqual(field_data['removed'], removed) self.assertIn('screenshot_captions', fields_changed) field_data = fields_changed['screenshot_captions'] self.assertEqual(len(field_data), 1) screenshot_data = field_data[0] self.assertIn('old', screenshot_data) self.assertIn('new', screenshot_data) self.assertIn('screenshot', screenshot_data) self.assertEqual(screenshot_data['old'], old_screenshot_caption) self.assertEqual(screenshot_data['new'], new_screenshot_caption) self.assertEqual(screenshot_data['screenshot']['id'], screenshot3.pk)
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 _populate_review_request(self): now = timezone.now() # Create the review request, diffs, attachments, and screenshots. self.review_request = self.create_review_request( create_repository=True, publish=True) self.diffset1 = self.create_diffset(self.review_request) self.filediff1 = self.create_filediff(self.diffset1) self.diffset2 = self.create_diffset(self.review_request) self.filediff2 = self.create_filediff(self.diffset2) self.file_attachment1 = \ self.create_file_attachment(self.review_request) self.file_attachment2 = \ self.create_file_attachment(self.review_request) self.inactive_file_attachment1 = \ self.create_file_attachment(self.review_request, active=False) self.screenshot1 = self.create_screenshot(self.review_request) self.screenshot2 = self.create_screenshot(self.review_request) self.inactive_screenshot1 = \ self.create_screenshot(self.review_request, active=False) # Create a draft for this review request. self.draft = ReviewRequestDraft.create(self.review_request) # Create some reviews. self.review1 = self.create_review(self.review_request, timestamp=now, publish=True) self.general_comment1 = self.create_general_comment( self.review1, issue_opened=True, issue_status=BaseComment.OPEN) self.diff_comment1 = self.create_diff_comment( self.review1, self.filediff1, issue_opened=True, issue_status=BaseComment.RESOLVED) self.file_attachment_comment1 = self.create_file_attachment_comment( self.review1, self.file_attachment1, issue_opened=True, issue_status=BaseComment.DROPPED) self.screenshot_comment1 = self.create_screenshot_comment( self.review1, self.screenshot1, issue_opened=False) self.review2 = self.create_review(self.review_request, timestamp=now + timedelta(days=3), publish=True) self.general_comment2 = self.create_general_comment( self.review2, issue_opened=True, issue_status=BaseComment.OPEN) self.diff_comment2 = self.create_diff_comment( self.review2, self.filediff2, issue_opened=True, issue_status=BaseComment.RESOLVED) self.file_attachment_comment2 = self.create_file_attachment_comment( self.review2, self.file_attachment2, issue_opened=True, issue_status=BaseComment.DROPPED) self.screenshot_comment2 = self.create_screenshot_comment( self.review2, self.screenshot2, issue_opened=False) # Create some change descriptions. self.changedesc1 = self.review_request.changedescs.create( timestamp=now + timedelta(days=2), public=True) self.changedesc2 = self.review_request.changedescs.create( timestamp=now + timedelta(days=4), public=True) # Create some status updates. self.status_update1 = self.create_status_update(self.review_request) self.status_update2 = self.create_status_update(self.review_request)
def create(self, filediff=None): file = self.files['path'] caption = self.cleaned_data['caption'] or file.name # There are several things that can go wrong with browser-provided # mimetypes. In one case (bug 3427), Firefox on Linux Mint was # providing a mimetype that looked like 'text/text/application/pdf', # which is unparseable. IE also has a habit of setting any unknown file # type to 'application/octet-stream', rather than just choosing not to # provide a mimetype. In the case where what we get from the browser # is obviously wrong, try to guess. if (file.content_type and len(file.content_type.split('/')) == 2 and file.content_type != 'application/octet-stream'): mimetype = file.content_type else: mimetype = self._guess_mimetype(file) filename = '%s__%s' % (uuid4(), file.name) if self.cleaned_data['attachment_history'] is None: # This is a new file: create a new FileAttachmentHistory for it attachment_history = FileAttachmentHistory() attachment_revision = 1 attachment_history.display_position = \ FileAttachmentHistory.compute_next_display_position( self.review_request) attachment_history.save() self.review_request.file_attachment_histories.add( attachment_history) else: attachment_history = self.cleaned_data['attachment_history'] try: latest = attachment_history.file_attachments.latest() except FileAttachment.DoesNotExist: latest = None if latest is None: # This should theoretically never happen, but who knows. attachment_revision = 1 elif latest.review_request.exists(): # This is a new update in the draft. attachment_revision = latest.attachment_revision + 1 else: # The most recent revision is part of the same draft. Delete it # and replace with the newly uploaded file. attachment_revision = latest.attachment_revision latest.delete() attachment_kwargs = { 'attachment_history': attachment_history, 'attachment_revision': attachment_revision, 'caption': '', 'draft_caption': caption, 'orig_filename': os.path.basename(file.name), 'mimetype': mimetype, } if filediff: file_attachment = FileAttachment.objects.create_from_filediff( filediff, save=False, **attachment_kwargs) else: file_attachment = FileAttachment(**attachment_kwargs) file_attachment.file.save(filename, file, save=True) draft = ReviewRequestDraft.create(self.review_request) draft.file_attachments.add(file_attachment) draft.save() return file_attachment