示例#1
0
    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.apiGet(
            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)
示例#2
0
    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)
示例#3
0
    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)
示例#4
0
    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)))
示例#5
0
    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)
示例#6
0
    def test_is_new_for_user_with_owner(self):
        """Testing ChangeDescription.is_new_for_user with owner"""
        user = User.objects.create_user(username='******',
                                        email='*****@*****.**')

        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)))
示例#7
0
    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()
示例#8
0
    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:
            # TODO: Use the user's default for rich_text.
            changedesc = ChangeDescription(public=True,
                                           text=description or "",
                                           rich_text=rich_text or False)
            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.rich_text = rich_text
            changedesc.save()

            # Needed to renew last-update.
            self.save()

        try:
            draft = self.draft.get()
        except ObjectDoesNotExist:
            pass
        else:
            draft.delete()
示例#9
0
    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)
示例#10
0
    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()
示例#11
0
    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.assert_("test" in changedesc.fields_changed)
        self.assert_("old" in changedesc.fields_changed["test"])
        self.assert_("new" in changedesc.fields_changed["test"])
        self.assert_("added" not in changedesc.fields_changed["test"])
        self.assert_("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,))
示例#12
0
 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)
示例#13
0
    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()
示例#14
0
    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,))
示例#15
0
    def test_record_list(self):
        """Testing ChangeDescription.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.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(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,)])
示例#16
0
    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,))
示例#17
0
    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.apiGet(
            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)
示例#18
0
    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,)])
示例#19
0
    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)))
示例#20
0
    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)))
示例#21
0
    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.apiGet(
            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)
示例#22
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)]),
        )
示例#23
0
    def test_get_changes(self):
        """Testing the GET review-requests/<id>/changes/ API"""
        review_request = self.create_review_request(publish=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.apiGet(get_change_list_url(review_request), 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)
示例#24
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)]))
示例#25
0
    def create(review_request):
        """
        Creates a draft based on a review request.

        This will copy over all the details of the review request that
        we care about. If a draft already exists for the review request,
        the draft will be returned.
        """
        draft, draft_is_new = \
            ReviewRequestDraft.objects.get_or_create(
                review_request=review_request,
                defaults={
                    'summary': review_request.summary,
                    'description': review_request.description,
                    'testing_done': review_request.testing_done,
                    'bugs_closed': review_request.bugs_closed,
                    'branch': review_request.branch,
                })

        if draft.changedesc is None and review_request.public:
            changedesc = ChangeDescription()
            changedesc.save()
            draft.changedesc = changedesc

        if draft_is_new:
            map(draft.target_groups.add, review_request.target_groups.all())
            map(draft.target_people.add, review_request.target_people.all())
            for screenshot in review_request.screenshots.all():
                screenshot.draft_caption = screenshot.caption
                screenshot.save()
                draft.screenshots.add(screenshot)

            for screenshot in review_request.inactive_screenshots.all():
                screenshot.draft_caption = screenshot.caption
                screenshot.save()
                draft.inactive_screenshots.add(screenshot)

            draft.save();

        return draft
    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)
示例#27
0
    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)
示例#28
0
    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)
示例#29
0
    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)
示例#30
0
    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)
示例#31
0
    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)
示例#32
0
    def publish(self, review_request=None, user=None,
                send_notification=True):
        """
        Publishes this draft. Uses the draft's assocated ReviewRequest
        object if one isn't passed in.

        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:

           *  'summary'
           *  'description'
           *  'testing_done'
           *  'bugs_closed'
           *  '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_field(a, b, name, record_changes=True):
            # Apparently django models don't have __getattr__ or __setattr__,
            # so we have to update __dict__ directly.  Sigh.
            value = b.__dict__[name]
            old_value = a.__dict__[name]

            if old_value != value:
                if record_changes and self.changedesc:
                    self.changedesc.record_field_change(name, old_value, value)

                a.__dict__[name] = value

        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()
                map(a.add, b.all())


        update_field(review_request, self, 'summary')
        update_field(review_request, self, 'description')
        update_field(review_request, self, 'testing_done')
        update_field(review_request, self, 'branch')

        update_list(review_request.target_groups, self.target_groups,
                    'target_groups', name_field="name")
        update_list(review_request.target_people, self.target_people,
                    'target_people', name_field="username")

        # Specifically handle bug numbers
        old_bugs = set(review_request.get_bug_list())
        new_bugs = set(self.get_bug_list())

        if old_bugs != new_bugs:
            update_field(review_request, self, 'bugs_closed',
                         record_changes=False)

            if self.changedesc:
                self.changedesc.record_field_change('bugs_closed',
                                                    old_bugs - new_bugs,
                                                    new_bugs - old_bugs)


        # Screenshots are a bit special.  The list of associated screenshots can
        # change, but so can captions within each screenshot.
        screenshots = 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()

        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.clear()
        map(review_request.inactive_screenshots.add,
            self.inactive_screenshots.all())

        if self.diffset:
            if self.changedesc:
                self.changedesc.fields_changed['diff'] = {
                    'added': [(_("Diff r%s") % self.diffset.revision,
                               reverse("view_diff_revision",
                                       args=[review_request.id,
                                             self.diffset.revision]),
                               self.diffset.id)],
                }

            self.diffset.history = review_request.diffset_history
            self.diffset.save()

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

        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
