-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Initial structure for APIv3 #5356
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
readthedocs/v3/views.py
Outdated
from readthedocs.projects.models import Project | ||
from readthedocs.restapi.permissions import IsOwner | ||
|
||
from .filters import ProjectFilter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This references something that doesn't exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ups... I forget to push this file.
b28c822
to
cd6839d
Compare
readthedocs/builds/models.py
Outdated
else: | ||
slug_url = self.slug | ||
|
||
if self.project.repo_type in ('git', 'gitlab'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about this, the meaning for repo_type is something different in the model
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repo_type
means what I expects. Here we need to know what kind of repository it is to generate a valid http linkfor the online VCS for that version.
This method should probably need to be refactored, anyway. For now, it does what I need, but it's not robust and does not work for all the cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what @stsewd is saying is that gitlab
is not even one of the REPO_CHOICES
.
In [2]: Project.objects.filter(repo_type='gitlab').count()
Out[2]: 0
This should probably be:
if self.project.repo_type == REPO_TYPE_GIT:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, you both are right!
I got confused here. I want to check if github
or gitlab
or bitbucket
is in the repository's URL since I'm trying to build the URL for the version on the external VCS service.
readthedocs/v3/serializers.py
Outdated
data = {} | ||
|
||
for k, v in downloads.items(): | ||
if k in ('htmlzip', 'pdf', 'epub'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should accept every key from get_downloas
instead of filtering them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to enforce to have a consistent response on the API, but now I realized that the breaking change will be produced if the field is missing and not if there is one new. So, this check should probably be removed.
readthedocs/v3/serializers.py
Outdated
privacy_level = PrivacyLevelSerializer(source='*') | ||
urls = ProjectURLsSerializer(source='*') | ||
subproject_of = serializers.SerializerMethodField() | ||
translation_of = serializers.SerializerMethodField() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we call these main_project
and main_language
internally
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I think that subproject_of
and translation_of
are better names for the users of the API, though.
readthedocs/v3/views.py
Outdated
* Retrieve only needed data: ``/api/v3/projects/?fields=slug,created`` | ||
* Retrieve specific project: ``/api/v3/projects/{project_slug}/`` | ||
* Expand required fields: ``/api/v3/projects/{project_slug}/?expand=active_versions`` | ||
* Translations of a projects: ``/api/v3/projects/{project_slug}/translations/`` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* Translations of a projects: ``/api/v3/projects/{project_slug}/translations/`` | |
* Translations of a project: ``/api/v3/projects/{project_slug}/translations/`` |
Same for the ones below
readthedocs/v3/views.py
Outdated
* Expand required fields: ``/api/v3/projects/{project_slug}/?expand=active_versions`` | ||
* Translations of a projects: ``/api/v3/projects/{project_slug}/translations/`` | ||
* Subprojects of a projects: ``/api/v3/projects/{project_slug}/subprojects/`` | ||
* Superprojects of a projects: ``/api/v3/projects/{project_slug}/superprojects/`` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we don't allow nested subprojects, so one project can only have one superproject
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. This endpoint could be complete removed or named in singular and return a detailed project object instead. Although, I don't think it will be useful.
Probably removing it is better for now.
514333a
to
00eceef
Compare
b7788b8
to
fda8d8f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I only tested the project endpoint so far. Here's my feedback:
-
I am on a bit of a performance kick but I found a number of performance issues related to object relationships.
-
I had to install coreapi. Otherwise I got the error:
AssertionError: `coreapi` must be installed for schema support.
readthedocs/builds/models.py
Outdated
else: | ||
slug_url = self.slug | ||
|
||
if self.project.repo_type in ('git', 'gitlab'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what @stsewd is saying is that gitlab
is not even one of the REPO_CHOICES
.
In [2]: Project.objects.filter(repo_type='gitlab').count()
Out[2]: 0
This should probably be:
if self.project.repo_type == REPO_TYPE_GIT:
readthedocs/builds/models.py
Outdated
if self.project.repo_type in ('git', 'gitlab'): | ||
url = f'/tree/{slug_url}/' | ||
|
||
if self.project.repo_type == 'bitbucket': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bitbucket is also not a valid repo_type
. Do you mean 'bitbucket' in repo
?
readthedocs/settings/base.py
Outdated
@@ -5,6 +5,7 @@ | |||
absolute_import, division, print_function, unicode_literals) | |||
|
|||
import os | |||
import datetime |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this needed for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we are no't using this
readthedocs/v3/serializers.py
Outdated
repository = RepositorySerializer(source='*') | ||
privacy_level = PrivacyLevelSerializer(source='*') | ||
urls = ProjectURLsSerializer(source='*') | ||
subproject_of = serializers.SerializerMethodField() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and translation_of
probably need to be prefetched in the queryset. I'm seeing a lot of duplicate queries to projects_projectrelationship
.
readthedocs/v3/serializers.py
Outdated
subproject_of = serializers.SerializerMethodField() | ||
translation_of = serializers.SerializerMethodField() | ||
default_branch = serializers.CharField(source='get_default_branch') | ||
tags = serializers.StringRelatedField(many=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be prefetched in the queryset.
readthedocs/v3/serializers.py
Outdated
'privacy_level', | ||
'subproject_of', | ||
'translation_of', | ||
'urls', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This field (via our resolver - see #3712) results in two extra queries per project returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with this urls
field is the usage of Project.get_docs_url
method. I need to check how to improve this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When running locally I get:
File "/Users/eric/projects/readthedocs.org/readthedocs/v3/urls.py", line 66, in <module>
public=True),
File "/Users/eric/.virtualenvs/readthedocs3/lib/python3.6/site-packages/rest_framework/documentation.py", line 68, in include_docs_urls
permission_classes=permission_classes,
File "/Users/eric/.virtualenvs/readthedocs3/lib/python3.6/site-packages/rest_framework/documentation.py", line 29, in get_docs_view
permission_classes=permission_classes,
File "/Users/eric/.virtualenvs/readthedocs3/lib/python3.6/site-packages/rest_framework/schemas/__init__.py", line 41, in get_schema_view
urlconf=urlconf, patterns=patterns,
File "/Users/eric/.virtualenvs/readthedocs3/lib/python3.6/site-packages/rest_framework/schemas/generators.py", line 257, in __init__
assert coreapi, '`coreapi` must be installed for schema support.'
AssertionError: `coreapi` must be installed for schema support.
A pip install coreapi
fixed it.
This is 💯 better than our current API. Super great work!! I read through most of the code and played around with it locally across all the documented endpoints. Feel free to ignore my comments on the REST Browsable API because I think we need to turn that off in production; so we need another solution for "browsing" it for our users.
I think there's some polish we need to do around field ordering and QA, but I think I'd be 👍 to get this merged and deployed behind a feature flag for internal testing.
if self.slug == STABLE: | ||
stable = determine_stable_version(self.project.versions.all()) | ||
if stable: | ||
return stable.slug |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will only return anything on the stable version, that seems confusing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would it return in case it's not stable?
This method return "where the current version points to" and it only makes sense for stable
which will point to a specific tag/version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just mean we should call the function stable_ref
or something -- it doesn't get the ref
for any Version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, we could wait till having aliases, also identifier
has the commit of the tag, maybe we can filter for that instead of calling determinate_stable_version
return stable.slug | ||
|
||
@property | ||
def vcs_url(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm surprised we don't already have this logic somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do have it, I didn't find it :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We assembled the url here https://github.com/rtfd/sphinx_rtd_theme/blob/a49a812c8821123091166fae1897d702cdc2d627/sphinx_rtd_theme/breadcrumbs.html#L45 but we get some metadata from here https://github.com/rtfd/readthedocs.org/blob/humitos/api/v3-implementation/readthedocs/doc_builder/backends/sphinx.py#L88-L104
readthedocs/builds/models.py
Outdated
@@ -258,6 +290,8 @@ def get_subdomain_url(self): | |||
def get_downloads(self, pretty=False): | |||
project = self.project | |||
data = {} | |||
# TODO: refactor the ``pretty`` argument. It seems to just return nicer | |||
# keys, but the logic looks the same |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Believe @stsewd already did this in another PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I need to rebase my branch.
readthedocs/urls.py
Outdated
@@ -77,9 +77,10 @@ | |||
# Keep the `doc_search` at root level, so the test does not fail for other API | |||
url(r'^api/v2/docsearch/$', PageSearchAPIView.as_view(), name='doc_search'), | |||
url( | |||
r'^api-auth/', | |||
r'^api/auth/', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this backwards compat? Do we care?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's not.
I think I left this change by mistake since it's not really necessary but I was testing another thing and I wanted to keep the pattern.
Although, I think we don't care since this is not really used as far as I know.
readthedocs/v3/views.py
Outdated
|
||
def get_queryset(self): | ||
queryset = super().get_queryset() | ||
return queryset.filter(users=self.request.user) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there an abstraction here that will work better between the .org & .com?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I need to use .public
/.api
method form the manager here probably so the migration to .com is easier and just in one place.
readthedocs/v3/serializers.py
Outdated
|
||
class ProjectURLsSerializer(serializers.Serializer): | ||
documentation = serializers.CharField(source='get_docs_url') | ||
project = serializers.SerializerMethodField() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks at this in the response was confusing:
urls: {
documentation: "http://time-test.dev.readthedocs.io:8000/en/latest/",
project: null
}
Perhaps it should be project_homepage
or something? Otherwise I might confuse it with the URL of the RTD page for the project (which isn't in the response, afaict)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm... good point. I didn't add "human" URLs for Project, Version and Build under Read the Docs. I may worth to add them all in each response.
readthedocs/v3/filters.py
Outdated
|
||
class VersionFilter(filters.FilterSet): | ||
verbose_name__contains = filters.CharFilter( | ||
field_name='versbose_name', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo
field_name='versbose_name', | |
field_name='verbose_name', |
readthedocs/v3/filters.py
Outdated
|
||
|
||
class BuildFilter(filters.FilterSet): | ||
running = filters.BooleanFilter(method='get_running') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is broken in testing "invalid_name":
running = filters.BooleanFilter(method='get_running') | |
running = filters.BooleanFilter(method='get_running', label="Running") |
readthedocs/v3/views.py
Outdated
if superproject: | ||
data = self.get_serializer(superproject).data | ||
return Response(data) | ||
return Response(status=404) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I don't have a superproject I get a 404, when I don't have a subproject I get a 200 with an empty list. Should fix one approach and stick to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The response is different because the relations are different: a project can have 0 to N subprojects (which returns a list view) but None or 1 superproject (which returns a detailed view).
readthedocs/api/v3/views.py
Outdated
return queryset | ||
|
||
# give access to the user if it's maintainer of the project | ||
if self.request.user in self.model.objects.filter(**self.get_parents_query_dict()): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be either a queryset method or a mixin. I think as a rule we should have no custom querysets unless we have a really good reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I refactored all the auth/permission checks into a Mixin and a Permission class. Please, let me know what you think about the new updates.
215c1c0
to
edf64a8
Compare
edf64a8
to
7eeeb7d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great on my side. I don't fully understand it, but the approach looks good.
readthedocs/api/v3/mixins.py
Outdated
Mixin to define queryset permissions for ViewSet only in one place. | ||
|
||
All APIv3 ViewSet should inherit this mixin, unless specific permissions | ||
required. In that case, an specific mixin for that case should be defined. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
required. In that case, an specific mixin for that case should be defined. | |
required. In that case, a specific mixin for that case should be defined. |
readthedocs/api/v3/mixins.py
Outdated
""" | ||
|
||
# Allow hitting ``/api/v3/projects/`` to list their own projects | ||
if self.basename == 'projects' and self.action == 'list': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels weird to have this here as a special case, as noted in the comment. Don't understand the code enough to fully evaluate it tho.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. This sort of a special case should be on the ProjectViewSet
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved it the ProjectViewSet. Makes more sense there.
""" | ||
|
||
def has_permission(self, request, view): | ||
is_authenticated = super().has_permission(request, view) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this correct? auth and permission are different to me, so we should be sure we about what it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as this class extends from IsAuthenticated
calling the superclass will just check if the user is authenticated.
Looking more generally at this permission class as a whole I think it is too complicated and will yield future bugs. Instead of having a complex permission class that has some tight coupling with the classes it is used on (this class checks view.basename
), I would rather see ProjectViewSet.get_queryset
limit the queryset that a user can query.
I see that @ericholscher suggested previously that having a custom get_queryset
is something we should avoid. I can't comment there directly since that commit was removed in a force push. I don't agree with that statement and fundamentally this looks like a case where the queryset does need to change. The queryset should return the user's projects on a listview and all projects on a details view.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved the chunk of specific code for ProjectViewSet and this class now basically does what David mentioned here:
- returns user's projects if admin (
detail_objects
method) - returns all projects the user can see (
listing_objects
method)
We may want to keep talking about this in another channel. I don't want to block the PR on this piece of auth since we may need a bigger refactor for that anyways.
if self.slug == STABLE: | ||
stable = determine_stable_version(self.project.versions.all()) | ||
if stable: | ||
return stable.slug |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just mean we should call the function stable_ref
or something -- it doesn't get the ref
for any Version.
The common submodule shouldn't be updated here (#5517) I have some questions:
Guessing we are going to use this for version's alias Some bugs I found
|
docs/api/v3.rst
Outdated
@@ -344,7 +347,7 @@ Build details | |||
|
|||
.. sourcecode:: bash | |||
|
|||
$ curl https://readthedocs.org/api/v3/projects/pip/builds/8592686/?include_config=true | |||
$ curl https://readthedocs.org/api/v3/projects/pip/builds/8592686/?expand=config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The query description needs to be updated too
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the structure of api v3 is fine, I left some questions and maybe bugs p:
|
||
|
||
class ProjectFilter(filters.FilterSet): | ||
name__contains = filters.CharFilter( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this some kind of standard way in rest framework? Feels weird having __
them in the query params (it also can be confused with _
, and be ignored without any error)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is the standard from django filters and it follow the idea of Django ORM.
readthedocs/api/v3/mixins.py
Outdated
slug = value | ||
break | ||
|
||
return get_object_or_404(model, slug=slug) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a bug here, slug can be ended up undefined. It's named as project_slug
above
readthedocs/api/v3/renderer.py
Outdated
@@ -0,0 +1,58 @@ | |||
import json |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file name shouldn't be renderers
? p:
readthedocs/api/v3/serializers.py
Outdated
fields = [ | ||
'username', | ||
'date_joined', | ||
'last_login', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aren't we leaking some info with last_login
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm... Not sure if this is really important.
In theory, you can see these info from people you share a project with. We already remove first and last name just in case, but those are bigger ones in case of leak.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we care about last_login
, we should remove date_joined
as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GitHub does show the date_joinded field, last login feels like a more private thing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm +1 on removing last login.
if self.slug == STABLE: | ||
stable = determine_stable_version(self.project.versions.all()) | ||
if stable: | ||
return stable.slug |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, we could wait till having aliases, also identifier
has the commit of the tag, maybe we can filter for that instead of calling determinate_stable_version
return stable.slug | ||
|
||
@property | ||
def vcs_url(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We assembled the url here https://github.com/rtfd/sphinx_rtd_theme/blob/a49a812c8821123091166fae1897d702cdc2d627/sphinx_rtd_theme/breadcrumbs.html#L45 but we get some metadata from here https://github.com/rtfd/readthedocs.org/blob/humitos/api/v3-implementation/readthedocs/doc_builder/backends/sphinx.py#L88-L104
readthedocs/settings/base.py
Outdated
@@ -5,6 +5,7 @@ | |||
absolute_import, division, print_function, unicode_literals) | |||
|
|||
import os | |||
import datetime |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we are no't using this
d4e5f43
to
5ebf37d
Compare
My idea here is that if you get Alias are different than this and we should add that value in the response when we implement them.
Hrm, good point. I think that it's better to remove them now and then add them if we need them --otherwise it will be a breaking change. I suppose that we will need
I'm not sure to understand this. That URL will lookup for a project with slug |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm in favor of shipping this or something close to it as a first version so we can start to actually use it internally in production and iterate on it. I do have some comments but none of them should block shipping.
I would like to better understand the thought process behind the current expandable fields. I can see a potential use case for expandable fields if the data was very verbose or expensive to calculate but in some of the cases in this PR that is not the case. Furthermore, the data seems pretty useful (the last build on a version, the active versions of a project). Is there instead a way to display say a short listing by default (eg. just the slugs of the active versions) and then optionally expand to something more verbose?
There are still some performance issues with this but I think we can track them down after the internal rollout. For example, querying http://localhost:8000/api/v3/projects/?expand=active_versions.last_build
resulted in 150 DB queries.
On a side note, moving restapi -> api/v2 in this PR made it quite a bit harder to review.
readthedocs/settings/base.py
Outdated
@@ -475,8 +475,13 @@ def USE_PROMOS(self): # noqa | |||
REST_FRAMEWORK = { | |||
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',), | |||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # NOQA | |||
'DEFAULT_THROTTLE_RATES': { | |||
'anon': '5/minute', | |||
'user': '10/minute', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we may need to monitor this pretty closely as I suspect we might hit this limit. For the initial rollout, this is fine (removing throttling altogether might be better though) but I think 10/m
is very low for a logged in user especially if we need to use the API to do anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I just wanted to have the settings there to decide the best values all together.
I'm going to use the ones listed in the DRF docs as example which seems reasonable: 60/minute for now. We can tune them later if we want to be less permissive.
readthedocs/api/v3/routers.py
Outdated
|
||
API is browseable by sending the header ``Authorization: Token <token>`` on each request. | ||
|
||
Full documentation at [https://docs.readthedocs.io/en/latest/api/v3.html](https://docs.readthedocs.io/en/latest/api/v3.html). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Full documentation at [https://docs.readthedocs.io/en/latest/api/v3.html](https://docs.readthedocs.io/en/latest/api/v3.html). | |
Full documentation at [https://docs.readthedocs.io/page/api/v3.html](https://docs.readthedocs.io/page/api/v3.html). |
""" | ||
|
||
def has_permission(self, request, view): | ||
is_authenticated = super().has_permission(request, view) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as this class extends from IsAuthenticated
calling the superclass will just check if the user is authenticated.
Looking more generally at this permission class as a whole I think it is too complicated and will yield future bugs. Instead of having a complex permission class that has some tight coupling with the classes it is used on (this class checks view.basename
), I would rather see ProjectViewSet.get_queryset
limit the queryset that a user can query.
I see that @ericholscher suggested previously that having a custom get_queryset
is something we should avoid. I can't comment there directly since that commit was removed in a force push. I don't agree with that statement and fundamentally this looks like a case where the queryset does need to change. The queryset should return the user's projects on a listview and all projects on a details view.
readthedocs/api/v3/mixins.py
Outdated
# NOTE: we don't override the manager in User model, so we don't have | ||
# ``.api`` method there | ||
if self.model is not User: | ||
queryset = queryset.api(user=user) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps I'm the only one but I think that the magic .api()
method on our managers is a bit of an anti-pattern. It isn't entirely clear to me what those methods do and I feel a number of bugs have been introduced due to confusion in what those methods do (in addition to .public()
and .private()
). Perhaps this is unavoidable for now until the codebases for .org and .com are merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We talked a little about this at the offsite. All of our code is using this and changing/removing it will require a bigger refactor.
I wrote a document explaining a little what each of these method do when I started touching auth in this API branch to match what we discussed: #5624
Although, I think this is outside the scope of this PR. We will need to talk more and agree on a pattern to follow, though.
) | ||
slug__contains = filters.CharFilter( | ||
field_name='slug', | ||
lookup_expr='contains', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Depending on the queryset, contains
on versions may not have great performance. This shouldn't stop the rollout but it will be something to look at in prod.
# Using only ``TokenAuthentication`` for now, so we can give access to | ||
# specific carefully selected users only | ||
authentication_classes = (TokenAuthentication,) | ||
permission_classes = (PublicDetailPrivateListing,) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This permission class is too specific to projects to be in this generic mixin which is for all viewsets. I think we should have something more generic like IsAuthenticated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, all of our endpoints are registered under /projects
so we can keep it like this for now and extend/move when need it.
'privacy_level', | ||
'programming_language', | ||
'repository_type', | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure of the use case behind adding so many different filterable elements. I'm also not sure what the use case is behind the contains
filters. These are a lot of filters on fields that are not indexed in the DB. We had to remove a number of filters in API v2 due to performance issues. This may not be a problem in v3 because they will only filter a single user's much smaller set of projects but at that point these filters aren't very useful.
readthedocs/api/v3/serializers.py
Outdated
modified = serializers.DateTimeField(source='modified_date') | ||
|
||
expandable_fields = dict( | ||
users=( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The maintainers of a project seems like pretty core information about the project. I'm not sure it should be an expandable field. I think it should always be expanded.
- All the users for all projects can be prefetched in one additional query. It won't be a performance issue.
- The
project/<slug>/users
endpoint is then pretty useless. The user serializer doesn't really have any useful info except the username. I say we get rid of the serializer and the endpoint. - We may in the future have a user endpoint (not under the project) and if we need to share additional details about a user (eg. all the projects they are a maintainer of) we can do that there and the user can query about a user given just the username.
readthedocs/api/v3/views.py
Outdated
|
||
class ProjectsViewSet(APIv3Settings, APIAuthMixin, NestedViewSetMixin, | ||
FlexFieldsMixin, ListModelMixin, RetrieveModelMixin, | ||
GenericViewSet): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than mixing in ListModelMixin
, RetrieveModelMixin
and GenericViewSet
couldn't we instead mixin ReadOnlyModelViewSet
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point.
The only reason is that I didn't know that it existed 😉 . Upgrading...
readthedocs/api/v3/mixins.py
Outdated
) | ||
|
||
|
||
class APIAuthMixin(NestedParentObjectMixin): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this class could use a better name. It isn't authentication per se, it is a class for getting the correct project queryset given a user and a request. How about ProjectQuerysetMixin?
readthedocs/api/v3/serializers.py
Outdated
'error', | ||
'commit', | ||
'builder', | ||
'cold_storage', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are leaking builder and cold_storage to common users, we only show this to admin users in v2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that cold_storage
could be removed from here, considering that we are going to have everything in cold storage in the near future. So, this won't be useful anymore.
builder
is not a problem, but you are right that this should probably be only for admins. I will remove it for now, and we can consider re-add it later if we want to expose different things to different users. Also, if we are going to use lambda functions or similar, the builder
won't make sense anymore.
readthedocs/api/v3/serializers.py
Outdated
'ref', | ||
'built', | ||
'active', | ||
'uploaded', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably we don't want show this, we don't use that feature anymore
subproject_of = serializers.SerializerMethodField() | ||
translation_of = serializers.SerializerMethodField() | ||
default_branch = serializers.CharField(source='get_default_branch') | ||
tags = serializers.StringRelatedField(many=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't remember if we wanted to remove this from versions and projects or only versions. I think tags aren't used anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we want to remove them from versions only, as far as I remember.
Also, I feel like |
OK! I think that I took a look at all the comments/suggestions and fix them all or replied. I think we can merge it once the tests pass. Then, we can keep talking about more specific details and make the adjustments we need. We are not going live immediately for the users, so we have some time to do "breaking changes" before making it public. |
1c0d0c0
to
7985539
Compare
No need to make this exception here.
7985539
to
8ebfb1c
Compare
This test is still not passing def test_detail_unique_version(self):
second_project = fixture.get(
Project,
users=[self.me]
)
second_version = fixture.get(
Version,
slug=self.version.slug,
verbose_name=self.version.verbose_name,
identifier='a1b2c3',
project=second_project,
active=True,
built=True,
type='tag',
)
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token.key}')
response = self.client.get(
reverse(
'projects-versions-detail',
kwargs={
'parent_lookup_project__slug': self.project.slug,
'version_slug': self. version.slug,
}),
)
self.assertEqual(response.status_code, 200)
self.assertDictEqual(
response.json(),
self._get_response_dict('projects-versions-detail'),
) When there are two projects or more with the same slug for its versions |
NestedViewSetMixin has to be on the left of ProjectQuerySetMixin to filter nested results properly.
Thanks @stsewd for mentioning this. I added your test to the suite and I think we are ready to merge this now if you agree. |
Structure for APIv3 (django app
readthedocs.api.v3
),Missing parts:
DEFAULT_THROTTLE_RATES
setting)privacy_level
anddescription
from Project responsereadthedocs.api.v3
''
to benull
on the responseversion.urls.vcs
version.downloads
as full URLs including schemaversion.last_build
as expansible fieldlinks
URLs/api/v3/users/
and/api/v3/projects/pip/users/
)implement BuildCommand endpoint (/api/v3/projects/pip/builds/1025/commands/
).
on URLs (the auto generated URL does not support it'api/v3/projects/(?P<parent_lookup_project__slug>[^/.]+)/versions/(?P<version_slug>[^/.]+)/$'
)Example of current project details response: https://gist.github.com/humitos/4c0eab5c4afbca4fa10894c59d2cde09