Skip to content

Commit 5f145f1

Browse files
authored
Merge #6176 to master (#6258)
Merge #6176 to master Co-authored-by: Manuel Kaufmann <[email protected]>
2 parents 1eae837 + eca2ce8 commit 5f145f1

16 files changed

+646
-130
lines changed

docs/api/v3.rst

+83-6
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,34 @@ This allows for documentation projects to share a search index and a namespace o
552552
but still be maintained independently.
553553
See :doc:`/subprojects` for more information.
554554

555+
556+
Subproject details
557+
++++++++++++++++++
558+
559+
560+
.. http:get:: /api/v3/projects/(str:project_slug)/subprojects/(str:alias_slug)/
561+
562+
Retrieve details of a subproject relationship.
563+
564+
**Example request**:
565+
566+
.. sourcecode:: bash
567+
568+
$ curl -H "Authorization: Token <token>" https://readthedocs.org/api/v3/projects/pip/subprojects/subproject-alias/
569+
570+
**Example response**:
571+
572+
.. sourcecode:: json
573+
574+
{
575+
"alias": "subproject-alias",
576+
"child": ["PROJECT"],
577+
"_links": {
578+
"parent": "/api/v3/projects/pip/"
579+
}
580+
}
581+
582+
555583
Subprojects listing
556584
+++++++++++++++++++
557585

@@ -574,15 +602,64 @@ Subprojects listing
574602
"count": 25,
575603
"next": "/api/v3/projects/pip/subprojects/?limit=10&offset=10",
576604
"previous": null,
577-
"results": ["PROJECT"]
605+
"results": ["SUBPROJECT RELATIONSHIP"]
578606
}
579607

580-
:>json integer count: total number of projects.
581-
:>json string next: URI for next set of projects.
582-
:>json string previous: URI for previous set of projects.
583-
:>json array results: array of ``project`` objects.
584608

585-
:requestheader Authorization: token to authenticate.
609+
Subproject create
610+
+++++++++++++++++
611+
612+
613+
.. http:post:: /api/v3/projects/(str:project_slug)/subprojects/
614+
615+
Create a subproject relationship between two projects.
616+
617+
**Example request**:
618+
619+
.. sourcecode:: bash
620+
621+
$ curl \
622+
-X POST \
623+
-H "Authorization: Token <token>" https://readthedocs.org/api/v3/projects/pip/subprojects/ \
624+
-H "Content-Type: application/json" \
625+
-d @body.json
626+
627+
The content of ``body.json`` is like,
628+
629+
.. sourcecode:: json
630+
631+
{
632+
"child": "subproject-child-slug",
633+
"alias": "subproject-alias"
634+
}
635+
636+
**Example response**:
637+
638+
`See Subproject details <#subproject-details>`_
639+
640+
:>json string child: slug of the child project in the relationship.
641+
:>json string alias: optional slug alias to be used in the URL (e.g ``/projects/<alias>/en/latest/``).
642+
If not provided, child project's slug is used as alias.
643+
644+
:statuscode 201: Subproject created sucessfully
645+
646+
647+
Subproject delete
648+
+++++++++++++++++
649+
650+
.. http:delete:: /api/v3/projects/(str:project_slug)/subprojects/(str:alias_slug)/
651+
652+
Delete a subproject relationship.
653+
654+
**Example request**:
655+
656+
.. sourcecode:: bash
657+
658+
$ curl \
659+
-X DELETE \
660+
-H "Authorization: Token <token>" https://readthedocs.org/api/v3/projects/pip/subprojects/subproject-alias/
661+
662+
:statuscode 204: Subproject deleted successfully
586663

587664

588665
Translations

readthedocs/api/v3/mixins.py

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class NestedParentObjectMixin:
1313
PROJECT_LOOKUP_NAMES = [
1414
'project__slug',
1515
'projects__slug',
16+
'parent__slug',
1617
'superprojects__parent__slug',
1718
'main_language_project__slug',
1819
]

readthedocs/api/v3/serializers.py

+124-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
PRIVACY_CHOICES,
1919
PROTECTED,
2020
)
21-
from readthedocs.projects.models import Project, EnvironmentVariable
21+
from readthedocs.projects.models import Project, EnvironmentVariable, ProjectRelationship
2222
from readthedocs.redirects.models import Redirect, TYPE_CHOICES as REDIRECT_TYPE_CHOICES
2323

2424

@@ -385,7 +385,7 @@ def get_subprojects(self, obj):
385385
path = reverse(
386386
'projects-subprojects-list',
387387
kwargs={
388-
'parent_lookup_superprojects__parent__slug': obj.slug,
388+
'parent_lookup_parent__slug': obj.slug,
389389
},
390390
)
391391
return self._absolute_url(path)
@@ -539,6 +539,128 @@ def get_subproject_of(self, obj):
539539
return None
540540

541541

