Implement new `canned_response` relationship in draft comments. (#12156)

* Implement new `canned_response` relationship in draft comments.

This also updates the api changelog along the way with a few things I
forgot to add in the past.

Fixes #11320
Fixes #11807

* Fix flake8

* Add missing migration
This commit is contained in:
Christopher Grebs 2019-08-21 17:13:04 +02:00 коммит произвёл GitHub
Родитель b63dff900d
Коммит f5653ba68a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 204 добавлений и 31 удалений

Просмотреть файл

@ -321,6 +321,7 @@ v4 API changelog
* 2019-01-10: added ``release_notes`` and ``license`` (except ``license.text``) to search API results ``current_version`` objects.
* 2019-01-11: added new /reviewers/browse/ endpoint. https://github.com/mozilla/addons-server/issues/10322
* 2019-01-16: removed /api/{v3,v4,v5}/github api entirely. They have been marked as experimental. https://github.com/mozilla/addons-server/issues/10411
* 2019-02-21: added new /api/v4/reviewers/addon/(addon_id)/versions/ endpoint. https://github.com/mozilla/addons-server/issues/10432
* 2019-03-14: added new /reviewers/compare/ endpoint. https://github.com/mozilla/addons-server/issues/10323
* 2019-04-11: removed ``id``, ``username`` and ``url`` from the ``user`` object in the activity review notes APIs. https://github.com/mozilla/addons-server/issues/11002
* 2019-05-09: added ``is_recommended`` to addons API. https://github.com/mozilla/addons-server/issues/11278
@ -329,6 +330,7 @@ v4 API changelog
* 2019-05-23: changed the addons search API default sort when no query string is passed - now ``sort=recommended,downloads``.
Also made ``recommended`` sort available generally to the addons search API. https://github.com/mozilla/addons-server/issues/11432
* 2019-06-27: removed ``sort`` parameter from addon autocomplete API. https://github.com/mozilla/addons-server/issues/11664
* 2019-07-18: completely changed the 2019-05-16 added draft-comment related APIs. See `#11380`_, `#11379`_, `#11378`_ and `#11374`_
* 2019-07-25: added /hero/ endpoint to expose recommended addons and other content to frontend to allow customizable promos https://github.com/mozilla/addons-server/issues/11842.
* 2019-08-01: added alias ``edition=MozillaOnline`` for ``edition=china`` in /discovery/ endpoint.
* 2019-08-08: add support for externally hosted addons to /hero/ endpoints. https://github.com/mozilla/addons-server/issues/11882
@ -337,6 +339,13 @@ v4 API changelog
* 2019-08-15: dropped support for LWT specific statuses.
* 2019-08-15: added promo modules to secondary hero shelves. https://github.com/mozilla/addons-server/issues/11780
* 2019-08-15: removed /addons/compat-override/ from v4 and above. Still exists in /v3/ but will always return an empty response. https://github.com/mozilla/addons-server/issues/12063
* 2019-08-22: added ``canned_response`` property to draft comment api. https://github.com/mozilla/addons-server/issues/11807
.. _`#11380`: https://github.com/mozilla/addons-server/issues/11380/
.. _`#11379`: https://github.com/mozilla/addons-server/issues/11379/
.. _`#11378`: https://github.com/mozilla/addons-server/issues/11378/
.. _`#11374`: https://github.com/mozilla/addons-server/issues/11374/
----------------

Просмотреть файл

@ -291,6 +291,8 @@ This endpoint allows you to retrieve a list of canned responses.
.. http:get:: /api/v4/reviewers/canned-responses/
.. _reviewers-canned-response-detail:
Retrieve canned responses
.. note::
@ -340,7 +342,7 @@ These endpoints allow you to draft comments that can be submitted through the re
:>json string user.name: The name for an author.
:>json string user.url: The link to the profile page for an author.
:>json string user.username: The username for an author.
:>json object canned_response: Object holding the :ref:`canned response <reviewers-canned-response-detail>` if set.
.. http:post:: /api/v4/reviewers/addon/(int:addon_id)/versions/(int:version_id)/draft_comments/
@ -349,6 +351,7 @@ These endpoints allow you to draft comments that can be submitted through the re
:<json string comment: The comment that is being drafted as part of a review.
:<json string filename: The filename this comment is related to (optional).
:<json int lineno: The line number this comment is related to (optional). Please make sure that in case of comments for git diffs, that the `lineno` used here belongs to the file in the version that belongs to `version_id` and not it's parent.
:<json int draft_comment.id: The id of the draft comment (optional).
:statuscode 201: New comment has been created.
:statuscode 400: An error occurred, check the `error` value in the JSON.
:statuscode 403: The user doesn't have the permission to create a comment. This might happen (among other cases) when someone without permissions for unlisted versions tries to add a comment for an unlisted version (which shouldn't happen as the user doesn't see unlisted versions, but it's blocked here too).
@ -369,5 +372,6 @@ These endpoints allow you to draft comments that can be submitted through the re
:<json string comment: The comment that is being drafted as part of a review.
:<json string filename: The filename this comment is related to.
:<json int lineno: The line number this comment is related to. Please make sure that in case of comments for git diffs, that the `lineno` used here belongs to the file in the version that belongs to `version_id` and not it's parent.
:<json int draft_comment.id: The id of the draft comment (optional).
:statuscode 200: The comment has been updated.
:statuscode 400: An error occurred, check the `error` value in the JSON.

