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

Create subproject relationship via APIv3 endpoint #6176

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -393,7 +393,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 @@ -547,6 +547,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

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