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": [ diff --git a/bothub/api/v1/views.py b/bothub/api/v1/views.py index 8f15eb626..aaa4f3e07 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): @@ -415,7 +421,8 @@ def create(self, request, *args, **kwargs): description='Nickname User to find repositories', type=openapi.TYPE_STRING ), - ] + ], + deprecated=True ) ) class SearchRepositoriesViewSet( @@ -443,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, @@ -473,6 +504,12 @@ class RepositoryViewSet( RepositoryPermission, ] + @method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) + ) @action( detail=True, methods=['GET'], @@ -533,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'], @@ -635,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): @@ -652,6 +701,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, @@ -682,6 +749,12 @@ def perform_destroy(self, obj): obj.delete() +@method_decorator( + name='create', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class NewRepositoryTranslatedExampleViewSet( mixins.CreateModelMixin, GenericViewSet): @@ -693,6 +766,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, @@ -952,6 +1049,12 @@ class UserProfileViewSet( lookup_field = 'nickname' +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True + ), +) class Categories( mixins.ListModelMixin, GenericViewSet): @@ -989,6 +1092,12 @@ class RepositoriesViewSet( ] +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class TranslationsViewSet( mixins.ListModelMixin, GenericViewSet): @@ -1000,6 +1109,12 @@ class TranslationsViewSet( filter_class = TranslationsFilter +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RepositoryAuthorizationViewSet( mixins.ListModelMixin, GenericViewSet): @@ -1030,7 +1145,7 @@ class RepositoryAuthorizationViewSet( type=openapi.TYPE_STRING, required=True ), - ] + ], deprecated=True ) ) @method_decorator( @@ -1051,7 +1166,8 @@ class RepositoryAuthorizationViewSet( type=openapi.TYPE_STRING, required=True ), - ] + ], + deprecated=True ) ) class RepositoryAuthorizationRoleViewSet( @@ -1121,6 +1237,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): @@ -1134,6 +1256,12 @@ class RequestAuthorizationViewSet( ] +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RepositoryAuthorizationRequestsViewSet( mixins.ListModelMixin, GenericViewSet): @@ -1149,6 +1277,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, @@ -1189,6 +1335,12 @@ class RepositoryEntitiesViewSet( ] +@method_decorator( + name='list', + decorator=swagger_auto_schema( + deprecated=True, + ) +) class RepositoryUpdatesViewSet( mixins.ListModelMixin, GenericViewSet): 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 new file mode 100644 index 000000000..099f7c75d --- /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): # pragma: no cover + 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/filters.py b/bothub/api/v2/repository/filters.py index 99d8651b0..45cf30fc6 100644 --- a/bothub/api/v2/repository/filters.py +++ b/bothub/api/v2/repository/filters.py @@ -1,7 +1,14 @@ 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 +from bothub.common.models import RepositoryUpdate +from bothub.common.models import RepositoryAuthorization +from bothub.common.models import RequestRepositoryAuthorization class RepositoriesFilter(filters.FilterSet): @@ -19,3 +26,124 @@ 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) + + +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')) + + +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')) + + +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/permissions.py b/bothub/api/v2/repository/permissions.py index 9b3b006f0..2a20e6663 100644 --- a/bothub/api/v2/repository/permissions.py +++ b/bothub/api/v2/repository/permissions.py @@ -18,3 +18,39 @@ 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 + + +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 + + +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 + + +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 94232b759..21067f84a 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -1,13 +1,37 @@ +from django.utils.translation import gettext as _ from rest_framework import serializers - -from bothub.common.models import Repository +from rest_framework.fields import empty + +from bothub.api.v2.example.serializers import RepositoryExampleEntitySerializer +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 rest_framework.exceptions import PermissionDenied +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 from bothub.common.models import RepositoryCategory 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.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): @@ -54,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 @@ -328,3 +321,430 @@ class Meta: 'created_at', ] ref_name = None + + +class RepositoryCategorySerializer(serializers.ModelSerializer): + class Meta: + model = RepositoryCategory + fields = [ + 'id', + 'name', + '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) + + +class RepositoryTranslatedExampleEntitySeralizer(serializers.ModelSerializer): + class Meta: + model = RepositoryTranslatedExampleEntity + fields = [ + 'id', + 'repository_translated_example', + 'start', + 'end', + 'entity', + 'created_at', + 'value', + ] + ref_name = None + + 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 + 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 + + +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 + + +class NewRepositoryTranslatedExampleEntitySeralizer( + serializers.ModelSerializer): + class Meta: + model = RepositoryTranslatedExampleEntity + fields = [ + 'start', + 'end', + 'entity', + ] + ref_name = None + + 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 + + +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) + + +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 + + +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) + + +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) + + +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 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 + + +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/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 f4379cdc9..795115c0a 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -1,24 +1,57 @@ 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 +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 from rest_framework.permissions import IsAuthenticatedOrReadOnly -from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.permissions import IsAuthenticated 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 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 bothub.common.models import RequestRepositoryAuthorization from ..metadata import Metadata from .serializers import RepositorySerializer from .serializers import RepositoryContributionsSerializer from .serializers import RepositoryVotesSerializer +from .serializers import RepositoryCategorySerializer from .serializers import ShortRepositorySerializer +from .serializers import NewRepositorySerializer +from .serializers import RepositoryTranslatedExampleSerializer +from .serializers import RepositoryExampleSerializer +from .serializers import NewRepositoryTranslatedExampleSerializer +from .serializers import RepositoryUpdateSerializer +from .serializers import NewRepositoryExampleSerializer +from .serializers import NewRequestRepositoryAuthorizationSerializer +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 +from .permissions import RepositoryUpdateHasPermission +from .permissions import RepositoryAdminManagerAuthorization from .filters import RepositoriesFilter +from .filters import RepositoryTranslationsFilter +from .filters import RepositoryUpdatesFilter +from .filters import RepositoryAuthorizationFilter +from .filters import RepositoryAuthorizationRequestsFilter class RepositoryViewSet( @@ -164,3 +197,310 @@ 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 + + +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) + + +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, + ] + + +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() + + +@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): + """ + 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() + + +class NewRepositoryTranslatedExampleViewSet( + mixins.CreateModelMixin, + GenericViewSet): + """ + Translate example + """ + 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 + + +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, + ] + + +class NewRepositoryExampleViewSet( + mixins.CreateModelMixin, + GenericViewSet): + """ + Create new repository example. + """ + 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, + ] + + +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) + + +class RepositoryAuthorizationViewSet( + mixins.ListModelMixin, + GenericViewSet): + queryset = RepositoryAuthorization.objects.exclude( + role=RepositoryAuthorization.ROLE_NOT_SETTED) + serializer_class = RepositoryAuthorizationSerializer + filter_class = RepositoryAuthorizationFilter + 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 + + +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/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/routers.py b/bothub/api/v2/routers.py index 80147f87f..b9b1f67c3 100644 --- a/bothub/api/v2/routers.py +++ b/bothub/api/v2/routers.py @@ -4,6 +4,20 @@ from .repository.views import RepositoryVotesViewSet from .repository.views import RepositoriesViewSet from .repository.views import RepositoriesContributionsViewSet +from .repository.views import RepositoryCategoriesViewSet +from .repository.views import NewRepositoryViewSet +from .repository.views import RepositoryTranslatedExampleViewSet +from .repository.views import RepositoryExampleViewSet +from .repository.views import SearchRepositoriesViewSet +from .repository.views import NewRepositoryTranslatedExampleViewSet +from .repository.views import RepositoryTranslationsViewSet +from .repository.views import RepositoryUpdatesViewSet +from .repository.views import NewRepositoryExampleViewSet +from .repository.views import RequestAuthorizationViewSet +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 @@ -86,13 +100,45 @@ 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/new', NewRepositoryViewSet) +router.register('repository/evaluate/results', ResultsListViewSet) +router.register('repository/evaluate', EvaluateViewSet) +router.register('repository/translation', RepositoryTranslatedExampleViewSet) +router.register('repository/example', RepositoryExampleViewSet) +router.register('repository/search-repositories', SearchRepositoriesViewSet) +router.register( + 'repository/translate-example', + NewRepositoryTranslatedExampleViewSet +) +router.register('repository/translations', RepositoryTranslationsViewSet) +router.register('repository/updates', RepositoryUpdatesViewSet) +router.register('repository/example/new', NewRepositoryExampleViewSet) +router.register( + 'repository/request-authorization', + RequestAuthorizationViewSet +) +router.register( + 'repository/review-authorization-request', + ReviewAuthorizationRequestViewSet +) +router.register('repository/authorizations', RepositoryAuthorizationViewSet) +router.register( + '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) 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 ec41e2343..685b47a67 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,20 +7,38 @@ 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 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 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.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 @@ -116,7 +135,7 @@ def request(self, data, token=None): } if token else {} request = self.factory.post( - '/v2/repository/', + '/v2/repository/repository/', data, **authorization_header) @@ -180,7 +199,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'})( @@ -225,7 +244,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) @@ -284,7 +303,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'})( @@ -333,7 +352,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'})( @@ -427,7 +446,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, ) @@ -507,7 +526,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, ) @@ -620,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 @@ -630,7 +656,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 @@ -689,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): @@ -709,7 +747,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 @@ -773,8 +811,9 @@ def request(self, token): 'HTTP_AUTHORIZATION': 'Token {}'.format(token), } request = self.factory.delete( - '/v2/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, @@ -828,22 +867,22 @@ def setUp(self): role=0 ) - def request(self): + def request(self, nickname): request = self.factory.get( - '/v2/repositories-contributions/?nickname={}'.format( - self.user.nickname + '/v2/repository/repositories-contributions/?nickname={}'.format( + 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 @@ -856,3 +895,1609 @@ def test_okay(self): len(content_data['results']), 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): + 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) + + +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) + + +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()) + + +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) 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) 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: