From c80fd05acddce1a183c52b4331910ee0e698aa33 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 25 Jun 2019 18:52:34 +0200 Subject: [PATCH 01/11] Make Project.respository.type a ChoiceField --- readthedocs/api/v3/serializers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index d0efcf066c1..995505c28e0 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -9,7 +9,7 @@ from rest_framework import serializers from readthedocs.builds.models import Build, Version -from readthedocs.projects.constants import LANGUAGES, PROGRAMMING_LANGUAGES +from readthedocs.projects.constants import LANGUAGES, PROGRAMMING_LANGUAGES, REPO_CHOICES from readthedocs.projects.models import Project @@ -312,7 +312,10 @@ def get_project_homepage(self, obj): class RepositorySerializer(serializers.Serializer): url = serializers.CharField(source='repo') - type = serializers.CharField(source='repo_type') + type = serializers.ChoiceField( + source='repo_type', + choices=REPO_CHOICES, + ) class ProjectLinksSerializer(BaseLinksSerializer): From 6f0f9fa6b76ddb6442b5a4258a4e8218342cc88a Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 25 Jun 2019 18:53:09 +0200 Subject: [PATCH 02/11] Grant permissions on 'create' action This is needed to render the Form for POST when using BrowsableAPIRenderer and hitting /projects/ --- readthedocs/api/v3/permissions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/readthedocs/api/v3/permissions.py b/readthedocs/api/v3/permissions.py index d822b9a1fea..fb86f1db49a 100644 --- a/readthedocs/api/v3/permissions.py +++ b/readthedocs/api/v3/permissions.py @@ -16,6 +16,7 @@ def has_permission(self, request, view): if is_authenticated: if view.basename == 'projects' and any([ view.action == 'list', + view.action == 'create', # used to create Form in BrowsableAPIRenderer view.action is None, # needed for BrowsableAPIRenderer ]): # hitting ``/projects/``, allowing From 5585e28b32a9cbd8e6a61415459af3ef43a2dbe3 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 25 Jun 2019 18:54:31 +0200 Subject: [PATCH 03/11] Use standard DRF status codes --- readthedocs/api/v3/views.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 0e8b2b55a0e..41d83775333 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -1,6 +1,7 @@ import django_filters.rest_framework as filters from django.utils.safestring import mark_safe from rest_flex_fields.views import FlexFieldsMixin +from rest_framework import status from rest_framework.authentication import TokenAuthentication from rest_framework.decorators import action from rest_framework.metadata import SimpleMetadata @@ -170,7 +171,7 @@ def superproject(self, request, project_slug): data = self.get_serializer(superproject).data return Response(data) except Exception: - return Response(status=404) + return Response(status=status.HTTP_404_NOT_FOUND) class SubprojectRelationshipViewSet(APIv3Settings, NestedViewSetMixin, @@ -259,7 +260,7 @@ def update(self, request, *args, **kwargs): # ``httpOnly`` on our cookies and the ``PUT/PATCH`` method are triggered # via Javascript super().update(request, *args, **kwargs) - return Response(status=204) + return Response(status=status.HTTP_204_NO_CONTENT) class BuildsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin, @@ -299,8 +300,8 @@ def create(self, request, **kwargs): # pylint: disable=arguments-differ if build: data.update({'triggered': True}) - status = 202 + code = status.HTTP_202_ACCEPTED else: data.update({'triggered': False}) - status = 400 - return Response(data=data, status=status) + code = status.HTTP_400_BAD_REQUEST + return Response(data=data, status=code) From 1077e026693effb4590012a07b13857d1bd8fffc Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 25 Jun 2019 18:55:09 +0200 Subject: [PATCH 04/11] Refactor 'trigger_initial_build' to allow calling from other places --- readthedocs/core/utils/__init__.py | 23 ++++++++++++++++++++++- readthedocs/projects/views/private.py | 17 +++-------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/readthedocs/core/utils/__init__.py b/readthedocs/core/utils/__init__.py index dbb8245c6fb..d7a4ef30fc2 100644 --- a/readthedocs/core/utils/__init__.py +++ b/readthedocs/core/utils/__init__.py @@ -5,7 +5,7 @@ import os import re -from celery import chord, group +from celery import chord, group, chain from django.conf import settings from django.utils.functional import keep_lazy from django.utils.safestring import SafeText, mark_safe @@ -165,6 +165,27 @@ def trigger_build(project, version=None, record=True, force=False): return (update_docs_task.apply_async(), build) +def trigger_initial_build(project, user): + """ + Trigger initial build after project is imported. + + :param project: project's documentation to be built + :returns: Celery AsyncResult promise + """ + + update_docs, build = prepare_build(project) + if (update_docs, build) == (None, None): + return None + + from readthedocs.oauth.tasks import attach_webhook + task_promise = chain( + attach_webhook.si(project.pk, user.pk), + update_docs, + ) + async_result = task_promise.apply_async() + return async_result + + def send_email( recipient, subject, template, template_html, context=None, request=None, from_email=None, **kwargs diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 7a06b2afa49..aeaf509bb56 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -3,7 +3,6 @@ import logging from allauth.socialaccount.models import SocialAccount -from celery import chain from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -24,9 +23,9 @@ from vanilla import CreateView, DeleteView, DetailView, GenericView, UpdateView from readthedocs.builds.forms import VersionForm -from readthedocs.builds.models import Build, Version +from readthedocs.builds.models import Version from readthedocs.core.mixins import ListViewWithForm, LoginRequiredMixin -from readthedocs.core.utils import broadcast, prepare_build, trigger_build +from readthedocs.core.utils import broadcast, trigger_build, trigger_initial_build from readthedocs.integrations.models import HttpExchange, Integration from readthedocs.oauth.services import registry from readthedocs.oauth.tasks import attach_webhook @@ -272,17 +271,7 @@ def done(self, form_list, **kwargs): ) def trigger_initial_build(self, project): - """Trigger initial build.""" - update_docs, build = prepare_build(project) - if (update_docs, build) == (None, None): - return None - - task_promise = chain( - attach_webhook.si(project.pk, self.request.user.pk), - update_docs, - ) - async_result = task_promise.apply_async() - return async_result + return trigger_initial_build(project, self.request.user) def is_advanced(self): """Determine if the user selected the `show advanced` field.""" From 766bb8d871fc74ec89200c6885ca7244d28bdb6a Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 25 Jun 2019 18:58:09 +0200 Subject: [PATCH 05/11] View/Serializer to import a Project from APIv3 --- readthedocs/api/v3/serializers.py | 16 ++++++++++ readthedocs/api/v3/views.py | 51 ++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/readthedocs/api/v3/serializers.py b/readthedocs/api/v3/serializers.py index 995505c28e0..b219b15c486 100644 --- a/readthedocs/api/v3/serializers.py +++ b/readthedocs/api/v3/serializers.py @@ -378,6 +378,22 @@ def get_translations(self, obj): return self._absolute_url(path) +class ProjectCreateSerializer(FlexFieldsModelSerializer): + + """Serializer used to Import a Project.""" + + repository = RepositorySerializer(source='*') + + class Meta: + model = Project + fields = ( + 'name', + 'language', + 'repository', + 'project_url', # project_homepage + ) + + class ProjectSerializer(FlexFieldsModelSerializer): language = LanguageSerializer() diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 41d83775333..8d04c1afda4 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -18,8 +18,10 @@ from rest_framework_extensions.mixins import NestedViewSetMixin from readthedocs.builds.models import Build, Version -from readthedocs.core.utils import trigger_build +from readthedocs.core.utils import trigger_build, trigger_initial_build from readthedocs.projects.models import Project +from readthedocs.projects.signals import project_import + from .filters import BuildFilter, ProjectFilter, VersionFilter from .mixins import ProjectQuerySetMixin @@ -29,6 +31,7 @@ BuildCreateSerializer, BuildSerializer, ProjectSerializer, + ProjectCreateSerializer, VersionSerializer, VersionUpdateSerializer, ) @@ -63,7 +66,7 @@ class APIv3Settings: class ProjectsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin, - FlexFieldsMixin, ReadOnlyModelViewSet): + FlexFieldsMixin, CreateModelMixin, ReadOnlyModelViewSet): # Markdown docstring is automatically rendered by BrowsableAPIRenderer. @@ -111,14 +114,13 @@ class ProjectsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin, * Subprojects of a project: ``/api/v3/projects/{project_slug}/subprojects/`` * Superproject of a project: ``/api/v3/projects/{project_slug}/superproject/`` - Go to [https://docs.readthedocs.io/en/stable/api/v3.html](https://docs.readthedocs.io/en/stable/api/v3.html) + Go to [https://docs.readthedocs.io/page/api/v3.html](https://docs.readthedocs.io/page/api/v3.html) for a complete documentation of the APIv3. """ # noqa model = Project lookup_field = 'slug' lookup_url_kwarg = 'project_slug' - serializer_class = ProjectSerializer filterset_class = ProjectFilter queryset = Project.objects.all() permit_list_expands = [ @@ -127,6 +129,20 @@ class ProjectsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin, 'active_versions.last_build.config', ] + def get_serializer_class(self): + """ + Return correct serializer depending on the action. + + For GET it returns a serializer with many fields and on PUT/PATCH/POST, + it return a serializer to validate just a few fields. + """ + if self.action in ('list', 'retrieve'): + return ProjectSerializer + elif self.action in ('create',): + return ProjectCreateSerializer + # elif self.action in ('update', 'partial_update'): + # return ProjectUpdateSerializer + def get_queryset(self): # Allow hitting ``/api/v3/projects/`` to list their own projects if self.basename == 'projects' and self.action == 'list': @@ -162,6 +178,33 @@ def get_view_description(self, *args, **kwargs): # pylint: disable=arguments-di return mark_safe(description.format(project_slug=project.slug)) return description + def create(self, request, *args, **kwargs): + """ + Override method to importing a Project. + + * Save the Project object + * Assign the user from the request as owner + * Sent project_import signal + * Trigger an initial Build + """ + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + project = serializer.save() + headers = self.get_success_headers(serializer.data) + + # TODO: these lines need to be adapted for Corporate + project.users.add(request.user) + project_import.send(sender=project, request=request) + trigger_initial_build(project, request.user) + + # Full render Project + serializer = ProjectSerializer(instance=project) + return Response( + serializer.data, + status=status.HTTP_201_CREATED, + headers=headers, + ) + @action(detail=True, methods=['get']) def superproject(self, request, project_slug): """Return the superproject of a ``Project``.""" From 67b713775dc718b64bcb99e7e2007a0cddc10e84 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 27 Jun 2019 13:32:40 +0200 Subject: [PATCH 06/11] Endpoint to import a project --- readthedocs/api/v3/mixins.py | 1 - .../tests/responses/projects-list_POST.json | 47 ++++++++++++++++ readthedocs/api/v3/tests/test_projects.py | 54 +++++++++++++++++++ readthedocs/api/v3/views.py | 2 +- readthedocs/settings/base.py | 1 + 5 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 readthedocs/api/v3/tests/responses/projects-list_POST.json diff --git a/readthedocs/api/v3/mixins.py b/readthedocs/api/v3/mixins.py index 66309219193..802d0769588 100644 --- a/readthedocs/api/v3/mixins.py +++ b/readthedocs/api/v3/mixins.py @@ -1,5 +1,4 @@ from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import NotFound from readthedocs.builds.models import Version from readthedocs.projects.models import Project diff --git a/readthedocs/api/v3/tests/responses/projects-list_POST.json b/readthedocs/api/v3/tests/responses/projects-list_POST.json new file mode 100644 index 00000000000..cbfd52297f6 --- /dev/null +++ b/readthedocs/api/v3/tests/responses/projects-list_POST.json @@ -0,0 +1,47 @@ +{ + "_links": { + "_self": "https://readthedocs.org/api/v3/projects/test-project/", + "builds": "https://readthedocs.org/api/v3/projects/test-project/builds/", + "subprojects": "https://readthedocs.org/api/v3/projects/test-project/subprojects/", + "superproject": "https://readthedocs.org/api/v3/projects/test-project/superproject/", + "translations": "https://readthedocs.org/api/v3/projects/test-project/translations/", + "versions": "https://readthedocs.org/api/v3/projects/test-project/versions/" + }, + "created": "2019-04-29T14:00:00Z", + "default_branch": "master", + "default_version": "latest", + "description": null, + "id": 4, + "language": { + "code": "en", + "name": "English" + }, + "modified": "2019-04-29T14:00:00Z", + "name": "Test Project", + "privacy_level": { + "code": "public", + "name": "Public" + }, + "programming_language": { + "code": "words", + "name": "Only Words" + }, + "repository": { + "type": "git", + "url": "https://github.com/rtfd/template" + }, + "slug": "test-project", + "subproject_of": null, + "tags": [], + "translation_of": null, + "urls": { + "documentation": "http://readthedocs.org/docs/test-project/en/latest/", + "project_homepage": null + }, + "users": [ + { + "created": "2019-04-29T10:00:00Z", + "username": "testuser" + } + ] +} diff --git a/readthedocs/api/v3/tests/test_projects.py b/readthedocs/api/v3/tests/test_projects.py index 797f31852ab..6ecca019a9c 100644 --- a/readthedocs/api/v3/tests/test_projects.py +++ b/readthedocs/api/v3/tests/test_projects.py @@ -1,4 +1,5 @@ import datetime +import mock import json from pathlib import Path @@ -372,3 +373,56 @@ def test_projects_versions_detail_unique(self): ) self.assertEqual(response.status_code, 200) + + @mock.patch('readthedocs.api.v3.views.trigger_initial_build') + @mock.patch('readthedocs.api.v3.views.project_import') + def test_import_project(self, project_import, trigger_initial_build): + data = { + 'name': 'Test Project', + 'repository': { + 'url': 'https://github.com/rtfd/template', + 'type': 'git', + }, + } + + self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}') + response = self.client.post(reverse('projects-list'), data) + self.assertEqual(response.status_code, 201) + + query = Project.objects.filter(slug='test-project') + self.assertTrue(query.exists()) + + project = query.first() + self.assertEqual(project.name, 'Test Project') + self.assertEqual(project.slug, 'test-project') + self.assertEqual(project.repo, 'https://github.com/rtfd/template') + self.assertEqual(project.language, 'en') + self.assertIn(self.me, project.users.all()) + + # Signal sent + project_import.send.assert_has_calls( + [ + mock.call( + sender=project, + request=mock.ANY, + ), + ], + ) + + # Build triggered + trigger_initial_build.assert_has_calls( + [ + mock.call( + project, + self.me, + ), + ], + ) + + response_json = response.json() + response_json['created'] = '2019-04-29T14:00:00Z' + response_json['modified'] = '2019-04-29T14:00:00Z' + self.assertDictEqual( + response_json, + self._get_response_dict('projects-list_POST'), + ) diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 8d04c1afda4..6f0d77448c3 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -136,7 +136,7 @@ def get_serializer_class(self): For GET it returns a serializer with many fields and on PUT/PATCH/POST, it return a serializer to validate just a few fields. """ - if self.action in ('list', 'retrieve'): + if self.action in ('list', 'retrieve', 'superproject'): return ProjectSerializer elif self.action in ('create',): return ProjectCreateSerializer diff --git a/readthedocs/settings/base.py b/readthedocs/settings/base.py index 7c4ae9ececa..900e3d1ed81 100644 --- a/readthedocs/settings/base.py +++ b/readthedocs/settings/base.py @@ -512,6 +512,7 @@ def USE_PROMOS(self): # noqa 'user': '60/minute', }, 'PAGE_SIZE': 10, + 'TEST_REQUEST_DEFAULT_FORMAT': 'json', } SILENCED_SYSTEM_CHECKS = ['fields.W342', 'guardian.W001'] From 064889abbbd8fff1f9083594c10f98f23ec6a2e3 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Thu, 27 Jun 2019 13:54:32 +0200 Subject: [PATCH 07/11] Make permissions class more modular --- readthedocs/api/v3/permissions.py | 45 +++++++++++++++++-------------- readthedocs/api/v3/views.py | 5 ++-- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/readthedocs/api/v3/permissions.py b/readthedocs/api/v3/permissions.py index fb86f1db49a..7ce4be0a61d 100644 --- a/readthedocs/api/v3/permissions.py +++ b/readthedocs/api/v3/permissions.py @@ -1,32 +1,37 @@ -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import BasePermission -class PublicDetailPrivateListing(IsAuthenticated): +class PublicDetailPrivateListing(BasePermission): """ Permission class for our custom use case. * Always give permission for a ``detail`` request * Only give permission for ``listing`` request if user is admin of the project + """ + + def has_permission(self, request, view): + if view.detail: + return True + + project = view._get_parent_project() + if view.has_admin_permission(request.user, project): + return True + + +class ListCreateProject(BasePermission): + + """ + Permission class to grant projects listing and project creation. + * Allow access to ``/projects`` (user's projects listing) """ def has_permission(self, request, view): - is_authenticated = super().has_permission(request, view) - if is_authenticated: - if view.basename == 'projects' and any([ - view.action == 'list', - view.action == 'create', # used to create Form in BrowsableAPIRenderer - view.action is None, # needed for BrowsableAPIRenderer - ]): - # hitting ``/projects/``, allowing - return True - - if view.detail: - return True - - project = view._get_parent_project() - if view.has_admin_permission(request.user, project): - return True - - return False + if view.basename == 'projects' and any([ + view.action == 'list', + view.action == 'create', # used to create Form in BrowsableAPIRenderer + view.action is None, # needed for BrowsableAPIRenderer + ]): + # hitting ``/projects/``, allowing + return True diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 6f0d77448c3..1c4da3e2d97 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -11,6 +11,7 @@ UpdateModelMixin, ) from rest_framework.pagination import LimitOffsetPagination +from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import BrowsableAPIRenderer from rest_framework.response import Response from rest_framework.throttling import AnonRateThrottle, UserRateThrottle @@ -25,7 +26,7 @@ from .filters import BuildFilter, ProjectFilter, VersionFilter from .mixins import ProjectQuerySetMixin -from .permissions import PublicDetailPrivateListing +from .permissions import PublicDetailPrivateListing, ListCreateProject from .renderers import AlphabeticalSortedJSONRenderer from .serializers import ( BuildCreateSerializer, @@ -54,7 +55,7 @@ class APIv3Settings: # Using only ``TokenAuthentication`` for now, so we can give access to # specific carefully selected users only authentication_classes = (TokenAuthentication,) - permission_classes = (PublicDetailPrivateListing,) + permission_classes = (IsAuthenticated & (ListCreateProject | PublicDetailPrivateListing),) pagination_class = LimitOffsetPagination LimitOffsetPagination.default_limit = 10 From 536a64a5952cb50b998f232bbfe0925f42b8a5f7 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 3 Jul 2019 18:52:51 +0200 Subject: [PATCH 08/11] Better method name --- readthedocs/api/v3/mixins.py | 4 ++-- readthedocs/api/v3/permissions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/readthedocs/api/v3/mixins.py b/readthedocs/api/v3/mixins.py index 802d0769588..6774a5c9e18 100644 --- a/readthedocs/api/v3/mixins.py +++ b/readthedocs/api/v3/mixins.py @@ -55,12 +55,12 @@ def detail_objects(self, queryset, user): def listing_objects(self, queryset, user): project = self._get_parent_project() - if self.has_admin_permission(user, project): + if self.is_project_maintainer(user, project): return queryset return queryset.none() - def has_admin_permission(self, user, project): + def is_project_maintainer(self, user, project): if project in self.admin_projects(user): return True diff --git a/readthedocs/api/v3/permissions.py b/readthedocs/api/v3/permissions.py index 7ce4be0a61d..66ab3d19202 100644 --- a/readthedocs/api/v3/permissions.py +++ b/readthedocs/api/v3/permissions.py @@ -15,7 +15,7 @@ def has_permission(self, request, view): return True project = view._get_parent_project() - if view.has_admin_permission(request.user, project): + if view.is_project_maintainer(request.user, project): return True From 18c4f32c7434dfb199ccc93744e1b134ac03d5dd Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 3 Jul 2019 18:54:48 +0200 Subject: [PATCH 09/11] Simplify selection of serializer --- readthedocs/api/v3/views.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index 1c4da3e2d97..a257fdea0e9 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -123,6 +123,7 @@ class ProjectsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin, lookup_field = 'slug' lookup_url_kwarg = 'project_slug' filterset_class = ProjectFilter + serializer_class = ProjectSerializer queryset = Project.objects.all() permit_list_expands = [ 'active_versions', @@ -130,20 +131,6 @@ class ProjectsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin, 'active_versions.last_build.config', ] - def get_serializer_class(self): - """ - Return correct serializer depending on the action. - - For GET it returns a serializer with many fields and on PUT/PATCH/POST, - it return a serializer to validate just a few fields. - """ - if self.action in ('list', 'retrieve', 'superproject'): - return ProjectSerializer - elif self.action in ('create',): - return ProjectCreateSerializer - # elif self.action in ('update', 'partial_update'): - # return ProjectUpdateSerializer - def get_queryset(self): # Allow hitting ``/api/v3/projects/`` to list their own projects if self.basename == 'projects' and self.action == 'list': @@ -188,7 +175,7 @@ def create(self, request, *args, **kwargs): * Sent project_import signal * Trigger an initial Build """ - serializer = self.get_serializer(data=request.data) + serializer = ProjectCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) project = serializer.save() headers = self.get_success_headers(serializer.data) From 23e17f3f92654b99317e3e66d2bf1f0ba3491493 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 23 Jul 2019 13:27:33 +0200 Subject: [PATCH 10/11] Rename IsProjectAdmin to IsProjectMaintainer for consistency --- readthedocs/api/v3/permissions.py | 2 +- readthedocs/api/v3/views.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/readthedocs/api/v3/permissions.py b/readthedocs/api/v3/permissions.py index 4ed896ee855..dfd712ccd54 100644 --- a/readthedocs/api/v3/permissions.py +++ b/readthedocs/api/v3/permissions.py @@ -37,7 +37,7 @@ def has_permission(self, request, view): return True -class IsProjectAdmin(BasePermission): +class IsProjectMaintainer(BasePermission): """Grant permission if user has admin rights on the Project.""" diff --git a/readthedocs/api/v3/views.py b/readthedocs/api/v3/views.py index e9a41eab3fa..3db7d8d6c08 100644 --- a/readthedocs/api/v3/views.py +++ b/readthedocs/api/v3/views.py @@ -28,7 +28,7 @@ from .filters import BuildFilter, ProjectFilter, VersionFilter from .mixins import ProjectQuerySetMixin -from .permissions import PublicDetailPrivateListing, ListCreateProject, IsProjectAdmin +from .permissions import PublicDetailPrivateListing, ListCreateProject, IsProjectMaintainer from .renderers import AlphabeticalSortedJSONRenderer from .serializers import ( BuildCreateSerializer, @@ -363,7 +363,7 @@ class RedirectsViewSet(APIv3Settings, NestedViewSetMixin, ProjectQuerySetMixin, lookup_field = 'pk' lookup_url_kwarg = 'redirect_pk' queryset = Redirect.objects.all() - permission_classes = (IsAuthenticated & IsProjectAdmin,) + permission_classes = (IsAuthenticated & IsProjectMaintainer,) def get_queryset(self): queryset = super().get_queryset() @@ -391,7 +391,7 @@ class EnvironmentVariablesViewSet(APIv3Settings, NestedViewSetMixin, lookup_url_kwarg = 'environmentvariable_pk' queryset = EnvironmentVariable.objects.all() serializer_class = EnvironmentVariableSerializer - permission_classes = (IsAuthenticated & IsProjectAdmin,) + permission_classes = (IsAuthenticated & IsProjectMaintainer,) def get_queryset(self): queryset = super().get_queryset() From 5ea105c96b081637606802c5360a4b37111f48ae Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 23 Jul 2019 13:29:27 +0200 Subject: [PATCH 11/11] Merge conflicts --- readthedocs/api/v3/mixins.py | 5 ++++- readthedocs/core/utils/__init__.py | 23 +---------------------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/readthedocs/api/v3/mixins.py b/readthedocs/api/v3/mixins.py index 8b378f97f17..7800193489b 100644 --- a/readthedocs/api/v3/mixins.py +++ b/readthedocs/api/v3/mixins.py @@ -61,7 +61,10 @@ def listing_objects(self, queryset, user): return queryset.none() def is_project_maintainer(self, user, project): - if project in self.admin_projects(user).only('id'): + # Use .only for small optimization + admin_projects = self.admin_projects(user).only('id') + + if project in admin_projects: return True return False diff --git a/readthedocs/core/utils/__init__.py b/readthedocs/core/utils/__init__.py index 4660d37a721..ab132ec7bc9 100644 --- a/readthedocs/core/utils/__init__.py +++ b/readthedocs/core/utils/__init__.py @@ -5,7 +5,7 @@ import os import re -from celery import chord, group, chain +from celery import chord, group from django.conf import settings from django.utils.functional import keep_lazy from django.utils.safestring import SafeText, mark_safe @@ -175,27 +175,6 @@ def trigger_build(project, version=None, record=True, force=False): return (update_docs_task.apply_async(), build) -def trigger_initial_build(project, user): - """ - Trigger initial build after project is imported. - - :param project: project's documentation to be built - :returns: Celery AsyncResult promise - """ - - update_docs, build = prepare_build(project) - if (update_docs, build) == (None, None): - return None - - from readthedocs.oauth.tasks import attach_webhook - task_promise = chain( - attach_webhook.si(project.pk, user.pk), - update_docs, - ) - async_result = task_promise.apply_async() - return async_result - - def send_email( recipient, subject, template, template_html, context=None, request=None, from_email=None, **kwargs