Skip to content
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

Merge #6176 to master #6258

Merged
merged 20 commits into from
Oct 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
27a30c1
Allow to create ProjectRelationship (subprojects)
humitos Sep 10, 2019
1dc1899
Allow to create ProjectRelationship (subprojects)
humitos Sep 10, 2019
f8e288f
Validate user is maintainer of child project
humitos Sep 12, 2019
2d8b2f1
Do not show child link on subproject _links
humitos Sep 12, 2019
0d42c1b
Use correct lookup field on RelatedProjectQuerySetBase
humitos Sep 12, 2019
3b2871a
Add tests for /api/v3/projects/<slug>/subprojects/ endpoint
humitos Sep 12, 2019
197d106
Merge branch 'humitos/subproject-api-projectrelationship' of github.c…
humitos Sep 12, 2019
85e6cb7
Documentation for APIv3 subprojects updated
humitos Sep 12, 2019
d9bb87c
Merge branch 'master' of github.com:readthedocs/readthedocs.org into …
humitos Oct 1, 2019
a189f59
More logic tests added for restrictions
humitos Oct 1, 2019
5d928f5
Small test and responses refactor
humitos Oct 1, 2019
cfad69b
Merge branch 'humitos/apiv3/project-update-endpoint' into humitos/sub…
humitos Oct 1, 2019
5494d98
Merge branch 'humitos/apiv3/project-update-endpoint' of github.com:re…
humitos Oct 2, 2019
eb744c3
Remove redundant headers and attributes from the response
humitos Oct 2, 2019
9c2d2bb
Update readthedocs/projects/querysets.py
humitos Oct 4, 2019
a7d1db2
Move meta class to the top of the class
humitos Oct 4, 2019
c355ada
Mention the alias defaults to project's slug
humitos Oct 4, 2019
193fec6
Merge pull request #6176 from readthedocs/humitos/subproject-api-proj…
humitos Oct 8, 2019
3c40f49
Merge branch 'master' into humitos/apiv3/project-update-endpoint
stsewd Oct 8, 2019
eca2ce8
Fix linter
stsewd Oct 8, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 83 additions & 6 deletions docs/api/v3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,34 @@ This allows for documentation projects to share a search index and a namespace o
but still be maintained independently.
See :doc:`/subprojects` for more information.


Subproject details
++++++++++++++++++


.. http:get:: /api/v3/projects/(str:project_slug)/subprojects/(str:alias_slug)/

Retrieve details of a subproject relationship.

**Example request**:

.. sourcecode:: bash

$ curl -H "Authorization: Token <token>" https://readthedocs.org/api/v3/projects/pip/subprojects/subproject-alias/

**Example response**:

.. sourcecode:: json

{
"alias": "subproject-alias",
"child": ["PROJECT"],
"_links": {
"parent": "/api/v3/projects/pip/"
}
}


Subprojects listing
+++++++++++++++++++

Expand All @@ -583,15 +611,64 @@ Subprojects listing
"count": 25,
"next": "/api/v3/projects/pip/subprojects/?limit=10&offset=10",
"previous": null,
"results": ["PROJECT"]
"results": ["SUBPROJECT RELATIONSHIP"]
}

:>json integer count: total number of projects.
:>json string next: URI for next set of projects.
:>json string previous: URI for previous set of projects.
:>json array results: array of ``project`` objects.

:requestheader Authorization: token to authenticate.
Subproject create
+++++++++++++++++


.. http:post:: /api/v3/projects/(str:project_slug)/subprojects/

Create a subproject relationship between two projects.

**Example request**:

.. sourcecode:: bash

$ curl \
-X POST \
-H "Authorization: Token <token>" https://readthedocs.org/api/v3/projects/pip/subprojects/ \
-H "Content-Type: application/json" \
-d @body.json

The content of ``body.json`` is like,

.. sourcecode:: json

{
"child": "subproject-child-slug",
"alias": "subproject-alias"
}

**Example response**:

`See Subproject details <#subproject-details>`_

:>json string child: slug of the child project in the relationship.
:>json string alias: optional slug alias to be used in the URL (e.g ``/projects/<alias>/en/latest/``).
If not provided, child project's slug is used as alias.

:statuscode 201: Subproject created sucessfully


Subproject delete
+++++++++++++++++

.. http:delete:: /api/v3/projects/(str:project_slug)/subprojects/(str:alias_slug)/

Delete a subproject relationship.

**Example request**:

.. sourcecode:: bash

$ curl \
-X DELETE \
-H "Authorization: Token <token>" https://readthedocs.org/api/v3/projects/pip/subprojects/subproject-alias/

:statuscode 204: Subproject deleted successfully


Translations
Expand Down
1 change: 1 addition & 0 deletions readthedocs/api/v3/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class NestedParentObjectMixin:
PROJECT_LOOKUP_NAMES = [
'project__slug',
'projects__slug',
'parent__slug',
'superprojects__parent__slug',
'main_language_project__slug',
]
Expand Down
126 changes: 124 additions & 2 deletions readthedocs/api/v3/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
PRIVACY_CHOICES,
PROTECTED,
)
from readthedocs.projects.models import Project, EnvironmentVariable
from readthedocs.projects.models import Project, EnvironmentVariable, ProjectRelationship
from readthedocs.redirects.models import Redirect, TYPE_CHOICES as REDIRECT_TYPE_CHOICES