class ReviewRequestDraft(BaseReviewRequestDetails):
    """A draft of a review request.

    When a review request is being modified, a special draft copy of it is
    created containing all the details of the review request. This copy can
    be modified and eventually saved or discarded. When saved, the new
    details are copied back over to the originating ReviewRequest.
    """
    summary = models.CharField(
        _("summary"), max_length=BaseReviewRequestDetails.MAX_SUMMARY_LENGTH)

    owner = models.ForeignKey(User,
                              verbose_name=_('owner'),
                              null=True,
                              related_name='draft')
    review_request = models.ForeignKey(ReviewRequest,
                                       related_name="draft",
                                       verbose_name=_("review request"),
                                       unique=True)
    last_updated = ModificationTimestampField(_("last updated"))
    diffset = models.ForeignKey(DiffSet,
                                verbose_name=_('diff set'),
                                blank=True,
                                null=True,
                                related_name='review_request_draft')
    changedesc = models.ForeignKey(ChangeDescription,
                                   verbose_name=_('change description'),
                                   blank=True,
                                   null=True)
    target_groups = models.ManyToManyField(Group,
                                           related_name="drafts",
                                           verbose_name=_("target groups"),
                                           blank=True)
    target_people = models.ManyToManyField(User,
                                           verbose_name=_("target people"),
                                           related_name="directed_drafts",
                                           blank=True)
    screenshots = models.ManyToManyField(Screenshot,
                                         related_name="drafts",
                                         verbose_name=_("screenshots"),
                                         blank=True)
    inactive_screenshots = models.ManyToManyField(
        Screenshot,
        verbose_name=_("inactive screenshots"),
        related_name="inactive_drafts",
        blank=True)

    file_attachments = models.ManyToManyField(
        FileAttachment,
        related_name="drafts",
        verbose_name=_("file attachments"),
        blank=True)
    inactive_file_attachments = models.ManyToManyField(
        FileAttachment,
        verbose_name=_("inactive files"),
        related_name="inactive_drafts",
        blank=True)

    submitter = property(lambda self: self.owner or self.review_request.owner)
    repository = property(lambda self: self.review_request.repository)
    local_site = property(lambda self: self.review_request.local_site)

    depends_on = models.ManyToManyField('ReviewRequest',
                                        blank=True,
                                        null=True,
                                        verbose_name=_('Dependencies'),
                                        related_name='draft_blocks')

    screenshots_count = RelationCounterField(
        'screenshots', verbose_name=_('screenshots count'))

    inactive_screenshots_count = RelationCounterField(
        'inactive_screenshots', verbose_name=_('inactive screenshots count'))

    file_attachments_count = RelationCounterField(
        'file_attachments', verbose_name=_('file attachments count'))

    inactive_file_attachments_count = RelationCounterField(
        'inactive_file_attachments',
        verbose_name=_('inactive file attachments count'))

    # Set this up with a ConcurrencyManager to help prevent race conditions.
    objects = ConcurrencyManager()

    commit = property(lambda self: self.commit_id,
                      lambda self, value: setattr(self, 'commit_id', value))

    def get_latest_diffset(self):
        """Returns the diffset for this draft."""
        return self.diffset

    def is_accessible_by(self, user):
        """Returns whether or not the user can access this draft."""
        return self.is_mutable_by(user)

    def is_mutable_by(self, user):
        """Returns whether or not the user can modify this draft."""
        return self.review_request.is_mutable_by(user)

    @staticmethod
    def create(review_request, changedesc=None):
        """Create a draft based on a review request.

        This will copy over all the details of the review request that
        we care about. If a draft already exists for the review request,
        the draft will be returned.

        Args:
            review_request (reviewboard.reviews.models.review_request.
                            ReviewRequest):
                The review request to fetch or create the draft from.

            changedesc (reviewboard.changedescs.models.ChangeDescription):
                A custom change description to set on the draft. This will
                always be set, overriding any previous one if already set.

        Returns:
            ReviewRequestDraft:
            The resulting draft.
        """
        draft, draft_is_new = \
            ReviewRequestDraft.objects.get_or_create(
                review_request=review_request,
                defaults={
                    'changedesc': changedesc,
                    'extra_data': review_request.extra_data or {},
                    'summary': review_request.summary,
                    'description': review_request.description,
                    'testing_done': review_request.testing_done,
                    'bugs_closed': review_request.bugs_closed,
                    'branch': review_request.branch,
                    'description_rich_text':
                        review_request.description_rich_text,
                    'testing_done_rich_text':
                        review_request.testing_done_rich_text,
                    'rich_text': review_request.rich_text,
                    'commit_id': review_request.commit_id,
                })

        if (changedesc is None and draft.changedesc_id is None
                and review_request.public):
            changedesc = ChangeDescription.objects.create()

        if changedesc is not None and draft.changedesc_id != changedesc.pk:
            old_changedesc_id = draft.changedesc_id
            draft.changedesc = changedesc
            draft.save(update_fields=('changedesc', ))

            if old_changedesc_id is not None:
                ChangeDescription.objects.filter(pk=old_changedesc_id).delete()

        if draft_is_new:
            rels_to_update = [
                ('depends_on', 'to_reviewrequest_id', 'from_reviewrequest_id'),
                ('target_groups', 'group_id', 'reviewrequest_id'),
                ('target_people', 'user_id', 'reviewrequest_id'),
            ]

            if review_request.screenshots_count > 0:
                review_request.screenshots.update(draft_caption=F('caption'))
                rels_to_update.append(
                    ('screenshots', 'screenshot_id', 'reviewrequest_id'))

            if review_request.inactive_screenshots_count > 0:
                review_request.inactive_screenshots.update(
                    draft_caption=F('caption'))
                rels_to_update.append(('inactive_screenshots', 'screenshot_id',
                                       'reviewrequest_id'))

            if review_request.file_attachments_count > 0:
                review_request.file_attachments.update(
                    draft_caption=F('caption'))
                rels_to_update.append(('file_attachments', 'fileattachment_id',
                                       'reviewrequest_id'))

            if review_request.inactive_file_attachments_count > 0:
                review_request.inactive_file_attachments.update(
                    draft_caption=F('caption'))
                rels_to_update.append(
                    ('inactive_file_attachments', 'fileattachment_id',
                     'reviewrequest_id'))

            for rel_field, id_field, lookup_field, in rels_to_update:
                # We don't need to query the entirety of each object, and
                # we'd like to avoid any JOINs. So, we'll be using the
                # M2M 'through' tables to perform lookups of the related
                # models' IDs.
                items = list(
                    getattr(review_request, rel_field).through.objects.filter(
                        **{
                            lookup_field: review_request.pk
                        }).values_list(id_field, flat=True))

                if items:
                    # Note that we're using add() instead of directly
                    # assigning the value. This lets us avoid a query that
                    # Django would perform to determine if it needed to clear
                    # out any existing values. Since we know this draft is
                    # new, there's no point in doing that.
                    getattr(draft, rel_field).add(*items)

        return draft

    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 (review_request.created_with_history and self.diffset
                    and self.diffset.commit_count == 0):
                raise PublishError(
                    ugettext('There are no commits attached to the diff.'))

        if self.diffset:
            if (review_request.created_with_history
                    and not self.diffset.is_commit_series_finalized):
                raise PublishError(
                    ugettext('This commit series is not finalized.'))

            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 update_from_commit_id(self, commit_id):
        """Update the data from a server-side changeset.

        If the commit ID refers to a pending changeset on an SCM which stores
        such things server-side (like Perforce), the details like the summary
        and description will be updated with the latest information.

        If the change number is the commit ID of a change which exists on the
        server, the summary and description will be set from the commit's
        message, and the diff will be fetched from the SCM.

        Args:
            commit_id (unicode):
                The commit ID or changeset ID that the draft will update
                from.

        Returns:
            list of unicode:
            The list of draft fields that have been updated from the commit.
        """
        scmtool = self.repository.get_scmtool()
        changeset = None

        if scmtool.supports_pending_changesets:
            changeset = scmtool.get_changeset(commit_id, allow_empty=True)

        if changeset and changeset.pending:
            return self.update_from_pending_change(commit_id, changeset)
        elif self.repository.supports_post_commit:
            return self.update_from_committed_change(commit_id)
        else:
            if changeset:
                raise InvalidChangeNumberError()
            else:
                raise NotImplementedError()

    def update_from_pending_change(self, commit_id, changeset):
        """Update the data from a server-side pending changeset.

        This will fetch the metadata from the server and update the fields on
        the draft.

        Args:
            commit_id (unicode):
                The changeset ID that the draft will update from.

            changeset (reviewboard.scmtools.core.ChangeSet):
                The changeset information to update from.

        Returns:
            list of unicode:
            The list of draft fields that have been updated from the change.
        """
        if not changeset:
            raise InvalidChangeNumberError()

        # If the SCM supports changesets, they should always include a number,
        # summary and description, parsed from the changeset description. Some
        # specialized systems may support the other fields, but we don't want
        # to clobber the user-entered values if they don't.
        self.commit = commit_id
        description = changeset.description
        testing_done = changeset.testing_done

        self.summary = changeset.summary
        self.description = description
        self.description_rich_text = False

        modified_fields = [
            'commit_id',
            'summary',
            'description',
            'description_rich_text',
        ]

        if testing_done:
            self.testing_done = testing_done
            self.testing_done_rich_text = False
            modified_fields += ['testing_done', 'testing_done_rich_text']

        if changeset.branch:
            self.branch = changeset.branch
            modified_fields.append('branch')

        if changeset.bugs_closed:
            self.bugs_closed = ','.join(changeset.bugs_closed)
            modified_fields.append('bugs_closed')

        return modified_fields

    def update_from_committed_change(self, commit_id):
        """Update from a committed change present on the server.

        Fetches the commit message and diff from the repository and sets the
        relevant fields.

        Args:
            commit_id (unicode):
                The commit ID to update from.

        Returns:
            list of unicode:
            The list of draft fields that have been updated from the commit
            message.
        """
        commit = self.repository.get_change(commit_id)
        summary, message = commit.split_message()
        message = message.strip()

        self.commit = commit_id
        self.summary = summary.strip()

        self.description = message
        self.description_rich_text = False

        self.diffset = DiffSet.objects.create_from_data(
            repository=self.repository,
            diff_file_name='diff',
            diff_file_contents=commit.diff,
            parent_diff_file_name=None,
            parent_diff_file_contents=None,
            diffset_history=None,
            basedir='/',
            request=None,
            base_commit_id=commit.parent,
            check_existence=False)

        # Compute a suitable revision for the diffset.
        self.diffset.update_revision_from_history(
            self.review_request.diffset_history)
        self.diffset.save(update_fields=('revision', ))

        return [
            'commit_id',
            'description',
            'description_rich_text',
            'diffset',
            'summary',
        ]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def get_review_request(self):
        """Returns the associated review request."""
        return self.review_request

    class Meta:
        app_label = 'reviews'
        db_table = 'reviews_reviewrequestdraft'
        ordering = ['-last_updated']
        verbose_name = _('Review Request Draft')
        verbose_name_plural = _('Review Request Drafts')
