-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
APIv3 "Import Project" endpoint #5857
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
d9f08a9
to
6302256
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.
This looks good, but I do think it might be useful to keep all the logic together from the view and code. This does it for the trigger_initial_build
logic, but it would be useful for the logic around adding the user to the Project, since this varies between sites.
@@ -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 |
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.
Do we want to support that? I feel like creating a project via the web form is probably not going to be the best UX.
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've been thinking on making the browsable API consistent with what the API offers. Besides consistency, it helps to explore it and have a better understanding of what's available and what can be done: not "self-documented" but in that direction.
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.
On the other hand, removing the create
from this check it fails with 404 when accessing to /api/v3/projects/
because of an internal DRF check. Actually, that's how I realized that I needed 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.
Overall I'm in favor of making the browsable API be consistent with what the API can do. We still can restrict creating with additional permissions or even turn off the browsable API in prod completely if needed (hopefully not as I do believe the browsable API makes the API more approachable). Generally though I hope regular users are not creating projects in production through the DRF UI.
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. |
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.
Does this not allow us to update the entire project via API?
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, in theory only the fields from ProjectCreateSerializer.Meta.fields
are allowed (name, language, repository and project_url). If there are extra fields passed, they are ignored.
We may want to add more, though.
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.
Seems like a good test case, actually.
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.
Added.
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'): |
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 superproject
here? They should probably be constants.
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.
superproject
is the @action
defined in the ProjectViewSet
that returns the superproject of a project.
I'm not sure if it makes sense to make it a constant. Although, maybe it's good to add a comment in this line to avoid confusions.
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.
Yea, we should add a comment at least, since it's not like the others.
readthedocs/api/v3/views.py
Outdated
# 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) |
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 wonder if this should all be abstracted into a single function called trigger_build(initial)
and used in all the builds, so we only have to change this logic in one place.
readthedocs/api/v3/views.py
Outdated
headers = self.get_success_headers(serializer.data) | ||
|
||
# TODO: these lines need to be adapted for Corporate | ||
project.users.add(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.
Where does this happen in the normal import workflow? I can't find it in the code.
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.
Ah, was on the subclass: https://github.com/rtfd/readthedocs.org/blob/master/readthedocs/projects/forms.py#L50-L53
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.
So perhaps we should be inheriting form this form for the Form we're using?
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 not sure I like to inherit a Serializer from a Wizard Form. Although, maybe these common methods could be extracted into a Mixin that it's usage by both. Actually, in that case, there is nothing to change in corporate since it's already done the override.
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 involves a bigger refactor than I thought. All the process to import a project is very tied to forms methods. ProjectBasicForm.save
links the remote repository.
ImportWizardView
manipulates the Basic and the Extra form to make it all fit in one project before saving it plus triggering a new build (done
method in the form)
Like a |
@ericholscher I refactored the Import Project code by extracting the shared bits into a Mixin. Looks better than the first option that I proposed, but still it's not perfect. All the manipulation of the fields (from the Form or Serializer) happen on each view and once those things are done, the data is passed to the common method that will finish the import. |
readthedocs/projects/views/mixins.py
Outdated
|
||
"""Helpers to import a Project.""" | ||
|
||
def import_project(self, project, tags, request): |
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.
Maybe finish_import_project
is a better name here, what do you think @ericholscher? Considering that the Project object is created outside this function and this one does the final steps.
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.
That seems better.
'language', | ||
'programming_language', | ||
'repository', | ||
'homepage', |
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 can add more fields here. Maybe these are also useful:
- default_branch
- default_version
- tags
- analytics_code
- show_version_warning
e81802b
to
83d21ac
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.
Lots of good stuff here, just a few comments.
We also need to add public-facing docs here, that is the biggest missing piece for me.
@@ -110,14 +115,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) |
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 should auto-link, no? Or you can put <>
around it I believe (depending on markdown implementation, 😦)
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. It's rendered as Markdown using "some implementation" and [Text](Link)
is the format that works.
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'): |
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.
Yea, we should add a comment at least, since it's not like the others.
readthedocs/api/v3/views.py
Outdated
if self.action in ('list', 'retrieve', 'superproject'): | ||
return ProjectSerializer | ||
|
||
if self.action in ('create',): |
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.
any reason not to just use a list here? Or check for the value explicitly?
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.
Just for consistency.
readthedocs/api/v3/views.py
Outdated
the database. | ||
""" | ||
project = serializer.save() | ||
self.import_project(project, [], self.request) |
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 the empty list for? We should use kwargs
to make it more explicit.
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 put request
as first argument and make tags
an optional with None
as default. I think it's better now.
readthedocs/projects/views/mixins.py
Outdated
|
||
"""Helpers to import a Project.""" | ||
|
||
def import_project(self, project, tags, request): |
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.
That seems better.
readthedocs/projects/views/mixins.py
Outdated
|
||
def import_project(self, project, tags, request): | ||
""" | ||
Import a Project into Read the Docs. |
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 you noted in the PR comment above, we should note what work it assumes has already been done in the code comment as well.
readthedocs/projects/views/mixins.py
Outdated
|
||
"""Helpers to import a Project.""" | ||
|
||
def import_project(self, project, tags, request): |
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 might put the request
first, as that is the normal order in Django in my experience.
task_promise = chain( | ||
attach_webhook.si(project.pk, user.pk), | ||
update_docs, | ||
) |
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.
Do we need to chain these? Seems like we still want to update the docs even if webhook setup fails, and they don't depend on each other in any real way.
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 do a chain just to follow an order. If attach_webhook
fails, it handles it by itself, set a message to the user and the build process continue. It does not block the update of the docs.
Anyways, this logic is not something I want to change/refactor in this PR because it's something bigger than this.
for field, value in list(form_data.items()): | ||
if field in extra_fields: | ||
setattr(project, field, value) | ||
project.save() | ||
|
||
# TODO: this signal could be removed, or used for sync task | ||
project_import.send(sender=project, request=self.request) | ||
self.import_project(project, tags, self.request) |
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.
kwargs here too would be good.
I was thinking on finish all the docs in another PR (#5895) so we can deploy this without blocking on docs, which are deployed just with a merge. It's on my list to write them. |
This is needed to render the Form for POST when using BrowsableAPIRenderer and hitting /projects/
tags is a special attribute and needs to be set via `.add` method.
c44f039
to
b29b3b5
Compare
@ericholscher I think I made all the changes needed from your comments. I had some issues when rebasing with master, hopefully I didn't break anything :) |
Why not just merge master into the branch? |
now I can't really see what commits have changed since my review :/ |
Looking at it though, the changes look good to me 👍 |
@ericholscher I'm sorry about that. I usually do rebase against master but, I may need to change that flow because of this problem |
This PR allows
POST
at/api/v3/projects
to Import a Project.It follows the same idea that importing from the Dashboard:
project_import
Django signal