542+
class SubprojectCreateSerializer(FlexFieldsModelSerializer):
543+
544+
"""Serializer used to define a Project as subproject of another Project."""
545+
546+
child = serializers.SlugRelatedField(
547+
slug_field='slug',
548+
queryset=Project.objects.all(),
549+
)
550+
551+
class Meta:
552+
model = ProjectRelationship
553+
fields = [
554+
'child',
555+
'alias',
556+
]
557+
558+
def __init__(self, *args, **kwargs):
559+
# Initialize the instance with the parent Project to be used in the
560+
# serializer validation.
561+
self.parent_project = kwargs.pop('parent')
562+
super().__init__(*args, **kwargs)
563+
564+
def validate_child(self, value):
565+
# Check the user is maintainer of the child project
566+
user = self.context['request'].user
567+
if user not in value.users.all():
568+
raise serializers.ValidationError(
569+
'You do not have permissions on the child project',
570+
)
571+
return value
572+
573+
def validate_alias(self, value):
574+
# Check there is not a subproject with this alias already
575+
subproject = self.parent_project.subprojects.filter(alias=value)
576+
if subproject.exists():
577+
raise serializers.ValidationError(
578+
'A subproject with this alias already exists',
579+
)
580+
return value
581+
582+
# pylint: disable=arguments-differ
583+
def validate(self, data):
584+
# Check the parent and child are not the same project
585+
if data['child'].slug == self.parent_project.slug:
586+
raise serializers.ValidationError(
587+
'Project can not be subproject of itself',
588+
)
589+
590+
# Check the parent project is not a subproject already
591+
if self.parent_project.superprojects.exists():
592+
raise serializers.ValidationError(
593+
'Subproject nesting is not supported',
594+
)
595+
return data
596+
597+
598+
class SubprojectLinksSerializer(BaseLinksSerializer):
599+
_self = serializers.SerializerMethodField()
600+
parent = serializers.SerializerMethodField()
601+
602+
def get__self(self, obj):
603+
path = reverse(
604+
'projects-subprojects-detail',
605+
kwargs={
606+
'parent_lookup_parent__slug': obj.parent.slug,
607+
'alias_slug': obj.alias,
608+
},
609+
)
610+
return self._absolute_url(path)
611+
612+
def get_parent(self, obj):
613+
path = reverse(
614+
'projects-detail',
615+
kwargs={
616+
'project_slug': obj.parent.slug,
617+
},
618+
)
619+
return self._absolute_url(path)
620+
621+
622+
class ChildProjectSerializer(ProjectSerializer):
623+
624+
"""
625+
Serializer to render a Project when listed under ProjectRelationship.
626+
627+
It's exactly the same as ``ProjectSerializer`` but without some fields.
628+
"""
629+
630+
class Meta(ProjectSerializer.Meta):
631+
fields = [
632+
field for field in ProjectSerializer.Meta.fields
633+
if field not in ['subproject_of']
634+
]
635+
636+
637+
class SubprojectSerializer(FlexFieldsModelSerializer):
638+
639+
"""Serializer to render a subproject (``ProjectRelationship``)."""
640+
641+
child = ChildProjectSerializer()
642+
_links = SubprojectLinksSerializer(source='*')
643+
644+
class Meta:
645+
model = ProjectRelationship
646+
fields = [
647+
'child',
648+
'alias',
649+
'_links',
650+
]
651+
652+
653+
class SubprojectDestroySerializer(FlexFieldsModelSerializer):
654+
655+
"""Serializer used to remove a subproject relationship to a Project."""
656+
657+
class Meta:
658+
model = ProjectRelationship
659+
fields = (
660+
'alias',
661+
)
662+
663+
542664
class RedirectLinksSerializer(BaseLinksSerializer):
543665
_self = serializers.SerializerMethodField()
544666
project = serializers.SerializerMethodField()

readthedocs/api/v3/tests/mixins.py

+35-16
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,6 @@ def setUp(self):
6060
project=self.project,
6161
)
6262

63-
self.subproject = fixture.get(
64-
Project,
65-
pub_date=self.created,
66-
modified_date=self.modified,
67-
description='SubProject description',
68-
repo='https://github.com/rtfd/subproject',
69-
project_url='http://subproject.com',
70-
name='subproject',
71-
slug='subproject',
72-
related_projects=[],
73-
main_language_project=None,
74-
users=[],
75-
versions=[],
76-
)
77-
self.project.add_subproject(self.subproject)
78-
7963
self.version = fixture.get(
8064
Version,
8165
slug='v1.0',
@@ -119,6 +103,41 @@ def tearDown(self):
119103
# Cleanup cache to avoid throttling on tests
120104
cache.clear()
121105

106+
def _create_new_project(self):
107+
"""Helper to create a project with all the fields set."""
108+
return fixture.get(
109+
Project,
110+
pub_date=self.created,
111+
modified_date=self.modified,
112+
description='Project description',
113+
repo='https://github.com/rtfd/project',
114+
project_url='http://project.com',
115+
name='new-project',
116+
slug='new-project',
117+
related_projects=[],
118+
main_language_project=None,
119+
users=[self.me],
120+
versions=[],
121+
)
122+
123+
def _create_subproject(self):
124+
"""Helper to create a sub-project with all the fields set."""
125+
self.subproject = fixture.get(
126+
Project,
127+
pub_date=self.created,
128+
modified_date=self.modified,
129+
description='SubProject description',
130+
repo='https://github.com/rtfd/subproject',
131+
project_url='http://subproject.com',
132+
name='subproject',
133+
slug='subproject',
134+
related_projects=[],
135+
main_language_project=None,
136+
users=[self.me],
137+
versions=[],
138+
)
139+
self.project_relationship = self.project.add_subproject(self.subproject)
140+
122141
def _get_response_dict(self, view_name):
123142
filename = Path(__file__).absolute().parent / 'responses' / f'{view_name}.json'
124143
return json.load(open(filename))

readthedocs/api/v3/tests/responses/projects-detail.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"active": true,
55
"built": true,
66
"downloads": {},
7-
"id": 3,
7+
"id": 2,
88
"identifier": "a1b2c3",
99
"last_build": {
1010
"commit": "a1b2c3",

readthedocs/api/v3/tests/responses/projects-list_POST.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"created": "2019-04-29T10:00:00Z",
1313
"default_branch": "master",
1414
"default_version": "latest",
15-
"id": 4,
15+
"id": 3,
1616
"homepage": "http://template.readthedocs.io/",
1717
"language": {
1818
"code": "en",

0 commit comments

Comments
 (0)