Skip to content

Commit

Permalink
Merge #6176 to master (#6258)
Browse files Browse the repository at this point in the history
Merge #6176 to master 

Co-authored-by: Manuel Kaufmann <[email protected]>
  • Loading branch information
humitos authored Oct 9, 2019
2 parents 1eae837 + eca2ce8 commit 5f145f1
Show file tree
Hide file tree
Showing 16 changed files with 646 additions and 130 deletions.
89 changes: 83 additions & 6 deletions docs/api/v3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,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 @@ -574,15 +602,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

0 comments on commit 5f145f1

Please sign in to comment.