Repository: readthedocs/readthedocs.org

Issue:
Dropdown to select Advanced Settings
There are bunch of input fields under `Admin -> Advanced Settings` that could be converted into a dropbox:

* Default branch
* Default version

(this will limit the user to just those _active_/possible versions instead of writting things that are invalid or does not exist)

Also, this would involve more coding (since we need to populate the dropdown with data that we don't have in the database at the moment), but would be a good addition also:

* Requirements file
* Python configuration file

> This will probably need more discussion before start working on the implementation regarding the UX design

Ref: https://github.com/readthedocs/readthedocs-corporate/issues/137


Discussion:
Referencing to my comment on the other issue https://github.com/rtfd/readthedocs.org/issues/3611#issuecomment-365809242. What about listing the valid options (like recommendations), but the user also can enter a _custom option_, with this the validation isn't necessary and probably a more simple solution.

Resolving patch:
diff --git a/readthedocs/builds/forms.py b/readthedocs/builds/forms.py
--- a/readthedocs/builds/forms.py
+++ b/readthedocs/builds/forms.py
@@ -1,12 +1,19 @@
 """Django forms for the builds app."""
 
-from __future__ import absolute_import
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
+
 from builtins import object
 from django import forms
+from django.utils.translation import ugettext_lazy as _
 
-from readthedocs.builds.models import VersionAlias, Version
-from readthedocs.projects.models import Project
+from readthedocs.builds.models import Version, VersionAlias
 from readthedocs.core.utils import trigger_build
+from readthedocs.projects.models import Project
 
 
 class AliasForm(forms.ModelForm):
@@ -33,6 +40,22 @@ class Meta(object):
         model = Version
         fields = ['active', 'privacy_level', 'tags']
 
+    def clean_active(self):
+        active = self.cleaned_data['active']
+        if self._is_default_version() and not active:
+            msg = _(
+                '{version} is the default version of the project, '
+                'it should be active.'
+            )
+            raise forms.ValidationError(
+                msg.format(version=self.instance.verbose_name)
+            )
+        return active
+
+    def _is_default_version(self):
+        project = self.instance.project
+        return project.default_version == self.instance.slug
+
     def save(self, commit=True):
         obj = super(VersionForm, self).save(commit=commit)
         if obj.active and not obj.built and not obj.uploaded:
diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py
--- a/readthedocs/projects/forms.py
+++ b/readthedocs/projects/forms.py
@@ -2,7 +2,11 @@
 """Project forms."""
 
 from __future__ import (
-    absolute_import, division, print_function, unicode_literals)
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
 
 from random import choice
 
@@ -23,9 +27,16 @@
 from readthedocs.integrations.models import Integration
 from readthedocs.oauth.models import RemoteRepository
 from readthedocs.projects import constants
+from readthedocs.projects.constants import PUBLIC
 from readthedocs.projects.exceptions import ProjectSpamError
 from readthedocs.projects.models import (
-    Domain, EmailHook, Feature, Project, ProjectRelationship, WebHook)
+    Domain,
+    EmailHook,
+    Feature,
+    Project,
+    ProjectRelationship,
+    WebHook,
+)
 from readthedocs.redirects.models import Redirect
 
 
@@ -202,6 +213,24 @@ class Meta(object):
             # 'num_major', 'num_minor', 'num_point',
         )
 
+    def __init__(self, *args, **kwargs):
+        super(ProjectAdvancedForm, self).__init__(*args, **kwargs)
+
+        default_choice = (None, '-' * 9)
+        all_versions = self.instance.versions.values_list(
+            'slug', 'verbose_name'
+        )
+        self.fields['default_branch'].widget = forms.Select(
+            choices=[default_choice] + list(all_versions)
+        )
+
+        active_versions = self.instance.all_active_versions().values_list(
+            'slug', 'verbose_name'
+        )
+        self.fields['default_version'].widget = forms.Select(
+            choices=active_versions
+        )
+
     def clean_conf_py_file(self):
         filename = self.cleaned_data.get('conf_py_file', '').strip()
         if filename and 'conf.py' not in filename:


