diff --git a/bothub/api/grpc/connect_grpc_client.py b/bothub/api/grpc/connect_grpc_client.py index 76dfcaf9..74c53897 100644 --- a/bothub/api/grpc/connect_grpc_client.py +++ b/bothub/api/grpc/connect_grpc_client.py @@ -21,7 +21,6 @@ def list_classifiers(self, project_uuid: str) -> List[Dict[str, str]]: result = [] try: stub = project_pb2_grpc.ProjectControllerStub(self.channel) - for project in stub.Classifier( project_pb2.ClassifierListRequest(project_uuid=project_uuid) ): @@ -30,6 +29,8 @@ def list_classifiers(self, project_uuid: str) -> List[Dict[str, str]]: "authorization_uuid": project.authorization_uuid, "classifier_type": project.classifier_type, "name": project.name, + "is_active": project.is_active, + "uuid": project.uuid, } ) except grpc.RpcError as e: @@ -41,3 +42,33 @@ def list_authorizations(self, project_uuid: str) -> List[str]: classifiers = self.list_classifiers(project_uuid=project_uuid) return [classifier.get("authorization_uuid") for classifier in classifiers] + + def get_authorization_classifier( + self, project_uuid: str, authorization_uuid: str + ) -> str: + """ + Recives a authorization UUID and returns the respective classifier UUID + """ + classifiers = self.list_classifiers(project_uuid) + classifier = filter( + lambda classifier: classifier["authorization_uuid"] == authorization_uuid, + classifiers, + ) + + return next(classifier).get("uuid") + + def remove_authorization(self, project_uuid: str, authorization_uuid: str): + classifier_uuid = self.get_authorization_classifier( + project_uuid, authorization_uuid + ) + + stub = project_pb2_grpc.ProjectControllerStub(self.channel) + stub.DestroyClassifier( + project_pb2.ClassifierDestroyRequest(uuid=classifier_uuid) + ) + + def create_classifier(self, **kwargs): + stub = project_pb2_grpc.ProjectControllerStub(self.channel) + return stub.CreateClassifier( + project_pb2.ClassifierCreateRequest(**kwargs, classifier_type="bothub") + ) diff --git a/bothub/api/v2/repository/serializers.py b/bothub/api/v2/repository/serializers.py index 036a1ba4..c6fad03f 100644 --- a/bothub/api/v2/repository/serializers.py +++ b/bothub/api/v2/repository/serializers.py @@ -1,4 +1,5 @@ import json +from os import access from django.conf import settings from django.shortcuts import get_object_or_404 @@ -1612,3 +1613,28 @@ class Meta: class RepositoryExampleSuggestionSerializer(serializers.Serializer): pass + + +class RemoveRepositoryProject(serializers.Serializer): + pass + + +class AddRepositoryProjectSerializer(serializers.Serializer): + + name = serializers.CharField(required=True) + user = serializers.EmailField(required=True) + access_token = serializers.CharField(required=True) + project_uuid = serializers.CharField(required=True) + + def create(self, validated_data): + task = celery_app.send_task( + name="create_repository_project", kwargs=validated_data + ) + task.wait() + return validated_data + + def to_representation(self, instance): + data = super().to_representation(instance) + if data.get("organization"): + data.pop("organization") + return data diff --git a/bothub/api/v2/repository/views.py b/bothub/api/v2/repository/views.py index 51391729..93326633 100644 --- a/bothub/api/v2/repository/views.py +++ b/bothub/api/v2/repository/views.py @@ -44,6 +44,7 @@ RepositoryVote, RequestRepositoryAuthorization, RepositoryVersionLanguage, + Organization, ) from ..metadata import Metadata @@ -97,6 +98,8 @@ ShortRepositorySerializer, TrainSerializer, WordDistributionSerializer, + RemoveRepositoryProject, + AddRepositoryProjectSerializer, ) @@ -198,6 +201,184 @@ def auto_translation(self, request, **kwargs): return Response({"id_queue": task.task_id}) + @action( + detail=True, + methods=["GET"], + url_name="project-repository", + lookup_fields=["repository__uuid", "pk"], + ) + def projectrepository(self, request, **kwargs): + repository = self.get_object().repository + + project_uuid = request.query_params.get("project_uuid") + organization_pk = request.query_params.get("organization") + + try: + organization = ( + Organization.objects.get(pk=organization_pk) + if organization_pk + else None + ) + except Organization.DoesNotExist: + raise ValidationError(_("Organization not found")) + + if not project_uuid: + raise ValidationError(_("Need to pass 'project_uuid' in query params")) + + authorization = repository.get_user_authorization(request.user) + + if not authorization.can_contribute: + raise PermissionDenied() + + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + task.wait() + + repositories = repository.authorizations.filter(uuid__in=task.result) + + data = dict(in_project=repositories.exists()) + + if organization: + + organization_authorization = organization.organization_authorizations.filter( + uuid__in=task.result + ) + data["in_project"] = ( + data["in_project"] or organization_authorization.exists() + ) + + return Response(data) + + @action( + detail=True, + methods=["POST"], + permission_classes=[RepositoryAdminManagerAuthorization], + url_name="remove-repository-project", + serializer_class=RemoveRepositoryProject, + ) + def remove_repository_project(self, request, **kwargs): + + repository_version = self.get_object() + repository = repository_version.repository + + user_authorization = repository_version.repository.get_user_authorization( + request.user + ) + + if not user_authorization.is_admin: + raise PermissionDenied() + + project_uuid = request.data.get("project_uuid") + organization_pk = request.data.get("organization") + + if not project_uuid: + raise ValidationError(_("Need to pass 'project_uuid' in query params")) + + try: + organization = ( + Organization.objects.get(pk=organization_pk) + if organization_pk + else None + ) + except Organization.DoesNotExist: + raise ValidationError(_("Organization not found")) + + project_organization = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + project_organization.wait() + + authorizations = list( + repository.authorizations.filter( + uuid__in=project_organization.result + ).values_list("uuid", flat=True) + ) + + if organization: + organization_authorization = organization.get_organization_authorization( + request.user + ) + if not organization_authorization.is_admin: + raise PermissionDenied() + + authorizations += list( + organization.organization_authorizations.filter( + uuid__in=project_organization.result + ).values_list("uuid", flat=True) + ) + + if not len(authorizations): + raise ValidationError( + _("Repository or organization is not be included on project") + ) + + authorizations_uuids = map( + lambda authorization: str(authorization), authorizations + ) + + task = celery_app.send_task( + name="remove_authorizations_project", + args=[project_uuid, list(authorizations_uuids)], + ) + task.wait() + + return Response(status=status.HTTP_204_NO_CONTENT) + + @action( + detail=True, + methods=["POST"], + url_name="add-repository-project", + serializer_class=AddRepositoryProjectSerializer, + ) + def add_repository_project(self, request, **kwargs): + + repository = self.get_object().repository + organization_pk = request.data.get("organization") + + try: + organization = Organization.objects.get(pk=organization_pk) + except Organization.DoesNotExist: + raise ValidationError(_("Organization not found")) + + organization_authorization = organization.get_organization_authorization( + request.user + ) + user_authorization = repository.get_user_authorization(request.user) + + if ( + not organization_authorization.can_contribute + or not user_authorization.can_contribute + ): + raise PermissionDenied() + + serializer_data = dict( + user=request.user.email, + access_token=str(organization_authorization.uuid), + **request.data, + ) + + serializer = AddRepositoryProjectSerializer(data=serializer_data) + serializer.is_valid(raise_exception=True) + + project_uuid = serializer.validated_data.get("project_uuid") + + task = celery_app.send_task( + name="get_project_organization", args=[project_uuid] + ) + task.wait() + + organization_authorization = organization.organization_authorizations.filter( + uuid__in=task.result + ) + + if organization_authorization.exists(): + raise ValidationError(_("Repository already added")) + + data = serializer.save() + + return Response(data) + class RepositoryTrainInfoViewSet( MultipleFieldLookupMixin, mixins.RetrieveModelMixin, GenericViewSet diff --git a/bothub/common/tasks.py b/bothub/common/tasks.py index 1963434f..d971d941 100644 --- a/bothub/common/tasks.py +++ b/bothub/common/tasks.py @@ -540,3 +540,17 @@ def get_project_organization(project_uuid: str): # pragma: no cover grpc_client = ConnectGRPCClient() authorizations = grpc_client.list_authorizations(project_uuid=project_uuid) return authorizations + + +@app.task(name="remove_authorizations_project") +def remove_authorizations_project(project_uuid: str, authorizations_uuids: list): + grpc_client = ConnectGRPCClient() + for authorization_uuid in authorizations_uuids: + grpc_client.remove_authorization(project_uuid, authorization_uuid) + + +@app.task(name="create_repository_project") +def create_repository_project(**kwargs): + grpc_client = ConnectGRPCClient() + grpc_client.create_classifier(**kwargs) + return kwargs diff --git a/bothub/protos/project.proto b/bothub/protos/project.proto index 39e5d96b..46adf472 100644 --- a/bothub/protos/project.proto +++ b/bothub/protos/project.proto @@ -2,16 +2,39 @@ syntax = "proto3"; package weni.connect.project; +import "google/protobuf/empty.proto"; + service ProjectController { rpc Classifier(ClassifierListRequest) returns (stream ClassifierResponse) {} + rpc CreateClassifier(ClassifierCreateRequest) returns (ClassifierResponse) {} + rpc RetrieveClassifier(ClassifierRetrieveRequest) returns (ClassifierResponse) {} + rpc DestroyClassifier(ClassifierDestroyRequest) returns (google.protobuf.Empty) {} } message ClassifierResponse { string authorization_uuid = 1; string classifier_type = 2; string name = 3; + bool is_active = 4; + string uuid = 5; } message ClassifierListRequest { string project_uuid = 1; } + +message ClassifierCreateRequest { + string project_uuid = 1; + string user = 2; + string classifier_type = 3; + string name = 4; + string access_token = 5; +} + +message ClassifierRetrieveRequest { + string uuid = 1; +} + +message ClassifierDestroyRequest { + string uuid = 1; +}