From dfab337474ae2aed4284086057c0ccd550baf9c7 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 11 Jul 2019 18:32:58 +0200 Subject: [PATCH] Endpoint to Create/List/Delete Environment Variables --- readthedocs/api/v3/serializers.py | 55 ++++++- readthedocs/api/v3/tests/mixins.py | 20 +-- .../v3/tests/responses/projects-detail.json | 1 + .../projects-environmentvariables-detail.json | 11 ++ .../projects-environmentvariables-list.json | 18 +++ ...ojects-environmentvariables-list_POST.json | 11 ++ .../api/v3/tests/responses/projects-list.json | 1 + .../tests/responses/projects-list_POST.json | 1 + .../responses/projects-subprojects-list.json | 2 + .../responses/projects-superproject.json | 1 + .../projects-versions-builds-list_POST.json | 1 + .../api/v3/tests/test_environmentvariables.py | 144 ++++++++++++++++++ readthedocs/api/v3/urls.py | 10 ++ readthedocs/api/v3/views.py | 27 +++- readthedocs/projects/models.py | 2 + 15 files changed, 293 insertions(+), 12 deletions(-) create mode 100644 readthedocs/api/v3/tests/responses/projects-environmentvariables-detail.json create mode 100644 readthedocs/api/v3/tests/responses/projects-environmentvariables-list.json create mode 100644 readthedocs/api/v3/tests/responses/projects-environmentvariables-list_POST.json create mode 100644 readthedocs/api/v3/tests/test_environmentvariables.py diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index 46221999494..b3921332cb7 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -10,7 +10,7 @@ from readthedocs.builds.models import Build, Version from readthedocs.projects.constants import LANGUAGES, PROGRAMMING_LANGUAGES, REPO_CHOICES -from readthedocs.projects.models import Project +from readthedocs.projects.models import Project, EnvironmentVariable from readthedocs.redirects.models import Redirect, TYPE_CHOICES as REDIRECT_TYPE_CHOICES @@ -325,6 +325,7 @@ class ProjectLinksSerializer(BaseLinksSerializer): versions = serializers.SerializerMethodField() builds = serializers.SerializerMethodField() + environmentvariables = serializers.SerializerMethodField() redirects = serializers.SerializerMethodField() subprojects = serializers.SerializerMethodField() superproject = serializers.SerializerMethodField() @@ -343,6 +344,15 @@ def get_versions(self, obj): ) return self._absolute_url(path) + def get_environmentvariables(self, obj): + path = reverse( + 'projects-environmentvariables-list', + kwargs={ + 'parent_lookup_project__slug': obj.slug, + }, + ) + return self._absolute_url(path) + def get_redirects(self, obj): path = reverse( 'projects-redirects-list', @@ -550,3 +560,46 @@ def get_from_url(self, obj): def get_to_url(self, obj): # Overridden only to return ``None`` when the description is ``''`` return obj.to_url or None + + +class EnvironmentVariableLinksSerializer(BaseLinksSerializer): + _self = serializers.SerializerMethodField() + project = serializers.SerializerMethodField() + + def get__self(self, obj): + path = reverse( + 'projects-environmentvariables-detail', + kwargs={ + 'parent_lookup_project__slug': obj.project.slug, + 'environmentvariable_pk': obj.pk, + }, + ) + return self._absolute_url(path) + + def get_project(self, obj): + path = reverse( + 'projects-detail', + kwargs={ + 'project_slug': obj.project.slug, + }, + ) + return self._absolute_url(path) + + +class EnvironmentVariableSerializer(serializers.ModelSerializer): + + value = serializers.CharField(write_only=True) + project = serializers.SlugRelatedField(slug_field='slug', read_only=True) + _links = EnvironmentVariableLinksSerializer(source='*', read_only=True) + + class Meta: + model = EnvironmentVariable + fields = [ + 'pk', + 'created', + 'modified', + 'name', + 'value', + 'project', + '_links', + ] diff --git a/readthedocs/api/v3/tests/mixins.py b/readthedocs/api/v3/tests/mixins.py index b77fa16d014..83185944dbe 100644 --- a/readthedocs/api/v3/tests/mixins.py +++ b/readthedocs/api/v3/tests/mixins.py @@ -20,12 +20,12 @@ class APIEndpointMixin(TestCase): fixtures = [] def setUp(self): - created = make_aware(datetime.datetime(2019, 4, 29, 10, 0, 0)) - modified = make_aware(datetime.datetime(2019, 4, 29, 12, 0, 0)) + self.created = make_aware(datetime.datetime(2019, 4, 29, 10, 0, 0)) + self.modified = make_aware(datetime.datetime(2019, 4, 29, 12, 0, 0)) self.me = fixture.get( User, - date_joined=created, + date_joined=self.created, username='testuser', projects=[], ) @@ -34,8 +34,8 @@ def setUp(self): # objects (like a Project for translations/subprojects) self.project = fixture.get( Project, - pub_date=created, - modified_date=modified, + pub_date=self.created, + modified_date=self.modified, description='Project description', repo='https://github.com/rtfd/project', project_url='http://project.com', @@ -51,8 +51,8 @@ def setUp(self): self.redirect = fixture.get( Redirect, - create_dt=created, - update_dt=modified, + create_dt=self.created, + update_dt=self.modified, from_url='/docs/', to_url='/documentation/', redirect_type='page', @@ -61,8 +61,8 @@ def setUp(self): self.subproject = fixture.get( Project, - pub_date=created, - modified_date=modified, + pub_date=self.created, + modified_date=self.modified, description='SubProject description', repo='https://github.com/rtfd/subproject', project_url='http://subproject.com', @@ -91,7 +91,7 @@ def setUp(self): self.build = fixture.get( Build, - date=created, + date=self.created, type='html', state='finished', error='', diff --git a/readthedocs/api/v3/tests/responses/projects-detail.json b/readthedocs/api/v3/tests/responses/projects-detail.json index 25e7b775e05..87e608785fb 100644 --- a/readthedocs/api/v3/tests/responses/projects-detail.json +++ b/readthedocs/api/v3/tests/responses/projects-detail.json @@ -60,6 +60,7 @@ "_links": { "_self": "https://readthedocs.org/api/v3/projects/project/", "builds": "https://readthedocs.org/api/v3/projects/project/builds/", + "environmentvariables": "https://readthedocs.org/api/v3/projects/project/environmentvariables/", "redirects": "https://readthedocs.org/api/v3/projects/project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", diff --git a/readthedocs/api/v3/tests/responses/projects-environmentvariables-detail.json b/readthedocs/api/v3/tests/responses/projects-environmentvariables-detail.json new file mode 100644 index 00000000000..41799d3d860 --- /dev/null +++ b/readthedocs/api/v3/tests/responses/projects-environmentvariables-detail.json @@ -0,0 +1,11 @@ +{ + "_links": { + "_self": "https://readthedocs.org/api/v3/projects/project/environmentvariables/1/", + "project": "https://readthedocs.org/api/v3/projects/project/" + }, + "created": "2019-04-29T10:00:00Z", + "modified": "2019-04-29T12:00:00Z", + "pk": 1, + "project": "project", + "name": "ENVVAR" +} diff --git a/readthedocs/api/v3/tests/responses/projects-environmentvariables-list.json b/readthedocs/api/v3/tests/responses/projects-environmentvariables-list.json new file mode 100644 index 00000000000..dab917503d9 --- /dev/null +++ b/readthedocs/api/v3/tests/responses/projects-environmentvariables-list.json @@ -0,0 +1,18 @@ +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "_links": { + "_self": "https://readthedocs.org/api/v3/projects/project/environmentvariables/1/", + "project": "https://readthedocs.org/api/v3/projects/project/" + }, + "created": "2019-04-29T10:00:00Z", + "modified": "2019-04-29T12:00:00Z", + "pk": 1, + "project": "project", + "name": "ENVVAR" + } + ] +} diff --git a/readthedocs/api/v3/tests/responses/projects-environmentvariables-list_POST.json b/readthedocs/api/v3/tests/responses/projects-environmentvariables-list_POST.json new file mode 100644 index 00000000000..602f139e4d3 --- /dev/null +++ b/readthedocs/api/v3/tests/responses/projects-environmentvariables-list_POST.json @@ -0,0 +1,11 @@ +{ + "_links": { + "_self": "https://readthedocs.org/api/v3/projects/project/environmentvariables/2/", + "project": "https://readthedocs.org/api/v3/projects/project/" + }, + "created": "2019-04-29T10:00:00Z", + "modified": "2019-04-29T12:00:00Z", + "pk": 2, + "project": "project", + "name": "NEWENVVAR" +} diff --git a/readthedocs/api/v3/tests/responses/projects-list.json b/readthedocs/api/v3/tests/responses/projects-list.json index 20f001fc8e5..349d3a6bf0c 100644 --- a/readthedocs/api/v3/tests/responses/projects-list.json +++ b/readthedocs/api/v3/tests/responses/projects-list.json @@ -49,6 +49,7 @@ "_self": "https://readthedocs.org/api/v3/projects/project/", "versions": "https://readthedocs.org/api/v3/projects/project/versions/", "builds": "https://readthedocs.org/api/v3/projects/project/builds/", + "environmentvariables": "https://readthedocs.org/api/v3/projects/project/environmentvariables/", "redirects": "https://readthedocs.org/api/v3/projects/project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", diff --git a/readthedocs/api/v3/tests/responses/projects-list_POST.json b/readthedocs/api/v3/tests/responses/projects-list_POST.json index 979aa133858..5d0fb7d023b 100644 --- a/readthedocs/api/v3/tests/responses/projects-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-list_POST.json @@ -2,6 +2,7 @@ "_links": { "_self": "https://readthedocs.org/api/v3/projects/test-project/", "builds": "https://readthedocs.org/api/v3/projects/test-project/builds/", + "environmentvariables": "https://readthedocs.org/api/v3/projects/test-project/environmentvariables/", "redirects": "https://readthedocs.org/api/v3/projects/test-project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/test-project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/test-project/superproject/", diff --git a/readthedocs/api/v3/tests/responses/projects-subprojects-list.json b/readthedocs/api/v3/tests/responses/projects-subprojects-list.json index 806fdbf2d0c..cd53dcd5e31 100644 --- a/readthedocs/api/v3/tests/responses/projects-subprojects-list.json +++ b/readthedocs/api/v3/tests/responses/projects-subprojects-list.json @@ -68,6 +68,7 @@ "_self": "https://readthedocs.org/api/v3/projects/project/", "versions": "https://readthedocs.org/api/v3/projects/project/versions/", "builds": "https://readthedocs.org/api/v3/projects/project/builds/", + "environmentvariables": "https://readthedocs.org/api/v3/projects/project/environmentvariables/", "redirects": "https://readthedocs.org/api/v3/projects/project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", @@ -91,6 +92,7 @@ "_self": "https://readthedocs.org/api/v3/projects/subproject/", "versions": "https://readthedocs.org/api/v3/projects/subproject/versions/", "builds": "https://readthedocs.org/api/v3/projects/subproject/builds/", + "environmentvariables": "https://readthedocs.org/api/v3/projects/subproject/environmentvariables/", "redirects": "https://readthedocs.org/api/v3/projects/subproject/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/subproject/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/subproject/superproject/", diff --git a/readthedocs/api/v3/tests/responses/projects-superproject.json b/readthedocs/api/v3/tests/responses/projects-superproject.json index 293ac76a07d..80c74afac99 100644 --- a/readthedocs/api/v3/tests/responses/projects-superproject.json +++ b/readthedocs/api/v3/tests/responses/projects-superproject.json @@ -11,6 +11,7 @@ "_links": { "_self": "https://readthedocs.org/api/v3/projects/project/", "builds": "https://readthedocs.org/api/v3/projects/project/builds/", + "environmentvariables": "https://readthedocs.org/api/v3/projects/project/environmentvariables/", "redirects": "https://readthedocs.org/api/v3/projects/project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", diff --git a/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json b/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json index 5375b95f12b..7ec4d9cf042 100644 --- a/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json +++ b/readthedocs/api/v3/tests/responses/projects-versions-builds-list_POST.json @@ -32,6 +32,7 @@ "_links": { "_self": "https://readthedocs.org/api/v3/projects/project/", "builds": "https://readthedocs.org/api/v3/projects/project/builds/", + "environmentvariables": "https://readthedocs.org/api/v3/projects/project/environmentvariables/", "redirects": "https://readthedocs.org/api/v3/projects/project/redirects/", "subprojects": "https://readthedocs.org/api/v3/projects/project/subprojects/", "superproject": "https://readthedocs.org/api/v3/projects/project/superproject/", diff --git a/readthedocs/api/v3/tests/test_environmentvariables.py b/readthedocs/api/v3/tests/test_environmentvariables.py new file mode 100644 index 00000000000..56e9ab8b8d5 --- /dev/null +++ b/readthedocs/api/v3/tests/test_environmentvariables.py @@ -0,0 +1,144 @@ +from .mixins import APIEndpointMixin +from django.urls import reverse + +import django_dynamic_fixture as fixture +from readthedocs.projects.models import EnvironmentVariable + + +class EnvironmentVariablessEndpointTests(APIEndpointMixin): + + def setUp(self): + super().setUp() + + self.environmentvariable = fixture.get( + EnvironmentVariable, + created=self.created, + modified=self.modified, + project=self.project, + name='ENVVAR', + value='a1b2c3', + ) + + def test_unauthed_projects_environmentvariables_list(self): + response = self.client.get( + reverse( + 'projects-environmentvariables-list', + kwargs={ + 'parent_lookup_project__slug': self.project.slug, + }), + ) + self.assertEqual(response.status_code, 401) + + def test_projects_environmentvariables_list(self): + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get( + reverse( + 'projects-environmentvariables-list', + kwargs={ + 'parent_lookup_project__slug': self.project.slug, + }), + ) + self.assertEqual(response.status_code, 200) + + response_json = response.json() + self.assertDictEqual( + response_json, + self._get_response_dict('projects-environmentvariables-list'), + ) + + def test_unauthed_projects_environmentvariables_detail(self): + response = self.client.get( + reverse( + 'projects-environmentvariables-detail', + kwargs={ + 'parent_lookup_project__slug': self.project.slug, + 'environmentvariable_pk': self.environmentvariable.pk, + }), + ) + self.assertEqual(response.status_code, 401) + + def test_projects_environmentvariables_detail(self): + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.get( + reverse( + 'projects-environmentvariables-detail', + kwargs={ + 'parent_lookup_project__slug': self.project.slug, + 'environmentvariable_pk': self.environmentvariable.pk, + }), + ) + self.assertEqual(response.status_code, 200) + + response_json = response.json() + self.assertDictEqual( + response_json, + self._get_response_dict('projects-environmentvariables-detail'), + ) + + def test_unauthed_projects_environmentvariables_list_post(self): + data = {} + + response = self.client.post( + reverse( + 'projects-environmentvariables-list', + kwargs={ + 'parent_lookup_project__slug': self.others_project.slug, + }), + data, + ) + self.assertEqual(response.status_code, 401) + + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.post( + reverse( + 'projects-environmentvariables-list', + kwargs={ + 'parent_lookup_project__slug': self.others_project.slug, + }), + data, + ) + self.assertEqual(response.status_code, 403) + + def test_projects_environmentvariables_list_post(self): + self.assertEqual(self.project.environmentvariable_set.count(), 1) + data = { + 'name': 'NEWENVVAR', + 'value': 'c3b2a1', + } + + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.post( + reverse( + 'projects-environmentvariables-list', + kwargs={ + 'parent_lookup_project__slug': self.project.slug, + }), + data, + ) + self.assertEqual(self.project.environmentvariable_set.count(), 2) + self.assertEqual(response.status_code, 201) + + environmentvariable = self.project.environmentvariable_set.get(name='NEWENVVAR') + self.assertEqual(environmentvariable.value, 'c3b2a1') + + response_json = response.json() + response_json['created'] = '2019-04-29T10:00:00Z' + response_json['modified'] = '2019-04-29T12:00:00Z' + self.assertDictEqual( + response_json, + self._get_response_dict('projects-environmentvariables-list_POST'), + ) + + def test_projects_environmentvariables_detail_delete(self): + self.assertEqual(self.project.environmentvariable_set.count(), 1) + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.delete( + reverse( + 'projects-environmentvariables-detail', + kwargs={ + 'parent_lookup_project__slug': self.project.slug, + 'environmentvariable_pk': self.environmentvariable.pk, + }), + ) + self.assertEqual(response.status_code, 204) + self.assertEqual(self.project.environmentvariable_set.count(), 0) diff --git a/readthedocs/api/v3/urls.py b/readthedocs/api/v3/urls.py index 74c163d206a..fb0d41b5981 100644 --- a/readthedocs/api/v3/urls.py +++ b/readthedocs/api/v3/urls.py @@ -2,6 +2,7 @@ from .views import ( BuildsCreateViewSet, BuildsViewSet, + EnvironmentVariablesViewSet, ProjectsViewSet, RedirectsViewSet, SubprojectRelationshipViewSet, @@ -76,5 +77,14 @@ parents_query_lookups=['project__slug'], ) +# allows /api/v3/projects/pip/environmentvariables/ +# allows /api/v3/projects/pip/environmentvariables/1053/ +projects.register( + r'environmentvariables', + EnvironmentVariablesViewSet, + basename='projects-environmentvariables', + parents_query_lookups=['project__slug'], +) + urlpatterns = [] urlpatterns += router.urls diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index ce1b85df191..ad738a16b9a 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -7,6 +7,7 @@ from rest_framework.metadata import SimpleMetadata from rest_framework.mixins import ( CreateModelMixin, + DestroyModelMixin, ListModelMixin, UpdateModelMixin, ) @@ -20,7 +21,7 @@ from readthedocs.builds.models import Build, Version from readthedocs.core.utils import trigger_build -from readthedocs.projects.models import Project +from readthedocs.projects.models import Project, EnvironmentVariable from readthedocs.projects.views.mixins import ProjectImportMixin from readthedocs.redirects.models import Redirect @@ -32,6 +33,7 @@ from .serializers import ( BuildCreateSerializer, BuildSerializer, + EnvironmentVariableSerializer, ProjectSerializer, ProjectCreateSerializer, RedirectCreateSerializer, @@ -378,3 +380,26 @@ def perform_create(self, serializer): 'project': self._get_parent_project(), }) serializer.save() + + +class EnvironmentVariablesViewSet(APIv3Settings, NestedViewSetMixin, + ProjectQuerySetMixin, FlexFieldsMixin, + CreateModelMixin, DestroyModelMixin, + ReadOnlyModelViewSet): + model = EnvironmentVariable + lookup_field = 'pk' + lookup_url_kwarg = 'environmentvariable_pk' + queryset = EnvironmentVariable.objects.all() + serializer_class = EnvironmentVariableSerializer + permission_classes = (IsAuthenticated & IsProjectAdmin,) + + def get_queryset(self): + queryset = super().get_queryset() + return queryset.select_related('project') + + def perform_create(self, serializer): + # Inject the project from the URL into the serializer + serializer.validated_data.update({ + 'project': self._get_parent_project(), + }) + serializer.save() diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index e60d77440c4..541228ea525 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -1483,6 +1483,8 @@ class EnvironmentVariable(TimeStampedModel, models.Model): help_text=_('Project where this variable will be used'), ) + objects = RelatedProjectQuerySet.as_manager() + def __str__(self): return self.name