Test patch:
diff --git a/readthedocs/rtd_tests/tests/test_build_forms.py b/readthedocs/rtd_tests/tests/test_build_forms.py
new file mode 100644
--- /dev/null
+++ b/readthedocs/rtd_tests/tests/test_build_forms.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import division, print_function, unicode_literals
+
+from django.test import TestCase
+from django_dynamic_fixture import get
+
+from readthedocs.builds.forms import VersionForm
+from readthedocs.builds.models import Version
+from readthedocs.projects.constants import PRIVATE
+from readthedocs.projects.models import Project
+
+
+class TestVersionForm(TestCase):
+
+    def setUp(self):
+        self.project = get(Project)
+
+    def test_default_version_is_active(self):
+        version = get(
+            Version,
+            project=self.project,
+            active=False,
+        )
+        self.project.default_version = version.slug
+        self.project.save()
+
+        form = VersionForm(
+            {
+                'active': True,
+                'privacy_level': PRIVATE,
+            },
+            instance=version
+        )
+        self.assertTrue(form.is_valid())
+
+    def test_default_version_is_inactive(self):
+        version = get(
+            Version,
+            project=self.project,
+            active=True,
+        )
+        self.project.default_version = version.slug
+        self.project.save()
+
+        form = VersionForm(
+            {
+                'active': False,
+                'privacy_level': PRIVATE,
+            },
+            instance=version
+        )
+        self.assertFalse(form.is_valid())
+        self.assertIn('active', form.errors)
diff --git a/readthedocs/rtd_tests/tests/test_project_forms.py b/readthedocs/rtd_tests/tests/test_project_forms.py
--- a/readthedocs/rtd_tests/tests/test_project_forms.py
+++ b/readthedocs/rtd_tests/tests/test_project_forms.py
@@ -1,7 +1,11 @@
 # -*- coding: utf-8 -*-
 
 from __future__ import (
-    absolute_import, division, print_function, unicode_literals)
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals,
+)
 
 import mock
 from django.contrib.auth.models import User
@@ -10,9 +14,17 @@
 from django_dynamic_fixture import get
 from textclassifier.validators import ClassifierValidator
 
+from readthedocs.builds.constants import LATEST
+from readthedocs.builds.models import Version
+from readthedocs.projects.constants import PRIVATE, PROTECTED, PUBLIC
 from readthedocs.projects.exceptions import ProjectSpamError
 from readthedocs.projects.forms import (
-    ProjectBasicsForm, ProjectExtraForm, TranslationForm, UpdateProjectForm)
+    ProjectAdvancedForm,
+    ProjectBasicsForm,
+    ProjectExtraForm,
+    TranslationForm,
+    UpdateProjectForm,
+)
 from readthedocs.projects.models import Project
 
 
@@ -94,6 +106,74 @@ def test_import_repo_url(self):
                 self.assertEqual(form.is_valid(), valid, msg=url)
 
 
+class TestProjectAdvancedForm(TestCase):
+
+    def setUp(self):
+        self.project = get(Project)
+        get(
+            Version,
+            project=self.project,
+            slug='public-1',
+            active=True,
+            privacy_level=PUBLIC,
+        )
+        get(
+            Version,
+            project=self.project,
+            slug='public-2',
+            active=True,
+            privacy_level=PUBLIC,
+        )
+        get(
+            Version,
+            project=self.project,
+            slug='public-3',
+            active=False,
+            privacy_level=PROTECTED,
+        )
+        get(
+            Version,
+            project=self.project,
+            slug='private',
+            active=True,
+            privacy_level=PRIVATE,
+        )
+        get(
+            Version,
+            project=self.project,
+            slug='protected',
+            active=True,
+            privacy_level=PROTECTED,
+        )
+
+    def test_list_only_active_versions_on_default_version(self):
+        form = ProjectAdvancedForm(instance=self.project)
+        # This version is created automatically by the project on save
+        self.assertTrue(self.project.versions.filter(slug=LATEST).exists())
+        self.assertEqual(
+            set(
+                slug
+                for slug, _ in form.fields['default_version'].widget.choices
+            ),
+            {'latest', 'public-1', 'public-2', 'private', 'protected'},
+        )
+
+    def test_list_all_versions_on_default_branch(self):
+        form = ProjectAdvancedForm(instance=self.project)
+        # This version is created automatically by the project on save
+        self.assertTrue(self.project.versions.filter(slug=LATEST).exists())
+        self.assertEqual(
+            set(
+                slug
+                for slug, _ in form.fields['default_branch'].widget.choices
+            ),
+            {
+                None, 'latest', 'public-1', 'public-2',
+                'public-3', 'protected', 'private'
+            },
+        )
+
+
 class TestTranslationForms(TestCase):
 
     def setUp(self):