Repository: readthedocs/readthedocs.org

Issue:
Don't allow to create subprojects with same alias
Currently we can create several subprojects with the same alias, when serving the docs from 

https://github.com/rtfd/readthedocs.org/blob/bdb4aa5da5b5b5fb45244176c2bee58fb5442414/readthedocs/core/views/serve.py#L71-L74

It gives a 500 error.


Discussion:
@stsewd I would like to work on this bug fix.
Sure

Resolving patch:
diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py
--- a/readthedocs/projects/forms.py
+++ b/readthedocs/projects/forms.py
@@ -378,6 +378,17 @@ def clean_child(self):
             )
         return child
 
+    def clean_alias(self):
+        alias = self.cleaned_data['alias']
+        subproject = self.project.subprojects.filter(
+            alias=alias).exclude(id=self.instance.pk)
+
+        if subproject.exists():
+            raise forms.ValidationError(
+                _('A subproject with this alias already exists'),
+            )
+        return alias
+
     def get_subproject_queryset(self):
         """
         Return scrubbed subproject choice queryset.


Test patch:
diff --git a/readthedocs/rtd_tests/tests/test_subprojects.py b/readthedocs/rtd_tests/tests/test_subprojects.py
--- a/readthedocs/rtd_tests/tests/test_subprojects.py
+++ b/readthedocs/rtd_tests/tests/test_subprojects.py
@@ -65,6 +65,10 @@ def test_adding_subproject_fails_when_user_is_not_admin(self):
             form.errors['child'][0],
             r'Select a valid choice.',
         )
+        self.assertEqual(
+            [proj_id for (proj_id, __) in form.fields['child'].choices],
+            [''],
+        )
 
     def test_adding_subproject_passes_when_user_is_admin(self):
         user = fixture.get(User)
@@ -161,6 +165,45 @@ def test_exclude_self_project_as_subproject(self):
             [proj_id for (proj_id, __) in form.fields['child'].choices],
         )
 
+    def test_alias_already_exists_for_a_project(self):
+        user = fixture.get(User)
+        project = fixture.get(Project, users=[user])
+        subproject = fixture.get(Project, users=[user])
+        subproject_2 = fixture.get(Project, users=[user])
+        relation = fixture.get(
+             ProjectRelationship, parent=project, child=subproject,
+             alias='subproject'
+        )
+        form = ProjectRelationshipForm(
+            {
+                'child': subproject_2.id,
+                'alias': 'subproject'
+            },
+            project=project,
+            user=user,
+        )
+        self.assertFalse(form.is_valid())
+        error_msg = 'A subproject with this alias already exists'
+        self.assertDictEqual(form.errors, {'alias': [error_msg]})
+
+    def test_edit_only_lists_instance_project_in_child_choices(self):
+        user = fixture.get(User)
+        project = fixture.get(Project, users=[user])
+        subproject = fixture.get(Project, users=[user])
+        relation = fixture.get(
+             ProjectRelationship, parent=project, child=subproject,
+             alias='subproject'
+        )
+        form = ProjectRelationshipForm(
+            instance=relation,
+            project=project,
+            user=user,
+        )
+        self.assertEqual(
+            [proj_id for (proj_id, __) in form.fields['child'].choices],
+            ['', relation.child.id],
+        )
+
 
 @override_settings(PUBLIC_DOMAIN='readthedocs.org')
 class ResolverBase(TestCase):
@@ -172,12 +215,12 @@ def setUp(self):
             self.pip = fixture.get(Project, slug='pip', users=[self.owner], main_language_project=None)
             self.subproject = fixture.get(
                 Project, slug='sub', language='ja',
-                users=[ self.owner],
+                users=[self.owner],
                 main_language_project=None,
             )
             self.translation = fixture.get(
                 Project, slug='trans', language='ja',
-                users=[ self.owner],
+                users=[self.owner],
                 main_language_project=None,
             )
             self.pip.add_subproject(self.subproject)