Просмотреть файл

@ -24,6 +24,7 @@ from olympia.amo.models import ManagerBase, ModelBase
from olympia.bandwagon.models import Collection
from olympia.files.models import File
from olympia.ratings.models import Rating
from olympia.reviewers.models import CannedResponse
from olympia.tags.models import Tag
from olympia.users.models import UserProfile
from olympia.users.templatetags.jinja_helpers import user_link
@ -171,7 +172,10 @@ class DraftComment(ModelBase):
user = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
filename = models.CharField(max_length=255, null=True, blank=True)
lineno = models.PositiveIntegerField(null=True)
comment = models.TextField()
canned_response = models.ForeignKey(
CannedResponse, null=True, default=None,
on_delete=models.SET_DEFAULT)
comment = models.TextField(blank=True)
class Meta:
db_table = 'log_activity_comment_draft'

Просмотреть файл

@ -6,12 +6,14 @@ from pyquery import PyQuery as pq
from olympia import amo, core
from olympia.activity.models import (
MAX_TOKEN_USE_COUNT, ActivityLog, ActivityLogToken, AddonLog)
MAX_TOKEN_USE_COUNT, ActivityLog, ActivityLogToken, AddonLog,
DraftComment)
from olympia.addons.models import Addon, AddonUser
from olympia.amo.tests import (
TestCase, addon_factory, user_factory, version_factory)
from olympia.bandwagon.models import Collection
from olympia.ratings.models import Rating
from olympia.reviewers.models import CannedResponse
from olympia.tags.models import Tag
from olympia.users.models import UserProfile
from olympia.versions.models import Version
@ -413,3 +415,39 @@ class TestActivityLogCount(TestCase):
ActivityLog.create(amo.LOG.EDIT_VERSION, Addon.objects.get())
assert len(ActivityLog.objects.admin_events()) == 0
assert len(ActivityLog.objects.for_developer()) == 1
class TestDraftComment(TestCase):
def test_default_requirements(self):
addon = addon_factory()
user = user_factory()
# user and version are the absolute minimum required to
# create a DraftComment
comment = DraftComment.objects.create(
user=user, version=addon.current_version)
assert comment.user == user
assert comment.version == addon.current_version
assert comment.filename is None
assert comment.lineno is None
assert comment.canned_response is None
assert comment.comment == ''
def test_canned_response_on_delete(self):
addon = addon_factory()
user = user_factory()
canned_response = CannedResponse.objects.create(
name=u'Terms of services',
response=u'test',
category=amo.CANNED_RESPONSE_CATEGORY_OTHER,
type=amo.CANNED_RESPONSE_TYPE_ADDON)
DraftComment.objects.create(
user=user, version=addon.current_version,
canned_response=canned_response)
canned_response.delete()
assert DraftComment.objects.get().canned_response is None

Просмотреть файл

@ -0,0 +1,7 @@
ALTER TABLE `log_activity_comment_draft`
ADD COLUMN `canned_response_id` integer UNSIGNED DEFAULT NULL;
ALTER TABLE `log_activity_comment_draft`
ADD CONSTRAINT `log_activity_comment_canned_response_id_6a9271d5_fk_cannedres`
FOREIGN KEY (`canned_response_id`)
REFERENCES `cannedresponses` (`id`);

Просмотреть файл

