From 7754364ac1eebe5bb54f6b0fdd8d3000a9909fad Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 14:42:44 -0300 Subject: [PATCH 01/31] Migration Categories v1 to v2 and Update URLS --- bothub/api/v2/repository/serializers.py | 11 +++++++++++ bothub/api/v2/repository/views.py | 11 +++++++++++ bothub/api/v2/routers.py | 19 ++++++++++++------- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 94232b759..27361a01f 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -328,3 +328,14 @@ class Meta: 'created_at', ] ref_name = None + + +class RepositoryCategorySerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryCategory + fields = [ + 'id', + 'name', + 'icon', + ] + ref_name = None diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index f4379cdc9..f3b36649c 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -9,6 +9,7 @@ from rest_framework.filters import SearchFilter from bothub.common.models import Repository +from bothub.common.models import RepositoryCategory from bothub.common.models import RepositoryVote from bothub.common.models import RepositoryAuthorization @@ -16,6 +17,7 @@ from .serializers import RepositorySerializer from .serializers import RepositoryContributionsSerializer from .serializers import RepositoryVotesSerializer +from .serializers import RepositoryCategorySerializer from .serializers import ShortRepositorySerializer from .permissions import RepositoryPermission from .filters import RepositoriesFilter @@ -164,3 +166,12 @@ def get_queryset(self): ) else: return self.queryset.none() + + +class RepositoryCategoriesViewSet(mixins.ListModelMixin, GenericViewSet): + """ + List all categories. + """ + serializer_class = RepositoryCategorySerializer + queryset = RepositoryCategory.objects.all() + pagination_class = None diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 80147f87f..ca6ce7efa 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -4,6 +4,7 @@ from .repository.views import RepositoryVotesViewSet from .repository.views import RepositoriesViewSet from .repository.views import RepositoriesContributionsViewSet +from .repository.views import RepositoryCategoriesViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -86,13 +87,17 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): router = Router() -router.register('repository', RepositoryViewSet) -router.register('repository-votes', RepositoryVotesViewSet) -router.register('repositories', RepositoriesViewSet) -router.register('repositories-contributions', RepositoriesContributionsViewSet) -router.register('examples', ExamplesViewSet) -router.register('evaluate/results', ResultsListViewSet) -router.register('evaluate', EvaluateViewSet) +router.register('repository/repository', RepositoryViewSet) +router.register('repository/categories', RepositoryCategoriesViewSet) +router.register('repository/repository-votes', RepositoryVotesViewSet) +router.register('repository/repositories', RepositoriesViewSet) +router.register( + 'repository/repositories-contributions', + RepositoriesContributionsViewSet +) +router.register('repository/examples', ExamplesViewSet) +router.register('repository/evaluate/results', ResultsListViewSet) +router.register('repository/evaluate', EvaluateViewSet) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From f736479d3b71ae0293da42b3032cc3db724bec40 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 14:51:31 -0300 Subject: [PATCH 02/31] Added Deprecated Category v1 --- bothub/api/v1/views.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 8f15eb626..d04e46565 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -384,6 +384,12 @@ def get_object(self): # ViewSets +@method_decorator( + name='create', + decorator=swagger_auto_schema( + deprecated=True + ), +) class NewRepositoryViewSet( mixins.CreateModelMixin, GenericViewSet): @@ -952,6 +958,12 @@ class UserProfileViewSet( lookup_field = 'nickname' +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True + ), +) class Categories( mixins.ListModelMixin, GenericViewSet): From e9af8726cbaeadaa1e967280d16518583e4fa629 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 16:14:58 -0300 Subject: [PATCH 03/31] Migration Repository v1 to v2 --- bothub/api/v2/repository/serializers.py | 37 +++++++++++++++++++++++++ bothub/api/v2/repository/views.py | 21 ++++++++++++++ bothub/api/v2/routers.py | 2 ++ 3 files changed, 60 insertions(+) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 27361a01f..2240c1bbc 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -1,3 +1,4 @@ +from django.utils.translation import gettext as _ from rest_framework import serializers from bothub.common.models import Repository @@ -8,6 +9,8 @@ from bothub.common.models import RequestRepositoryAuthorization from bothub.common.languages import LANGUAGE_CHOICES from ..request.serializers import RequestRepositoryAuthorizationSerializer +from ..fields import ModelMultipleChoiceField +from ..fields import TextField class RepositoryCategorySerializer(serializers.ModelSerializer): @@ -339,3 +342,37 @@ class Meta: 'icon', ] ref_name = None + + +class NewRepositorySerializer(serializers.ModelSerializer): + class Meta: + model = Repository + fields = [ + 'uuid', + 'owner', + 'name', + 'slug', + 'language', + 'algorithm', + 'use_competing_intents', + 'use_name_entities', + 'categories', + 'description', + 'is_private', + ] + ref_name = None + + uuid = serializers.ReadOnlyField( + style={'show': False}) + owner = serializers.HiddenField(default=serializers.CurrentUserDefault()) + language = serializers.ChoiceField( + LANGUAGE_CHOICES, + label=_('Language')) + categories = ModelMultipleChoiceField( + child_relation=serializers.PrimaryKeyRelatedField( + queryset=RepositoryCategory.objects.all()), + allow_empty=False, + help_text=Repository.CATEGORIES_HELP_TEXT) + description = TextField( + allow_blank=True, + help_text=Repository.DESCRIPTION_HELP_TEXT) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index f3b36649c..d45f132bb 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -5,6 +5,7 @@ from rest_framework.viewsets import GenericViewSet from rest_framework import mixins, status from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.permissions import IsAuthenticated from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter @@ -19,6 +20,7 @@ from .serializers import RepositoryVotesSerializer from .serializers import RepositoryCategorySerializer from .serializers import ShortRepositorySerializer +from .serializers import NewRepositorySerializer from .permissions import RepositoryPermission from .filters import RepositoriesFilter @@ -175,3 +177,22 @@ class RepositoryCategoriesViewSet(mixins.ListModelMixin, GenericViewSet): serializer_class = RepositoryCategorySerializer queryset = RepositoryCategory.objects.all() pagination_class = None + + +class NewRepositoryViewSet(mixins.CreateModelMixin, GenericViewSet): + """ + Create a new Repository, add examples and train a bot. + """ + queryset = Repository.objects + serializer_class = NewRepositorySerializer + permission_classes = [IsAuthenticated] + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + headers = self.get_success_headers(serializer.data) + return Response( + RepositorySerializer(instance).data, + status=status.HTTP_201_CREATED, + headers=headers) diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index ca6ce7efa..1d7c3f5aa 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -5,6 +5,7 @@ from .repository.views import RepositoriesViewSet from .repository.views import RepositoriesContributionsViewSet from .repository.views import RepositoryCategoriesViewSet +from .repository.views import NewRepositoryViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -96,6 +97,7 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): RepositoriesContributionsViewSet ) router.register('repository/examples', ExamplesViewSet) +router.register('repository/new', NewRepositoryViewSet) router.register('repository/evaluate/results', ResultsListViewSet) router.register('repository/evaluate', EvaluateViewSet) router.register('account/login', LoginViewSet) From 71474af13080555f24c678be5f0f64c81b1dabda Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 16:21:40 -0300 Subject: [PATCH 04/31] Migration Translations v1 to v2 and refact --- bothub/api/v1/views.py | 24 +++++++++++++++ bothub/api/v2/repository/permissions.py | 9 ++++++ bothub/api/v2/repository/serializers.py | 39 +++++++++++++++++++++++++ bothub/api/v2/repository/views.py | 31 ++++++++++++++++++++ bothub/api/v2/routers.py | 2 ++ 5 files changed, 105 insertions(+) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index d04e46565..13ca41004 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -699,6 +699,30 @@ class NewRepositoryTranslatedExampleViewSet( permission_classes = [permissions.IsAuthenticated] +@method_decorator( + name='retrieve', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='update', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='partial_update', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='destroy', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RepositoryTranslatedExampleViewSet( mixins.RetrieveModelMixin, mixins.UpdateModelMixin, diff --git a/bothub/api/v2/repository/permissions.py b/bothub/api/v2/repository/permissions.py index 9b3b006f0..55b8e7d18 100644 --- a/bothub/api/v2/repository/permissions.py +++ b/bothub/api/v2/repository/permissions.py @@ -18,3 +18,12 @@ def has_object_permission(self, request, view, obj): return authorization.can_write return authorization.is_admin return False + + +class RepositoryTranslatedExamplePermission(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + repository = obj.original_example.repository_update.repository + authorization = repository.get_user_authorization(request.user) + if request.method in READ_METHODS: + return authorization.can_read + return authorization.can_contribute diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 2240c1bbc..14dda59b5 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -1,7 +1,12 @@ from django.utils.translation import gettext as _ from rest_framework import serializers +from bothub.api.v1.serializers import \ + RepositoryTranslatedExampleEntitySeralizer +from bothub.api.v1.validators import CanContributeInRepositoryExampleValidator from bothub.common.models import Repository +from bothub.common.models import RepositoryTranslatedExample +from bothub.common.models import RepositoryExample from bothub.common.models import RepositoryVote from bothub.common.models import RepositoryCategory from bothub.common.models import RepositoryEntityLabel @@ -376,3 +381,37 @@ class Meta: description = TextField( allow_blank=True, help_text=Repository.DESCRIPTION_HELP_TEXT) + + +class RepositoryTranslatedExampleSerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryTranslatedExample + fields = [ + 'id', + 'original_example', + 'from_language', + 'language', + 'text', + 'has_valid_entities', + 'entities', + 'created_at', + ] + ref_name = None + + original_example = serializers.PrimaryKeyRelatedField( + queryset=RepositoryExample.objects, + validators=[ + CanContributeInRepositoryExampleValidator(), + ], + help_text=_('Example\'s ID')) + from_language = serializers.SerializerMethodField() + has_valid_entities = serializers.SerializerMethodField() + entities = RepositoryTranslatedExampleEntitySeralizer( + many=True, + read_only=True) + + def get_from_language(self, obj): + return obj.original_example.repository_update.language + + def get_has_valid_entities(self, obj): + return obj.has_valid_entities diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index d45f132bb..23658e7bf 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -13,6 +13,7 @@ from bothub.common.models import RepositoryCategory from bothub.common.models import RepositoryVote from bothub.common.models import RepositoryAuthorization +from bothub.common.models import RepositoryTranslatedExample from ..metadata import Metadata from .serializers import RepositorySerializer @@ -21,7 +22,9 @@ from .serializers import RepositoryCategorySerializer from .serializers import ShortRepositorySerializer from .serializers import NewRepositorySerializer +from .serializers import RepositoryTranslatedExampleSerializer from .permissions import RepositoryPermission +from .permissions import RepositoryTranslatedExamplePermission from .filters import RepositoriesFilter @@ -196,3 +199,31 @@ def create(self, request, *args, **kwargs): RepositorySerializer(instance).data, status=status.HTTP_201_CREATED, headers=headers) + + +class RepositoryTranslatedExampleViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericViewSet): + """ + Manager example translation. + + retrieve: + Get example translation data. + + update: + Update example translation. + + partial_update: + Update, partially, example translation. + + delete: + Delete example translation. + """ + queryset = RepositoryTranslatedExample.objects + serializer_class = RepositoryTranslatedExampleSerializer + permission_classes = [ + IsAuthenticated, + RepositoryTranslatedExamplePermission, + ] diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 1d7c3f5aa..d49d47ccc 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -6,6 +6,7 @@ from .repository.views import RepositoriesContributionsViewSet from .repository.views import RepositoryCategoriesViewSet from .repository.views import NewRepositoryViewSet +from .repository.views import RepositoryTranslatedExampleViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -100,6 +101,7 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): router.register('repository/new', NewRepositoryViewSet) router.register('repository/evaluate/results', ResultsListViewSet) router.register('repository/evaluate', EvaluateViewSet) +router.register('repository/translation', RepositoryTranslatedExampleViewSet) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From 959ebee4c76e6b5b2c9e8a0dbe4f2c5fff47c25a Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 16:28:34 -0300 Subject: [PATCH 05/31] Added tests Repository v2 --- bothub/api/v2/tests/test_repository.py | 278 +++++++++++++++++++++++++ 1 file changed, 278 insertions(+) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index ec41e2343..131c9af1e 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -20,6 +20,9 @@ from bothub.api.v2.repository.views import RepositoriesContributionsViewSet from bothub.api.v2.repository.views import RepositoriesViewSet from bothub.api.v2.repository.views import RepositoryVotesViewSet +from bothub.api.v2.repository.views import RepositoryCategoriesViewSet +from bothub.api.v2.repository.views import NewRepositoryViewSet +from bothub.api.v2.repository.views import RepositoryTranslatedExampleViewSet from bothub.api.v2.repository.serializers import RepositorySerializer @@ -856,3 +859,278 @@ def test_okay(self): len(content_data['results']), 1 ) + + +class ListRepositoryCategoriesTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.category = RepositoryCategory.objects.create(name='Category 1') + self.business_category = RepositoryCategory.objects.create( + name='Business', + icon='business') + + def request(self): + request = self.factory.get('/v2/repository/categories/') + response = RepositoryCategoriesViewSet.as_view( + {'get': 'list'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_default_category_icon(self): + response, content_data = self.request() + self.assertEqual( + content_data[0].get('id'), + self.category.id) + self.assertEqual( + content_data[0].get('icon'), + 'botinho') + + def test_custom_category_icon(self): + response, content_data = self.request() + self.assertEqual( + content_data[1].get('id'), + self.business_category.id) + self.assertEqual( + content_data[1].get('icon'), + self.business_category.icon) + + +class NewRepositoryTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.user, self.token = create_user_and_token() + self.authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(self.token.key), + } + + self.category = RepositoryCategory.objects.create( + name='ID') + + def request(self, data): + request = self.factory.post( + '/v2/repository/new/', + data, + **self.authorization_header) + response = NewRepositoryViewSet.as_view({'post': 'create'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request({ + 'name': 'Testing', + 'slug': 'test', + 'language': languages.LANGUAGE_EN, + 'categories': [self.category.id], + 'description': '', + }) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + + def test_fields_required(self): + def request_and_check(field, data): + response, content_data = self.request(data) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn(field, content_data.keys()) + + request_and_check('name', { + 'slug': 'test', + 'language': languages.LANGUAGE_EN, + 'categories': [self.category.id], + }) + + request_and_check('slug', { + 'name': 'Testing', + 'language': languages.LANGUAGE_EN, + 'categories': [self.category.id], + }) + + request_and_check('language', { + 'name': 'Testing', + 'slug': 'test', + 'categories': [self.category.id], + }) + + request_and_check('categories', { + 'name': 'Testing', + 'slug': 'test', + 'language': languages.LANGUAGE_EN, + }) + + def test_invalid_slug(self): + response, content_data = self.request({ + 'name': 'Testing', + 'slug': 'invalid slug', + 'language': languages.LANGUAGE_EN, + 'categories': [self.category.id], + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn('slug', content_data.keys()) + + def test_invalid_language(self): + response, content_data = self.request({ + 'name': 'Testing', + 'slug': 'test', + 'language': 'jj', + 'categories': [self.category.id], + 'description': '', + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn('language', content_data.keys()) + + def test_unique_slug(self): + same_slug = 'test' + Repository.objects.create( + owner=self.user, + name='Testing', + slug=same_slug, + language=languages.LANGUAGE_EN) + response, content_data = self.request({ + 'name': 'Testing', + 'slug': same_slug, + 'language': languages.LANGUAGE_EN, + 'categories': [self.category.id], + 'description': '', + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn('non_field_errors', content_data.keys()) + + +class RepositoryTranslatedExampleRetrieveTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + self.example = RepositoryExample.objects.create( + repository_update=self.repository.current_update(), + text='hi') + self.translated = RepositoryTranslatedExample.objects.create( + original_example=self.example, + language=languages.LANGUAGE_PT, + text='oi') + + self.private_repository = Repository.objects.create( + owner=self.owner, + name='Private', + slug='private', + language=languages.LANGUAGE_EN, + is_private=True) + self.private_example = RepositoryExample.objects.create( + repository_update=self.private_repository.current_update(), + text='hi') + self.private_translated = RepositoryTranslatedExample.objects.create( + original_example=self.private_example, + language=languages.LANGUAGE_PT, + text='oi') + + def request(self, translated, token): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.get( + '/v2/repository/translation/{}/'.format(translated.id), + **authorization_header) + response = RepositoryTranslatedExampleViewSet.as_view( + {'get': 'retrieve'})(request, pk=translated.id) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request( + self.translated, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('id'), + self.translated.id) + + def test_private_okay(self): + response, content_data = self.request( + self.private_translated, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('id'), + self.private_translated.id) + + def test_forbidden(self): + user, user_token = create_user_and_token() + + response, content_data = self.request( + self.private_translated, + user_token) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + +class RepositoryTranslatedExampleDestroyTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + self.example = RepositoryExample.objects.create( + repository_update=self.repository.current_update(), + text='hi') + self.translated = RepositoryTranslatedExample.objects.create( + original_example=self.example, + language=languages.LANGUAGE_PT, + text='oi') + + def request(self, translated, token): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.delete( + '/v2/repository/translation/{}/'.format(translated.id), + **authorization_header) + response = RepositoryTranslatedExampleViewSet.as_view( + {'delete': 'destroy'})(request, pk=translated.id) + return response + + def test_okay(self): + response = self.request( + self.translated, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_204_NO_CONTENT) + + def test_forbidden(self): + user, user_token = create_user_and_token() + + response = self.request( + self.translated, + user_token) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) From 3f1eadbcecdae54038077c2a910bc394e5ae7144 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 16:31:59 -0300 Subject: [PATCH 06/31] Update URLS Tests --- bothub/api/v2/tests/test_evaluate.py | 12 ++++++------ bothub/api/v2/tests/test_examples.py | 2 +- bothub/api/v2/tests/test_repository.py | 22 +++++++++++----------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bothub/api/v2/tests/test_evaluate.py b/bothub/api/v2/tests/test_evaluate.py index 771af98ee..f6f8a3bdf 100644 --- a/bothub/api/v2/tests/test_evaluate.py +++ b/bothub/api/v2/tests/test_evaluate.py @@ -54,7 +54,7 @@ def request(self, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), } request = self.factory.get( - '/v2/evaluate/?repository_uuid={}'.format( + '/v2/repository/evaluate/?repository_uuid={}'.format( self.repository.uuid ), **authorization_header ) @@ -107,7 +107,7 @@ def request(self, data, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), } request = self.factory.post( - '/v2/evaluate/?repository_uuid={}'.format( + '/v2/repository/evaluate/?repository_uuid={}'.format( self.repository.uuid ), json.dumps(data), @@ -218,7 +218,7 @@ def request(self, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), } request = self.factory.delete( - '/v2/evaluate/{}/?repository_uuid={}'.format( + '/v2/repository/evaluate/{}/?repository_uuid={}'.format( self.repository_evaluate.id, self.repository.uuid ), **authorization_header @@ -293,7 +293,7 @@ def request(self, data, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), } request = self.factory.patch( - '/v2/evaluate/{}/?repository_uuid={}'.format( + '/v2/repository/evaluate/{}/?repository_uuid={}'.format( self.repository_evaluate.id, self.repository.uuid ), @@ -501,7 +501,7 @@ def request(self, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), } request = self.factory.get( - '/v2/evaluate/results/?repository_uuid={}'.format( + '/v2/repository/evaluate/results/?repository_uuid={}'.format( self.repository.uuid ), **authorization_header ) @@ -680,7 +680,7 @@ def request(self, token, params): 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), } request = self.factory.get( - '/v2/evaluate/results/{}/{}'.format( + '/v2/repository/evaluate/results/{}/{}'.format( self.evaluate_result.id, params ), **authorization_header diff --git a/bothub/api/v2/tests/test_examples.py b/bothub/api/v2/tests/test_examples.py index c7777a50f..0d3ebaa1a 100644 --- a/bothub/api/v2/tests/test_examples.py +++ b/bothub/api/v2/tests/test_examples.py @@ -74,7 +74,7 @@ def request(self, data={}, token=None): } if token else {} request = self.factory.get( - '/v2/examples/', + '/v2/repository/examples/', data, **authorization_header) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index 131c9af1e..3eca7058e 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -119,7 +119,7 @@ def request(self, data, token=None): } if token else {} request = self.factory.post( - '/v2/repository/', + '/v2/repository/repository/', data, **authorization_header) @@ -183,7 +183,7 @@ def request(self, repository, token=None): } if token else {} request = self.factory.get( - '/v2/repository/{}/'.format(repository.uuid), + '/v2/repository/repository/{}/'.format(repository.uuid), **authorization_header) response = RepositoryViewSet.as_view({'get': 'retrieve'})( @@ -228,7 +228,7 @@ def request(self, repository, data={}, token=None): } if token else {} request = self.factory.patch( - '/v2/repository/{}/'.format(repository.uuid), + '/v2/repository/repository/{}/'.format(repository.uuid), self.factory._encode_data(data, MULTIPART_CONTENT), MULTIPART_CONTENT, **authorization_header) @@ -287,7 +287,7 @@ def request(self, repository, token=None): } if token else {} request = self.factory.get( - '/v2/repository/{}/'.format(repository.uuid), + '/v2/repository/repository/{}/'.format(repository.uuid), **authorization_header) response = RepositoryViewSet.as_view({'get': 'retrieve'})( @@ -336,7 +336,7 @@ def request(self, repository, token=None): } if token else {} request = self.factory.get( - '/v2/repository/{}/'.format(repository.uuid), + '/v2/repository/repository/{}/'.format(repository.uuid), **authorization_header) response = RepositoryViewSet.as_view({'get': 'retrieve'})( @@ -430,7 +430,7 @@ def request(self, data={}, token=None): 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), } if token else {} request = self.factory.get( - '/v2/repositories/', + '/v2/repository/repositories/', data, **authorization_header, ) @@ -510,7 +510,7 @@ def request(self, data={}, token=None): 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), } if token else {} request = self.factory.get( - '/v2/repositories/', + '/v2/repository/repositories/', data, **authorization_header, ) @@ -633,7 +633,7 @@ def request(self, param, value, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token), } request = self.factory.get( - '/v2/repository-votes/?{}={}'.format( + '/v2/repository/repository-votes/?{}={}'.format( param, value ), **authorization_header @@ -712,7 +712,7 @@ def request(self, data, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token), } request = self.factory.post( - '/v2/repository-votes/', + '/v2/repository/repository-votes/', json.dumps(data), content_type='application/json', **authorization_header @@ -776,7 +776,7 @@ def request(self, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token), } request = self.factory.delete( - '/v2/repository-votes/{}/'.format(str(self.repository.uuid)), + '/v2/repository/repository-votes/{}/'.format(str(self.repository.uuid)), **authorization_header ) response = RepositoryVotesViewSet.as_view({'delete': 'destroy'})( @@ -833,7 +833,7 @@ def setUp(self): def request(self): request = self.factory.get( - '/v2/repositories-contributions/?nickname={}'.format( + '/v2/repository/repositories-contributions/?nickname={}'.format( self.user.nickname ) ) From 5c7b67dc52126686019c5c37abd44bff19181445 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 16:32:53 -0300 Subject: [PATCH 07/31] PEP8 --- bothub/api/v2/tests/test_repository.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index 3eca7058e..35d70fffe 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -776,8 +776,9 @@ def request(self, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token), } request = self.factory.delete( - '/v2/repository/repository-votes/{}/'.format(str(self.repository.uuid)), - **authorization_header + '/v2/repository/repository-votes/{}/'.format( + str(self.repository.uuid) + ), **authorization_header ) response = RepositoryVotesViewSet.as_view({'delete': 'destroy'})( request, From 05dc971dcd60a3d2f4c7d26fc171e276df6b9f1b Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 16:42:34 -0300 Subject: [PATCH 08/31] Migration Repository Example v1 to v2 and refact --- bothub/api/v1/views.py | 18 +++++++++++++ bothub/api/v2/repository/permissions.py | 9 +++++++ bothub/api/v2/repository/serializers.py | 33 +++++++++++++++++++++++ bothub/api/v2/repository/views.py | 36 ++++++++++++++++++++++++- bothub/api/v2/routers.py | 2 ++ 5 files changed, 97 insertions(+), 1 deletion(-) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 13ca41004..af70b01ba 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -658,6 +658,24 @@ class NewRepositoryExampleViewSet( deprecated=True, ) ) +@method_decorator( + name='update', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='partial_update', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='destroy', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RepositoryExampleViewSet( mixins.RetrieveModelMixin, mixins.DestroyModelMixin, diff --git a/bothub/api/v2/repository/permissions.py b/bothub/api/v2/repository/permissions.py index 55b8e7d18..ed6ddadca 100644 --- a/bothub/api/v2/repository/permissions.py +++ b/bothub/api/v2/repository/permissions.py @@ -27,3 +27,12 @@ def has_object_permission(self, request, view, obj): if request.method in READ_METHODS: return authorization.can_read return authorization.can_contribute + + +class RepositoryExamplePermission(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + authorization = obj.repository_update.repository \ + .get_user_authorization(request.user) + if request.method in READ_METHODS: + return authorization.can_read + return authorization.can_contribute diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 14dda59b5..cc20d26b4 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -4,6 +4,7 @@ from bothub.api.v1.serializers import \ RepositoryTranslatedExampleEntitySeralizer from bothub.api.v1.validators import CanContributeInRepositoryExampleValidator +from bothub.api.v2.example.serializers import RepositoryExampleEntitySerializer from bothub.common.models import Repository from bothub.common.models import RepositoryTranslatedExample from bothub.common.models import RepositoryExample @@ -415,3 +416,35 @@ def get_from_language(self, obj): def get_has_valid_entities(self, obj): return obj.has_valid_entities + + +class RepositoryExampleSerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryExample + fields = [ + 'id', + 'repository_update', + 'deleted_in', + 'text', + 'intent', + 'language', + 'created_at', + 'entities', + 'translations', + ] + read_only_fields = [ + 'repository_update', + 'deleted_in', + ] + ref_name = None + + entities = RepositoryExampleEntitySerializer( + many=True, + read_only=True) + translations = RepositoryTranslatedExampleSerializer( + many=True, + read_only=True) + language = serializers.SerializerMethodField() + + def get_language(self, obj): + return obj.language diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 23658e7bf..232371a4d 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -1,12 +1,14 @@ from django.utils.decorators import method_decorator +from django.utils.translation import gettext as _ +from django_filters.rest_framework import DjangoFilterBackend from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema +from rest_framework.exceptions import APIException from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from rest_framework import mixins, status from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.permissions import IsAuthenticated -from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter from bothub.common.models import Repository @@ -14,6 +16,7 @@ from bothub.common.models import RepositoryVote from bothub.common.models import RepositoryAuthorization from bothub.common.models import RepositoryTranslatedExample +from bothub.common.models import RepositoryExample from ..metadata import Metadata from .serializers import RepositorySerializer @@ -23,8 +26,10 @@ from .serializers import ShortRepositorySerializer from .serializers import NewRepositorySerializer from .serializers import RepositoryTranslatedExampleSerializer +from .serializers import RepositoryExampleSerializer from .permissions import RepositoryPermission from .permissions import RepositoryTranslatedExamplePermission +from .permissions import RepositoryExamplePermission from .filters import RepositoriesFilter @@ -227,3 +232,32 @@ class RepositoryTranslatedExampleViewSet( IsAuthenticated, RepositoryTranslatedExamplePermission, ] + + +class RepositoryExampleViewSet( + mixins.DestroyModelMixin, + mixins.UpdateModelMixin, + GenericViewSet): + """ + Manager repository example. + + retrieve: + Get repository example data. + + delete: + Delete repository example. + + update: + Update repository example. + + """ + queryset = RepositoryExample.objects + serializer_class = RepositoryExampleSerializer + permission_classes = [ + RepositoryExamplePermission, + ] + + def perform_destroy(self, obj): + if obj.deleted_in: + raise APIException(_('Example already deleted')) + obj.delete() diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index d49d47ccc..4895dceae 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -7,6 +7,7 @@ from .repository.views import RepositoryCategoriesViewSet from .repository.views import NewRepositoryViewSet from .repository.views import RepositoryTranslatedExampleViewSet +from .repository.views import RepositoryExampleViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -102,6 +103,7 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): router.register('repository/evaluate/results', ResultsListViewSet) router.register('repository/evaluate', EvaluateViewSet) router.register('repository/translation', RepositoryTranslatedExampleViewSet) +router.register('repository/example', RepositoryExampleViewSet) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From f726ea5192713bc481e2685a4e68d78cb7c741ba Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 16:45:04 -0300 Subject: [PATCH 09/31] Migration Search Repositories v1 to v2 --- bothub/api/v2/repository/views.py | 25 +++++++++++++++++++++++++ bothub/api/v2/routers.py | 2 ++ 2 files changed, 27 insertions(+) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 232371a4d..38deb2686 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -261,3 +261,28 @@ def perform_destroy(self, obj): if obj.deleted_in: raise APIException(_('Example already deleted')) obj.delete() + + +class SearchRepositoriesViewSet( + mixins.ListModelMixin, + GenericViewSet): + """ + List all user's repositories + """ + queryset = Repository.objects + serializer_class = RepositorySerializer + lookup_field = 'nickname' + + def get_queryset(self, *args, **kwargs): + try: + if self.request.query_params.get('nickname', None): + return self.queryset.filter( + owner__nickname=self.request.query_params.get( + 'nickname', + self.request.user + ) + ) + else: + return self.queryset.filter(owner=self.request.user) + except TypeError: + return self.queryset.none() diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 4895dceae..0644027dc 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -8,6 +8,7 @@ from .repository.views import NewRepositoryViewSet from .repository.views import RepositoryTranslatedExampleViewSet from .repository.views import RepositoryExampleViewSet +from .repository.views import SearchRepositoriesViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -104,6 +105,7 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): router.register('repository/evaluate', EvaluateViewSet) router.register('repository/translation', RepositoryTranslatedExampleViewSet) router.register('repository/example', RepositoryExampleViewSet) +router.register('repository/search-repositories', SearchRepositoriesViewSet) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From a51f6dffd167540c108cc5d29fa4667d435e2028 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 16:46:32 -0300 Subject: [PATCH 10/31] Migration Search Repositories v1 to v2 --- bothub/api/v1/views.py | 3 ++- bothub/api/v2/repository/views.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index af70b01ba..1c2a9b066 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -421,7 +421,8 @@ def create(self, request, *args, **kwargs): description='Nickname User to find repositories', type=openapi.TYPE_STRING ), - ] + ], + deprecated=True ) ) class SearchRepositoriesViewSet( diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 38deb2686..db16036ad 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -263,6 +263,19 @@ def perform_destroy(self, obj): obj.delete() +@method_decorator( + name='list', + decorator=swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + 'nickname', + openapi.IN_QUERY, + description='Nickname User to find repositories', + type=openapi.TYPE_STRING + ), + ] + ) +) class SearchRepositoriesViewSet( mixins.ListModelMixin, GenericViewSet): From 00c43a7fa30b19421ca04f261696de90b2f6e06b Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 16:59:25 -0300 Subject: [PATCH 11/31] Migration Repository Translate Example v1 to v2 and refact --- bothub/api/v2/repository/serializers.py | 102 +++++++++++++++++++++- bothub/api/v2/repository/validators.py | 109 ++++++++++++++++++++++++ bothub/api/v2/repository/views.py | 12 +++ bothub/api/v2/routers.py | 5 ++ 4 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 bothub/api/v2/repository/validators.py diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index cc20d26b4..e45a537a7 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -1,11 +1,16 @@ from django.utils.translation import gettext as _ from rest_framework import serializers -from bothub.api.v1.serializers import \ - RepositoryTranslatedExampleEntitySeralizer -from bothub.api.v1.validators import CanContributeInRepositoryExampleValidator from bothub.api.v2.example.serializers import RepositoryExampleEntitySerializer -from bothub.common.models import Repository +from bothub.api.v2.repository.validators import \ + TranslatedExampleEntitiesValidator +from bothub.api.v2.repository.validators import \ + CanContributeInRepositoryTranslatedExampleValidator +from bothub.api.v2.repository.validators import \ + TranslatedExampleLanguageValidator +from bothub.api.v2.repository.validators import \ + CanContributeInRepositoryExampleValidator +from bothub.common.models import Repository, RepositoryTranslatedExampleEntity from bothub.common.models import RepositoryTranslatedExample from bothub.common.models import RepositoryExample from bothub.common.models import RepositoryVote @@ -17,6 +22,7 @@ from ..request.serializers import RequestRepositoryAuthorizationSerializer from ..fields import ModelMultipleChoiceField from ..fields import TextField +from ..fields import EntityValueField class RepositoryCategorySerializer(serializers.ModelSerializer): @@ -384,6 +390,35 @@ class Meta: help_text=Repository.DESCRIPTION_HELP_TEXT) +class RepositoryTranslatedExampleEntitySeralizer(serializers.ModelSerializer): + class Meta: + model = RepositoryTranslatedExampleEntity + fields = [ + 'id', + 'repository_translated_example', + 'start', + 'end', + 'entity', + 'created_at', + 'value', + ] + + repository_translated_example = serializers.PrimaryKeyRelatedField( + queryset=RepositoryTranslatedExample.objects, + validators=[ + CanContributeInRepositoryTranslatedExampleValidator(), + ], + help_text='Example translation ID') + entity = serializers.SerializerMethodField() + value = serializers.SerializerMethodField() + + def get_entity(self, obj): + return obj.entity.value + + def get_value(self, obj): + return obj.value + + class RepositoryTranslatedExampleSerializer(serializers.ModelSerializer): class Meta: model = RepositoryTranslatedExample @@ -448,3 +483,62 @@ class Meta: def get_language(self, obj): return obj.language + + +class NewRepositoryTranslatedExampleEntitySeralizer( + serializers.ModelSerializer): + class Meta: + model = RepositoryTranslatedExampleEntity + fields = [ + 'start', + 'end', + 'entity', + ] + + entity = EntityValueField() + + +class NewRepositoryTranslatedExampleSerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryTranslatedExample + fields = [ + 'id', + 'original_example', + 'language', + 'text', + 'has_valid_entities', + 'entities', + ] + ref_name = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.validators.append(TranslatedExampleEntitiesValidator()) + self.validators.append(TranslatedExampleLanguageValidator()) + + original_example = serializers.PrimaryKeyRelatedField( + queryset=RepositoryExample.objects, + validators=[ + CanContributeInRepositoryExampleValidator(), + ], + help_text=_('Example\'s ID')) + language = serializers.ChoiceField( + LANGUAGE_CHOICES, + label=_('Language')) + has_valid_entities = serializers.SerializerMethodField() + entities = NewRepositoryTranslatedExampleEntitySeralizer( + many=True, + style={'text_field': 'text'}) + + def get_has_valid_entities(self, obj): + return obj.has_valid_entities + + def create(self, validated_data): + entities_data = validated_data.pop('entities') + + translated = self.Meta.model.objects.create(**validated_data) + for entity_data in entities_data: + RepositoryTranslatedExampleEntity.objects.create( + repository_translated_example=translated, + **entity_data) + return translated diff --git a/bothub/api/v2/repository/validators.py b/bothub/api/v2/repository/validators.py new file mode 100644 index 000000000..6b96c3b8e --- /dev/null +++ b/bothub/api/v2/repository/validators.py @@ -0,0 +1,109 @@ +from django.utils.translation import gettext as _ +from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import ValidationError + +from bothub.common.models import RepositoryTranslatedExample +from bothub.common.models import RepositoryExample + + +class CanContributeInRepositoryValidator(object): + def __call__(self, value): + user_authorization = value.get_user_authorization( + self.request.user) + if not user_authorization.can_contribute: + raise PermissionDenied( + _('You can\'t contribute in this repository')) + + def set_context(self, serializer): + self.request = serializer.context.get('request') + + +class CanContributeInRepositoryExampleValidator(object): + def __call__(self, value): + repository = value.repository_update.repository + user_authorization = repository.get_user_authorization( + self.request.user) + if not user_authorization.can_contribute: + raise PermissionDenied( + _('You can\'t contribute in this repository')) + + def set_context(self, serializer): + self.request = serializer.context.get('request') + + +class CanContributeInRepositoryTranslatedExampleValidator(object): + def __call__(self, value): + repository = value.original_example.repository_update.repository + user_authorization = repository.get_user_authorization( + self.request.user) + if not user_authorization.can_contribute: + raise PermissionDenied( + _('You can\'t contribute in this repository')) + + def set_context(self, serializer): + self.request = serializer.context.get('request') + + +class TranslatedExampleEntitiesValidator(object): + def __call__(self, attrs): + original_example = attrs.get('original_example') + entities_list = list(map(lambda x: dict(x), attrs.get('entities'))) + original_entities_list = list(map( + lambda x: x.to_dict, + original_example.entities.all())) + entities_valid = RepositoryTranslatedExample.same_entities_validator( + entities_list, + original_entities_list) + if not entities_valid: + raise ValidationError({'entities': _( + 'Entities need to match from the original content. ' + + 'Entities: {0}. Original entities: {1}.').format( + RepositoryTranslatedExample.count_entities( + entities_list, + to_str=True), + RepositoryTranslatedExample.count_entities( + original_entities_list, + to_str=True), + )}) + + +class TranslatedExampleLanguageValidator(object): + def __call__(self, attrs): + original_example = attrs.get('original_example') + language = attrs.get('language') + if original_example.repository_update.language == language: + raise ValidationError({'language': _( + 'Can\'t translate to the same language')}) + + +class ExampleWithIntentOrEntityValidator(object): + def __call__(self, attrs): + intent = attrs.get('intent') + entities = attrs.get('entities') + + if not intent and not entities: + raise ValidationError(_('Define a intent or one entity')) + + +class IntentAndSentenceNotExistsValidator(object): + def __call__(self, attrs): + repository = attrs.get('repository') + intent = attrs.get('intent') + sentence = attrs.get('text') + + if RepositoryExample.objects.filter( + text=sentence, + intent=intent, + repository_update__repository=repository + ).count(): + raise ValidationError(_('Intention and Sentence already exists')) + + +class EntityNotEqualLabelValidator(object): + def __call__(self, attrs): + entity = attrs.get('entity') + label = attrs.get('label') + + if entity == label: + raise ValidationError({'label': _( + 'Label name can\'t be equal to entity name')}) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index db16036ad..72a462ffd 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -27,6 +27,7 @@ from .serializers import NewRepositorySerializer from .serializers import RepositoryTranslatedExampleSerializer from .serializers import RepositoryExampleSerializer +from .serializers import NewRepositoryTranslatedExampleSerializer from .permissions import RepositoryPermission from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission @@ -299,3 +300,14 @@ def get_queryset(self, *args, **kwargs): return self.queryset.filter(owner=self.request.user) except TypeError: return self.queryset.none() + + +class NewRepositoryTranslatedExampleViewSet( + mixins.CreateModelMixin, + GenericViewSet): + """ + Translate example + """ + queryset = RepositoryTranslatedExample.objects + serializer_class = NewRepositoryTranslatedExampleSerializer + permission_classes = [IsAuthenticated] diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 0644027dc..418685354 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -9,6 +9,7 @@ from .repository.views import RepositoryTranslatedExampleViewSet from .repository.views import RepositoryExampleViewSet from .repository.views import SearchRepositoriesViewSet +from .repository.views import NewRepositoryTranslatedExampleViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -106,6 +107,10 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): router.register('repository/translation', RepositoryTranslatedExampleViewSet) router.register('repository/example', RepositoryExampleViewSet) router.register('repository/search-repositories', SearchRepositoriesViewSet) +router.register( + 'repository/translate-example', + NewRepositoryTranslatedExampleViewSet +) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From 7925ef6565ea8227e157e56e86311e34cb0399b4 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 17:05:40 -0300 Subject: [PATCH 12/31] Migration Repository Translations v1 to v2 and refact --- bothub/api/v1/views.py | 12 +++++++ bothub/api/v2/repository/filters.py | 46 +++++++++++++++++++++++++ bothub/api/v2/repository/serializers.py | 2 ++ bothub/api/v2/repository/views.py | 12 +++++++ bothub/api/v2/routers.py | 2 ++ 5 files changed, 74 insertions(+) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 1c2a9b066..01fcb7a3d 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -707,6 +707,12 @@ def perform_destroy(self, obj): obj.delete() +@method_decorator( + name='create', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class NewRepositoryTranslatedExampleViewSet( mixins.CreateModelMixin, GenericViewSet): @@ -1044,6 +1050,12 @@ class RepositoriesViewSet( ] +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class TranslationsViewSet( mixins.ListModelMixin, GenericViewSet): diff --git a/bothub/api/v2/repository/filters.py b/bothub/api/v2/repository/filters.py index 99d8651b0..ac7a72326 100644 --- a/bothub/api/v2/repository/filters.py +++ b/bothub/api/v2/repository/filters.py @@ -1,7 +1,11 @@ from django_filters import rest_framework as filters from django.utils.translation import gettext as _ +from django.core.exceptions import ValidationError as DjangoValidationError +from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import NotFound from bothub.common.models import Repository +from bothub.common.models import RepositoryTranslatedExample class RepositoriesFilter(filters.FilterSet): @@ -19,3 +23,45 @@ class Meta: def filter_language(self, queryset, name, value): return queryset.supported_language(value) + + +class RepositoryTranslationsFilter(filters.FilterSet): + class Meta: + model = RepositoryTranslatedExample + fields = [] + + repository_uuid = filters.CharFilter( + field_name='repository_uuid', + method='filter_repository_uuid', + required=True, + help_text=_('Repository\'s UUID')) + from_language = filters.CharFilter( + field_name='language', + method='filter_from_language', + help_text='Filter by original language') + to_language = filters.CharFilter( + field_name='language', + method='filter_to_language', + help_text='Filter by translated language') + + def filter_repository_uuid(self, queryset, name, value): + request = self.request + try: + repository = Repository.objects.get(uuid=value) + authorization = repository.get_user_authorization(request.user) + if not authorization.can_read: + raise PermissionDenied() + return RepositoryTranslatedExample.objects.filter( + original_example__repository_update__repository=repository) + except Repository.DoesNotExist: + raise NotFound( + _('Repository {} does not exist').format(value)) + except DjangoValidationError: + raise NotFound(_('Invalid repository_uuid')) + + def filter_from_language(self, queryset, name, value): + return queryset.filter( + original_example__repository_update__language=value) + + def filter_to_language(self, queryset, name, value): + return queryset.filter(language=value) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index e45a537a7..5ad0715cf 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -402,6 +402,7 @@ class Meta: 'created_at', 'value', ] + ref_name = None repository_translated_example = serializers.PrimaryKeyRelatedField( queryset=RepositoryTranslatedExample.objects, @@ -494,6 +495,7 @@ class Meta: 'end', 'entity', ] + ref_name = None entity = EntityValueField() diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 72a462ffd..3c68cb8b2 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -32,6 +32,7 @@ from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission from .filters import RepositoriesFilter +from .filters import RepositoryTranslationsFilter class RepositoryViewSet( @@ -311,3 +312,14 @@ class NewRepositoryTranslatedExampleViewSet( queryset = RepositoryTranslatedExample.objects serializer_class = NewRepositoryTranslatedExampleSerializer permission_classes = [IsAuthenticated] + + +class RepositoryTranslationsViewSet( + mixins.ListModelMixin, + GenericViewSet): + """ + List repository translations. + """ + serializer_class = RepositoryTranslatedExampleSerializer + queryset = RepositoryTranslatedExample.objects.all() + filter_class = RepositoryTranslationsFilter diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 418685354..51a8a3448 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -10,6 +10,7 @@ from .repository.views import RepositoryExampleViewSet from .repository.views import SearchRepositoriesViewSet from .repository.views import NewRepositoryTranslatedExampleViewSet +from .repository.views import RepositoryTranslationsViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -111,6 +112,7 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): 'repository/translate-example', NewRepositoryTranslatedExampleViewSet ) +router.register('repository/translations', RepositoryTranslationsViewSet) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From 7d097353e251276e399f6abadfb1f9b55c338b53 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 17:10:13 -0300 Subject: [PATCH 13/31] Migration Repository Updates v1 to v2 and refact --- bothub/api/v1/views.py | 6 +++++ bothub/api/v2/repository/filters.py | 29 +++++++++++++++++++++++++ bothub/api/v2/repository/permissions.py | 12 ++++++++++ bothub/api/v2/repository/serializers.py | 23 ++++++++++++++++++++ bothub/api/v2/repository/views.py | 17 +++++++++++++++ bothub/api/v2/routers.py | 2 ++ 6 files changed, 89 insertions(+) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 01fcb7a3d..07c2e8ba1 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -1256,6 +1256,12 @@ class RepositoryEntitiesViewSet( ] +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RepositoryUpdatesViewSet( mixins.ListModelMixin, GenericViewSet): diff --git a/bothub/api/v2/repository/filters.py b/bothub/api/v2/repository/filters.py index ac7a72326..82c1db845 100644 --- a/bothub/api/v2/repository/filters.py +++ b/bothub/api/v2/repository/filters.py @@ -6,6 +6,7 @@ from bothub.common.models import Repository from bothub.common.models import RepositoryTranslatedExample +from bothub.common.models import RepositoryUpdate class RepositoriesFilter(filters.FilterSet): @@ -65,3 +66,31 @@ def filter_from_language(self, queryset, name, value): def filter_to_language(self, queryset, name, value): return queryset.filter(language=value) + + +class RepositoryUpdatesFilter(filters.FilterSet): + class Meta: + model = RepositoryUpdate + fields = [ + 'repository_uuid', + ] + + repository_uuid = filters.CharFilter( + field_name='repository_uuid', + required=True, + method='filter_repository_uuid', + help_text=_('Repository\'s UUID')) + + def filter_repository_uuid(self, queryset, name, value): + request = self.request + try: + repository = Repository.objects.get(uuid=value) + authorization = repository.get_user_authorization(request.user) + if not authorization.can_read: + raise PermissionDenied() + return queryset.filter(repository=repository) + except Repository.DoesNotExist: + raise NotFound( + _('Repository {} does not exist').format(value)) + except DjangoValidationError: + raise NotFound(_('Invalid repository UUID')) diff --git a/bothub/api/v2/repository/permissions.py b/bothub/api/v2/repository/permissions.py index ed6ddadca..ce960d092 100644 --- a/bothub/api/v2/repository/permissions.py +++ b/bothub/api/v2/repository/permissions.py @@ -36,3 +36,15 @@ def has_object_permission(self, request, view, obj): if request.method in READ_METHODS: return authorization.can_read return authorization.can_contribute + + +class RepositoryUpdateHasPermission(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + authorization = obj.repository.get_user_authorization(request.user) + if request.method in READ_METHODS: + return authorization.can_read + if request.user.is_authenticated: + if request.method in WRITE_METHODS: + return authorization.can_write + return authorization.is_admin + return False diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 5ad0715cf..fefd48063 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -18,6 +18,7 @@ from bothub.common.models import RepositoryEntityLabel from bothub.common.models import RepositoryAuthorization from bothub.common.models import RequestRepositoryAuthorization +from bothub.common.models import RepositoryUpdate from bothub.common.languages import LANGUAGE_CHOICES from ..request.serializers import RequestRepositoryAuthorizationSerializer from ..fields import ModelMultipleChoiceField @@ -544,3 +545,25 @@ def create(self, validated_data): repository_translated_example=translated, **entity_data) return translated + + +class RepositoryUpdateSerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryUpdate + fields = [ + 'id', + 'repository', + 'language', + 'created_at', + 'by', + 'by__nickname', + 'training_started_at', + 'trained_at', + 'failed_at', + ] + ref_name = None + + by__nickname = serializers.SlugRelatedField( + source='by', + slug_field='nickname', + read_only=True) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 3c68cb8b2..45150abe5 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -17,6 +17,7 @@ from bothub.common.models import RepositoryAuthorization from bothub.common.models import RepositoryTranslatedExample from bothub.common.models import RepositoryExample +from bothub.common.models import RepositoryUpdate from ..metadata import Metadata from .serializers import RepositorySerializer @@ -28,11 +29,14 @@ from .serializers import RepositoryTranslatedExampleSerializer from .serializers import RepositoryExampleSerializer from .serializers import NewRepositoryTranslatedExampleSerializer +from .serializers import RepositoryUpdateSerializer from .permissions import RepositoryPermission from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission +from .permissions import RepositoryUpdateHasPermission from .filters import RepositoriesFilter from .filters import RepositoryTranslationsFilter +from .filters import RepositoryUpdatesFilter class RepositoryViewSet( @@ -323,3 +327,16 @@ class RepositoryTranslationsViewSet( serializer_class = RepositoryTranslatedExampleSerializer queryset = RepositoryTranslatedExample.objects.all() filter_class = RepositoryTranslationsFilter + + +class RepositoryUpdatesViewSet( + mixins.ListModelMixin, + GenericViewSet): + queryset = RepositoryUpdate.objects.filter( + training_started_at__isnull=False).order_by('-trained_at') + serializer_class = RepositoryUpdateSerializer + filter_class = RepositoryUpdatesFilter + permission_classes = [ + IsAuthenticated, + RepositoryUpdateHasPermission, + ] diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 51a8a3448..03b1a1859 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -11,6 +11,7 @@ from .repository.views import SearchRepositoriesViewSet from .repository.views import NewRepositoryTranslatedExampleViewSet from .repository.views import RepositoryTranslationsViewSet +from .repository.views import RepositoryUpdatesViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -113,6 +114,7 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): NewRepositoryTranslatedExampleViewSet ) router.register('repository/translations', RepositoryTranslationsViewSet) +router.register('repository/updates', RepositoryUpdatesViewSet) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From c593bf89a44b4167f0fb31344ff2db4687d0ead8 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 17:22:59 -0300 Subject: [PATCH 14/31] Added Tests Repository --- bothub/api/v2/tests/test_repository.py | 631 +++++++++++++++++++++++++ 1 file changed, 631 insertions(+) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index 35d70fffe..1a1eacd5e 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -1,4 +1,5 @@ import json +import uuid from django.test import TestCase from django.test import RequestFactory @@ -6,6 +7,7 @@ from rest_framework import status from bothub.common.models import RepositoryCategory +from bothub.common.models import RepositoryExampleEntity from bothub.common.models import RepositoryVote from bothub.common.models import RepositoryAuthorization from bothub.common.models import Repository @@ -17,12 +19,18 @@ from bothub.api.v2.tests.utils import create_user_and_token from bothub.api.v2.repository.views import RepositoryViewSet +from bothub.api.v2.repository.views import \ + NewRepositoryTranslatedExampleViewSet from bothub.api.v2.repository.views import RepositoriesContributionsViewSet from bothub.api.v2.repository.views import RepositoriesViewSet from bothub.api.v2.repository.views import RepositoryVotesViewSet from bothub.api.v2.repository.views import RepositoryCategoriesViewSet from bothub.api.v2.repository.views import NewRepositoryViewSet from bothub.api.v2.repository.views import RepositoryTranslatedExampleViewSet +from bothub.api.v2.repository.views import RepositoryExampleViewSet +from bothub.api.v2.repository.views import SearchRepositoriesViewSet +from bothub.api.v2.repository.views import RepositoryTranslationsViewSet +from bothub.api.v2.repository.views import RepositoryUpdatesViewSet from bothub.api.v2.repository.serializers import RepositorySerializer @@ -1135,3 +1143,626 @@ def test_forbidden(self): self.assertEqual( response.status_code, status.HTTP_403_FORBIDDEN) + + +class RepositoryExampleDestroyTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.user, self.user_token = create_user_and_token() + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + self.example = RepositoryExample.objects.create( + repository_update=self.repository.current_update(), + text='hi') + + self.private_repository = Repository.objects.create( + owner=self.owner, + name='Testing Private', + slug='private', + language=languages.LANGUAGE_EN, + is_private=True) + self.private_example = RepositoryExample.objects.create( + repository_update=self.private_repository.current_update(), + text='hi') + + def request(self, example, token): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.delete( + '/v2/repository/example/{}/'.format(example.id), + **authorization_header) + response = RepositoryExampleViewSet.as_view( + {'delete': 'destroy'})(request, pk=example.id) + return response + + def test_okay(self): + response = self.request( + self.example, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_204_NO_CONTENT) + + def test_private_okay(self): + response = self.request( + self.private_example, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_204_NO_CONTENT) + + def test_forbidden(self): + response = self.request( + self.example, + self.user_token) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + def test_private_forbidden(self): + response = self.request( + self.private_example, + self.user_token) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + def test_already_deleted(self): + self.example.delete() + + response = self.request( + self.example, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_500_INTERNAL_SERVER_ERROR) + + +class RepositoryExampleUpdateTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.user, self.user_token = create_user_and_token() + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + self.example = RepositoryExample.objects.create( + repository_update=self.repository.current_update(), + text='hi') + + self.private_repository = Repository.objects.create( + owner=self.owner, + name='Testing Private', + slug='private', + language=languages.LANGUAGE_EN, + is_private=True) + self.private_example = RepositoryExample.objects.create( + repository_update=self.private_repository.current_update(), + text='hi') + + def request(self, example, token, data): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.patch( + '/v2/repository/example/{}/'.format(example.id), + json.dumps(data), + content_type='application/json', + **authorization_header) + response = RepositoryExampleViewSet.as_view( + {'patch': 'update'})(request, pk=example.id) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + text = 'teste' + intent = 'teste1234' + + response, content_data = self.request( + self.example, + self.owner_token, + {"text": text, "intent": intent} + ) + + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('text'), + text) + self.assertEqual( + content_data.get('intent'), + intent) + + def test_private_forbidden(self): + response, content_data = self.request( + self.private_example, + self.user_token, + {"text": 'teste', "intent": 'teste1234'}) + + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + +class SearchRepositoriesTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.user, self.user_token = create_user_and_token() + + self.category = RepositoryCategory.objects.create( + name='ID') + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + self.repository.categories.add(self.category) + + def request(self, nickname): + request = self.factory.get( + '/v2/repository/search-repositories/?nickname={}'.format(nickname) + ) + response = SearchRepositoriesViewSet.as_view( + {'get': 'list'} + )(request, nickname=nickname) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request('owner') + self.assertEqual(response.status_code, 200) + self.assertEqual( + content_data.get('count'), + 1) + self.assertEqual( + uuid.UUID(content_data.get('results')[0].get('uuid')), + self.repository.uuid) + + def test_empty_with_user_okay(self): + response, content_data = self.request('fake') + self.assertEqual(response.status_code, 200) + self.assertEqual( + content_data.get('count'), + 0) + + def test_empty_without_user_okay(self): + response, content_data = self.request('') + self.assertEqual(response.status_code, 200) + self.assertEqual( + content_data.get('count'), + 0) + + +class TranslateExampleTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + self.example = RepositoryExample.objects.create( + repository_update=self.repository.current_update(), + text='hi') + + def request(self, data, user_token): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(user_token.key), + } + request = self.factory.post( + '/v2/repository/translate-example/', + json.dumps(data), + content_type='application/json', + **authorization_header) + response = NewRepositoryTranslatedExampleViewSet.as_view( + {'post': 'create'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request( + { + 'original_example': self.example.id, + 'language': languages.LANGUAGE_PT, + 'text': 'oi', + 'entities': [], + }, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + + def test_unique_translate(self): + language = languages.LANGUAGE_PT + text = 'oi' + + RepositoryTranslatedExample.objects.create( + original_example=self.example, + language=language, + text=text) + + response, content_data = self.request( + { + 'original_example': self.example.id, + 'language': language, + 'text': text, + 'entities': [], + }, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'non_field_errors', + content_data.keys()) + + def test_forbidden(self): + user, user_token = create_user_and_token() + + response, content_data = self.request( + { + 'original_example': self.example.id, + 'language': languages.LANGUAGE_PT, + 'text': 'oi', + 'entities': [], + }, + user_token) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + def test_okay_with_entities(self): + example = RepositoryExample.objects.create( + repository_update=self.repository.current_update(), + text='my name is douglas') + RepositoryExampleEntity.objects.create( + repository_example=example, + start=11, + end=18, + entity='name') + response, content_data = self.request( + { + 'original_example': example.id, + 'language': languages.LANGUAGE_PT, + 'text': 'meu nome é douglas', + 'entities': [ + { + 'start': 11, + 'end': 18, + 'entity': 'name', + }, + ], + }, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + self.assertEqual( + len(content_data.get('entities')), + 1) + + def test_entities_no_valid(self): + example = RepositoryExample.objects.create( + repository_update=self.repository.current_update(), + text='my name is douglas') + RepositoryExampleEntity.objects.create( + repository_example=self.example, + start=11, + end=18, + entity='name') + response, content_data = self.request( + { + 'original_example': example.id, + 'language': languages.LANGUAGE_PT, + 'text': 'meu nome é douglas', + 'entities': [ + { + 'start': 11, + 'end': 18, + 'entity': 'nome', + }, + ], + }, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertEqual( + len(content_data.get('entities')), + 1) + + def test_entities_no_valid_2(self): + example = RepositoryExample.objects.create( + repository_update=self.repository.current_update(), + text='my name is douglas') + RepositoryExampleEntity.objects.create( + repository_example=self.example, + start=11, + end=18, + entity='name') + response, content_data = self.request( + { + 'original_example': example.id, + 'language': languages.LANGUAGE_PT, + 'text': 'meu nome é douglas', + 'entities': [ + { + 'start': 11, + 'end': 18, + 'entity': 'name', + }, + { + 'start': 0, + 'end': 3, + 'entity': 'my', + }, + ], + }, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertEqual( + len(content_data.get('entities')), + 1) + + def test_can_not_translate_to_same_language(self): + response, content_data = self.request( + { + 'original_example': self.example.id, + 'language': self.example.repository_update.language, + 'text': 'oi', + 'entities': [], + }, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'language', + content_data.keys()) + + +class TranslationsViewTest(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + self.example = RepositoryExample.objects.create( + repository_update=self.repository.current_update(), + text='hi') + self.translated = RepositoryTranslatedExample.objects.create( + original_example=self.example, + language=languages.LANGUAGE_PT, + text='oi') + + def request(self, data, user_token=None): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(user_token.key), + } if user_token else {} + request = self.factory.get( + '/v2/repository/translations/', + data, + **authorization_header) + response = RepositoryTranslationsViewSet.as_view( + {'get': 'list'} + )(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request({ + 'repository_uuid': self.repository.uuid, + }) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('count'), + 1) + + def test_repository_not_found(self): + response, content_data = self.request({ + 'repository_uuid': uuid.uuid4(), + }) + self.assertEqual( + response.status_code, + status.HTTP_404_NOT_FOUND) + + def test_repository_uuid_invalid(self): + response, content_data = self.request({ + 'repository_uuid': 'invalid', + }) + self.assertEqual( + response.status_code, + status.HTTP_404_NOT_FOUND) + + def test_forbidden(self): + private_repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='private', + language=languages.LANGUAGE_EN, + is_private=True) + + response, content_data = self.request({ + 'repository_uuid': private_repository.uuid, + }) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + user, user_token = create_user_and_token('user') + response, content_data = self.request( + { + 'repository_uuid': private_repository.uuid, + }, + user_token) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + def test_filter_from_language(self): + example = RepositoryExample.objects.create( + repository_update=self.repository.current_update( + languages.LANGUAGE_ES), + text='hola') + translated = RepositoryTranslatedExample.objects.create( + original_example=example, + language=languages.LANGUAGE_PT, + text='oi') + + response, content_data = self.request({ + 'repository_uuid': self.repository.uuid, + 'from_language': self.example.repository_update.language, + }) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('count'), + 1) + self.assertEqual( + content_data.get('results')[0].get('id'), + self.translated.id) + + response, content_data = self.request({ + 'repository_uuid': self.repository.uuid, + 'from_language': example.repository_update.language, + }) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('count'), + 1) + self.assertEqual( + content_data.get('results')[0].get('id'), + translated.id) + + def test_filter_to_language(self): + example = RepositoryExample.objects.create( + repository_update=self.repository.current_update( + languages.LANGUAGE_ES), + text='hola') + RepositoryTranslatedExample.objects.create( + original_example=example, + language=languages.LANGUAGE_PT, + text='oi') + + response, content_data = self.request({ + 'repository_uuid': self.repository.uuid, + 'to_language': self.translated.language, + }) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('count'), + 2) + + response, content_data = self.request({ + 'repository_uuid': self.repository.uuid, + 'to_language': languages.LANGUAGE_DE, + }) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('count'), + 0) + + +class RepositoryUpdatesTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + current_update = self.repository.current_update() + RepositoryExample.objects.create( + repository_update=current_update, + text='my name is Douglas', + intent='greet') + RepositoryExample.objects.create( + repository_update=current_update, + text='my name is John', + intent='greet') + current_update.start_training(self.owner) + + def request(self, data, token=None): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } if token else {} + request = self.factory.get( + '/v2/repository/updates/', + data, + **authorization_header) + response = RepositoryUpdatesViewSet.as_view( + {'get': 'list'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request( + { + 'repository_uuid': str(self.repository.uuid), + }, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('count'), + 1) + + def test_not_authenticated(self): + response, content_data = self.request( + { + 'repository_uuid': str(self.repository.uuid), + }) + self.assertEqual( + response.status_code, + status.HTTP_401_UNAUTHORIZED) + + def test_without_repository(self): + response, content_data = self.request( + {}, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) From 09ef94c7e3ca196aa6f8f14d1be5e27a87948bb3 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 17:47:25 -0300 Subject: [PATCH 15/31] Migration Repository v1 to v2 and refact --- bothub/api/v1/views.py | 36 +++++++++ bothub/api/v2/mixins.py | 20 +++++ bothub/api/v2/repository/permissions.py | 5 ++ bothub/api/v2/repository/serializers.py | 37 +++++++++ bothub/api/v2/repository/views.py | 103 ++++++++++++++++++++++-- 5 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 bothub/api/v2/mixins.py diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 07c2e8ba1..a4ebb5fd9 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -450,6 +450,30 @@ def get_queryset(self, *args, **kwargs): return self.queryset.none() +@method_decorator( + name='retrieve', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='update', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='partial_update', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='destroy', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RepositoryViewSet( MultipleFieldLookupMixin, mixins.RetrieveModelMixin, @@ -480,6 +504,12 @@ class RepositoryViewSet( RepositoryPermission, ] + @method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) + ) @action( detail=True, methods=['GET'], @@ -540,6 +570,12 @@ def train(self, request, **kwargs): code=request.status_code) return Response(request.json()) # pragma: no cover + @method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) + ) @action( detail=True, methods=['POST'], diff --git a/bothub/api/v2/mixins.py b/bothub/api/v2/mixins.py new file mode 100644 index 000000000..420659ddf --- /dev/null +++ b/bothub/api/v2/mixins.py @@ -0,0 +1,20 @@ +from django.shortcuts import get_object_or_404 + + +class MultipleFieldLookupMixin(object): + """ + Apply this mixin to any view or viewset to get multiple field filtering + based on a `lookup_fields` attribute, instead of the default single field + filtering. + """ + + def get_object(self): + queryset = self.get_queryset() + queryset = self.filter_queryset(queryset) + filter = {} + for field in self.lookup_fields: + if self.kwargs.get(field): + filter[field] = self.kwargs[field] + obj = get_object_or_404(queryset, **filter) + self.check_object_permissions(self.request, obj) + return obj diff --git a/bothub/api/v2/repository/permissions.py b/bothub/api/v2/repository/permissions.py index ce960d092..979511a56 100644 --- a/bothub/api/v2/repository/permissions.py +++ b/bothub/api/v2/repository/permissions.py @@ -4,6 +4,11 @@ from .. import WRITE_METHODS +CUSTOM_READ_METHODS = permissions.SAFE_METHODS +CUSTOM_WRITE_METHODS = ['POST', 'PUT', 'PATCH'] +CUSTOM_ADMIN_METHODS = ['DELETE'] + + class RepositoryPermission(permissions.BasePermission): def has_object_permission(self, request, view, obj): authorization = obj.get_user_authorization(request.user) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index fefd48063..7dd3e2a39 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -567,3 +567,40 @@ class Meta: source='by', slug_field='nickname', read_only=True) + + +class EditRepositorySerializer(NewRepositorySerializer): + pass + + +class RepositoryAuthorizationSerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryAuthorization + fields = [ + 'uuid', + 'user', + 'user__nickname', + 'repository', + 'role', + 'level', + 'can_read', + 'can_contribute', + 'can_write', + 'is_admin', + 'created_at', + ] + ref_name = None + + user__nickname = serializers.SlugRelatedField( + source='user', + slug_field='nickname', + read_only=True) + + +class AnalyzeTextSerializer(serializers.Serializer): + language = serializers.ChoiceField(LANGUAGE_CHOICES, required=True) + text = serializers.CharField(allow_blank=False) + + +class EvaluateSerializer(serializers.Serializer): + language = serializers.ChoiceField(LANGUAGE_CHOICES, required=True) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 45150abe5..32abe193b 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -3,6 +3,7 @@ from django_filters.rest_framework import DjangoFilterBackend from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema +from rest_framework.decorators import action from rest_framework.exceptions import APIException from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -10,7 +11,9 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.permissions import IsAuthenticated from rest_framework.filters import SearchFilter +from rest_framework.exceptions import PermissionDenied +from bothub.api.v2.mixins import MultipleFieldLookupMixin from bothub.common.models import Repository from bothub.common.models import RepositoryCategory from bothub.common.models import RepositoryVote @@ -30,7 +33,12 @@ from .serializers import RepositoryExampleSerializer from .serializers import NewRepositoryTranslatedExampleSerializer from .serializers import RepositoryUpdateSerializer +from .serializers import EditRepositorySerializer +from .serializers import RepositoryAuthorizationSerializer +from .serializers import AnalyzeTextSerializer +from .serializers import EvaluateSerializer from .permissions import RepositoryPermission +from .permissions import CUSTOM_WRITE_METHODS from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission from .permissions import RepositoryUpdateHasPermission @@ -40,22 +48,107 @@ class RepositoryViewSet( - mixins.CreateModelMixin, + MultipleFieldLookupMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, GenericViewSet): """ - Manager repository (bot). + Manager repository. + + retrieve: + Get repository data. + + update: + Update your repository. + + partial_update: + Update, partially, your repository. + + delete: + Delete your repository. """ queryset = Repository.objects - lookup_field = 'uuid' + lookup_field = 'slug' + lookup_fields = ['owner__nickname', 'slug'] serializer_class = RepositorySerializer + edit_serializer_class = EditRepositorySerializer permission_classes = [ - IsAuthenticatedOrReadOnly, RepositoryPermission, ] - metadata_class = Metadata + + @action( + detail=True, + methods=['GET'], + url_name='repository-languages-status') + def languagesstatus(self, request, **kwargs): + """ + Get current language status. + """ + repository = self.get_object() + return Response({ + 'languages_status': repository.languages_status, + }) + + @action( + detail=True, + methods=['POST'], + url_name='repository-analyze', + permission_classes=[]) + def analyze(self, request, **kwargs): + repository = self.get_object() + user_authorization = repository.get_user_authorization(request.user) + serializer = AnalyzeTextSerializer( + data=request.data) # pragma: no cover + serializer.is_valid(raise_exception=True) # pragma: no cover + request = Repository.request_nlp_analyze( + user_authorization, + serializer.data) # pragma: no cover + + if request.status_code == status.HTTP_200_OK: # pragma: no cover + return Response(request.json()) # pragma: no cover + + response = None # pragma: no cover + try: # pragma: no cover + response = request.json() # pragma: no cover + except Exception: + pass + + if not response: # pragma: no cover + raise APIException( # pragma: no cover + detail=_('Something unexpected happened! ' + \ + 'We couldn\'t analyze your text.')) + error = response.get('error') # pragma: no cover + message = error.get('message') # pragma: no cover + raise APIException(detail=message) # pragma: no cover + + def get_serializer_class(self): + if self.request and self.request.method in \ + ['OPTIONS'] + CUSTOM_WRITE_METHODS or not self.request: + return self.edit_serializer_class + return self.serializer_class + + def get_action_permissions_classes(self): + if not self.action: + return None + fn = getattr(self, self.action, None) + if not fn: + return None + fn_kwargs = getattr(fn, 'kwargs', None) + if not fn_kwargs: + return None + permission_classes = fn_kwargs.get('permission_classes') + if not permission_classes: + return None + return permission_classes + + def get_permissions(self): + action_permissions_classes = self.get_action_permissions_classes() + if action_permissions_classes: + return [permission() + for permission + in action_permissions_classes] + return super().get_permissions() @method_decorator( From dd1447f9ad672bc56491e51cb47abb8a013fd403 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 18:20:27 -0300 Subject: [PATCH 16/31] [fix] Repository Router --- bothub/api/v2/mixins.py | 20 ------ bothub/api/v2/repository/permissions.py | 5 -- bothub/api/v2/repository/serializers.py | 37 ---------- bothub/api/v2/repository/views.py | 95 ++----------------------- 4 files changed, 5 insertions(+), 152 deletions(-) delete mode 100644 bothub/api/v2/mixins.py diff --git a/bothub/api/v2/mixins.py b/bothub/api/v2/mixins.py deleted file mode 100644 index 420659ddf..000000000 --- a/bothub/api/v2/mixins.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.shortcuts import get_object_or_404 - - -class MultipleFieldLookupMixin(object): - """ - Apply this mixin to any view or viewset to get multiple field filtering - based on a `lookup_fields` attribute, instead of the default single field - filtering. - """ - - def get_object(self): - queryset = self.get_queryset() - queryset = self.filter_queryset(queryset) - filter = {} - for field in self.lookup_fields: - if self.kwargs.get(field): - filter[field] = self.kwargs[field] - obj = get_object_or_404(queryset, **filter) - self.check_object_permissions(self.request, obj) - return obj diff --git a/bothub/api/v2/repository/permissions.py b/bothub/api/v2/repository/permissions.py index 979511a56..ce960d092 100644 --- a/bothub/api/v2/repository/permissions.py +++ b/bothub/api/v2/repository/permissions.py @@ -4,11 +4,6 @@ from .. import WRITE_METHODS -CUSTOM_READ_METHODS = permissions.SAFE_METHODS -CUSTOM_WRITE_METHODS = ['POST', 'PUT', 'PATCH'] -CUSTOM_ADMIN_METHODS = ['DELETE'] - - class RepositoryPermission(permissions.BasePermission): def has_object_permission(self, request, view, obj): authorization = obj.get_user_authorization(request.user) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 7dd3e2a39..fefd48063 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -567,40 +567,3 @@ class Meta: source='by', slug_field='nickname', read_only=True) - - -class EditRepositorySerializer(NewRepositorySerializer): - pass - - -class RepositoryAuthorizationSerializer(serializers.ModelSerializer): - class Meta: - model = RepositoryAuthorization - fields = [ - 'uuid', - 'user', - 'user__nickname', - 'repository', - 'role', - 'level', - 'can_read', - 'can_contribute', - 'can_write', - 'is_admin', - 'created_at', - ] - ref_name = None - - user__nickname = serializers.SlugRelatedField( - source='user', - slug_field='nickname', - read_only=True) - - -class AnalyzeTextSerializer(serializers.Serializer): - language = serializers.ChoiceField(LANGUAGE_CHOICES, required=True) - text = serializers.CharField(allow_blank=False) - - -class EvaluateSerializer(serializers.Serializer): - language = serializers.ChoiceField(LANGUAGE_CHOICES, required=True) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 32abe193b..bba445f74 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -48,107 +48,22 @@ class RepositoryViewSet( - MultipleFieldLookupMixin, + mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, GenericViewSet): """ - Manager repository. - - retrieve: - Get repository data. - - update: - Update your repository. - - partial_update: - Update, partially, your repository. - - delete: - Delete your repository. + Manager repository (bot). """ queryset = Repository.objects - lookup_field = 'slug' - lookup_fields = ['owner__nickname', 'slug'] + lookup_field = 'uuid' serializer_class = RepositorySerializer - edit_serializer_class = EditRepositorySerializer permission_classes = [ + IsAuthenticatedOrReadOnly, RepositoryPermission, ] - - @action( - detail=True, - methods=['GET'], - url_name='repository-languages-status') - def languagesstatus(self, request, **kwargs): - """ - Get current language status. - """ - repository = self.get_object() - return Response({ - 'languages_status': repository.languages_status, - }) - - @action( - detail=True, - methods=['POST'], - url_name='repository-analyze', - permission_classes=[]) - def analyze(self, request, **kwargs): - repository = self.get_object() - user_authorization = repository.get_user_authorization(request.user) - serializer = AnalyzeTextSerializer( - data=request.data) # pragma: no cover - serializer.is_valid(raise_exception=True) # pragma: no cover - request = Repository.request_nlp_analyze( - user_authorization, - serializer.data) # pragma: no cover - - if request.status_code == status.HTTP_200_OK: # pragma: no cover - return Response(request.json()) # pragma: no cover - - response = None # pragma: no cover - try: # pragma: no cover - response = request.json() # pragma: no cover - except Exception: - pass - - if not response: # pragma: no cover - raise APIException( # pragma: no cover - detail=_('Something unexpected happened! ' + \ - 'We couldn\'t analyze your text.')) - error = response.get('error') # pragma: no cover - message = error.get('message') # pragma: no cover - raise APIException(detail=message) # pragma: no cover - - def get_serializer_class(self): - if self.request and self.request.method in \ - ['OPTIONS'] + CUSTOM_WRITE_METHODS or not self.request: - return self.edit_serializer_class - return self.serializer_class - - def get_action_permissions_classes(self): - if not self.action: - return None - fn = getattr(self, self.action, None) - if not fn: - return None - fn_kwargs = getattr(fn, 'kwargs', None) - if not fn_kwargs: - return None - permission_classes = fn_kwargs.get('permission_classes') - if not permission_classes: - return None - return permission_classes - - def get_permissions(self): - action_permissions_classes = self.get_action_permissions_classes() - if action_permissions_classes: - return [permission() - for permission - in action_permissions_classes] - return super().get_permissions() + metadata_class = Metadata @method_decorator( From e4c40a253d25f3a69be79fde926cc715291d2ef3 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 18:21:08 -0300 Subject: [PATCH 17/31] PEP8 --- bothub/api/v2/repository/views.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index bba445f74..74de7c5c7 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -3,7 +3,6 @@ from django_filters.rest_framework import DjangoFilterBackend from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema -from rest_framework.decorators import action from rest_framework.exceptions import APIException from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -11,9 +10,6 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.permissions import IsAuthenticated from rest_framework.filters import SearchFilter -from rest_framework.exceptions import PermissionDenied - -from bothub.api.v2.mixins import MultipleFieldLookupMixin from bothub.common.models import Repository from bothub.common.models import RepositoryCategory from bothub.common.models import RepositoryVote @@ -33,12 +29,7 @@ from .serializers import RepositoryExampleSerializer from .serializers import NewRepositoryTranslatedExampleSerializer from .serializers import RepositoryUpdateSerializer -from .serializers import EditRepositorySerializer -from .serializers import RepositoryAuthorizationSerializer -from .serializers import AnalyzeTextSerializer -from .serializers import EvaluateSerializer from .permissions import RepositoryPermission -from .permissions import CUSTOM_WRITE_METHODS from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission from .permissions import RepositoryUpdateHasPermission From 3a7dbe0e46069beba00b902847c4cfebfbdd6561 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 18:30:53 -0300 Subject: [PATCH 18/31] Migration Repository Example New v1 to v2 and refact --- bothub/api/v1/views.py | 6 ++ bothub/api/v2/repository/serializers.py | 108 +++++++++++++++++++++++- bothub/api/v2/repository/views.py | 11 +++ bothub/api/v2/routers.py | 2 + 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index a4ebb5fd9..154246950 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -678,6 +678,12 @@ def get_permissions(self): return super().get_permissions() +@method_decorator( + name='create', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class NewRepositoryExampleViewSet( mixins.CreateModelMixin, GenericViewSet): diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index fefd48063..686f91962 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -1,5 +1,6 @@ from django.utils.translation import gettext as _ from rest_framework import serializers +from rest_framework.fields import empty from bothub.api.v2.example.serializers import RepositoryExampleEntitySerializer from bothub.api.v2.repository.validators import \ @@ -19,12 +20,18 @@ from bothub.common.models import RepositoryAuthorization from bothub.common.models import RequestRepositoryAuthorization from bothub.common.models import RepositoryUpdate +from bothub.common.models import RepositoryExampleEntity from bothub.common.languages import LANGUAGE_CHOICES from ..request.serializers import RequestRepositoryAuthorizationSerializer from ..fields import ModelMultipleChoiceField from ..fields import TextField from ..fields import EntityValueField - +from ..fields import LabelValueField +from ..fields import EntityText +from .validators import EntityNotEqualLabelValidator +from .validators import CanContributeInRepositoryValidator +from .validators import ExampleWithIntentOrEntityValidator +from .validators import IntentAndSentenceNotExistsValidator class RepositoryCategorySerializer(serializers.ModelSerializer): class Meta: @@ -567,3 +574,102 @@ class Meta: source='by', slug_field='nickname', read_only=True) + + +class NewRepositoryExampleEntitySerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryExampleEntity + fields = [ + 'repository_example', + 'start', + 'end', + 'entity', + 'label', + ] + ref_name = None + + repository_example = serializers.PrimaryKeyRelatedField( + queryset=RepositoryExample.objects, + required=False) + + entity = EntityValueField() + label = LabelValueField( + allow_blank=True, + required=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.validators.append(EntityNotEqualLabelValidator()) + + def create(self, validated_data): + repository_example = validated_data.pop('repository_example', None) + assert repository_example + label = validated_data.pop('label', empty) + example_entity = self.Meta.model.objects.create( + repository_example=repository_example, + **validated_data) + if label is not empty: + example_entity.entity.set_label(label) + example_entity.entity.save(update_fields=['label']) + return example_entity + + +class NewRepositoryExampleSerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryExample + fields = [ + 'id', + 'repository', + 'repository_update', + 'text', + 'language', + 'intent', + 'entities', + ] + ref_name = None + + id = serializers.PrimaryKeyRelatedField( + read_only=True, + style={'show': False}) + text = EntityText(style={'entities_field': 'entities'}) + repository = serializers.PrimaryKeyRelatedField( + queryset=Repository.objects, + validators=[ + CanContributeInRepositoryValidator(), + ], + write_only=True, + style={'show': False}) + repository_update = serializers.PrimaryKeyRelatedField( + read_only=True, + style={'show': False}) + language = serializers.ChoiceField( + LANGUAGE_CHOICES, + allow_blank=True, + required=False) + entities = NewRepositoryExampleEntitySerializer( + many=True, + style={'text_field': 'text'}) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.validators.append(ExampleWithIntentOrEntityValidator()) + self.validators.append(IntentAndSentenceNotExistsValidator()) + + def create(self, validated_data): + entities_data = validated_data.pop('entities') + repository = validated_data.pop('repository') + + try: + language = validated_data.pop('language') + except KeyError: + language = None + repository_update = repository.current_update(language or None) + validated_data.update({'repository_update': repository_update}) + example = self.Meta.model.objects.create(**validated_data) + for entity_data in entities_data: + entity_data.update({'repository_example': example.pk}) + entity_serializer = NewRepositoryExampleEntitySerializer( + data=entity_data) + entity_serializer.is_valid(raise_exception=True) + entity_serializer.save() + return example diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 74de7c5c7..3bd5838f3 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -339,3 +339,14 @@ class RepositoryUpdatesViewSet( IsAuthenticated, RepositoryUpdateHasPermission, ] + + +class NewRepositoryExampleViewSet( + mixins.CreateModelMixin, + GenericViewSet): + """ + Create new repository example. + """ + queryset = RepositoryExample.objects + serializer_class = NewRepositoryExampleSerializer + permission_classes = [IsAuthenticated] diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 03b1a1859..da271d598 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -12,6 +12,7 @@ from .repository.views import NewRepositoryTranslatedExampleViewSet from .repository.views import RepositoryTranslationsViewSet from .repository.views import RepositoryUpdatesViewSet +from .repository.views import NewRepositoryExampleViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -115,6 +116,7 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): ) router.register('repository/translations', RepositoryTranslationsViewSet) router.register('repository/updates', RepositoryUpdatesViewSet) +router.register('repository/example/new', NewRepositoryExampleViewSet) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From 27375b45f3c60e2aff8980d8eccf7097bc56c798 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 18:32:30 -0300 Subject: [PATCH 19/31] Migration Request Authorization v1 to v2 and refact --- bothub/api/v1/views.py | 6 ++++++ bothub/api/v2/repository/serializers.py | 23 +++++++++++++++++++++++ bothub/api/v2/repository/views.py | 16 ++++++++++++++++ bothub/api/v2/routers.py | 5 +++++ 4 files changed, 50 insertions(+) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 154246950..3e43a7299 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -1230,6 +1230,12 @@ def list(self, request, *args, **kwargs): return Response(serializer.data) +@method_decorator( + name='create', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RequestAuthorizationViewSet( mixins.CreateModelMixin, GenericViewSet): diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 686f91962..214b953b6 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -673,3 +673,26 @@ def create(self, validated_data): entity_serializer.is_valid(raise_exception=True) entity_serializer.save() return example + + +class NewRequestRepositoryAuthorizationSerializer(serializers.ModelSerializer): + class Meta: + model = RequestRepositoryAuthorization + fields = [ + 'user', + 'repository', + 'text', + ] + ref_name = None + + repository = serializers.PrimaryKeyRelatedField( + queryset=Repository.objects, + style={'show': False}) + user = serializers.HiddenField( + default=serializers.CurrentUserDefault(), + style={'show': False}) + text = TextField( + label=_('Leave a message for repository administrators'), + min_length=5, + max_length=RequestRepositoryAuthorization._meta.get_field( + 'text').max_length) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 3bd5838f3..b8df818aa 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -17,6 +17,7 @@ from bothub.common.models import RepositoryTranslatedExample from bothub.common.models import RepositoryExample from bothub.common.models import RepositoryUpdate +from bothub.common.models import RequestRepositoryAuthorization from ..metadata import Metadata from .serializers import RepositorySerializer @@ -29,6 +30,8 @@ from .serializers import RepositoryExampleSerializer from .serializers import NewRepositoryTranslatedExampleSerializer from .serializers import RepositoryUpdateSerializer +from .serializers import NewRepositoryExampleSerializer +from .serializers import NewRequestRepositoryAuthorizationSerializer from .permissions import RepositoryPermission from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission @@ -350,3 +353,16 @@ class NewRepositoryExampleViewSet( queryset = RepositoryExample.objects serializer_class = NewRepositoryExampleSerializer permission_classes = [IsAuthenticated] + + +class RequestAuthorizationViewSet( + mixins.CreateModelMixin, + GenericViewSet): + """ + Request authorization in the repository + """ + serializer_class = NewRequestRepositoryAuthorizationSerializer + queryset = RequestRepositoryAuthorization.objects + permission_classes = [ + IsAuthenticated, + ] diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index da271d598..7989879f3 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -13,6 +13,7 @@ from .repository.views import RepositoryTranslationsViewSet from .repository.views import RepositoryUpdatesViewSet from .repository.views import NewRepositoryExampleViewSet +from .repository.views import RequestAuthorizationViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -117,6 +118,10 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): router.register('repository/translations', RepositoryTranslationsViewSet) router.register('repository/updates', RepositoryUpdatesViewSet) router.register('repository/example/new', NewRepositoryExampleViewSet) +router.register( + 'repository/request-authorization', + RequestAuthorizationViewSet +) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From 0675689e293ba051c935955756455d03e1f97435 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 18:38:44 -0300 Subject: [PATCH 20/31] Added tests Request Authorization and Repository Example --- bothub/api/v2/tests/test_repository.py | 375 +++++++++++++++++++++++++ 1 file changed, 375 insertions(+) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index 1a1eacd5e..d1caceed6 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -14,6 +14,7 @@ from bothub.common.models import RequestRepositoryAuthorization from bothub.common.models import RepositoryExample from bothub.common.models import RepositoryTranslatedExample +from bothub.common.models import RepositoryUpdate from bothub.common import languages from bothub.api.v2.tests.utils import create_user_and_token @@ -31,6 +32,8 @@ from bothub.api.v2.repository.views import SearchRepositoriesViewSet from bothub.api.v2.repository.views import RepositoryTranslationsViewSet from bothub.api.v2.repository.views import RepositoryUpdatesViewSet +from bothub.api.v2.repository.views import NewRepositoryExampleViewSet +from bothub.api.v2.repository.views import RequestAuthorizationViewSet from bothub.api.v2.repository.serializers import RepositorySerializer @@ -1766,3 +1769,375 @@ def test_without_repository(self): self.assertEqual( response.status_code, status.HTTP_400_BAD_REQUEST) + + +class NewRepositoryExampleTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.user, self.user_token = create_user_and_token() + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + + def request(self, token, data): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.post( + '/v2/repository/example/new/', + json.dumps(data), + content_type='application/json', + **authorization_header) + response = NewRepositoryExampleViewSet.as_view( + {'post': 'create'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + text = 'hi' + intent = 'greet' + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': text, + 'intent': intent, + 'entities': [], + }) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + self.assertEqual( + content_data.get('text'), + text) + self.assertEqual( + content_data.get('intent'), + intent) + + def test_okay_with_language(self): + text = 'hi' + intent = 'greet' + language = languages.LANGUAGE_PT + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': text, + 'language': language, + 'intent': intent, + 'entities': [], + }) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + self.assertEqual( + content_data.get('text'), + text) + self.assertEqual( + content_data.get('intent'), + intent) + repository_update_pk = content_data.get('repository_update') + repository_update = RepositoryUpdate.objects.get( + pk=repository_update_pk) + self.assertEqual(repository_update.language, language) + + def test_forbidden(self): + response, content_data = self.request( + self.user_token, + { + 'repository': str(self.repository.uuid), + 'text': 'hi', + 'intent': 'greet', + 'entities': [], + }) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + def test_repository_uuid_required(self): + response, content_data = self.request( + self.owner_token, + { + 'text': 'hi', + 'intent': 'greet', + 'entities': [], + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + + def test_repository_does_not_exists(self): + response, content_data = self.request( + self.owner_token, + { + 'repository': str(uuid.uuid4()), + 'text': 'hi', + 'intent': 'greet', + 'entities': [], + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'repository', + content_data.keys()) + + def test_invalid_repository_uuid(self): + response, content_data = self.request( + self.owner_token, + { + 'repository': 'invalid', + 'text': 'hi', + 'intent': 'greet', + 'entities': [], + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + + def test_with_entities(self): + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': 'my name is douglas', + 'intent': 'greet', + 'entities': [ + { + 'start': 11, + 'end': 18, + 'entity': 'name', + }, + ], + }) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + self.assertEqual( + len(content_data.get('entities')), + 1) + + def test_exists_example(self): + text = 'hi' + intent = 'greet' + response_created, content_data_created = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': text, + 'intent': intent, + 'entities': [], + }) + + self.assertEqual( + response_created.status_code, + status.HTTP_201_CREATED) + + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': text, + 'intent': intent, + 'entities': [], + }) + + self.assertEqual( + content_data.get('non_field_errors')[0], + 'Intention and Sentence already exists' + ) + + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + + def test_with_entities_with_label(self): + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': 'my name is douglas', + 'intent': 'greet', + 'entities': [ + { + 'start': 11, + 'end': 18, + 'entity': 'name', + 'label': 'subject', + }, + ], + }) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + self.assertEqual( + len(content_data.get('entities')), + 1) + id = content_data.get('id') + repository_example = RepositoryExample.objects.get(id=id) + example_entity = repository_example.entities.all()[0] + self.assertIsNotNone(example_entity.entity.label) + + def test_with_entities_with_invalid_label(self): + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': 'my name is douglas', + 'intent': 'greet', + 'entities': [ + { + 'start': 11, + 'end': 18, + 'entity': 'name', + 'label': 'other', + }, + ], + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'entities', + content_data.keys()) + entities_errors = content_data.get('entities') + self.assertIn( + 'label', + entities_errors[0]) + + def test_with_entities_with_equal_label(self): + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': 'my name is douglas', + 'intent': 'greet', + 'entities': [ + { + 'start': 11, + 'end': 18, + 'entity': 'name', + 'label': 'name', + }, + ], + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'entities', + content_data.keys()) + entities_errors = content_data.get('entities') + self.assertIn( + 'label', + entities_errors[0]) + + def test_intent_or_entity_required(self): + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': 'hi', + 'intent': '', + 'entities': [], + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + + def test_entity_with_special_char(self): + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': 'my name is douglas', + 'intent': '', + 'entities': [ + { + 'start': 11, + 'end': 18, + 'entity': 'nam&', + }, + ], + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertEqual( + len(content_data.get('entities')), + 1) + + def test_intent_with_special_char(self): + response, content_data = self.request( + self.owner_token, + { + 'repository': str(self.repository.uuid), + 'text': 'my name is douglas', + 'intent': 'nam$s', + 'entities': [], + }) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertEqual( + len(content_data.get('intent')), + 1) + + +class RequestAuthorizationTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.user, self.token = create_user_and_token() + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + + def request(self, data, token=None): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } if token else {} + request = self.factory.post( + '/v2/repository/request-authorization/', + data, + **authorization_header) + response = RequestAuthorizationViewSet.as_view( + {'post': 'create'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request({ + 'repository': self.repository.uuid, + 'text': 'I can contribute', + }, self.token) + self.assertEqual( + response.status_code, + status.HTTP_201_CREATED) + + def test_forbidden_two_requests(self): + RequestRepositoryAuthorization.objects.create( + user=self.user, + repository=self.repository, + text='I can contribute') + response, content_data = self.request({ + 'repository': self.repository.uuid, + 'text': 'I can contribute', + }, self.token) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertIn( + 'non_field_errors', + content_data.keys()) From 56a8fffc230dded1611be965b9c61769e0ce2cdf Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 18:43:44 -0300 Subject: [PATCH 21/31] Migration Review Auth Request v1 to v2 and refact --- bothub/api/v1/views.py | 18 +++++++++++++++++ bothub/api/v2/repository/permissions.py | 6 ++++++ bothub/api/v2/repository/serializers.py | 19 ++++++++++++++++++ bothub/api/v2/repository/views.py | 26 +++++++++++++++++++++++++ bothub/api/v2/routers.py | 5 +++++ 5 files changed, 74 insertions(+) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 3e43a7299..372a956b2 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -1264,6 +1264,24 @@ class RepositoryAuthorizationRequestsViewSet( ] +@method_decorator( + name='update', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='partial_update', + decorator=swagger_auto_schema( + deprecated=True, + ) +) +@method_decorator( + name='destroy', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class ReviewAuthorizationRequestViewSet( mixins.UpdateModelMixin, mixins.DestroyModelMixin, diff --git a/bothub/api/v2/repository/permissions.py b/bothub/api/v2/repository/permissions.py index ce960d092..2a20e6663 100644 --- a/bothub/api/v2/repository/permissions.py +++ b/bothub/api/v2/repository/permissions.py @@ -48,3 +48,9 @@ def has_object_permission(self, request, view, obj): return authorization.can_write return authorization.is_admin return False + + +class RepositoryAdminManagerAuthorization(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + authorization = obj.repository.get_user_authorization(request.user) + return authorization.is_admin diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 214b953b6..cc7bd5b4c 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -696,3 +696,22 @@ class Meta: min_length=5, max_length=RequestRepositoryAuthorization._meta.get_field( 'text').max_length) + + +class ReviewAuthorizationRequestSerializer(serializers.ModelSerializer): + class Meta: + model = RequestRepositoryAuthorization + fields = [ + 'approved_by' + ] + ref_name = None + + approved_by = serializers.PrimaryKeyRelatedField( + read_only=True, + style={'show': False}) + + def update(self, instance, validated_data): + validated_data.update({ + 'approved_by': self.context['request'].user, + }) + return super().update(instance, validated_data) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index b8df818aa..6e9d629a2 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -1,9 +1,11 @@ from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ from django_filters.rest_framework import DjangoFilterBackend +from django.core.exceptions import ValidationError as DjangoValidationError from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework.exceptions import APIException +from rest_framework.exceptions import ValidationError from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from rest_framework import mixins, status @@ -32,10 +34,12 @@ from .serializers import RepositoryUpdateSerializer from .serializers import NewRepositoryExampleSerializer from .serializers import NewRequestRepositoryAuthorizationSerializer +from .serializers import ReviewAuthorizationRequestSerializer from .permissions import RepositoryPermission from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission from .permissions import RepositoryUpdateHasPermission +from .permissions import RepositoryAdminManagerAuthorization from .filters import RepositoriesFilter from .filters import RepositoryTranslationsFilter from .filters import RepositoryUpdatesFilter @@ -366,3 +370,25 @@ class RequestAuthorizationViewSet( permission_classes = [ IsAuthenticated, ] + + +class ReviewAuthorizationRequestViewSet( + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericViewSet): + """ + Authorizes or Removes the user who requested + authorization from a repository + """ + queryset = RequestRepositoryAuthorization.objects + serializer_class = ReviewAuthorizationRequestSerializer + permission_classes = [ + IsAuthenticated, + RepositoryAdminManagerAuthorization, + ] + + def update(self, *args, **kwargs): + try: + return super().update(*args, **kwargs) + except DjangoValidationError as e: + raise ValidationError(e.message) diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 7989879f3..179d0636f 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -14,6 +14,7 @@ from .repository.views import RepositoryUpdatesViewSet from .repository.views import NewRepositoryExampleViewSet from .repository.views import RequestAuthorizationViewSet +from .repository.views import ReviewAuthorizationRequestViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -122,6 +123,10 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): 'repository/request-authorization', RequestAuthorizationViewSet ) +router.register( + 'repository/review-authorization-request', + ReviewAuthorizationRequestViewSet +) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From 15fd28f2919d7cdc05069faf0c82ee20df3e11ca Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 18:46:49 -0300 Subject: [PATCH 22/31] Migration Authorizations v1 to v2 and refact --- bothub/api/v1/views.py | 6 ++++++ bothub/api/v2/repository/filters.py | 26 +++++++++++++++++++++++++ bothub/api/v2/repository/serializers.py | 24 +++++++++++++++++++++++ bothub/api/v2/repository/views.py | 14 +++++++++++++ bothub/api/v2/routers.py | 2 ++ 5 files changed, 72 insertions(+) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 372a956b2..a50b5003b 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -1109,6 +1109,12 @@ class TranslationsViewSet( filter_class = TranslationsFilter +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RepositoryAuthorizationViewSet( mixins.ListModelMixin, GenericViewSet): diff --git a/bothub/api/v2/repository/filters.py b/bothub/api/v2/repository/filters.py index 82c1db845..0ea5528e4 100644 --- a/bothub/api/v2/repository/filters.py +++ b/bothub/api/v2/repository/filters.py @@ -7,6 +7,7 @@ from bothub.common.models import Repository from bothub.common.models import RepositoryTranslatedExample from bothub.common.models import RepositoryUpdate +from bothub.common.models import RepositoryAuthorization class RepositoriesFilter(filters.FilterSet): @@ -94,3 +95,28 @@ def filter_repository_uuid(self, queryset, name, value): _('Repository {} does not exist').format(value)) except DjangoValidationError: raise NotFound(_('Invalid repository UUID')) + + +class RepositoryAuthorizationFilter(filters.FilterSet): + class Meta: + model = RepositoryAuthorization + fields = ['repository'] + + repository = filters.CharFilter( + field_name='repository', + method='filter_repository_uuid', + help_text=_('Repository\'s UUID')) + + def filter_repository_uuid(self, queryset, name, value): + request = self.request + try: + repository = Repository.objects.get(uuid=value) + authorization = repository.get_user_authorization(request.user) + if not authorization.is_admin: + raise PermissionDenied() + return queryset.filter(repository=repository) + except Repository.DoesNotExist: + raise NotFound( + _('Repository {} does not exist').format(value)) + except DjangoValidationError: + raise NotFound(_('Invalid repository UUID')) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index cc7bd5b4c..9308cb3a5 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -715,3 +715,27 @@ def update(self, instance, validated_data): 'approved_by': self.context['request'].user, }) return super().update(instance, validated_data) + + +class RepositoryAuthorizationSerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryAuthorization + fields = [ + 'uuid', + 'user', + 'user__nickname', + 'repository', + 'role', + 'level', + 'can_read', + 'can_contribute', + 'can_write', + 'is_admin', + 'created_at', + ] + ref_name = None + + user__nickname = serializers.SlugRelatedField( + source='user', + slug_field='nickname', + read_only=True) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 6e9d629a2..a1d2ee433 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -35,6 +35,7 @@ from .serializers import NewRepositoryExampleSerializer from .serializers import NewRequestRepositoryAuthorizationSerializer from .serializers import ReviewAuthorizationRequestSerializer +from .serializers import RepositoryAuthorizationSerializer from .permissions import RepositoryPermission from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission @@ -43,6 +44,7 @@ from .filters import RepositoriesFilter from .filters import RepositoryTranslationsFilter from .filters import RepositoryUpdatesFilter +from .filters import RepositoryAuthorizationFilter class RepositoryViewSet( @@ -392,3 +394,15 @@ def update(self, *args, **kwargs): return super().update(*args, **kwargs) except DjangoValidationError as e: raise ValidationError(e.message) + + +class RepositoryAuthorizationViewSet( + mixins.ListModelMixin, + GenericViewSet): + queryset = RepositoryAuthorization.objects.exclude( + role=RepositoryAuthorization.ROLE_NOT_SETTED) + serializer_class = RepositoryAuthorizationSerializer + filter_class = RepositoryAuthorizationFilter + permission_classes = [ + IsAuthenticated, + ] diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 179d0636f..fa8a49a17 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -15,6 +15,7 @@ from .repository.views import NewRepositoryExampleViewSet from .repository.views import RequestAuthorizationViewSet from .repository.views import ReviewAuthorizationRequestViewSet +from .repository.views import RepositoryAuthorizationViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -127,6 +128,7 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): 'repository/review-authorization-request', ReviewAuthorizationRequestViewSet ) +router.register('repository/authorizations', RepositoryAuthorizationViewSet) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From d45dda229e0be0d0f7975dd3d5b0ba6b570e4fbd Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 18:51:21 -0300 Subject: [PATCH 23/31] Migration Auth Role v1 to v2 and refact --- bothub/api/v1/views.py | 5 +- bothub/api/v2/mixins.py | 20 +++++++ bothub/api/v2/repository/serializers.py | 15 +++++ bothub/api/v2/repository/views.py | 80 +++++++++++++++++++++++++ bothub/api/v2/routers.py | 5 ++ 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 bothub/api/v2/mixins.py diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index a50b5003b..98581c139 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -1145,7 +1145,7 @@ class RepositoryAuthorizationViewSet( type=openapi.TYPE_STRING, required=True ), - ] + ], deprecated=True ) ) @method_decorator( @@ -1166,7 +1166,8 @@ class RepositoryAuthorizationViewSet( type=openapi.TYPE_STRING, required=True ), - ] + ], + deprecated=True ) ) class RepositoryAuthorizationRoleViewSet( diff --git a/bothub/api/v2/mixins.py b/bothub/api/v2/mixins.py new file mode 100644 index 000000000..420659ddf --- /dev/null +++ b/bothub/api/v2/mixins.py @@ -0,0 +1,20 @@ +from django.shortcuts import get_object_or_404 + + +class MultipleFieldLookupMixin(object): + """ + Apply this mixin to any view or viewset to get multiple field filtering + based on a `lookup_fields` attribute, instead of the default single field + filtering. + """ + + def get_object(self): + queryset = self.get_queryset() + queryset = self.filter_queryset(queryset) + filter = {} + for field in self.lookup_fields: + if self.kwargs.get(field): + filter[field] = self.kwargs[field] + obj = get_object_or_404(queryset, **filter) + self.check_object_permissions(self.request, obj) + return obj diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 9308cb3a5..970760316 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -11,6 +11,7 @@ TranslatedExampleLanguageValidator from bothub.api.v2.repository.validators import \ CanContributeInRepositoryExampleValidator +from rest_framework.exceptions import PermissionDenied from bothub.common.models import Repository, RepositoryTranslatedExampleEntity from bothub.common.models import RepositoryTranslatedExample from bothub.common.models import RepositoryExample @@ -739,3 +740,17 @@ class Meta: source='user', slug_field='nickname', read_only=True) + + +class RepositoryAuthorizationRoleSerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryAuthorization + fields = [ + 'role', + ] + ref_name = None + + def validate(self, data): + if self.instance.user == self.instance.repository.owner: + raise PermissionDenied(_('The owner role can\'t be changed.')) + return data diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index a1d2ee433..8ab3e8a91 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -1,6 +1,7 @@ from django.utils.decorators import method_decorator from django.utils.translation import gettext as _ from django_filters.rest_framework import DjangoFilterBackend +from django.shortcuts import get_object_or_404 from django.core.exceptions import ValidationError as DjangoValidationError from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema @@ -12,6 +13,8 @@ from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.permissions import IsAuthenticated from rest_framework.filters import SearchFilter + +from bothub.api.v2.mixins import MultipleFieldLookupMixin from bothub.common.models import Repository from bothub.common.models import RepositoryCategory from bothub.common.models import RepositoryVote @@ -36,6 +39,7 @@ from .serializers import NewRequestRepositoryAuthorizationSerializer from .serializers import ReviewAuthorizationRequestSerializer from .serializers import RepositoryAuthorizationSerializer +from .serializers import RepositoryAuthorizationRoleSerializer from .permissions import RepositoryPermission from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission @@ -406,3 +410,79 @@ class RepositoryAuthorizationViewSet( permission_classes = [ IsAuthenticated, ] + + +@method_decorator( + name='update', + decorator=swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + 'repository__uuid', + openapi.IN_PATH, + description='Repository UUID', + type=openapi.TYPE_STRING, + required=True + ), + openapi.Parameter( + 'user__nickname', + openapi.IN_QUERY, + description='Nickname User', + type=openapi.TYPE_STRING, + required=True + ), + ] + ) +) +@method_decorator( + name='partial_update', + decorator=swagger_auto_schema( + manual_parameters=[ + openapi.Parameter( + 'repository__uuid', + openapi.IN_PATH, + description='Repository UUID', + type=openapi.TYPE_STRING, + required=True + ), + openapi.Parameter( + 'user__nickname', + openapi.IN_QUERY, + description='Nickname User', + type=openapi.TYPE_STRING, + required=True + ), + ] + ) +) +class RepositoryAuthorizationRoleViewSet( + MultipleFieldLookupMixin, + mixins.UpdateModelMixin, + GenericViewSet): + queryset = RepositoryAuthorization.objects.exclude( + role=RepositoryAuthorization.ROLE_NOT_SETTED) + lookup_field = 'user__nickname' + lookup_fields = ['repository__uuid', 'user__nickname'] + serializer_class = RepositoryAuthorizationRoleSerializer + permission_classes = [ + IsAuthenticated, + RepositoryAdminManagerAuthorization, + ] + + def get_object(self): + repository_uuid = self.kwargs.get('repository__uuid') + user_nickname = self.kwargs.get('user__nickname') + + repository = get_object_or_404(Repository, uuid=repository_uuid) + user = get_object_or_404(User, nickname=user_nickname) + + obj = repository.get_user_authorization(user) + + self.check_object_permissions(self.request, obj) + return obj + + def update(self, *args, **kwargs): + response = super().update(*args, **kwargs) + instance = self.get_object() + if instance.role is not RepositoryAuthorization.ROLE_NOT_SETTED: + instance.send_new_role_email(self.request.user) + return response diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index fa8a49a17..1361a8202 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -16,6 +16,7 @@ from .repository.views import RequestAuthorizationViewSet from .repository.views import ReviewAuthorizationRequestViewSet from .repository.views import RepositoryAuthorizationViewSet +from .repository.views import RepositoryAuthorizationRoleViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -129,6 +130,10 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): ReviewAuthorizationRequestViewSet ) router.register('repository/authorizations', RepositoryAuthorizationViewSet) +router.register( + 'repository/authorization-role', + RepositoryAuthorizationRoleViewSet +) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From 561e16c844826da7c0d19cd0b75a266ba6f0eb9e Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 18:54:40 -0300 Subject: [PATCH 24/31] Migration Repository Auth Requests v1 to v2 and refact --- bothub/api/v1/views.py | 6 ++++++ bothub/api/v2/repository/filters.py | 27 +++++++++++++++++++++++++ bothub/api/v2/repository/serializers.py | 25 +++++++++++++++++++++++ bothub/api/v2/repository/views.py | 18 +++++++++++++++++ bothub/api/v2/routers.py | 5 +++++ 5 files changed, 81 insertions(+) diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 98581c139..aaa4f3e07 100644 --- a/bothub/api/v1/views.py +++ b/bothub/api/v1/views.py @@ -1256,6 +1256,12 @@ class RequestAuthorizationViewSet( ] +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RepositoryAuthorizationRequestsViewSet( mixins.ListModelMixin, GenericViewSet): diff --git a/bothub/api/v2/repository/filters.py b/bothub/api/v2/repository/filters.py index 0ea5528e4..45cf30fc6 100644 --- a/bothub/api/v2/repository/filters.py +++ b/bothub/api/v2/repository/filters.py @@ -8,6 +8,7 @@ from bothub.common.models import RepositoryTranslatedExample from bothub.common.models import RepositoryUpdate from bothub.common.models import RepositoryAuthorization +from bothub.common.models import RequestRepositoryAuthorization class RepositoriesFilter(filters.FilterSet): @@ -120,3 +121,29 @@ def filter_repository_uuid(self, queryset, name, value): _('Repository {} does not exist').format(value)) except DjangoValidationError: raise NotFound(_('Invalid repository UUID')) + + +class RepositoryAuthorizationRequestsFilter(filters.FilterSet): + class Meta: + model = RequestRepositoryAuthorization + fields = ['repository_uuid'] + + repository_uuid = filters.CharFilter( + field_name='repository_uuid', + required=True, + method='filter_repository_uuid', + help_text=_('Repository\'s UUID')) + + def filter_repository_uuid(self, queryset, name, value): + request = self.request + try: + repository = Repository.objects.get(uuid=value) + authorization = repository.get_user_authorization(request.user) + if not authorization.is_admin: + raise PermissionDenied() + return queryset.filter(repository=repository) + except Repository.DoesNotExist: + raise NotFound( + _('Repository {} does not exist').format(value)) + except DjangoValidationError: + raise NotFound(_('Invalid repository UUID')) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 970760316..6a0f9ccec 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -754,3 +754,28 @@ def validate(self, data): if self.instance.user == self.instance.repository.owner: raise PermissionDenied(_('The owner role can\'t be changed.')) return data + + +class RequestRepositoryAuthorizationSerializer(serializers.ModelSerializer): + class Meta: + model = RequestRepositoryAuthorization + fields = [ + 'id', + 'user', + 'user__nickname', + 'repository', + 'text', + 'approved_by', + 'approved_by__nickname', + 'created_at', + ] + ref_name = None + + user__nickname = serializers.SlugRelatedField( + source='user', + slug_field='nickname', + read_only=True) + approved_by__nickname = serializers.SlugRelatedField( + source='approved_by', + slug_field='nickname', + read_only=True) diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 8ab3e8a91..795115c0a 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -15,6 +15,7 @@ from rest_framework.filters import SearchFilter from bothub.api.v2.mixins import MultipleFieldLookupMixin +from bothub.authentication.models import User from bothub.common.models import Repository from bothub.common.models import RepositoryCategory from bothub.common.models import RepositoryVote @@ -40,6 +41,7 @@ from .serializers import ReviewAuthorizationRequestSerializer from .serializers import RepositoryAuthorizationSerializer from .serializers import RepositoryAuthorizationRoleSerializer +from .serializers import RequestRepositoryAuthorizationSerializer from .permissions import RepositoryPermission from .permissions import RepositoryTranslatedExamplePermission from .permissions import RepositoryExamplePermission @@ -49,6 +51,7 @@ from .filters import RepositoryTranslationsFilter from .filters import RepositoryUpdatesFilter from .filters import RepositoryAuthorizationFilter +from .filters import RepositoryAuthorizationRequestsFilter class RepositoryViewSet( @@ -486,3 +489,18 @@ def update(self, *args, **kwargs): if instance.role is not RepositoryAuthorization.ROLE_NOT_SETTED: instance.send_new_role_email(self.request.user) return response + + +class RepositoryAuthorizationRequestsViewSet( + mixins.ListModelMixin, + GenericViewSet): + """ + List of all authorization requests for a repository + """ + queryset = RequestRepositoryAuthorization.objects.exclude( + approved_by__isnull=False) + serializer_class = RequestRepositoryAuthorizationSerializer + filter_class = RepositoryAuthorizationRequestsFilter + permission_classes = [ + IsAuthenticated, + ] diff --git a/bothub/api/v2/routers.py b/bothub/api/v2/routers.py index 1361a8202..b9b1f67c3 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -17,6 +17,7 @@ from .repository.views import ReviewAuthorizationRequestViewSet from .repository.views import RepositoryAuthorizationViewSet from .repository.views import RepositoryAuthorizationRoleViewSet +from .repository.views import RepositoryAuthorizationRequestsViewSet from .examples.views import ExamplesViewSet from .evaluate.views import EvaluateViewSet from .evaluate.views import ResultsListViewSet @@ -134,6 +135,10 @@ def get_lookup_regex(self, viewset, lookup_prefix=''): 'repository/authorization-role', RepositoryAuthorizationRoleViewSet ) +router.register( + 'repository/authorization-requests', + RepositoryAuthorizationRequestsViewSet +) router.register('account/login', LoginViewSet) router.register('account/register', RegisterUserViewSet) router.register('account/change-password', ChangePasswordViewSet) From 9679d56d12738ec1cf4c3d7fa7730369ee16eecb Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 19:04:59 -0300 Subject: [PATCH 25/31] PEP8 --- bothub/api/v2/repository/serializers.py | 33 +-- bothub/api/v2/request/__init__.py | 0 bothub/api/v2/request/serializers.py | 28 -- bothub/api/v2/tests/test_repository.py | 332 ++++++++++++++++++++++++ 4 files changed, 333 insertions(+), 60 deletions(-) delete mode 100644 bothub/api/v2/request/__init__.py delete mode 100644 bothub/api/v2/request/serializers.py diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 6a0f9ccec..21067f84a 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -23,7 +23,6 @@ from bothub.common.models import RepositoryUpdate from bothub.common.models import RepositoryExampleEntity from bothub.common.languages import LANGUAGE_CHOICES -from ..request.serializers import RequestRepositoryAuthorizationSerializer from ..fields import ModelMultipleChoiceField from ..fields import TextField from ..fields import EntityValueField @@ -34,6 +33,7 @@ from .validators import ExampleWithIntentOrEntityValidator from .validators import IntentAndSentenceNotExistsValidator + class RepositoryCategorySerializer(serializers.ModelSerializer): class Meta: model = RepositoryCategory @@ -78,37 +78,6 @@ class IntentSerializer(serializers.Serializer): examples__count = serializers.IntegerField() -class RepositoryAuthorizationSerializer(serializers.ModelSerializer): - class Meta: - model = RepositoryAuthorization - fields = [ - 'uuid', - 'user', - 'user__nickname', - 'repository', - 'role', - 'level', - 'can_read', - 'can_contribute', - 'can_write', - 'is_admin', - 'created_at', - ] - read_only = [ - 'user', - 'user__nickname', - 'repository', - 'role', - 'created_at', - ] - ref_name = None - - user__nickname = serializers.SlugRelatedField( - source='user', - slug_field='nickname', - read_only=True) - - class RepositorySerializer(serializers.ModelSerializer): class Meta: model = Repository diff --git a/bothub/api/v2/request/__init__.py b/bothub/api/v2/request/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/bothub/api/v2/request/serializers.py b/bothub/api/v2/request/serializers.py deleted file mode 100644 index ea16940b3..000000000 --- a/bothub/api/v2/request/serializers.py +++ /dev/null @@ -1,28 +0,0 @@ -from rest_framework import serializers - -from bothub.common.models import RequestRepositoryAuthorization - - -class RequestRepositoryAuthorizationSerializer(serializers.ModelSerializer): - class Meta: - model = RequestRepositoryAuthorization - fields = [ - 'id', - 'user', - 'user__nickname', - 'repository', - 'text', - 'approved_by', - 'approved_by__nickname', - 'created_at', - ] - ref_name = None - - user__nickname = serializers.SlugRelatedField( - source='user', - slug_field='nickname', - read_only=True) - approved_by__nickname = serializers.SlugRelatedField( - source='approved_by', - slug_field='nickname', - read_only=True) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index d1caceed6..e17c5e6e0 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -34,6 +34,11 @@ from bothub.api.v2.repository.views import RepositoryUpdatesViewSet from bothub.api.v2.repository.views import NewRepositoryExampleViewSet from bothub.api.v2.repository.views import RequestAuthorizationViewSet +from bothub.api.v2.repository.views import ReviewAuthorizationRequestViewSet +from bothub.api.v2.repository.views import RepositoryAuthorizationViewSet +from bothub.api.v2.repository.views import RepositoryAuthorizationRoleViewSet +from bothub.api.v2.repository.views import \ + RepositoryAuthorizationRequestsViewSet from bothub.api.v2.repository.serializers import RepositorySerializer @@ -2141,3 +2146,330 @@ def test_forbidden_two_requests(self): self.assertIn( 'non_field_errors', content_data.keys()) + + +class ReviewAuthorizationRequestTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.admin, self.admin_token = create_user_and_token('admin') + self.user, self.user_token = create_user_and_token() + + repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + + self.ra = RequestRepositoryAuthorization.objects.create( + user=self.user, + repository=repository, + text='I can contribute') + + admin_autho = repository.get_user_authorization(self.admin) + admin_autho.role = RepositoryAuthorization.ROLE_ADMIN + admin_autho.save() + + def request_approve(self, ra, token=None): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } if token else {} + request = self.factory.put( + '/v2/repository/review-authorization-request/{}/'.format(ra.pk), + self.factory._encode_data({}, MULTIPART_CONTENT), + MULTIPART_CONTENT, + **authorization_header) + response = ReviewAuthorizationRequestViewSet.as_view( + {'put': 'update'})(request, pk=ra.pk) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def request_reject(self, ra, token=None): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } if token else {} + request = self.factory.delete( + '/v1/review-authorization-request/{}/'.format(ra.pk), + **authorization_header) + response = ReviewAuthorizationRequestViewSet.as_view( + {'delete': 'destroy'})(request, pk=ra.pk) + response.render() + return response + + def test_approve_okay(self): + response, content_data = self.request_approve( + self.ra, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('approved_by'), + self.owner.id) + + def test_admin_approve_okay(self): + response, content_data = self.request_approve( + self.ra, + self.admin_token) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('approved_by'), + self.admin.id) + + def test_approve_twice(self): + self.ra.approved_by = self.owner + self.ra.save() + response, content_data = self.request_approve( + self.ra, + self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + + def test_approve_forbidden(self): + response, content_data = self.request_approve( + self.ra, + self.user_token) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + def test_reject_okay(self): + response = self.request_reject(self.ra, self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_204_NO_CONTENT) + + def test_admin_reject_okay(self): + response = self.request_reject(self.ra, self.admin_token) + self.assertEqual( + response.status_code, + status.HTTP_204_NO_CONTENT) + + def test_reject_forbidden(self): + response = self.request_reject(self.ra, self.user_token) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + +class ListAuthorizationTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.user, self.user_token = create_user_and_token() + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + + self.user_auth = self.repository.get_user_authorization(self.user) + self.user_auth.role = RepositoryAuthorization.ROLE_CONTRIBUTOR + self.user_auth.save() + + def request(self, repository, token): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.get( + '/v2/repository/authorizations/', + { + 'repository': repository.uuid, + }, + **authorization_header) + response = RepositoryAuthorizationViewSet.as_view( + {'get': 'list'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request( + self.repository, + self.owner_token) + + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + + self.assertEqual( + content_data.get('count'), + 1) + + self.assertEqual( + content_data.get('results')[0].get('user'), + self.user.id) + + def test_user_forbidden(self): + response, content_data = self.request( + self.repository, + self.user_token) + + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + +class UpdateAuthorizationRoleTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.user, self.user_token = create_user_and_token() + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + + def request(self, repository, token, user, data): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } + request = self.factory.patch( + '/v2/repository/authorization-role/{}/{}/'.format( + repository.uuid, user.nickname), + self.factory._encode_data(data, MULTIPART_CONTENT), + MULTIPART_CONTENT, + **authorization_header) + view = RepositoryAuthorizationRoleViewSet.as_view( + {'patch': 'update'}) + response = view( + request, + repository__uuid=repository.uuid, + user__nickname=user.nickname) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request( + self.repository, + self.owner_token, + self.user, + { + 'role': RepositoryAuthorization.ROLE_CONTRIBUTOR, + }) + + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('role'), + RepositoryAuthorization.ROLE_CONTRIBUTOR) + + user_authorization = self.repository.get_user_authorization(self.user) + self.assertEqual( + user_authorization.role, + RepositoryAuthorization.ROLE_CONTRIBUTOR) + + def test_forbidden(self): + response, content_data = self.request( + self.repository, + self.user_token, + self.user, + { + 'role': RepositoryAuthorization.ROLE_CONTRIBUTOR, + }) + + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + def test_owner_can_t_set_your_role(self): + response, content_data = self.request( + self.repository, + self.owner_token, + self.owner, + { + 'role': RepositoryAuthorization.ROLE_CONTRIBUTOR, + }) + + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) + + +class RepositoryAuthorizationRequestsTestCase(TestCase): + def setUp(self): + self.factory = RequestFactory() + + self.owner, self.owner_token = create_user_and_token('owner') + self.admin, self.admin_token = create_user_and_token('admin') + self.user, self.user_token = create_user_and_token() + + self.repository = Repository.objects.create( + owner=self.owner, + name='Testing', + slug='test', + language=languages.LANGUAGE_EN) + + RequestRepositoryAuthorization.objects.create( + user=self.user, + repository=self.repository, + text='I can contribute') + + admin_autho = self.repository.get_user_authorization(self.admin) + admin_autho.role = RepositoryAuthorization.ROLE_ADMIN + admin_autho.save() + + def request(self, data, token=None): + authorization_header = { + 'HTTP_AUTHORIZATION': 'Token {}'.format(token.key), + } if token else {} + request = self.factory.get( + '/v2/repository/authorization-requests/', + data, + **authorization_header) + response = RepositoryAuthorizationRequestsViewSet.as_view( + {'get': 'list'})(request) + response.render() + content_data = json.loads(response.content) + return (response, content_data,) + + def test_okay(self): + response, content_data = self.request({ + 'repository_uuid': self.repository.uuid, + }, self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('count'), + 1) + + def test_admin_okay(self): + response, content_data = self.request({ + 'repository_uuid': self.repository.uuid, + }, self.admin_token) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + self.assertEqual( + content_data.get('count'), + 1) + + def test_repository_uuid_empty(self): + response, content_data = self.request({}, self.owner_token) + self.assertEqual( + response.status_code, + status.HTTP_400_BAD_REQUEST) + self.assertEqual( + len(content_data.get('repository_uuid')), + 1) + + def test_forbidden(self): + response, content_data = self.request({ + 'repository_uuid': self.repository.uuid, + }, self.user_token) + self.assertEqual( + response.status_code, + status.HTTP_403_FORBIDDEN) From 8abeabebf69e2a8b1e0b3bc4acc69f8d7717a687 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 23:26:59 -0300 Subject: [PATCH 26/31] Added test without nickname ListRepositoryContributionsTestCase --- bothub/api/v2/tests/test_repository.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index e17c5e6e0..f918d3a9b 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -848,22 +848,22 @@ def setUp(self): role=0 ) - def request(self): + def request(self, nickname): request = self.factory.get( '/v2/repository/repositories-contributions/?nickname={}'.format( - self.user.nickname + nickname ) ) response = RepositoriesContributionsViewSet.as_view({'get': 'list'})( request, - nickname=self.user.nickname + nickname=nickname ) response.render() content_data = json.loads(response.content) return (response, content_data,) def test_okay(self): - response, content_data = self.request() + response, content_data = self.request(self.user.nickname) self.assertEqual( response.status_code, status.HTTP_200_OK @@ -877,6 +877,21 @@ def test_okay(self): 1 ) + def test_without_nickname(self): + response, content_data = self.request('') + self.assertEqual( + response.status_code, + status.HTTP_200_OK + ) + self.assertEqual( + content_data['count'], + 0 + ) + self.assertEqual( + len(content_data['results']), + 0 + ) + class ListRepositoryCategoriesTestCase(TestCase): def setUp(self): From c359c78eb28bd3612cdd63ade4a1c907ee70809c Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 23:30:20 -0300 Subject: [PATCH 27/31] Added test without nickname Contributions --- bothub/api/v2/tests/test_repository.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index f918d3a9b..736c50a70 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -883,14 +883,8 @@ def test_without_nickname(self): response.status_code, status.HTTP_200_OK ) - self.assertEqual( - content_data['count'], - 0 - ) - self.assertEqual( - len(content_data['results']), - 0 - ) + self.assertEqual(content_data['count'], 0) + self.assertEqual(len(content_data['results']), 0) class ListRepositoryCategoriesTestCase(TestCase): From 5ec230f9fa2a29ae7d001d792e8445058f8361ad Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 23:42:05 -0300 Subject: [PATCH 28/31] Added test Repository Votes empty --- bothub/api/v2/tests/test_repository.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/bothub/api/v2/tests/test_repository.py b/bothub/api/v2/tests/test_repository.py index 736c50a70..685b47a67 100644 --- a/bothub/api/v2/tests/test_repository.py +++ b/bothub/api/v2/tests/test_repository.py @@ -639,6 +639,13 @@ def setUp(self): language=languages.LANGUAGE_EN ) + self.repository_empty = Repository.objects.create( + owner=self.owner, + name='Testing_empty', + slug='test_empty', + language=languages.LANGUAGE_EN + ) + self.repository_votes = RepositoryVote.objects.create( user=self.owner, repository=self.repository @@ -708,6 +715,18 @@ def test_private_user_okay(self): response.status_code, status.HTTP_401_UNAUTHORIZED) + def test_repository_empty(self): + response, content_data = self.request( + 'repository', + self.repository_empty.uuid, + self.owner_token.key + ) + self.assertEqual(content_data['count'], 0) + self.assertEqual(len(content_data['results']), 0) + self.assertEqual( + response.status_code, + status.HTTP_200_OK) + class NewRepositoryVoteTestCase(TestCase): def setUp(self): From 44093dd6fd3d6821739f760da85a9074ad90ebe5 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Tue, 23 Jul 2019 23:49:32 -0300 Subject: [PATCH 29/31] Pragma no cover --- bothub/common/models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bothub/common/models.py b/bothub/common/models.py index b06e7c5fb..1f5ba2ab4 100644 --- a/bothub/common/models.py +++ b/bothub/common/models.py @@ -184,7 +184,7 @@ class Meta: @classmethod def request_nlp_train(cls, user_authorization): - try: + try: # pragma: no cover r = requests.post( # pragma: no cover cls.nlp_train_url, data={}, @@ -344,7 +344,7 @@ def __str__(self): self.name, self.owner.nickname, self.slug, - ) + ) # pragma: no cover def examples(self, language=None, exclude_deleted=True, queryset=None): if queryset is None: @@ -368,7 +368,7 @@ def evaluations(self, language=None, exclude_deleted=True, queryset=None): repository_update__language=language) if exclude_deleted: return query.exclude(deleted_in__isnull=False) - return query + return query # pragma: no cover def evaluations_results(self, queryset=None): if queryset is None: @@ -515,9 +515,9 @@ def examples(self): def requirements_to_train(self): try: self.validate_init_train() - except RepositoryUpdateAlreadyTrained: + except RepositoryUpdateAlreadyTrained: # pragma: no cover return [_('This bot version has already been trained.')] - except RepositoryUpdateAlreadyStartedTraining: + except RepositoryUpdateAlreadyStartedTraining: # pragma: no cover return [_('This bot version is being trained.')] r = [] @@ -557,7 +557,7 @@ def requirements_to_train(self): @property def ready_for_train(self): if self.training_started_at: - return False + return False # pragma: no cover if len(self.requirements_to_train) > 0: return False @@ -608,7 +608,7 @@ def use_language_model_featurizer(self): return self.algorithm != Repository.ALGORITHM_NEURAL_NETWORK_INTERNAL def __str__(self): - return 'Repository Update #{}'.format(self.id) + return 'Repository Update #{}'.format(self.id) # pragma: no cover def validate_init_train(self, by=None): if self.trained_at: From 68f1f2d777f47bc97729b2be699b0aaea4b2da39 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Wed, 24 Jul 2019 08:47:52 -0300 Subject: [PATCH 30/31] [fix] pyparsing version 2.4.0 --- Pipfile | 1 + Pipfile.lock | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Pipfile b/Pipfile index 4ef22d6f1..1fdfd560f 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,7 @@ coreapi = "==2.3.3" whitenoise = "==4.1.2" pytz = "==2018.7" drf-yasg = "*" +pyparsing = "==2.4.0" [dev-packages] "flake8" = "*" diff --git a/Pipfile.lock b/Pipfile.lock index a6675b6f0..cefcddb21 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "648422adf916fcfeff0b93f66fb660e85da82a5fcf4944bc8d53b7cdf4eb5b82" + "sha256": "a92fbb3645f78bc49a73bd28314ee510bb5f330917059e9572744d24dcdd1852" }, "pipfile-spec": 6, "requires": { @@ -152,6 +152,14 @@ ], "version": "==1.1.1" }, + "pyparsing": { + "hashes": [ + "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", + "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + ], + "index": "pypi", + "version": "==2.4.0" + }, "python-decouple": { "hashes": [ "sha256:1317df14b43efee4337a4aa02914bf004f010cd56d6c4bd894e6474ec8c4fe2d" @@ -408,10 +416,11 @@ }, "pyparsing": { "hashes": [ - "sha256:530d8bf8cc93a34019d08142593cf4d78a05c890da8cf87ffa3120af53772238", - "sha256:f78e99616b6f1a4745c0580e170251ef1bbafc0d0513e270c4bd281bf29d2800" + "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", + "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" ], - "version": "==2.4.1" + "index": "pypi", + "version": "==2.4.0" }, "six": { "hashes": [ From 46beb28a7a1c2570ff25aaed0be5cf3139390628 Mon Sep 17 00:00:00 2001 From: Daniel Yohan Date: Wed, 24 Jul 2019 08:58:12 -0300 Subject: [PATCH 31/31] Coveralls --- bothub/api/v2/metadata.py | 8 ++++---- bothub/api/v2/mixins.py | 2 +- bothub/api/v2/views.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bothub/api/v2/metadata.py b/bothub/api/v2/metadata.py index 6a0f707ca..7b431c551 100644 --- a/bothub/api/v2/metadata.py +++ b/bothub/api/v2/metadata.py @@ -42,7 +42,7 @@ class Metadata(BaseMetadata): EntityText: 'entity text', }) - def determine_metadata(self, request, view): + def determine_metadata(self, request, view): # pragma: no cover metadata = OrderedDict() metadata['name'] = view.get_view_name() metadata['description'] = view.get_view_description() @@ -60,7 +60,7 @@ def determine_metadata(self, request, view): metadata['actions'] = actions return metadata - def determine_actions(self, request, view): + def determine_actions(self, request, view): # pragma: no cover actions = {} for method in {'PUT', 'POST'} & set(view.allowed_methods): serializer = view.get_serializer() @@ -68,7 +68,7 @@ def determine_actions(self, request, view): view.request = request return actions - def get_serializer_info(self, serializer): + def get_serializer_info(self, serializer): # pragma: no cover if hasattr(serializer, 'child'): serializer = serializer.child return OrderedDict([ @@ -76,7 +76,7 @@ def get_serializer_info(self, serializer): for field_name, field in serializer.fields.items() ]) - def get_field_info(self, field): + def get_field_info(self, field): # pragma: no cover field_info = OrderedDict() field_info['type'] = self.label_lookup[field] or 'field' field_info['required'] = getattr(field, 'required', False) diff --git a/bothub/api/v2/mixins.py b/bothub/api/v2/mixins.py index 420659ddf..099f7c75d 100644 --- a/bothub/api/v2/mixins.py +++ b/bothub/api/v2/mixins.py @@ -8,7 +8,7 @@ class MultipleFieldLookupMixin(object): filtering. """ - def get_object(self): + def get_object(self): # pragma: no cover queryset = self.get_queryset() queryset = self.filter_queryset(queryset) filter = {} diff --git a/bothub/api/v2/views.py b/bothub/api/v2/views.py index dbf0b5aca..f757a426e 100644 --- a/bothub/api/v2/views.py +++ b/bothub/api/v2/views.py @@ -4,6 +4,6 @@ from bothub.common.models import Repository -def repository_shortcut(self, **kwargs): +def repository_shortcut(self, **kwargs): # pragma: no cover repository = get_object_or_404(Repository, **kwargs) return redirect('repository-detail', uuid=repository.uuid)