Expand Down Expand Up @@ -385,7 +385,7 @@ def get_subprojects(self, obj):
path = reverse(
'projects-subprojects-list',
kwargs={
'parent_lookup_superprojects__parent__slug': obj.slug,
'parent_lookup_parent__slug': obj.slug,
},
)
return self._absolute_url(path)
Expand Down Expand Up @@ -539,6 +539,128 @@ def get_subproject_of(self, obj):
return None


class SubprojectCreateSerializer(FlexFieldsModelSerializer):

"""Serializer used to define a Project as subproject of another Project."""

child = serializers.SlugRelatedField(
slug_field='slug',
queryset=Project.objects.all(),
)

class Meta:
model = ProjectRelationship
fields = [
'child',
'alias',
]

def __init__(self, *args, **kwargs):
# Initialize the instance with the parent Project to be used in the
# serializer validation.
self.parent_project = kwargs.pop('parent')
super().__init__(*args, **kwargs)

def validate_child(self, value):
# Check the user is maintainer of the child project
user = self.context['request'].user
if user not in value.users.all():
raise serializers.ValidationError(
'You do not have permissions on the child project',
)
return value

def validate_alias(self, value):
# Check there is not a subproject with this alias already
subproject = self.parent_project.subprojects.filter(alias=value)
if subproject.exists():
raise serializers.ValidationError(
'A subproject with this alias already exists',
)
return value

# pylint: disable=arguments-differ
def validate(self, data):
# Check the parent and child are not the same project
if data['child'].slug == self.parent_project.slug:
raise serializers.ValidationError(
'Project can not be subproject of itself',
)

# Check the parent project is not a subproject already
if self.parent_project.superprojects.exists():
raise serializers.ValidationError(
'Subproject nesting is not supported',
)
return data


class SubprojectLinksSerializer(BaseLinksSerializer):
_self = serializers.SerializerMethodField()
parent = serializers.SerializerMethodField()

def get__self(self, obj):
path = reverse(
'projects-subprojects-detail',
kwargs={
'parent_lookup_parent__slug': obj.parent.slug,
'alias_slug': obj.alias,
},
)
return self._absolute_url(path)

def get_parent(self, obj):
path = reverse(
'projects-detail',
kwargs={
'project_slug': obj.parent.slug,
},
)
return self._absolute_url(path)


class ChildProjectSerializer(ProjectSerializer):

"""
Serializer to render a Project when listed under ProjectRelationship.

It's exactly the same as ``ProjectSerializer`` but without some fields.
"""

class Meta(ProjectSerializer.Meta):
fields = [
field for field in ProjectSerializer.Meta.fields
if field not in ['subproject_of']
]


class SubprojectSerializer(FlexFieldsModelSerializer):

"""Serializer to render a subproject (``ProjectRelationship``)."""

child = ChildProjectSerializer()
_links = SubprojectLinksSerializer(source='*')

class Meta:
model = ProjectRelationship
fields = [
'child',
'alias',
'_links',
]


class SubprojectDestroySerializer(FlexFieldsModelSerializer):

"""Serializer used to remove a subproject relationship to a Project."""

class Meta:
model = ProjectRelationship
fields = (
'alias',
)


class RedirectLinksSerializer(BaseLinksSerializer):
_self = serializers.SerializerMethodField()
project = serializers.SerializerMethodField()
Expand Down
51 changes: 35 additions & 16 deletions readthedocs/api/v3/tests/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,6 @@ def setUp(self):
project=self.project,
)

self.subproject = fixture.get(
Project,
pub_date=self.created,
modified_date=self.modified,
description='SubProject description',
repo='https://github.com/rtfd/subproject',
project_url='http://subproject.com',
name='subproject',
slug='subproject',
related_projects=[],
main_language_project=None,
users=[],
versions=[],
)
self.project.add_subproject(self.subproject)

self.version = fixture.get(
Version,
slug='v1.0',
Expand Down Expand Up @@ -119,6 +103,41 @@ def tearDown(self):
# Cleanup cache to avoid throttling on tests
cache.clear()

def _create_new_project(self):
"""Helper to create a project with all the fields set."""
return fixture.get(
Project,
pub_date=self.created,
modified_date=self.modified,
description='Project description',
repo='https://github.com/rtfd/project',
project_url='http://project.com',
name='new-project',
slug='new-project',
related_projects=[],
main_language_project=None,
users=[self.me],
versions=[],
)

def _create_subproject(self):
"""Helper to create a sub-project with all the fields set."""
self.subproject = fixture.get(
Project,
pub_date=self.created,
modified_date=self.modified,
description='SubProject description',
repo='https://github.com/rtfd/subproject',
project_url='http://subproject.com',
name='subproject',
slug='subproject',
related_projects=[],
main_language_project=None,
users=[self.me],
versions=[],
)
self.project_relationship = self.project.add_subproject(self.subproject)

def _get_response_dict(self, view_name):
filename = Path(__file__).absolute().parent / 'responses' / f'{view_name}.json'
return json.load(open(filename))
Expand Down
2 changes: 1 addition & 1 deletion readthedocs/api/v3/tests/responses/projects-detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"active": true,
"built": true,
"downloads": {},
"id": 3,
"id": 2,
"identifier": "a1b2c3",
"last_build": {
"commit": "a1b2c3",
Expand Down
2 changes: 1 addition & 1 deletion readthedocs/api/v3/tests/responses/projects-list_POST.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"created": "2019-04-29T10:00:00Z",
"default_branch": "master",
"default_version": "latest",
"id": 4,
"id": 3,
"homepage": "http://template.readthedocs.io/",
"language": {
"code": "en",
Expand Down
Loading