Skip to content

Commit c7d7360

Browse files
authored
Merge pull request #3054 from rtfd/ginstrom-migrate-v1-to-v2
Remove apiv1 from our internal usage.
2 parents 60f5d79 + c261684 commit c7d7360

File tree

12 files changed

+130
-93
lines changed

12 files changed

+130
-93
lines changed

readthedocs/doc_builder/environments.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from readthedocs.builds.constants import BUILD_STATE_FINISHED
2323
from readthedocs.builds.models import BuildCommandResultMixin
2424
from readthedocs.projects.constants import LOG_TEMPLATE
25-
from readthedocs.api.client import api as api_v1
2625
from readthedocs.restapi.client import api as api_v2
2726

2827
from .exceptions import (BuildEnvironmentException, BuildEnvironmentError,
@@ -37,7 +36,7 @@
3736

3837

3938
__all__ = (
40-
'api_v1', 'api_v2',
39+
'api_v2',
4140
'BuildCommand', 'DockerBuildCommand',
4241
'BuildEnvironment', 'LocalEnvironment', 'DockerEnvironment',
4342
)

readthedocs/projects/models.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from guardian.shortcuts import assign
1818
from taggit.managers import TaggableManager
1919

20-
from readthedocs.api.client import api
2120
from readthedocs.builds.constants import LATEST, LATEST_VERBOSE_NAME, STABLE
2221
from readthedocs.core.resolver import resolve, resolve_domain
2322
from readthedocs.core.utils import broadcast, slugify
@@ -32,7 +31,7 @@
3231
from readthedocs.projects.templatetags.projects_tags import sort_version_aware
3332
from readthedocs.projects.utils import make_api_version
3433
from readthedocs.projects.version_handling import determine_stable_version, version_windows
35-
from readthedocs.restapi.client import api as apiv2
34+
from readthedocs.restapi.client import api
3635
from readthedocs.vcs_support.backends import backend_cls
3736
from readthedocs.vcs_support.base import VCSProject
3837
from readthedocs.vcs_support.utils import Lock, NonBlockingLock
@@ -357,7 +356,7 @@ def get_builds_url(self):
357356

358357
def get_canonical_url(self):
359358
if getattr(settings, 'DONT_HIT_DB', True):
360-
return apiv2.project(self.pk).canonical_url().get()['url']
359+
return api.project(self.pk).canonical_url().get()['url']
361360
return self.get_docs_url()
362361

363362
def get_subproject_urls(self):
@@ -368,7 +367,7 @@ def get_subproject_urls(self):
368367
if getattr(settings, 'DONT_HIT_DB', True):
369368
return [(proj['slug'], proj['canonical_url'])
370369
for proj in (
371-
apiv2.project(self.pk)
370+
api.project(self.pk)
372371
.subprojects()
373372
.get()['subprojects'])]
374373
return [(proj.child.slug, proj.child.get_docs_url())
@@ -641,8 +640,7 @@ def get_latest_build(self, finished=True):
641640

642641
def api_versions(self):
643642
ret = []
644-
for version_data in api.version.get(project=self.pk,
645-
active=True)['objects']:
643+
for version_data in api.project(self.pk).active_versions.get()['versions']:
646644
version = make_api_version(version_data)
647645
ret.append(version)
648646
return sort_version_aware(ret)

readthedocs/projects/tasks.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,17 +234,17 @@ def run_build(self, docker=False, record=True):
234234
@staticmethod
235235
def get_project(project_pk):
236236
"""Get project from API"""
237-
project_data = api_v1.project(project_pk).get()
237+
project_data = api_v2.project(project_pk).get()
238238
project = make_api_project(project_data)
239239
return project
240240

241241
@staticmethod
242242
def get_version(project, version_pk):
243243
"""Ensure we're using a sane version"""
244244
if version_pk:
245-
version_data = api_v1.version(version_pk).get()
245+
version_data = api_v2.version(version_pk).get()
246246
else:
247-
version_data = (api_v1
247+
version_data = (api_v2
248248
.version(project.slug)
249249
.get(slug=LATEST)['objects'][0])
250250
return make_api_version(version_data)
@@ -509,7 +509,7 @@ def update_imported_docs(version_pk):
509509
510510
:param version_pk: Version id to update
511511
"""
512-
version_data = api_v1.version(version_pk).get()
512+
version_data = api_v2.version(version_pk).get()
513513
version = make_api_version(version_data)
514514
project = version.project
515515
ret_dict = {}

readthedocs/restapi/serializers.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,27 @@ class Meta(object):
2626
)
2727

2828

29+
class ProjectAdminSerializer(ProjectSerializer):
30+
31+
class Meta(ProjectSerializer.Meta):
32+
fields = ProjectSerializer.Meta.fields + (
33+
'enable_epub_build',
34+
'enable_pdf_build',
35+
'conf_py_file',
36+
'analytics_code',
37+
'cdn_enabled',
38+
'container_image',
39+
'container_mem_limit',
40+
'container_time_limit',
41+
'install_project',
42+
'use_system_packages',
43+
'suffix',
44+
'skip',
45+
'requirements_file',
46+
'python_interpreter',
47+
)
48+
49+
2950
class VersionSerializer(serializers.ModelSerializer):
3051
project = ProjectSerializer()
3152
downloads = serializers.DictField(source='get_downloads', read_only=True)
@@ -41,7 +62,15 @@ class Meta(object):
4162
)
4263

4364

65+
class VersionAdminSerializer(VersionSerializer):
66+
67+
"""Version serializer that returns admin project data"""
68+
69+
project = ProjectAdminSerializer()
70+
71+
4472
class BuildCommandSerializer(serializers.ModelSerializer):
73+
4574
run_time = serializers.ReadOnlyField()
4675

4776
class Meta(object):
@@ -51,7 +80,7 @@ class Meta(object):
5180

5281
class BuildSerializer(serializers.ModelSerializer):
5382

54-
"""Readonly version of the build serializer, used for user facing display"""
83+
"""Build serializer for user display, doesn't display internal fields"""
5584

5685
commands = BuildCommandSerializer(many=True, read_only=True)
5786
state_display = serializers.ReadOnlyField(source='get_state_display')
@@ -61,13 +90,12 @@ class Meta(object):
6190
exclude = ('builder',)
6291

6392

64-
class BuildSerializerFull(BuildSerializer):
93+
class BuildAdminSerializer(BuildSerializer):
6594

66-
"""Writeable Build instance serializer, for admin access by builders"""
95+
"""Build serializer for display to admin users and build instances"""
6796

68-
class Meta(object):
69-
model = Build
70-
exclude = ('')
97+
class Meta(BuildSerializer.Meta):
98+
exclude = ()
7199

72100

73101
class SearchIndexSerializer(serializers.Serializer):

readthedocs/restapi/utils.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,8 @@ def index_search_request(version, page_list, commit, project_scale, page_scale,
115115
routes.extend([p.parent.slug for p in project.superprojects.all()])
116116
for page in page_list:
117117
log.debug("Indexing page: %s:%s", project.slug, page['path'])
118-
page_id = (hashlib
119-
.md5('-'.join([project.slug, version.slug, page['path']]))
120-
.hexdigest())
118+
to_hash = '-'.join([project.slug, version.slug, page['path']])
119+
page_id = hashlib.md5(to_hash.encode('utf-8')).hexdigest()
121120
index_list.append({
122121
'id': page_id,
123122
'project': project.slug,
@@ -132,11 +131,10 @@ def index_search_request(version, page_list, commit, project_scale, page_scale,
132131
})
133132
if section:
134133
for sect in page['sections']:
134+
id_to_hash = '-'.join([project.slug, version.slug,
135+
page['path'], sect['id']])
135136
section_index_list.append({
136-
'id': (hashlib
137-
.md5('-'.join([project.slug, version.slug,
138-
page['path'], sect['id']]))
139-
.hexdigest()),
137+
'id': (hashlib.md5(id_to_hash.encode('utf-8')).hexdigest()),
140138
'project': project.slug,
141139
'version': version.slug,
142140
'path': page['path'],

readthedocs/restapi/views/model_views.py

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,31 +21,52 @@
2121

2222
from ..permissions import (APIPermission, APIRestrictedPermission,
2323
RelatedProjectIsOwner, IsOwner)
24-
from ..serializers import (BuildSerializerFull, BuildSerializer,
25-
BuildCommandSerializer, ProjectSerializer,
26-
VersionSerializer, DomainSerializer,
27-
RemoteOrganizationSerializer,
24+
from ..serializers import (BuildSerializer, BuildAdminSerializer,
25+
BuildCommandSerializer,
26+
ProjectSerializer, ProjectAdminSerializer,
27+
VersionSerializer, VersionAdminSerializer,
28+
DomainSerializer, RemoteOrganizationSerializer,
2829
RemoteRepositorySerializer)
2930
from .. import utils as api_utils
3031

3132
log = logging.getLogger(__name__)
3233

3334

34-
class ProjectViewSet(viewsets.ModelViewSet):
35+
class UserSelectViewSet(viewsets.ModelViewSet):
36+
37+
"""View set that varies serializer class based on request user credentials
38+
39+
Viewsets using this class should have an attribute `admin_serializer_class`,
40+
which is a serializer that might have more fields that only admin/staff
41+
users require. If the user is staff, this class will be returned instead.
42+
"""
43+
44+
def get_serializer_class(self):
45+
try:
46+
if self.request.user.is_staff and self.admin_serializer_class is not None:
47+
return self.admin_serializer_class
48+
except AttributeError:
49+
pass
50+
return self.serializer_class
51+
52+
def get_queryset(self):
53+
"""Use our API manager method to determine authorization on queryset"""
54+
return self.model.objects.api(self.request.user)
55+
56+
57+
class ProjectViewSet(UserSelectViewSet):
3558

3659
"""List, filter, etc. Projects."""
3760

3861
permission_classes = [APIPermission]
3962
renderer_classes = (JSONRenderer,)
4063
serializer_class = ProjectSerializer
64+
admin_serializer_class = ProjectAdminSerializer
4165
model = Project
4266
paginate_by = 100
4367
paginate_by_param = 'page_size'
4468
max_paginate_by = 1000
4569

46-
def get_queryset(self):
47-
return self.model.objects.api(self.request.user)
48-
4970
@decorators.detail_route()
5071
def valid_versions(self, request, **kwargs):
5172
"""Maintain state of versions that are wanted."""
@@ -81,6 +102,15 @@ def subprojects(self, request, **kwargs):
81102
'subprojects': ProjectSerializer(children, many=True).data
82103
})
83104

105+
@detail_route()
106+
def active_versions(self, request, **kwargs):
107+
project = get_object_or_404(
108+
Project.objects.api(request.user), pk=kwargs['pk'])
109+
versions = project.versions.filter(active=True)
110+
return Response({
111+
'versions': VersionSerializer(versions, many=True).data
112+
})
113+
84114
@decorators.detail_route(permission_classes=[permissions.IsAdminUser])
85115
def token(self, request, **kwargs):
86116
project = get_object_or_404(
@@ -157,35 +187,22 @@ def sync_versions(self, request, **kwargs):
157187
})
158188

159189

160-
class VersionViewSet(viewsets.ModelViewSet):
190+
class VersionViewSet(UserSelectViewSet):
161191

162192
permission_classes = [APIRestrictedPermission]
163193
renderer_classes = (JSONRenderer,)
164194
serializer_class = VersionSerializer
195+
admin_serializer_class = VersionAdminSerializer
165196
model = Version
166197

167-
def get_queryset(self):
168-
return self.model.objects.api(self.request.user)
169-
170198

171-
class BuildViewSetBase(viewsets.ModelViewSet):
199+
class BuildViewSetBase(UserSelectViewSet):
172200
permission_classes = [APIRestrictedPermission]
173201
renderer_classes = (JSONRenderer,)
202+
serializer_class = BuildSerializer
203+
admin_serializer_class = BuildAdminSerializer
174204
model = Build
175205

176-
def get_queryset(self):
177-
return self.model.objects.api(self.request.user)
178-
179-
def get_serializer_class(self):
180-
"""Vary serializer class based on user status
181-
182-
This is used to allow write to write-only fields on Build by admin users
183-
and to not return those fields to non-admin users.
184-
"""
185-
if self.request.user.is_staff:
186-
return BuildSerializerFull
187-
return BuildSerializer
188-
189206

190207
class BuildViewSet(SettingsOverrideObject):
191208

@@ -194,18 +211,12 @@ class BuildViewSet(SettingsOverrideObject):
194211
_default_class = BuildViewSetBase
195212

196213

197-
class BuildCommandViewSet(viewsets.ModelViewSet):
198-
199-
"""This is currently a write-only way to update the commands on build."""
200-
214+
class BuildCommandViewSet(UserSelectViewSet):
201215
permission_classes = [APIRestrictedPermission]
202216
renderer_classes = (JSONRenderer,)
203217
serializer_class = BuildCommandSerializer
204218
model = BuildCommandResult
205219

206-
def get_queryset(self):
207-
return self.model.objects.api(self.request.user)
208-
209220

210221
class NotificationViewSet(viewsets.ReadOnlyModelViewSet):
211222
permission_classes = (permissions.IsAuthenticated, RelatedProjectIsOwner)
@@ -216,15 +227,12 @@ def get_queryset(self):
216227
return self.model.objects.api(self.request.user)
217228

218229

219-
class DomainViewSet(viewsets.ModelViewSet):
230+
class DomainViewSet(UserSelectViewSet):
220231
permission_classes = [APIRestrictedPermission]
221232
renderer_classes = (JSONRenderer,)
222233
serializer_class = DomainSerializer
223234
model = Domain
224235

225-
def get_queryset(self):
226-
return self.model.objects.api(self.request.user)
227-
228236

229237
class RemoteOrganizationViewSet(viewsets.ReadOnlyModelViewSet):
230238
permission_classes = [IsOwner]

readthedocs/rtd_tests/mocks/mock_api.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,5 @@ def mock_api(repo):
9696
with mock.patch('readthedocs.restapi.client.api', api_mock), \
9797
mock.patch('readthedocs.api.client.api', api_mock), \
9898
mock.patch('readthedocs.projects.tasks.api_v2', api_mock), \
99-
mock.patch('readthedocs.projects.tasks.api_v1', api_mock), \
100-
mock.patch('readthedocs.doc_builder.environments.api_v1', api_mock), \
10199
mock.patch('readthedocs.doc_builder.environments.api_v2', api_mock):
102100
yield api_mock

readthedocs/rtd_tests/tests/test_api.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from __future__ import absolute_import
2-
from builtins import str
2+
33
import json
44
import base64
55
import datetime
66

77
import mock
8+
from builtins import str
89
from django.test import TestCase
910
from django.contrib.auth.models import User
1011
from django_dynamic_fixture import get
@@ -168,6 +169,23 @@ def test_make_project(self):
168169
obj = json.loads(resp.content)
169170
self.assertEqual(obj['slug'], 'awesome-project')
170171

172+
def test_user_doesnt_get_full_api_return(self):
173+
user_normal = get(User, is_staff=False)
174+
user_admin = get(User, is_staff=True)
175+
project = get(Project, main_language_project=None, conf_py_file='foo')
176+
client = APIClient()
177+
178+
client.force_authenticate(user=user_normal)
179+
resp = client.get('/api/v2/project/%s/' % (project.pk))
180+
self.assertEqual(resp.status_code, 200)
181+
self.assertNotIn('conf_py_file', resp.data)
182+
183+
client.force_authenticate(user=user_admin)
184+
resp = client.get('/api/v2/project/%s/' % (project.pk))
185+
self.assertEqual(resp.status_code, 200)
186+
self.assertIn('conf_py_file', resp.data)
187+
self.assertEqual(resp.data['conf_py_file'], 'foo')
188+
171189
def test_invalid_make_project(self):
172190
"""
173191
Test that the authentication is turned on.

0 commit comments

Comments
 (0)