From 6f87764c99472f3041899c949445de2c9e299666 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 27 Nov 2023 13:46:42 -0500 Subject: [PATCH] Rebase+squash. No-Issue Signed-off-by: James Tanner --- galaxy_ng/app/access_control/access_policy.py | 40 ++++ galaxy_ng/app/api/v1/filtersets_scores.py | 54 +++++ galaxy_ng/app/api/v1/filtersets_survey.py | 53 +++++ galaxy_ng/app/api/v1/models.py | 68 ++++++ galaxy_ng/app/api/v1/serializers_survey.py | 126 ++++++++++ galaxy_ng/app/api/v1/urls.py | 49 ++++ galaxy_ng/app/api/v1/utils_survey.py | 24 ++ galaxy_ng/app/api/v1/views.py | 4 +- galaxy_ng/app/api/v1/viewsets/__init__.py | 11 + galaxy_ng/app/api/v1/viewsets/survey.py | 202 ++++++++++++++++ ...yrollup_collectionsurveyrollup_and_more.py | 212 +++++++++++++++++ .../community/test_community_surveys.py | 225 ++++++++++++++++++ 12 files changed, 1067 insertions(+), 1 deletion(-) create mode 100644 galaxy_ng/app/api/v1/filtersets_scores.py create mode 100644 galaxy_ng/app/api/v1/filtersets_survey.py create mode 100644 galaxy_ng/app/api/v1/serializers_survey.py create mode 100644 galaxy_ng/app/api/v1/utils_survey.py create mode 100644 galaxy_ng/app/api/v1/viewsets/survey.py create mode 100644 galaxy_ng/app/migrations/0046_legacyrolesurveyrollup_collectionsurveyrollup_and_more.py create mode 100644 galaxy_ng/tests/integration/community/test_community_surveys.py diff --git a/galaxy_ng/app/access_control/access_policy.py b/galaxy_ng/app/access_control/access_policy.py index b2d91c4443..5192a164b5 100644 --- a/galaxy_ng/app/access_control/access_policy.py +++ b/galaxy_ng/app/access_control/access_policy.py @@ -820,3 +820,43 @@ def is_namespace_owner(self, request, viewset, action): return True return False + + +class SurveyAccessPolicy(AccessPolicyBase): + + NAME = "SurveyAccessPolicy" + + @classmethod + def get_access_policy(cls, view): + + statements = [ + { + "action": [ + "get", + "list", + "retrieve", + ], + "principal": "*", + "effect": "allow", + }, + { + "action": [ + "create", + "update", + ], + "principal": "authenticated", + "effect": "allow", + "condition": "is_survey_user", + }, + ] + + # As a last resort, require admin rights + return MockPulpAccessPolicy( + { + "statements": statements, + } + ) + + def is_survey_user(self, request, viewset, action): + print(f'IS_SURVEY_USER ... {viewset} {action}') + return True diff --git a/galaxy_ng/app/api/v1/filtersets_scores.py b/galaxy_ng/app/api/v1/filtersets_scores.py new file mode 100644 index 0000000000..5f33c99857 --- /dev/null +++ b/galaxy_ng/app/api/v1/filtersets_scores.py @@ -0,0 +1,54 @@ +from django_filters import filters +from django_filters.rest_framework import filterset + +from galaxy_ng.app.api.v1.models import LegacyRoleSurveyRollup +from galaxy_ng.app.api.v1.models import CollectionSurveyRollup + + +class BaseSurveyRollupFilter(filterset.FilterSet): + + sort = filters.OrderingFilter( + fields=( + ('created', 'created'), + ) + ) + + +class LegacyRoleSurveyRollupFilter(BaseSurveyRollupFilter): + + role = filters.CharFilter(method='role_filter') + namespace = filters.CharFilter(method='namespace_filter') + name = filters.CharFilter(method='name_filter') + + class Meta: + model = LegacyRoleSurveyRollup + fields = ['created', 'role'] + + def role_filter(self, queryset, name, value): + return queryset.filter(role__id=int(value)) + + def namespace_filter(self, queryset, name, value): + return queryset.filter(role__namespace__name=value) + + def name_filter(self, queryset, name, value): + return queryset.filter(role__name=value) + + +class CollectionSurveyRollupFilter(BaseSurveyRollupFilter): + + collection = filters.CharFilter(method='collection_filter') + namespace = filters.CharFilter(method='namespace_filter') + name = filters.CharFilter(method='name_filter') + + class Meta: + model = CollectionSurveyRollup + fields = ['created', 'collection'] + + def collection_filter(self, queryset, name, value): + return queryset.filter(collection__pulp_id=value) + + def namespace_filter(self, queryset, name, value): + return queryset.filter(collection__namespace=value) + + def name_filter(self, queryset, name, value): + return queryset.filter(collection__name=value) diff --git a/galaxy_ng/app/api/v1/filtersets_survey.py b/galaxy_ng/app/api/v1/filtersets_survey.py new file mode 100644 index 0000000000..334856b9c4 --- /dev/null +++ b/galaxy_ng/app/api/v1/filtersets_survey.py @@ -0,0 +1,53 @@ +from django.db.models import Q +from django_filters import filters +from django_filters.rest_framework import filterset + +from galaxy_ng.app.api.v1.models import LegacyRoleSurvey +from galaxy_ng.app.api.v1.models import CollectionSurvey + + +class BaseSurveyFilter(filterset.FilterSet): + + user = filters.CharFilter(method='user_filter') + + sort = filters.OrderingFilter( + fields=( + ('created', 'created'), + ) + ) + + def user_filter(self, queryset, name, value): + + # allow filtering on uid and username ... + if value.isdigit(): + queryset = queryset.filter( + Q(user__id=int(value)) | Q(user__username=value) + ) + else: + queryset = queryset.filter(user__username=value) + + return queryset + + +class LegacyRoleSurveyFilter(BaseSurveyFilter): + + role = filters.CharFilter(method='role_filter') + + class Meta: + model = LegacyRoleSurvey + fields = ['created', 'user', 'role'] + + def role_filter(self, queryset, name, value): + return queryset.filter(role__id=int(value)) + + +class CollectionSurveyFilter(BaseSurveyFilter): + + collection = filters.CharFilter(method='collection_filter') + + class Meta: + model = CollectionSurvey + fields = ['created', 'user', 'collection'] + + def collection_filter(self, queryset, name, value): + return queryset.filter(collection__pulp_id=value) diff --git a/galaxy_ng/app/api/v1/models.py b/galaxy_ng/app/api/v1/models.py index adb6f4c979..a5134c8565 100644 --- a/galaxy_ng/app/api/v1/models.py +++ b/galaxy_ng/app/api/v1/models.py @@ -1,11 +1,13 @@ from django.db import models from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.indexes import GinIndex +from django.core.validators import MinValueValidator, MaxValueValidator from galaxy_ng.app.models import Namespace from galaxy_ng.app.models.auth import User from pulpcore.plugin.models import Task +from pulp_ansible.app.models import Collection """ @@ -230,3 +232,69 @@ def add_log_record(self, log_record, state=None): "level": log_record.levelname, "time": log_record.created }) + + +class SurveyBase(models.Model): + + class Meta: + abstract = True + + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + docs = models.IntegerField( + null=True, + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + + ease_of_use = models.IntegerField( + null=True, + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + + does_what_it_says = models.IntegerField( + null=True, + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + + works_as_is = models.IntegerField( + null=True, + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + + used_in_production = models.IntegerField( + null=True, + validators=[MinValueValidator(0), MaxValueValidator(5)] + ) + + +class CollectionSurvey(SurveyBase): + user = models.ForeignKey(User, on_delete=models.CASCADE) + collection = models.ForeignKey(Collection, on_delete=models.CASCADE) + + class Meta: + unique_together = ('user', 'collection',) + + +class LegacyRoleSurvey(SurveyBase): + user = models.ForeignKey(User, on_delete=models.CASCADE) + role = models.ForeignKey(LegacyRole, on_delete=models.CASCADE) + + class Meta: + unique_together = ('user', 'role',) + + +class CollectionSurveyRollup(models.Model): + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + collection = models.ForeignKey(Collection, on_delete=models.CASCADE) + score = models.DecimalField(max_digits=5, decimal_places=2) + + +class LegacyRoleSurveyRollup(models.Model): + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + role = models.ForeignKey(LegacyRole, on_delete=models.CASCADE) + score = models.DecimalField(max_digits=5, decimal_places=2) diff --git a/galaxy_ng/app/api/v1/serializers_survey.py b/galaxy_ng/app/api/v1/serializers_survey.py new file mode 100644 index 0000000000..3519a061a6 --- /dev/null +++ b/galaxy_ng/app/api/v1/serializers_survey.py @@ -0,0 +1,126 @@ +from rest_framework import serializers + +from galaxy_ng.app.api.v1.models import ( + CollectionSurvey, + CollectionSurveyRollup, + LegacyRoleSurvey, + LegacyRoleSurveyRollup, +) + +from galaxy_ng.app.api.v1.utils_survey import SURVEY_FIELDS + + +class CollectionSurveySerializer(serializers.ModelSerializer): + + responses = serializers.SerializerMethodField() + user = serializers.SerializerMethodField() + collection = serializers.SerializerMethodField() + + class Meta: + model = CollectionSurvey + fields = [ + 'id', + 'created', + 'modified', + 'collection', + 'user', + 'responses' + ] + + def get_user(self, obj): + return { + 'id': obj.user.id, + 'username': obj.user.username + } + + def get_collection(self, obj): + return { + 'id': obj.collection.pulp_id, + 'namespace': obj.collection.namespace, + 'name': obj.collection.name + } + + def get_responses(self, obj): + return dict((k, getattr(obj, k)) for k in SURVEY_FIELDS) + + +class CollectionSurveyRollupSerializer(serializers.ModelSerializer): + + namespace = serializers.SerializerMethodField() + name = serializers.SerializerMethodField() + + class Meta: + model = CollectionSurveyRollup + fields = [ + 'id', + 'created', + 'modified', + 'collection', + 'namespace', + 'name', + 'score' + ] + + def get_namespace(self, obj): + return obj.collection.namespace + + def get_name(self, obj): + return obj.collection.name + + +class LegacyRoleSurveySerializer(serializers.ModelSerializer): + + responses = serializers.SerializerMethodField() + user = serializers.SerializerMethodField() + role = serializers.SerializerMethodField() + + class Meta: + model = LegacyRoleSurvey + fields = [ + 'id', + 'created', + 'modified', + 'role', + 'user', + 'responses', + ] + + def get_user(self, obj): + return { + 'id': obj.user.id, + 'username': obj.user.username + } + + def get_role(self, obj): + return { + 'id': obj.role.id, + 'namespace': obj.role.namespace.name, + 'name': obj.role.name + } + + def get_responses(self, obj): + return dict((k, getattr(obj, k)) for k in SURVEY_FIELDS) + + +class LegacyRoleSurveyRollupSerializer(serializers.ModelSerializer): + + namespace = serializers.SerializerMethodField() + name = serializers.SerializerMethodField() + + class Meta: + model = LegacyRoleSurveyRollup + fields = [ + 'id', + 'created', + 'modified', + 'role', + 'namespace', + 'name', + 'score' + ] + + def get_namespace(self, obj): + return obj.role.namespace.name + + def get_name(self, obj): + return obj.role.name diff --git a/galaxy_ng/app/api/v1/urls.py b/galaxy_ng/app/api/v1/urls.py index 6da9146ac2..0e512502fe 100644 --- a/galaxy_ng/app/api/v1/urls.py +++ b/galaxy_ng/app/api/v1/urls.py @@ -14,6 +14,13 @@ LegacyUsersViewSet ) +from galaxy_ng.app.api.v1.viewsets import ( + CollectionSurveyRollupList, + CollectionSurveyList, + LegacyRoleSurveyRollupList, + LegacyRoleSurveyList, +) + urlpatterns = [ path( @@ -116,5 +123,47 @@ name='legacy_namespace_providers-list' ), + path( + "scores/collections/", + CollectionSurveyRollupList.as_view({'get': 'list'}), + name='collection-survey-rollup-list' + ), + path( + "scores/collections///", + CollectionSurveyRollupList.as_view({'get': 'retrieve_collection'}), + name='collection-survey-rollup-list-by-fqn' + ), + path( + "scores/roles/", + LegacyRoleSurveyRollupList.as_view({'get': 'list'}), + name='legacyrole-survey-rollup-list' + ), + + path( + "surveys/collections/", + CollectionSurveyList.as_view({'get': 'list'}), + name='collection-survey-list' + ), + path( + "surveys/collections///", + CollectionSurveyList.as_view({'post': 'create'}), + name='collection-survey-create1' + ), + path( + "surveys/collections//", + CollectionSurveyList.as_view({'post': 'create'}), + name='collection-survey-create2' + ), + path( + "surveys/roles/", + LegacyRoleSurveyList.as_view({'get': 'list'}), + name='legacyrole-survey-list' + ), + path( + "surveys/roles//", + LegacyRoleSurveyList.as_view({'post': 'create'}), + name='legacyrole-survey-create' + ), + path('', LegacyRootView.as_view(), name='legacy-root') ] diff --git a/galaxy_ng/app/api/v1/utils_survey.py b/galaxy_ng/app/api/v1/utils_survey.py new file mode 100644 index 0000000000..91888c6276 --- /dev/null +++ b/galaxy_ng/app/api/v1/utils_survey.py @@ -0,0 +1,24 @@ +SURVEY_FIELDS = [ + 'docs', + 'ease_of_use', + 'does_what_it_says', + 'works_as_is', + 'used_in_production' +] + + +def calculate_survey_score(surveys): + + answer_count = 0 + survey_score = 0.0 + + for survey in surveys: + for k in SURVEY_FIELDS: + data = getattr(survey, k) + if data is not None: + answer_count += 1 + survey_score += (data - 1) / 4 + + score = (survey_score / answer_count) * 5 + + return score diff --git a/galaxy_ng/app/api/v1/views.py b/galaxy_ng/app/api/v1/views.py index 4cc92b937a..55114b7492 100644 --- a/galaxy_ng/app/api/v1/views.py +++ b/galaxy_ng/app/api/v1/views.py @@ -16,5 +16,7 @@ def get(self, request, *args, **kwargs): 'imports': f'/{API_PATH_PREFIX}/v1/imports/', 'roles': f'/{API_PATH_PREFIX}/v1/roles/', 'users': f'/{API_PATH_PREFIX}/v1/users/', - 'namespaces': f'/{API_PATH_PREFIX}/v1/namespaces/' + 'namespaces': f'/{API_PATH_PREFIX}/v1/namespaces/', + 'surveys': f'/{API_PATH_PREFIX}/v1/surveys/', + 'scores': f'/{API_PATH_PREFIX}/v1/scores/', }) diff --git a/galaxy_ng/app/api/v1/viewsets/__init__.py b/galaxy_ng/app/api/v1/viewsets/__init__.py index bfdc57ce38..a01b393426 100644 --- a/galaxy_ng/app/api/v1/viewsets/__init__.py +++ b/galaxy_ng/app/api/v1/viewsets/__init__.py @@ -20,6 +20,13 @@ LegacyRolesSyncViewSet, ) +from .survey import ( + CollectionSurveyRollupList, + LegacyRoleSurveyRollupList, + CollectionSurveyList, + LegacyRoleSurveyList, +) + __all__ = ( LegacyNamespacesViewSet, @@ -32,4 +39,8 @@ LegacyRoleContentViewSet, LegacyRoleVersionsViewSet, LegacyRoleImportsViewSet, + CollectionSurveyRollupList, + LegacyRoleSurveyRollupList, + CollectionSurveyList, + LegacyRoleSurveyList, ) diff --git a/galaxy_ng/app/api/v1/viewsets/survey.py b/galaxy_ng/app/api/v1/viewsets/survey.py new file mode 100644 index 0000000000..680248d613 --- /dev/null +++ b/galaxy_ng/app/api/v1/viewsets/survey.py @@ -0,0 +1,202 @@ +from django.conf import settings +from django_filters import rest_framework as filters +from django.shortcuts import get_object_or_404 + +from rest_framework import viewsets +from rest_framework.settings import perform_import +from rest_framework.permissions import IsAuthenticatedOrReadOnly +from rest_framework.response import Response +from rest_framework import status +from rest_framework.pagination import PageNumberPagination + +from galaxy_ng.app.access_control.access_policy import SurveyAccessPolicy +from galaxy_ng.app.api.v1.utils_survey import calculate_survey_score + + +from galaxy_ng.app.api.v1.models import ( + CollectionSurvey, + CollectionSurveyRollup, + LegacyRoleSurveyRollup, + LegacyRoleSurvey, + LegacyRole +) + +from galaxy_ng.app.api.v1.serializers_survey import ( + CollectionSurveyRollupSerializer, + CollectionSurveySerializer, + LegacyRoleSurveyRollupSerializer, + LegacyRoleSurveySerializer, +) + +from galaxy_ng.app.api.v1.filtersets_survey import ( + CollectionSurveyFilter, + LegacyRoleSurveyFilter, +) + +from galaxy_ng.app.api.v1.filtersets_scores import ( + CollectionSurveyRollupFilter, + LegacyRoleSurveyRollupFilter, +) + +from pulp_ansible.app.models import Collection + + +GALAXY_AUTHENTICATION_CLASSES = perform_import( + settings.GALAXY_AUTHENTICATION_CLASSES, + 'GALAXY_AUTHENTICATION_CLASSES' +) + + +class SurveyPagination(PageNumberPagination): + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 1000 + + +class CollectionSurveyRollupList(viewsets.ModelViewSet): + queryset = CollectionSurveyRollup.objects.all() + serializer_class = CollectionSurveyRollupSerializer + pagination_class = SurveyPagination + + filter_backends = [filters.DjangoFilterBackend] + filterset_class = CollectionSurveyRollupFilter + + # access_policy.py is lame. + permission_classes = [IsAuthenticatedOrReadOnly] + + def retrieve_collection(self, *args, **kwargs): + """Get the score object by namespace/name path.""" + + print(f'PAGINATION: {self.pagination_class}') + + namespace = kwargs['namespace'] + name = kwargs['name'] + + collection = get_object_or_404(Collection, namespace=namespace, name=name) + score = get_object_or_404(CollectionSurveyRollup, collection=collection) + + serializer = CollectionSurveyRollupSerializer(score) + data = serializer.data + + resp = { + 'count': 1, + 'next': None, + 'previous': None, + 'results': [data] + } + + # return Response(serializer.data) + # return self.get_paginated_response(serializer.data) + return Response(resp) + + +class LegacyRoleSurveyRollupList(viewsets.ModelViewSet): + queryset = LegacyRoleSurveyRollup.objects.all() + serializer_class = LegacyRoleSurveyRollupSerializer + pagination_class = SurveyPagination + + filter_backends = [filters.DjangoFilterBackend] + filterset_class = LegacyRoleSurveyRollupFilter + + # access_policy.py is lame. + permission_classes = [IsAuthenticatedOrReadOnly] + + +class CollectionSurveyList(viewsets.ModelViewSet): + queryset = CollectionSurvey.objects.all() + serializer_class = CollectionSurveySerializer + pagination_class = SurveyPagination + + permission_classes = [SurveyAccessPolicy] + + filter_backends = [filters.DjangoFilterBackend] + filterset_class = CollectionSurveyFilter + + def get_queryset(self): + return CollectionSurvey.objects.filter( + user=self.request.user + ) + + def create(self, *args, **kwargs): + print(f'ARGS:{args} KWARGS:{kwargs}') + + # the collection serializer doesn't include an ID, + # so all we have to go by is namespace.name ... + namespace = kwargs.get('namespace') + name = kwargs.get('name') + + if not namespace or not name: + return Response( + {"message": f"{namespace}.{name} not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + collection = get_object_or_404(Collection, namespace=namespace, name=name) + + defaults = self.request.data + + survey, _ = CollectionSurvey.objects.get_or_create( + user=self.request.user, + collection=collection, + defaults=defaults + ) + + # re-compute score ... + new_score = calculate_survey_score(CollectionSurvey.objects.filter(collection=collection)) + score, _ = CollectionSurveyRollup.objects.get_or_create( + collection=collection, + defaults={'score': new_score} + ) + if score.score != new_score: + score.score = new_score + score.save() + + return Response({'id': survey.id}, status=status.HTTP_201_CREATED) + + +class LegacyRoleSurveyList(viewsets.ModelViewSet): + queryset = LegacyRoleSurvey.objects.all() + serializer_class = LegacyRoleSurveySerializer + pagination_class = SurveyPagination + + permission_classes = [SurveyAccessPolicy] + + filter_backends = [filters.DjangoFilterBackend] + filterset_class = LegacyRoleSurveyFilter + + def get_queryset(self): + return LegacyRoleSurvey.objects.filter( + user=self.request.user + ) + + def create(self, *args, **kwargs): + print(f'ARGS:{args} KWARGS:{kwargs}') + role_id = kwargs.get('id') + + if not role_id: + return Response( + {"message": "role id not found"}, + status=status.HTTP_404_NOT_FOUND + ) + + role = get_object_or_404(LegacyRole, id=role_id) + + defaults = self.request.data + + survey, _ = LegacyRoleSurvey.objects.get_or_create( + user=self.request.user, + role=role, + defaults=defaults + ) + + # re-compute score ... + new_score = calculate_survey_score(LegacyRoleSurvey.objects.filter(role=role)) + score, _ = LegacyRoleSurveyRollup.objects.get_or_create( + role=role, + defaults={'score': new_score} + ) + if score.score != new_score: + score.score = new_score + score.save() + + return Response({'id': survey.id}, status=status.HTTP_201_CREATED) diff --git a/galaxy_ng/app/migrations/0046_legacyrolesurveyrollup_collectionsurveyrollup_and_more.py b/galaxy_ng/app/migrations/0046_legacyrolesurveyrollup_collectionsurveyrollup_and_more.py new file mode 100644 index 0000000000..0a6067d3d2 --- /dev/null +++ b/galaxy_ng/app/migrations/0046_legacyrolesurveyrollup_collectionsurveyrollup_and_more.py @@ -0,0 +1,212 @@ +# Generated by Django 4.2.7 on 2023-11-20 23:41 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("ansible", "0055_alter_collectionversion_version_alter_role_version"), + ("galaxy", "0045_setting"), + ] + + operations = [ + migrations.CreateModel( + name="LegacyRoleSurveyRollup", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("score", models.DecimalField(decimal_places=2, max_digits=5)), + ( + "role", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="galaxy.legacyrole" + ), + ), + ], + ), + migrations.CreateModel( + name="CollectionSurveyRollup", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ("score", models.DecimalField(decimal_places=2, max_digits=5)), + ( + "collection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="ansible.collection" + ), + ), + ], + ), + migrations.CreateModel( + name="LegacyRoleSurvey", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "docs", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "ease_of_use", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "does_what_it_says", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "works_as_is", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "used_in_production", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "role", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="galaxy.legacyrole" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "unique_together": {("user", "role")}, + }, + ), + migrations.CreateModel( + name="CollectionSurvey", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("modified", models.DateTimeField(auto_now=True)), + ( + "docs", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "ease_of_use", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "does_what_it_says", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "works_as_is", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "used_in_production", + models.IntegerField( + null=True, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(5), + ], + ), + ), + ( + "collection", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="ansible.collection" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL + ), + ), + ], + options={ + "unique_together": {("user", "collection")}, + }, + ), + ] diff --git a/galaxy_ng/tests/integration/community/test_community_surveys.py b/galaxy_ng/tests/integration/community/test_community_surveys.py new file mode 100644 index 0000000000..2937df010d --- /dev/null +++ b/galaxy_ng/tests/integration/community/test_community_surveys.py @@ -0,0 +1,225 @@ +# import copy +# import json +import pytest +import random +# import string + +from ..utils import ( + get_client, + SocialGithubClient, + GithubAdminClient, + # cleanup_namespace, +) +from ..utils.legacy import ( + cleanup_social_user, + wait_for_v1_task, +) + + +pytestmark = pytest.mark.qa # noqa: F821 + + +SURVEY_FIELDS = [ + 'docs', + 'ease_of_use', + 'does_what_it_says', + 'works_as_is', + 'used_in_production' +] + + +def extract_default_config(ansible_config): + base_cfg = ansible_config('github_user_1') + cfg = {} + cfg['token'] = None + cfg['url'] = base_cfg.get('url') + cfg['auth_url'] = base_cfg.get('auth_url') + cfg['github_url'] = base_cfg.get('github_url') + cfg['github_api_url'] = base_cfg.get('github_api_url') + return cfg + + +@pytest.fixture() +def default_config(ansible_config): + yield extract_default_config(ansible_config) + + +@pytest.fixture +def imported_role(ansible_config): + github_user = 'jctannerTEST' + github_repo = 'role1' + cleanup_social_user(github_user, ansible_config) + cleanup_social_user(github_user.lower(), ansible_config) + + admin_config = ansible_config("admin") + admin_client = get_client( + config=admin_config, + request_token=False, + require_auth=True + ) + + # make the legacy namespace + ns_payload = { + 'name': github_user + } + resp = admin_client('/api/v1/namespaces/', method='POST', args=ns_payload) + assert resp['name'] == github_user, resp + assert not resp['summary_fields']['owners'], resp + assert not resp['summary_fields']['provider_namespaces'], resp + v1_id = resp['id'] + + # make the v3 namespace + v3_payload = { + 'name': github_user.lower(), + 'groups': [], + } + resp = admin_client('/api/_ui/v1/namespaces/', method='POST', args=v3_payload) + assert resp['name'] == github_user.lower(), resp + v3_id = resp['id'] + + # bind the v3 namespace to the v1 namespace + v3_bind = { + 'id': v3_id + } + admin_client(f'/api/v1/namespaces/{v1_id}/providers/', method='POST', args=v3_bind) + + # do an import with the admin ... + payload = { + 'github_repo': github_repo, + 'github_user': github_user, + } + resp = admin_client('/api/v1/imports/', method='POST', args=payload) + task_id = resp['results'][0]['id'] + wait_for_v1_task(task_id=task_id, api_client=admin_client, check=False) + + # get the role ... + role_qs = admin_client(f'/api/v1/roles/?owner__username={github_user}&name={github_repo}') + role_ds = role_qs['results'][0] + + yield role_ds + + +@pytest.fixture +def published_collection(ansible_config, published): + admin_config = ansible_config("admin") + admin_client = get_client( + config=admin_config, + request_token=False, + require_auth=True + ) + + return admin_client(f'/api/v3/collections/{published.namespace}/{published.name}/') + + +@pytest.mark.deployment_community +def test_community_role_survey(ansible_config, default_config, imported_role): + roleid = imported_role['id'] + + ga = GithubAdminClient() + + possible_values = [None] + list(range(0, 6)) + user_survey_map = { + 'bob1': dict((x, random.choice(possible_values)) for x in SURVEY_FIELDS), + 'bob2': dict((x, random.choice(possible_values)) for x in SURVEY_FIELDS), + 'bob3': dict((x, random.choice(possible_values)) for x in SURVEY_FIELDS), + } + + for username, payload in user_survey_map.items(): + cleanup_social_user(username, ansible_config) + ga.delete_user(login=username) + gcfg = ga.create_user( + login=username, + password='foobar1234', + email=username + '@noreply.github.com' + ) + gcfg['username'] = username + gcfg.update(default_config) + + with SocialGithubClient(config=gcfg) as sclient: + + me = sclient.get('_ui/v1/me/').json() + + # submit the survey ... + rkwargs = { + 'absolute_url': f'/api/v1/surveys/roles/{roleid}/', + 'data': payload, + } + resp = sclient.post(**rkwargs) + assert resp.status_code == 201 + + # now check that we can find the survey ... + resp = sclient.get(absolute_url='/api/v1/surveys/roles/') + ds = resp.json() + + assert ds['count'] == 1 + assert ds['results'][0]['user']['id'] == me['id'] + assert ds['results'][0]['user']['username'] == username + for k, v in payload.items(): + assert ds['results'][0]['responses'][k] == v + + # validate the score has been computed for the role ... + with SocialGithubClient(config=gcfg) as sclient: + resp = sclient.get(absolute_url=f'/api/v1/scores/roles/?role={roleid}') + ds = resp.json() + assert ds['count'] == 1 + given_score = float(ds['results'][0]['score']) + + assert given_score > 0 and given_score <= 5 + + +@pytest.mark.deployment_community +def test_community_collection_survey(ansible_config, default_config, published_collection): + + col_namespace = published_collection['namespace'] + col_name = published_collection['name'] + + ga = GithubAdminClient() + + possible_values = [None] + list(range(0, 6)) + user_survey_map = { + 'bob1': dict((x, random.choice(possible_values)) for x in SURVEY_FIELDS), + 'bob2': dict((x, random.choice(possible_values)) for x in SURVEY_FIELDS), + 'bob3': dict((x, random.choice(possible_values)) for x in SURVEY_FIELDS), + } + + for username, payload in user_survey_map.items(): + cleanup_social_user(username, ansible_config) + ga.delete_user(login=username) + gcfg = ga.create_user( + login=username, + password='foobar1234', + email=username + '@noreply.github.com' + ) + gcfg['username'] = username + gcfg.update(default_config) + + with SocialGithubClient(config=gcfg) as sclient: + + me = sclient.get('_ui/v1/me/').json() + + # submit the survey ... + rkwargs = { + 'absolute_url': f'/api/v1/surveys/collections/{col_namespace}/{col_name}/', + 'data': payload, + } + resp = sclient.post(**rkwargs) + assert resp.status_code == 201 + + # now check that we can find the survey ... + resp = sclient.get(absolute_url='/api/v1/surveys/collections/') + ds = resp.json() + + assert ds['count'] == 1 + assert ds['results'][0]['user']['id'] == me['id'] + assert ds['results'][0]['user']['username'] == username + for k, v in payload.items(): + assert ds['results'][0]['responses'][k] == v + + # validate the score has been computed for the role ... + with SocialGithubClient(config=gcfg) as sclient: + resp = sclient.get(absolute_url=f'/api/v1/scores/collections/{col_namespace}/{col_name}/') + ds = resp.json() + assert ds['count'] == 1 + given_score = float(ds['results'][0]['score']) + + assert given_score > 0 and given_score <= 5