示例#34
0
class ReviewRequestDraft(models.Model):
    """
    A draft of a review request.

    When a review request is being modified, a special draft copy of it is
    created containing all the details of the review request. This copy can
    be modified and eventually saved or discarded. When saved, the new
    details are copied back over to the originating ReviewRequest.
    """
    review_request = models.ForeignKey(ReviewRequest,
                                       related_name="draft",
                                       verbose_name=_("review request"),
                                       unique=True)
    last_updated = ModificationTimestampField(_("last updated"))
    summary = models.CharField(_("summary"), max_length=300)
    description = models.TextField(_("description"))
    testing_done = models.TextField(_("testing done"))
    bugs_closed = models.CommaSeparatedIntegerField(_("bugs"),
                                                    max_length=300, blank=True)
    diffset = models.ForeignKey(DiffSet, verbose_name=_('diff set'),
                                blank=True, null=True,
                                related_name='review_request_draft')
    changedesc = models.ForeignKey(ChangeDescription,
                                   verbose_name=_('change description'),
                                   blank=True, null=True)
    branch = models.CharField(_("branch"), max_length=300, blank=True)
    target_groups = models.ManyToManyField(Group,
                                           related_name="drafts",
                                           verbose_name=_("target groups"),
                                           blank=True)
    target_people = models.ManyToManyField(User,
                                           verbose_name=_("target people"),
                                           related_name="directed_drafts",
                                           blank=True)
    screenshots = models.ManyToManyField(Screenshot,
                                         related_name="drafts",
                                         verbose_name=_("screenshots"),
                                         blank=True)
    inactive_screenshots = models.ManyToManyField(Screenshot,
        verbose_name=_("inactive screenshots"),
        related_name="inactive_drafts",
        blank=True)

    submitter = property(lambda self: self.review_request.submitter)

    # Set this up with a ConcurrencyManager to help prevent race conditions.
    objects = ConcurrencyManager()

    def get_bug_list(self):
        """
        Returns a sorted list of bugs associated with this review request.
        """
        if self.bugs_closed == "":
            return []

        bugs = re.split(r"[, ]+", self.bugs_closed)

        # First try a numeric sort, to show the best results for the majority
        # case of bug trackers with numeric IDs.  If that fails, sort
        # alphabetically.
        try:
            bugs.sort(cmp=lambda x,y: cmp(int(x), int(y)))
        except ValueError:
            bugs.sort()

        return bugs

    def __unicode__(self):
        return self.summary

    def save(self, **kwargs):
        self.bugs_closed = self.bugs_closed.strip()
        self.summary = truncate(self.summary, MAX_SUMMARY_LENGTH)
        super(ReviewRequestDraft, self).save()

    @staticmethod
    def create(review_request):
        """
        Creates a draft based on a review request.

        This will copy over all the details of the review request that
        we care about. If a draft already exists for the review request,
        the draft will be returned.
        """
        draft, draft_is_new = \
            ReviewRequestDraft.objects.get_or_create(
                review_request=review_request,
                defaults={
                    'summary': review_request.summary,
                    'description': review_request.description,
                    'testing_done': review_request.testing_done,
                    'bugs_closed': review_request.bugs_closed,
                    'branch': review_request.branch,
                })

        if draft.changedesc is None and review_request.public:
            changedesc = ChangeDescription()
            changedesc.save()
            draft.changedesc = changedesc

        if draft_is_new:
            map(draft.target_groups.add, review_request.target_groups.all())
            map(draft.target_people.add, review_request.target_people.all())
            for screenshot in review_request.screenshots.all():
                screenshot.draft_caption = screenshot.caption
                screenshot.save()
                draft.screenshots.add(screenshot)

            for screenshot in review_request.inactive_screenshots.all():
                screenshot.draft_caption = screenshot.caption
                screenshot.save()
                draft.inactive_screenshots.add(screenshot)

            draft.save();

        return draft

    def add_default_reviewers(self):
        """
        Add default reviewers to this draft based on the diffset.

        This method goes through the DefaultReviewer objects in the database and
        adds any missing reviewers based on regular expression comparisons with
        the set of files in the diff.
        """

        if not self.diffset:
            return

        repository = self.review_request.repository
        people = set()
        groups = set()

        # TODO: This is kind of inefficient, and could maybe be optimized in
        # some fancy way.  Certainly the most superficial optimization that
        # could be made would be to cache the compiled regexes somewhere.
        files = self.diffset.files.all()
        for default in DefaultReviewer.objects.for_repository(repository):
            try:
                regex = re.compile(default.file_regex)
            except:
                continue

            for filediff in files:
                if regex.match(filediff.source_file or filediff.dest_file):
                    for person in default.people.all():
                        people.add(person)
                    for group in default.groups.all():
                        groups.add(group)
                    break

        existing_people = self.target_people.all()
        for person in people:
            if person not in existing_people:
                self.target_people.add(person)

        existing_groups = self.target_groups.all()
        for group in groups:
            if group not in existing_groups:
                self.target_groups.add(group)

    def publish(self, review_request=None, user=None,
                send_notification=True):
        """
        Publishes this draft. Uses the draft's assocated ReviewRequest
        object if one isn't passed in.

        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:

           *  'summary'
           *  'description'
           *  'testing_done'
           *  'bugs_closed'
           *  '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_field(a, b, name, record_changes=True):
            # Apparently django models don't have __getattr__ or __setattr__,
            # so we have to update __dict__ directly.  Sigh.
            value = b.__dict__[name]
            old_value = a.__dict__[name]

            if old_value != value:
                if record_changes and self.changedesc:
                    self.changedesc.record_field_change(name, old_value, value)

                a.__dict__[name] = value

        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()
                map(a.add, b.all())


        update_field(review_request, self, 'summary')
        update_field(review_request, self, 'description')
        update_field(review_request, self, 'testing_done')
        update_field(review_request, self, 'branch')

        update_list(review_request.target_groups, self.target_groups,
                    'target_groups', name_field="name")
        update_list(review_request.target_people, self.target_people,
                    'target_people', name_field="username")

        # Specifically handle bug numbers
        old_bugs = set(review_request.get_bug_list())
        new_bugs = set(self.get_bug_list())

        if old_bugs != new_bugs:
            update_field(review_request, self, 'bugs_closed',
                         record_changes=False)

            if self.changedesc:
                self.changedesc.record_field_change('bugs_closed',
                                                    old_bugs - new_bugs,
                                                    new_bugs - old_bugs)


        # Screenshots are a bit special.  The list of associated screenshots can
        # change, but so can captions within each screenshot.
        screenshots = 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()

        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.clear()
        map(review_request.inactive_screenshots.add,
            self.inactive_screenshots.all())

        if self.diffset:
            if self.changedesc:
                self.changedesc.fields_changed['diff'] = {
                    'added': [(_("Diff r%s") % self.diffset.revision,
                               reverse("view_diff_revision",
                                       args=[review_request.id,
                                             self.diffset.revision]),
                               self.diffset.id)],
                }

            self.diffset.history = review_request.diffset_history
            self.diffset.save()

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

        review_request.save()

        if send_notification:
            review_request_published.send(sender=review_request.__class__,
                                          user=user,
                                          review_request=review_request,
                                          changedesc=self.changedesc)

        return self.changedesc

    def update_from_changenum(self, changenum):
        """
        Updates this draft from the specified changeset's contents on
        the server.
        """
        update_obj_with_changenum(self, self.review_request.repository,
                                  changenum)

    class Meta:
        ordering = ['-last_updated']
    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 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()

        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.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
示例#36
0
    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()
示例#37
0
    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
示例#38
0
    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()
示例#39
0
    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 (review_request.created_with_history and
                self.diffset and
                self.diffset.commit_count == 0):
                raise PublishError(
                    ugettext('There are no commits attached to the diff.'))

        if self.diffset:
            if (review_request.created_with_history and not
                self.diffset.is_commit_series_finalized):
                raise PublishError(ugettext(
                    'This commit series is not finalized.'))

            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
示例#40
0
class ReviewRequestDraft(BaseReviewRequestDetails):
    """A draft of a review request.

    When a review request is being modified, a special draft copy of it is
    created containing all the details of the review request. This copy can
    be modified and eventually saved or discarded. When saved, the new
    details are copied back over to the originating ReviewRequest.
    """
    review_request = models.ForeignKey(ReviewRequest,
                                       related_name="draft",
                                       verbose_name=_("review request"),
                                       unique=True)
    last_updated = ModificationTimestampField(_("last updated"))
    diffset = models.ForeignKey(DiffSet,
                                verbose_name=_('diff set'),
                                blank=True,
                                null=True,
                                related_name='review_request_draft')
    changedesc = models.ForeignKey(ChangeDescription,
                                   verbose_name=_('change description'),
                                   blank=True,
                                   null=True)
    target_groups = models.ManyToManyField(Group,
                                           related_name="drafts",
                                           verbose_name=_("target groups"),
                                           blank=True)
    target_people = models.ManyToManyField(User,
                                           verbose_name=_("target people"),
                                           related_name="directed_drafts",
                                           blank=True)
    screenshots = models.ManyToManyField(Screenshot,
                                         related_name="drafts",
                                         verbose_name=_("screenshots"),
                                         blank=True)
    inactive_screenshots = models.ManyToManyField(
        Screenshot,
        verbose_name=_("inactive screenshots"),
        related_name="inactive_drafts",
        blank=True)

    file_attachments = models.ManyToManyField(
        FileAttachment,
        related_name="drafts",
        verbose_name=_("file attachments"),
        blank=True)
    inactive_file_attachments = models.ManyToManyField(
        FileAttachment,
        verbose_name=_("inactive files"),
        related_name="inactive_drafts",
        blank=True)

    submitter = property(lambda self: self.review_request.submitter)
    repository = property(lambda self: self.review_request.repository)
    local_site = property(lambda self: self.review_request.local_site)

    depends_on = models.ManyToManyField('ReviewRequest',
                                        blank=True,
                                        null=True,
                                        verbose_name=_('Dependencies'),
                                        related_name='draft_blocks')

    # Set this up with a ConcurrencyManager to help prevent race conditions.
    objects = ConcurrencyManager()

    commit = property(lambda self: self.commit_id,
                      lambda self, value: setattr(self, 'commit_id', value))

    def get_latest_diffset(self):
        """Returns the diffset for this draft."""
        return self.diffset

    def is_accessible_by(self, user):
        """Returns whether or not the user can access this draft."""
        return self.is_mutable_by(user)

    def is_mutable_by(self, user):
        """Returns whether or not the user can modify this draft."""
        return self.review_request.is_mutable_by(user)

    @staticmethod
    def create(review_request):
        """Creates a draft based on a review request.

        This will copy over all the details of the review request that
        we care about. If a draft already exists for the review request,
        the draft will be returned.
        """
        draft, draft_is_new = \
            ReviewRequestDraft.objects.get_or_create(
                review_request=review_request,
                defaults={
                    'summary': review_request.summary,
                    'description': review_request.description,
                    'testing_done': review_request.testing_done,
                    'bugs_closed': review_request.bugs_closed,
                    'branch': review_request.branch,
                    'rich_text': review_request.rich_text,
                    'commit_id': review_request.commit_id,
                })

        if draft.changedesc is None and review_request.public:
            draft.changedesc = ChangeDescription.objects.create(
                rich_text=draft.rich_text)

        if draft_is_new:
            draft.target_groups = review_request.target_groups.all()
            draft.target_people = review_request.target_people.all()
            draft.depends_on = review_request.depends_on.all()
            draft.extra_data = review_request.extra_data
            draft.save()

            review_request.screenshots.update(draft_caption=F('caption'))
            draft.screenshots = review_request.screenshots.all()

            review_request.inactive_screenshots.update(
                draft_caption=F('caption'))
            draft.inactive_screenshots = \
                review_request.inactive_screenshots.all()

            review_request.file_attachments.update(draft_caption=F('caption'))
            draft.file_attachments = review_request.file_attachments.all()

            review_request.inactive_file_attachments.update(
                draft_caption=F('caption'))
            draft.inactive_file_attachments = \
                review_request.inactive_file_attachments.all()

        return draft

    def publish(self, review_request=None, user=None, send_notification=True):
        """Publishes this draft.

        This updates and returns the draft's ChangeDescription, which
        contains the changed fields. This is used by the e-mail template
        to tell people what's new and interesting.

        The draft's assocated ReviewRequest object will be used if one isn't
        passed in.

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

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

        Each field in 'fields_changed' represents a changed field. This will
        save fields in the standard formats as defined by the
        'ChangeDescription' documentation, with the exception of the
        'screenshot_captions' and 'diff' fields.

        For the 'screenshot_captions' field, the value will be a dictionary
        of screenshot ID/dict pairs with the following fields:

           * 'old': The old value of the field
           * 'new': The new value of the field

        For the 'diff' field, there is only ever an 'added' field, containing
        the ID of the new diffset.

        The 'send_notification' parameter is intended for internal use only,
        and is there to prevent duplicate notifications when being called by
        ReviewRequest.publish.
        """
        if not review_request:
            review_request = self.review_request

        if not user:
            user = review_request.submitter

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        review_request.rich_text = self.rich_text
        review_request.save()

        if send_notification:
            review_request_published.send(sender=review_request.__class__,
                                          user=user,
                                          review_request=review_request,
                                          changedesc=self.changedesc)

        return self.changedesc

    def get_review_request(self):
        """Returns the associated review request."""
        return self.review_request

    class Meta:
        app_label = 'reviews'
        ordering = ['-last_updated']
    def publish(self, review_request=None, user=None,
                send_notification=True):
        """Publishes this draft.

        This updates and returns the draft's ChangeDescription, which
        contains the changed fields. This is used by the e-mail template
        to tell people what's new and interesting.

        The draft's assocated ReviewRequest object will be used if one isn't
        passed in.

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

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

        Each field in 'fields_changed' represents a changed field. This will
        save fields in the standard formats as defined by the
        'ChangeDescription' documentation, with the exception of the
        'screenshot_captions' and 'diff' fields.

        For the 'screenshot_captions' field, the value will be a dictionary
        of screenshot ID/dict pairs with the following fields:

           * 'old': The old value of the field
           * 'new': The new value of the field

        For the 'diff' field, there is only ever an 'added' field, containing
        the ID of the new diffset.

        The 'send_notification' parameter is intended for internal use only,
        and is there to prevent duplicate notifications when being called by
        ReviewRequest.publish.
        """
        if not review_request:
            review_request = self.review_request

        if not user:
            user = review_request.submitter

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # If no changes were made, raise exception and do not save
        if self.changedesc and not self.changedesc.has_modified_fields():
            raise NotModifiedError()

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

        review_request.rich_text = self.rich_text
        review_request.save()

        if send_notification:
            review_request_published.send(sender=review_request.__class__,
                                          user=user,
                                          review_request=review_request,
                                          changedesc=self.changedesc)

        return self.changedesc
示例#42
0
    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
示例#43
0
class ReviewRequestDraft(BaseReviewRequestDetails):
    """A draft of a review request.

    When a review request is being modified, a special draft copy of it is
    created containing all the details of the review request. This copy can
    be modified and eventually saved or discarded. When saved, the new
    details are copied back over to the originating ReviewRequest.
    """
    summary = models.CharField(
        _("summary"), max_length=BaseReviewRequestDetails.MAX_SUMMARY_LENGTH)

    owner = models.ForeignKey(User,
                              verbose_name=_('owner'),
                              null=True,
                              related_name='draft')
    review_request = models.ForeignKey(ReviewRequest,
                                       related_name="draft",
                                       verbose_name=_("review request"),
                                       unique=True)
    last_updated = ModificationTimestampField(_("last updated"))
    diffset = models.ForeignKey(DiffSet,
                                verbose_name=_('diff set'),
                                blank=True,
                                null=True,
                                related_name='review_request_draft')
    changedesc = models.ForeignKey(ChangeDescription,
                                   verbose_name=_('change description'),
                                   blank=True,
                                   null=True)
    target_groups = models.ManyToManyField(Group,
                                           related_name="drafts",
                                           verbose_name=_("target groups"),
                                           blank=True)
    target_people = models.ManyToManyField(User,
                                           verbose_name=_("target people"),
                                           related_name="directed_drafts",
                                           blank=True)
    screenshots = models.ManyToManyField(Screenshot,
                                         related_name="drafts",
                                         verbose_name=_("screenshots"),
                                         blank=True)
    inactive_screenshots = models.ManyToManyField(
        Screenshot,
        verbose_name=_("inactive screenshots"),
        related_name="inactive_drafts",
        blank=True)

    file_attachments = models.ManyToManyField(
        FileAttachment,
        related_name="drafts",
        verbose_name=_("file attachments"),
        blank=True)
    inactive_file_attachments = models.ManyToManyField(
        FileAttachment,
        verbose_name=_("inactive files"),
        related_name="inactive_drafts",
        blank=True)

    submitter = property(
        lambda self: self.owner or self.review_request.submitter)
    repository = property(lambda self: self.review_request.repository)
    local_site = property(lambda self: self.review_request.local_site)

    depends_on = models.ManyToManyField('ReviewRequest',
                                        blank=True,
                                        null=True,
                                        verbose_name=_('Dependencies'),
                                        related_name='draft_blocks')

    # Set this up with a ConcurrencyManager to help prevent race conditions.
    objects = ConcurrencyManager()

    commit = property(lambda self: self.commit_id,
                      lambda self, value: setattr(self, 'commit_id', value))

    def get_latest_diffset(self):
        """Returns the diffset for this draft."""
        return self.diffset

    def is_accessible_by(self, user):
        """Returns whether or not the user can access this draft."""
        return self.is_mutable_by(user)

    def is_mutable_by(self, user):
        """Returns whether or not the user can modify this draft."""
        return self.review_request.is_mutable_by(user)

    @staticmethod
    def create(review_request):
        """Creates a draft based on a review request.

        This will copy over all the details of the review request that
        we care about. If a draft already exists for the review request,
        the draft will be returned.
        """
        draft, draft_is_new = \
            ReviewRequestDraft.objects.get_or_create(
                review_request=review_request,
                defaults={
                    'summary': review_request.summary,
                    'description': review_request.description,
                    'testing_done': review_request.testing_done,
                    'bugs_closed': review_request.bugs_closed,
                    'branch': review_request.branch,
                    'description_rich_text':
                        review_request.description_rich_text,
                    'testing_done_rich_text':
                        review_request.testing_done_rich_text,
                    'rich_text': review_request.rich_text,
                    'commit_id': review_request.commit_id,
                })

        if draft.changedesc is None and review_request.public:
            draft.changedesc = ChangeDescription.objects.create()

        if draft_is_new:
            draft.target_groups = review_request.target_groups.all()
            draft.target_people = review_request.target_people.all()
            draft.depends_on = review_request.depends_on.all()
            draft.extra_data = copy.deepcopy(review_request.extra_data)
            draft.save()

            review_request.screenshots.update(draft_caption=F('caption'))
            draft.screenshots = review_request.screenshots.all()

            review_request.inactive_screenshots.update(
                draft_caption=F('caption'))
            draft.inactive_screenshots = \
                review_request.inactive_screenshots.all()

            review_request.file_attachments.update(draft_caption=F('caption'))
            draft.file_attachments = review_request.file_attachments.all()

            review_request.inactive_file_attachments.update(
                draft_caption=F('caption'))
            draft.inactive_file_attachments = \
                review_request.inactive_file_attachments.all()

        return draft

    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 update_from_commit_id(self, commit_id):
        """Update the data from a server-side changeset.

        If the commit ID refers to a pending changeset on an SCM which stores
        such things server-side (like Perforce), the details like the summary
        and description will be updated with the latest information.

        If the change number is the commit ID of a change which exists on the
        server, the summary and description will be set from the commit's
        message, and the diff will be fetched from the SCM.

        Args:
            commit_id (unicode):
                The commit ID or changeset ID that the draft will update
                from.
        """
        scmtool = self.repository.get_scmtool()
        changeset = None

        if scmtool.supports_pending_changesets:
            changeset = scmtool.get_changeset(commit_id, allow_empty=True)

        if changeset and changeset.pending:
            self.update_from_pending_change(commit_id, changeset)
        elif self.repository.supports_post_commit:
            self.update_from_committed_change(commit_id)
        else:
            if changeset:
                raise InvalidChangeNumberError()
            else:
                raise NotImplementedError()

    def update_from_pending_change(self, commit_id, changeset):
        """Update the data from a server-side pending changeset.

        This will fetch the metadata from the server and update the fields on
        the draft.

        Args:
            commit_id (unicode):
                The changeset ID that the draft will update from.

            changeset (reviewboard.scmtools.core.ChangeSet):
                The changeset information to update from.
        """
        if not changeset:
            raise InvalidChangeNumberError()

        # If the SCM supports changesets, they should always include a number,
        # summary and description, parsed from the changeset description. Some
        # specialized systems may support the other fields, but we don't want
        # to clobber the user-entered values if they don't.
        self.commit = commit_id
        description = changeset.description
        testing_done = changeset.testing_done

        self.summary = changeset.summary
        self.description = description
        self.description_rich_text = False

        if testing_done:
            self.testing_done = testing_done
            self.testing_done_rich_text = False

        if changeset.branch:
            self.branch = changeset.branch

        if changeset.bugs_closed:
            self.bugs_closed = ','.join(changeset.bugs_closed)

    def update_from_committed_change(self, commit_id):
        """Update from a committed change present on the server.

        Fetches the commit message and diff from the repository and sets the
        relevant fields.

        Args:
            commit_id (unicode):
                The commit ID to update from.
        """
        commit = self.repository.get_change(commit_id)
        summary, message = commit.split_message()
        message = message.strip()

        self.commit = commit_id
        self.summary = summary.strip()

        self.description = message
        self.description_rich_text = False

        self.diffset = DiffSet.objects.create_from_data(
            repository=self.repository,
            diff_file_name='diff',
            diff_file_contents=commit.diff.encode('utf-8'),
            parent_diff_file_name=None,
            parent_diff_file_contents=None,
            diffset_history=None,
            basedir='/',
            request=None,
            base_commit_id=commit.parent)

        # Compute a suitable revision for the diffset.
        self.diffset.update_revision_from_history(
            self.review_request.diffset_history)
        self.diffset.save(update_fields=('revision', ))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def get_review_request(self):
        """Returns the associated review request."""
        return self.review_request

    class Meta:
        app_label = 'reviews'
        db_table = 'reviews_reviewrequestdraft'
        ordering = ['-last_updated']
        verbose_name = _('Review Request Draft')
        verbose_name_plural = _('Review Request Drafts')
示例#44
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)
示例#45
0
class ReviewRequestDraft(BaseReviewRequestDetails):
    """A draft of a review request.

    When a review request is being modified, a special draft copy of it is
    created containing all the details of the review request. This copy can
    be modified and eventually saved or discarded. When saved, the new
    details are copied back over to the originating ReviewRequest.
    """
    summary = models.CharField(
        _("summary"),
        max_length=BaseReviewRequestDetails.MAX_SUMMARY_LENGTH)

    owner = models.ForeignKey(
        User,
        verbose_name=_('owner'),
        null=True,
        related_name='draft')
    review_request = models.ForeignKey(
        ReviewRequest,
        related_name="draft",
        verbose_name=_("review request"),
        unique=True)
    last_updated = ModificationTimestampField(
        _("last updated"))
    diffset = models.ForeignKey(
        DiffSet,
        verbose_name=_('diff set'),
        blank=True,
        null=True,
        related_name='review_request_draft')
    changedesc = models.ForeignKey(
        ChangeDescription,
        verbose_name=_('change description'),
        blank=True,
        null=True)
    target_groups = models.ManyToManyField(
        Group,
        related_name="drafts",
        verbose_name=_("target groups"),
        blank=True)
    target_people = models.ManyToManyField(
        User,
        verbose_name=_("target people"),
        related_name="directed_drafts",
        blank=True)
    screenshots = models.ManyToManyField(
        Screenshot,
        related_name="drafts",
        verbose_name=_("screenshots"),
        blank=True)
    inactive_screenshots = models.ManyToManyField(
        Screenshot,
        verbose_name=_("inactive screenshots"),
        related_name="inactive_drafts",
        blank=True)

    file_attachments = models.ManyToManyField(
        FileAttachment,
        related_name="drafts",
        verbose_name=_("file attachments"),
        blank=True)
    inactive_file_attachments = models.ManyToManyField(
        FileAttachment,
        verbose_name=_("inactive files"),
        related_name="inactive_drafts",
        blank=True)

    submitter = property(lambda self: self.owner or
                         self.review_request.owner)
    repository = property(lambda self: self.review_request.repository)
    local_site = property(lambda self: self.review_request.local_site)

    depends_on = models.ManyToManyField('ReviewRequest',
                                        blank=True, null=True,
                                        verbose_name=_('Dependencies'),
                                        related_name='draft_blocks')

    screenshots_count = RelationCounterField(
        'screenshots',
        verbose_name=_('screenshots count'))

    inactive_screenshots_count = RelationCounterField(
        'inactive_screenshots',
        verbose_name=_('inactive screenshots count'))

    file_attachments_count = RelationCounterField(
        'file_attachments',
        verbose_name=_('file attachments count'))

    inactive_file_attachments_count = RelationCounterField(
        'inactive_file_attachments',
        verbose_name=_('inactive file attachments count'))

    # Set this up with a ConcurrencyManager to help prevent race conditions.
    objects = ConcurrencyManager()

    commit = property(lambda self: self.commit_id,
                      lambda self, value: setattr(self, 'commit_id', value))

    def get_latest_diffset(self):
        """Returns the diffset for this draft."""
        return self.diffset

    def is_accessible_by(self, user):
        """Returns whether or not the user can access this draft."""
        return self.is_mutable_by(user)

    def is_mutable_by(self, user):
        """Returns whether or not the user can modify this draft."""
        return self.review_request.is_mutable_by(user)

    @staticmethod
    def create(review_request):
        """Creates a draft based on a review request.

        This will copy over all the details of the review request that
        we care about. If a draft already exists for the review request,
        the draft will be returned.
        """
        draft, draft_is_new = \
            ReviewRequestDraft.objects.get_or_create(
                review_request=review_request,
                defaults={
                    'summary': review_request.summary,
                    'description': review_request.description,
                    'testing_done': review_request.testing_done,
                    'bugs_closed': review_request.bugs_closed,
                    'branch': review_request.branch,
                    'description_rich_text':
                        review_request.description_rich_text,
                    'testing_done_rich_text':
                        review_request.testing_done_rich_text,
                    'rich_text': review_request.rich_text,
                    'commit_id': review_request.commit_id,
                })

        if draft.changedesc is None and review_request.public:
            draft.changedesc = ChangeDescription.objects.create()
        if draft_is_new:
            draft.target_groups = review_request.target_groups.all()
            draft.target_people = review_request.target_people.all()
            draft.depends_on = review_request.depends_on.all()
            draft.extra_data = copy.deepcopy(review_request.extra_data)
            draft.save()

            if review_request.screenshots_count > 0:
                review_request.screenshots.update(draft_caption=F('caption'))
                draft.screenshots = review_request.screenshots.all()

            if review_request.inactive_screenshots_count > 0:
                review_request.inactive_screenshots.update(
                    draft_caption=F('caption'))
                draft.inactive_screenshots = \
                    review_request.inactive_screenshots.all()

            if review_request.file_attachments_count > 0:
                review_request.file_attachments.update(
                    draft_caption=F('caption'))
                draft.file_attachments = review_request.file_attachments.all()

            if review_request.inactive_file_attachments_count > 0:
                review_request.inactive_file_attachments.update(
                    draft_caption=F('caption'))
                draft.inactive_file_attachments = \
                    review_request.inactive_file_attachments.all()

        return draft

    def publish(self, review_request=None, user=None, trivial=False,
                send_notification=True, validate_fields=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 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.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 update_from_commit_id(self, commit_id):
        """Update the data from a server-side changeset.

        If the commit ID refers to a pending changeset on an SCM which stores
        such things server-side (like Perforce), the details like the summary
        and description will be updated with the latest information.

        If the change number is the commit ID of a change which exists on the
        server, the summary and description will be set from the commit's
        message, and the diff will be fetched from the SCM.

        Args:
            commit_id (unicode):
                The commit ID or changeset ID that the draft will update
                from.
        """
        scmtool = self.repository.get_scmtool()
        changeset = None

        if scmtool.supports_pending_changesets:
            changeset = scmtool.get_changeset(commit_id, allow_empty=True)

        if changeset and changeset.pending:
            self.update_from_pending_change(commit_id, changeset)
        elif self.repository.supports_post_commit:
            self.update_from_committed_change(commit_id)
        else:
            if changeset:
                raise InvalidChangeNumberError()
            else:
                raise NotImplementedError()

    def update_from_pending_change(self, commit_id, changeset):
        """Update the data from a server-side pending changeset.

        This will fetch the metadata from the server and update the fields on
        the draft.

        Args:
            commit_id (unicode):
                The changeset ID that the draft will update from.

            changeset (reviewboard.scmtools.core.ChangeSet):
                The changeset information to update from.
        """
        if not changeset:
            raise InvalidChangeNumberError()

        # If the SCM supports changesets, they should always include a number,
        # summary and description, parsed from the changeset description. Some
        # specialized systems may support the other fields, but we don't want
        # to clobber the user-entered values if they don't.
        self.commit = commit_id
        description = changeset.description
        testing_done = changeset.testing_done

        self.summary = changeset.summary
        self.description = description
        self.description_rich_text = False

        if testing_done:
            self.testing_done = testing_done
            self.testing_done_rich_text = False

        if changeset.branch:
            self.branch = changeset.branch

        if changeset.bugs_closed:
            self.bugs_closed = ','.join(changeset.bugs_closed)

    def update_from_committed_change(self, commit_id):
        """Update from a committed change present on the server.

        Fetches the commit message and diff from the repository and sets the
        relevant fields.

        Args:
            commit_id (unicode):
                The commit ID to update from.
        """
        commit = self.repository.get_change(commit_id)
        summary, message = commit.split_message()
        message = message.strip()

        self.commit = commit_id
        self.summary = summary.strip()

        self.description = message
        self.description_rich_text = False

        self.diffset = DiffSet.objects.create_from_data(
            repository=self.repository,
            diff_file_name='diff',
            diff_file_contents=commit.diff,
            parent_diff_file_name=None,
            parent_diff_file_contents=None,
            diffset_history=None,
            basedir='/',
            request=None,
            base_commit_id=commit.parent,
            check_existence=False)

        # Compute a suitable revision for the diffset.
        self.diffset.update_revision_from_history(
            self.review_request.diffset_history)
        self.diffset.save(update_fields=('revision',))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def get_review_request(self):
        """Returns the associated review request."""
        return self.review_request

    class Meta:
        app_label = 'reviews'
        db_table = 'reviews_reviewrequestdraft'
        ordering = ['-last_updated']
        verbose_name = _('Review Request Draft')
        verbose_name_plural = _('Review Request Drafts')
示例#46
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)
示例#47
0
class ReviewRequestDraft(BaseReviewRequestDetails):
    """A draft of a review request.

    When a review request is being modified, a special draft copy of it is
    created containing all the details of the review request. This copy can
    be modified and eventually saved or discarded. When saved, the new
    details are copied back over to the originating ReviewRequest.
    """
    summary = models.CharField(
        _("summary"),
        max_length=BaseReviewRequestDetails.MAX_SUMMARY_LENGTH)

    owner = models.ForeignKey(
        User,
        verbose_name=_('owner'),
        null=True,
        related_name='draft')
    review_request = models.ForeignKey(
        ReviewRequest,
        related_name="draft",
        verbose_name=_("review request"),
        unique=True)
    last_updated = ModificationTimestampField(
        _("last updated"))
    diffset = models.ForeignKey(
        DiffSet,
        verbose_name=_('diff set'),
        blank=True,
        null=True,
        related_name='review_request_draft')
    changedesc = models.ForeignKey(
        ChangeDescription,
        verbose_name=_('change description'),
        blank=True,
        null=True)
    target_groups = models.ManyToManyField(
        Group,
        related_name="drafts",
        verbose_name=_("target groups"),
        blank=True)
    target_people = models.ManyToManyField(
        User,
        verbose_name=_("target people"),
        related_name="directed_drafts",
        blank=True)
    screenshots = models.ManyToManyField(
        Screenshot,
        related_name="drafts",
        verbose_name=_("screenshots"),
        blank=True)
    inactive_screenshots = models.ManyToManyField(
        Screenshot,
        verbose_name=_("inactive screenshots"),
        related_name="inactive_drafts",
        blank=True)

    file_attachments = models.ManyToManyField(
        FileAttachment,
        related_name="drafts",
        verbose_name=_("file attachments"),
        blank=True)
    inactive_file_attachments = models.ManyToManyField(
        FileAttachment,
        verbose_name=_("inactive files"),
        related_name="inactive_drafts",
        blank=True)

    submitter = property(lambda self: self.owner or
                         self.review_request.owner)
    repository = property(lambda self: self.review_request.repository)
    local_site = property(lambda self: self.review_request.local_site)

    depends_on = models.ManyToManyField('ReviewRequest',
                                        blank=True, null=True,
                                        verbose_name=_('Dependencies'),
                                        related_name='draft_blocks')

    screenshots_count = RelationCounterField(
        'screenshots',
        verbose_name=_('screenshots count'))

    inactive_screenshots_count = RelationCounterField(
        'inactive_screenshots',
        verbose_name=_('inactive screenshots count'))

    file_attachments_count = RelationCounterField(
        'file_attachments',
        verbose_name=_('file attachments count'))

    inactive_file_attachments_count = RelationCounterField(
        'inactive_file_attachments',
        verbose_name=_('inactive file attachments count'))

    # Set this up with a ConcurrencyManager to help prevent race conditions.
    objects = ConcurrencyManager()

    commit = property(lambda self: self.commit_id,
                      lambda self, value: setattr(self, 'commit_id', value))

    def get_latest_diffset(self):
        """Returns the diffset for this draft."""
        return self.diffset

    def is_accessible_by(self, user):
        """Returns whether or not the user can access this draft."""
        return self.is_mutable_by(user)

    def is_mutable_by(self, user):
        """Returns whether or not the user can modify this draft."""
        return self.review_request.is_mutable_by(user)

    @staticmethod
    def create(review_request, changedesc=None):
        """Create a draft based on a review request.

        This will copy over all the details of the review request that
        we care about. If a draft already exists for the review request,
        the draft will be returned.

        Args:
            review_request (reviewboard.reviews.models.review_request.
                            ReviewRequest):
                The review request to fetch or create the draft from.

            changedesc (reviewboard.changedescs.models.ChangeDescription):
                A custom change description to set on the draft. This will
                always be set, overriding any previous one if already set.

        Returns:
            ReviewRequestDraft:
            The resulting draft.
        """
        draft, draft_is_new = \
            ReviewRequestDraft.objects.get_or_create(
                review_request=review_request,
                defaults={
                    'changedesc': changedesc,
                    'extra_data': review_request.extra_data or {},
                    'summary': review_request.summary,
                    'description': review_request.description,
                    'testing_done': review_request.testing_done,
                    'bugs_closed': review_request.bugs_closed,
                    'branch': review_request.branch,
                    'description_rich_text':
                        review_request.description_rich_text,
                    'testing_done_rich_text':
                        review_request.testing_done_rich_text,
                    'rich_text': review_request.rich_text,
                    'commit_id': review_request.commit_id,
                })

        if (changedesc is None and
            draft.changedesc_id is None and
            review_request.public):
            changedesc = ChangeDescription.objects.create()

        if changedesc is not None and draft.changedesc_id != changedesc.pk:
            old_changedesc_id = draft.changedesc_id
            draft.changedesc = changedesc
            draft.save(update_fields=('changedesc',))

            if old_changedesc_id is not None:
                ChangeDescription.objects.filter(pk=old_changedesc_id).delete()

        if draft_is_new:
            rels_to_update = [
                ('depends_on', 'to_reviewrequest_id', 'from_reviewrequest_id'),
                ('target_groups', 'group_id', 'reviewrequest_id'),
                ('target_people', 'user_id', 'reviewrequest_id'),
            ]

            if review_request.screenshots_count > 0:
                review_request.screenshots.update(draft_caption=F('caption'))
                rels_to_update.append(('screenshots', 'screenshot_id',
                                       'reviewrequest_id'))

            if review_request.inactive_screenshots_count > 0:
                review_request.inactive_screenshots.update(
                    draft_caption=F('caption'))
                rels_to_update.append(('inactive_screenshots', 'screenshot_id',
                                       'reviewrequest_id'))

            if review_request.file_attachments_count > 0:
                review_request.file_attachments.update(
                    draft_caption=F('caption'))
                rels_to_update.append(('file_attachments', 'fileattachment_id',
                                       'reviewrequest_id'))

            if review_request.inactive_file_attachments_count > 0:
                review_request.inactive_file_attachments.update(
                    draft_caption=F('caption'))
                rels_to_update.append(('inactive_file_attachments',
                                       'fileattachment_id',
                                       'reviewrequest_id'))

            for rel_field, id_field, lookup_field, in rels_to_update:
                # We don't need to query the entirety of each object, and
                # we'd like to avoid any JOINs. So, we'll be using the
                # M2M 'through' tables to perform lookups of the related
                # models' IDs.
                items = list(
                    getattr(review_request, rel_field).through.objects
                    .filter(**{lookup_field: review_request.pk})
                    .values_list(id_field, flat=True)
                )

                if items:
                    # Note that we're using add() instead of directly
                    # assigning the value. This lets us avoid a query that
                    # Django would perform to determine if it needed to clear
                    # out any existing values. Since we know this draft is
                    # new, there's no point in doing that.
                    getattr(draft, rel_field).add(*items)

        return draft

    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 (review_request.created_with_history and
                self.diffset and
                self.diffset.commit_count == 0):
                raise PublishError(
                    ugettext('There are no commits attached to the diff.'))

        if self.diffset:
            if (review_request.created_with_history and not
                self.diffset.is_commit_series_finalized):
                raise PublishError(ugettext(
                    'This commit series is not finalized.'))

            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 update_from_commit_id(self, commit_id):
        """Update the data from a server-side changeset.

        If the commit ID refers to a pending changeset on an SCM which stores
        such things server-side (like Perforce), the details like the summary
        and description will be updated with the latest information.

        If the change number is the commit ID of a change which exists on the
        server, the summary and description will be set from the commit's
        message, and the diff will be fetched from the SCM.

        Args:
            commit_id (unicode):
                The commit ID or changeset ID that the draft will update
                from.

        Returns:
            list of unicode:
            The list of draft fields that have been updated from the commit.
        """
        scmtool = self.repository.get_scmtool()
        changeset = None

        if scmtool.supports_pending_changesets:
            changeset = scmtool.get_changeset(commit_id, allow_empty=True)

        if changeset and changeset.pending:
            return self.update_from_pending_change(commit_id, changeset)
        elif self.repository.supports_post_commit:
            return self.update_from_committed_change(commit_id)
        else:
            if changeset:
                raise InvalidChangeNumberError()
            else:
                raise NotImplementedError()

    def update_from_pending_change(self, commit_id, changeset):
        """Update the data from a server-side pending changeset.

        This will fetch the metadata from the server and update the fields on
        the draft.

        Args:
            commit_id (unicode):
                The changeset ID that the draft will update from.

            changeset (reviewboard.scmtools.core.ChangeSet):
                The changeset information to update from.

        Returns:
            list of unicode:
            The list of draft fields that have been updated from the change.
        """
        if not changeset:
            raise InvalidChangeNumberError()

        # If the SCM supports changesets, they should always include a number,
        # summary and description, parsed from the changeset description. Some
        # specialized systems may support the other fields, but we don't want
        # to clobber the user-entered values if they don't.
        self.commit = commit_id
        description = changeset.description
        testing_done = changeset.testing_done

        self.summary = changeset.summary
        self.description = description
        self.description_rich_text = False

        modified_fields = [
            'commit_id', 'summary', 'description', 'description_rich_text',
        ]

        if testing_done:
            self.testing_done = testing_done
            self.testing_done_rich_text = False
            modified_fields += ['testing_done', 'testing_done_rich_text']

        if changeset.branch:
            self.branch = changeset.branch
            modified_fields.append('branch')

        if changeset.bugs_closed:
            self.bugs_closed = ','.join(changeset.bugs_closed)
            modified_fields.append('bugs_closed')

        return modified_fields

    def update_from_committed_change(self, commit_id):
        """Update from a committed change present on the server.

        Fetches the commit message and diff from the repository and sets the
        relevant fields.

        Args:
            commit_id (unicode):
                The commit ID to update from.

        Returns:
            list of unicode:
            The list of draft fields that have been updated from the commit
            message.
        """
        commit = self.repository.get_change(commit_id)
        summary, message = commit.split_message()
        message = message.strip()

        self.commit = commit_id
        self.summary = summary.strip()

        self.description = message
        self.description_rich_text = False

        self.diffset = DiffSet.objects.create_from_data(
            repository=self.repository,
            diff_file_name='diff',
            diff_file_contents=commit.diff,
            parent_diff_file_name=None,
            parent_diff_file_contents=None,
            diffset_history=None,
            basedir='/',
            request=None,
            base_commit_id=commit.parent,
            check_existence=False)

        # Compute a suitable revision for the diffset.
        self.diffset.update_revision_from_history(
            self.review_request.diffset_history)
        self.diffset.save(update_fields=('revision',))

        return [
            'commit_id',
            'description',
            'description_rich_text',
            'diffset',
            'summary',
        ]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def get_review_request(self):
        """Returns the associated review request."""
        return self.review_request

    class Meta:
        app_label = 'reviews'
        db_table = 'reviews_reviewrequestdraft'
        ordering = ['-last_updated']
        verbose_name = _('Review Request Draft')
        verbose_name_plural = _('Review Request Drafts')
示例#48
0
    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)
示例#49
0
    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
class ReviewRequestDraft(BaseReviewRequestDetails):
    """A draft of a review request.

    When a review request is being modified, a special draft copy of it is
    created containing all the details of the review request. This copy can
    be modified and eventually saved or discarded. When saved, the new
    details are copied back over to the originating ReviewRequest.
    """
    summary = models.CharField(
        _("summary"),
        max_length=BaseReviewRequestDetails.MAX_SUMMARY_LENGTH)

    review_request = models.ForeignKey(
        ReviewRequest,
        related_name="draft",
        verbose_name=_("review request"),
        unique=True)
    last_updated = ModificationTimestampField(
        _("last updated"))
    diffset = models.ForeignKey(
        DiffSet,
        verbose_name=_('diff set'),
        blank=True,
        null=True,
        related_name='review_request_draft')
    changedesc = models.ForeignKey(
        ChangeDescription,
        verbose_name=_('change description'),
        blank=True,
        null=True)
    target_groups = models.ManyToManyField(
        Group,
        related_name="drafts",
        verbose_name=_("target groups"),
        blank=True)
    target_people = models.ManyToManyField(
        User,
        verbose_name=_("target people"),
        related_name="directed_drafts",
        blank=True)
    screenshots = models.ManyToManyField(
        Screenshot,
        related_name="drafts",
        verbose_name=_("screenshots"),
        blank=True)
    inactive_screenshots = models.ManyToManyField(
        Screenshot,
        verbose_name=_("inactive screenshots"),
        related_name="inactive_drafts",
        blank=True)

    file_attachments = models.ManyToManyField(
        FileAttachment,
        related_name="drafts",
        verbose_name=_("file attachments"),
        blank=True)
    inactive_file_attachments = models.ManyToManyField(
        FileAttachment,
        verbose_name=_("inactive files"),
        related_name="inactive_drafts",
        blank=True)

    submitter = property(lambda self: self.review_request.submitter)
    repository = property(lambda self: self.review_request.repository)
    local_site = property(lambda self: self.review_request.local_site)

    depends_on = models.ManyToManyField('ReviewRequest',
                                        blank=True, null=True,
                                        verbose_name=_('Dependencies'),
                                        related_name='draft_blocks')

    # Set this up with a ConcurrencyManager to help prevent race conditions.
    objects = ConcurrencyManager()

    commit = property(lambda self: self.commit_id,
                      lambda self, value: setattr(self, 'commit_id', value))

    def get_latest_diffset(self):
        """Returns the diffset for this draft."""
        return self.diffset

    def is_accessible_by(self, user):
        """Returns whether or not the user can access this draft."""
        return self.is_mutable_by(user)

    def is_mutable_by(self, user):
        """Returns whether or not the user can modify this draft."""
        return self.review_request.is_mutable_by(user)

    @staticmethod
    def create(review_request):
        """Creates a draft based on a review request.

        This will copy over all the details of the review request that
        we care about. If a draft already exists for the review request,
        the draft will be returned.
        """
        draft, draft_is_new = \
            ReviewRequestDraft.objects.get_or_create(
                review_request=review_request,
                defaults={
                    'summary': review_request.summary,
                    'description': review_request.description,
                    'testing_done': review_request.testing_done,
                    'bugs_closed': review_request.bugs_closed,
                    'branch': review_request.branch,
                    'rich_text': review_request.rich_text,
                    'commit_id': review_request.commit_id,
                })

        if draft.changedesc is None and review_request.public:
            draft.changedesc = ChangeDescription.objects.create(
                rich_text=draft.rich_text)

        if draft_is_new:
            draft.target_groups = review_request.target_groups.all()
            draft.target_people = review_request.target_people.all()
            draft.depends_on = review_request.depends_on.all()
            draft.extra_data = review_request.extra_data
            draft.save()

            review_request.screenshots.update(draft_caption=F('caption'))
            draft.screenshots = review_request.screenshots.all()

            review_request.inactive_screenshots.update(
                draft_caption=F('caption'))
            draft.inactive_screenshots = \
                review_request.inactive_screenshots.all()

            review_request.file_attachments.update(draft_caption=F('caption'))
            draft.file_attachments = review_request.file_attachments.all()

            review_request.inactive_file_attachments.update(
                draft_caption=F('caption'))
            draft.inactive_file_attachments = \
                review_request.inactive_file_attachments.all()

        return draft

    def publish(self, review_request=None, user=None,
                send_notification=True):
        """Publishes this draft.

        This updates and returns the draft's ChangeDescription, which
        contains the changed fields. This is used by the e-mail template
        to tell people what's new and interesting.

        The draft's assocated ReviewRequest object will be used if one isn't
        passed in.

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

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

        Each field in 'fields_changed' represents a changed field. This will
        save fields in the standard formats as defined by the
        'ChangeDescription' documentation, with the exception of the
        'screenshot_captions' and 'diff' fields.

        For the 'screenshot_captions' field, the value will be a dictionary
        of screenshot ID/dict pairs with the following fields:

           * 'old': The old value of the field
           * 'new': The new value of the field

        For the 'diff' field, there is only ever an 'added' field, containing
        the ID of the new diffset.

        The 'send_notification' parameter is intended for internal use only,
        and is there to prevent duplicate notifications when being called by
        ReviewRequest.publish.
        """
        if not review_request:
            review_request = self.review_request

        if not user:
            user = review_request.submitter

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # If no changes were made, raise exception and do not save
        if self.changedesc and not self.changedesc.has_modified_fields():
            raise NotModifiedError()

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

        review_request.rich_text = self.rich_text
        review_request.save()

        if send_notification:
            review_request_published.send(sender=review_request.__class__,
                                          user=user,
                                          review_request=review_request,
                                          changedesc=self.changedesc)

        return self.changedesc

    def get_review_request(self):
        """Returns the associated review request."""
        return self.review_request

    class Meta:
        app_label = 'reviews'
        ordering = ['-last_updated']
示例#51
0
    def publish(self, review_request=None, user=None, send_notification=True):
        """
        Publishes this draft. Uses the draft's assocated ReviewRequest
        object if one isn't passed in.

        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:

           *  'summary'
           *  'description'
           *  'testing_done'
           *  'bugs_closed'
           *  '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.
        """
        from reviewboard.accounts.models import LocalSiteProfile

        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_field(a, b, name, record_changes=True):
            # Apparently django models don't have __getattr__ or __setattr__,
            # so we have to update __dict__ directly.  Sigh.
            value = b.__dict__[name]
            old_value = a.__dict__[name]

            if old_value != value:
                if record_changes and self.changedesc:
                    self.changedesc.record_field_change(name, old_value, value)

                a.__dict__[name] = value

        def update_list(a, b, name, record_changes=True, name_field=None, counter_infos=[]):
            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()
                map(a.add, b.all())

                # Decrement the counts on everything we had before.
                # we lose them. We'll increment the resulting set
                # during ReviewRequest.save.
                for model, counter, pk_field in counter_infos:
                    counter.decrement(
                        model.objects.filter(**{pk_field + "__in": aset, "local_site": review_request.local_site})
                    )

        update_field(review_request, self, "summary")
        update_field(review_request, self, "description")
        update_field(review_request, self, "testing_done")
        update_field(review_request, self, "branch")

        update_list(
            review_request.target_groups,
            self.target_groups,
            "target_groups",
            name_field="name",
            counter_infos=[
                (Group, Group.incoming_request_count, "pk"),
                (LocalSiteProfile, LocalSiteProfile.total_incoming_request_count, "user__review_groups"),
            ],
        )
        update_list(
            review_request.target_people,
            self.target_people,
            "target_people",
            name_field="username",
            counter_infos=[
                (LocalSiteProfile, LocalSiteProfile.direct_incoming_request_count, "user"),
                (LocalSiteProfile, LocalSiteProfile.total_incoming_request_count, "user"),
            ],
        )

        # Specifically handle bug numbers
        old_bugs = set(review_request.get_bug_list())
        new_bugs = set(self.get_bug_list())

        if old_bugs != new_bugs:
            update_field(review_request, self, "bugs_closed", record_changes=False)

            if self.changedesc:
                self.changedesc.record_field_change("bugs_closed", old_bugs - new_bugs, new_bugs - old_bugs)

        # Screenshots are a bit special.  The list of associated screenshots can
        # change, but so can captions within each screenshot.
        screenshots = 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()

        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.clear()
        map(review_request.inactive_screenshots.add, self.inactive_screenshots.all())

        if self.diffset:
            if self.changedesc:
                if review_request.local_site:
                    local_site_name = review_request.local_site.name
                else:
                    local_site_name = None

                url = local_site_reverse(
                    "view_diff_revision",
                    local_site_name=local_site_name,
                    args=[review_request.display_id, self.diffset.revision],
                )
                self.changedesc.fields_changed["diff"] = {
                    "added": [(_("Diff r%s") % self.diffset.revision, url, self.diffset.id)]
                }

            self.diffset.history = review_request.diffset_history
            self.diffset.save()

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

        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