@ -12,6 +12,7 @@ from rest_framework.exceptions import NotFound
from django.utils.functional import cached_property
from django.utils.encoding import force_text
from django.utils.timezone import FixedOffset
from django.utils.translation import ugettext
from olympia import amo
from olympia.activity.models import DraftComment
@ -340,23 +341,6 @@ class AddonCompareVersionSerializer(AddonBrowseVersionSerializer):
pass
class DraftCommentSerializer(serializers.ModelSerializer):
user = SplitField(
serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all()),
BaseUserSerializer())
version = SplitField(
serializers.PrimaryKeyRelatedField(
queryset=Version.unfiltered.all()),
VersionSerializer())
class Meta:
model = DraftComment
fields = (
'id', 'filename', 'lineno', 'comment',
'version', 'user'
)
class CannedResponseSerializer(serializers.ModelSerializer):
# Title is actually more fitting than the internal "name"
title = serializers.CharField(source='name')
@ -368,3 +352,33 @@ class CannedResponseSerializer(serializers.ModelSerializer):
def get_category(self, obj):
return amo.CANNED_RESPONSE_CATEGORY_CHOICES[obj.category]
class DraftCommentSerializer(serializers.ModelSerializer):
user = SplitField(
serializers.PrimaryKeyRelatedField(queryset=UserProfile.objects.all()),
BaseUserSerializer())
version = SplitField(
serializers.PrimaryKeyRelatedField(
queryset=Version.unfiltered.all()),
VersionSerializer())
canned_response = SplitField(
serializers.PrimaryKeyRelatedField(
queryset=CannedResponse.objects.all(),
required=False),
CannedResponseSerializer())
class Meta:
model = DraftComment
fields = (
'id', 'filename', 'lineno', 'comment',
'version', 'user', 'canned_response'
)
def validate(self, data):
if data.get('comment') and data.get('canned_response'):
raise serializers.ValidationError(
{'comment': ugettext(
'You can\'t submit a comment if `canned_response` is '
'defined.')})
return data

Просмотреть файл

@ -53,6 +53,7 @@ from olympia.reviewers.models import (
Whiteboard)
from olympia.reviewers.utils import ContentReviewTable
from olympia.reviewers.views import _queue
from olympia.reviewers.serializers import CannedResponseSerializer
from olympia.users.models import UserProfile
from olympia.versions.models import ApplicationsVersions, AppVersion
from olympia.versions.tasks import extract_version_to_git
@ -5743,7 +5744,21 @@ class TestReviewAddonVersionViewSetList(TestCase):
},
]
def test_draft_comment_create_and_retrieve(self):
class TestDraftCommentViewSet(TestCase):
client_class = APITestClient
def setUp(self):
super().setUp()
self.addon = addon_factory(
name=u'My Addôn', slug='my-addon',
file_kw={'filename': 'webextension_no_id.xpi'})
self.version = self.addon.current_version
self.version.refresh_from_db()
def test_create_and_retrieve(self):
user = user_factory(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
@ -5779,6 +5794,7 @@ class TestReviewAddonVersionViewSetList(TestCase):
'filename': 'manifest.json',
'lineno': 20,
'comment': 'Some really fancy comment',
'canned_response': None,
'version': json.loads(json.dumps(
VersionSerializer(self.version).data,
cls=amo.utils.AMOJSONEncoder)),
@ -5788,7 +5804,7 @@ class TestReviewAddonVersionViewSetList(TestCase):
cls=amo.utils.AMOJSONEncoder))
}
def test_draft_comment_create_retrieve_and_update(self):
def test_create_retrieve_and_update(self):
user = user_factory(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
@ -5855,7 +5871,7 @@ class TestReviewAddonVersionViewSetList(TestCase):
assert response.json()['lineno'] == 16
assert response.json()['filename'] == 'new_manifest.json'
def test_draft_comment_lineno_filename_optional(self):
def test_draft_optional_fields(self):
user = user_factory(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
@ -5885,8 +5901,9 @@ class TestReviewAddonVersionViewSetList(TestCase):
assert response.json()['comment'] == 'Some really fancy comment'
assert response.json()['lineno'] is None
assert response.json()['filename'] is None
assert response.json()['canned_response'] is None
def test_draft_comment_delete(self):
def test_delete(self):
user = user_factory(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
@ -5906,7 +5923,87 @@ class TestReviewAddonVersionViewSetList(TestCase):
assert DraftComment.objects.first() is None
def test_draft_comment_delete_not_comment_owner(self):
def test_canned_response_and_comment_not_together(self):
user = user_factory(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
canned_response = CannedResponse.objects.create(
name=u'Terms of services',
response=u'doesn\'t regard our terms of services',
category=amo.CANNED_RESPONSE_CATEGORY_OTHER,
type=amo.CANNED_RESPONSE_TYPE_ADDON)
data = {
'comment': 'Some really fancy comment',
'canned_response': canned_response.pk,
'lineno': 20,
'filename': 'manifest.json',
}
url = reverse_ns('reviewers-versions-draft-comment-list', kwargs={
'addon_pk': self.addon.pk,
'version_pk': self.version.pk,
})
response = self.client.post(url, data)
assert response.status_code == 400
assert (
str(response.data['comment'][0]) ==
"You can't submit a comment if `canned_response` is defined.")
def test_canned_response(self):
user = user_factory(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
canned_response = CannedResponse.objects.create(
name=u'Terms of services',
response=u'doesn\'t regard our terms of services',
category=amo.CANNED_RESPONSE_CATEGORY_OTHER,
type=amo.CANNED_RESPONSE_TYPE_ADDON)
data = {
'canned_response': canned_response.pk,
'lineno': 20,
'filename': 'manifest.json',
}
url = reverse_ns('reviewers-versions-draft-comment-list', kwargs={
'addon_pk': self.addon.pk,
'version_pk': self.version.pk,
})
response = self.client.post(url, data)
comment_id = response.json()['id']
assert response.status_code == 201
assert DraftComment.objects.count() == 1
response = self.client.get(url)
request = APIRequestFactory().get('/')
request.user = user
assert response.json()['count'] == 1
assert response.json()['results'][0] == {
'id': comment_id,
'filename': 'manifest.json',
'lineno': 20,
'comment': '',
'canned_response': json.loads(json.dumps(
CannedResponseSerializer(canned_response).data,
cls=amo.utils.AMOJSONEncoder)),
'version': json.loads(json.dumps(
VersionSerializer(self.version).data,
cls=amo.utils.AMOJSONEncoder)),
'user': json.loads(json.dumps(
BaseUserSerializer(
user, context={'request': request}).data,
cls=amo.utils.AMOJSONEncoder))
}
def test_delete_not_comment_owner(self):
user = user_factory(username='reviewer')
self.grant_permission(user, 'Addons:Review')
@ -5931,7 +6028,7 @@ class TestReviewAddonVersionViewSetList(TestCase):
response = self.client.delete(url)
assert response.status_code == 404
def test_draft_comment_disabled_version_user_but_not_author(self):
def test_disabled_version_user_but_not_author(self):
user = user_factory(username='simpleuser')
self.client.login_api(user)
self.version.files.update(status=amo.STATUS_DISABLED)
@ -5950,7 +6047,7 @@ class TestReviewAddonVersionViewSetList(TestCase):
response = self.client.post(url, data)
assert response.status_code == 403
def test_draft_comment_deleted_version_reviewer(self):
def test_deleted_version_reviewer(self):
user = user_factory(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
@ -5970,7 +6067,7 @@ class TestReviewAddonVersionViewSetList(TestCase):
response = self.client.post(url, data)
assert response.status_code == 404
def test_draft_comment_deleted_version_author(self):
def test_deleted_version_author(self):
user = user_factory(username='author')
AddonUser.objects.create(user=user, addon=self.addon)
self.client.login_api(user)
@ -5990,7 +6087,7 @@ class TestReviewAddonVersionViewSetList(TestCase):
response = self.client.post(url, data)
assert response.status_code == 404
def test_draft_comment_deleted_version_user_but_not_author(self):
def test_deleted_version_user_but_not_author(self):
user = user_factory(username='simpleuser')
self.client.login_api(user)
self.version.delete()
@ -6009,7 +6106,7 @@ class TestReviewAddonVersionViewSetList(TestCase):
response = self.client.post(url, data)
assert response.status_code == 404
def test_draft_comment_unlisted_version_reviewer(self):
def test_unlisted_version_reviewer(self):
user = user_factory(username='reviewer')
self.grant_permission(user, 'Addons:Review')
self.client.login_api(user)
@ -6029,7 +6126,7 @@ class TestReviewAddonVersionViewSetList(TestCase):
response = self.client.post(url, data)
assert response.status_code == 403
def test_draft_comment_unlisted_version_user_but_not_author(self):
def test_unlisted_version_user_but_not_author(self):
user = user_factory(username='simpleuser')
self.client.login_api(user)
self.version.update(channel=amo.RELEASE_CHANNEL_UNLISTED)