diff --git a/media/css/core.css b/media/css/core.css index fa58300a696..fba3c2eebbd 100644 --- a/media/css/core.css +++ b/media/css/core.css @@ -1397,3 +1397,7 @@ div.highlight pre .vc { color: #bb60d5 } /* Name.Variable.Class */ div.highlight pre .vg { color: #bb60d5 } /* Name.Variable.Global */ div.highlight pre .vi { color: #bb60d5 } /* Name.Variable.Instance */ div.highlight pre .il { color: #40a070 } /* Literal.Number.Integer.Long */ + +pre.small { + font-size: 0.85em; +} diff --git a/readthedocs/projects/forms.py b/readthedocs/projects/forms.py index a962cb5e90e..3d236d51321 100644 --- a/readthedocs/projects/forms.py +++ b/readthedocs/projects/forms.py @@ -191,6 +191,16 @@ def clean_tags(self): return tags +class ProjectConfigForm(forms.Form): + + """Simple intermediate step to communicate about the .readthedocs.yaml file.""" + + def __init__(self, *args, **kwargs): + # Remove 'user' field since it's not expected by BaseForm. + kwargs.pop("user") + super().__init__(*args, **kwargs) + + class ProjectAdvancedForm(ProjectTriggerBuildMixin, ProjectForm): """Advanced project option form.""" diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 368798bd7c0..c6ef3e0a8d8 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -53,6 +53,7 @@ ProjectAdvancedForm, ProjectAdvertisingForm, ProjectBasicsForm, + ProjectConfigForm, ProjectExtraForm, ProjectRelationshipForm, RedirectForm, @@ -260,8 +261,9 @@ class ImportWizardView(ProjectImportMixin, PrivateViewMixin, SessionWizardView): """ form_list = [ - ('basics', ProjectBasicsForm), - ('extra', ProjectExtraForm), + ("basics", ProjectBasicsForm), + ("config", ProjectConfigForm), + ("extra", ProjectExtraForm), ] condition_dict = {'extra': lambda self: self.is_advanced()} @@ -315,9 +317,7 @@ def done(self, form_list, **kwargs): """ form_data = self.get_all_cleaned_data() extra_fields = ProjectExtraForm.Meta.fields - # expect the first form; manually wrap in a list in case it's a - # View Object, as it is in Python 3. - basics_form = list(form_list)[0] + basics_form = form_list[0] # Save the basics form to create the project instance, then alter # attributes directly from other forms project = basics_form.save() diff --git a/readthedocs/rtd_tests/tests/test_project_views.py b/readthedocs/rtd_tests/tests/test_project_views.py index 12194d168c8..c0aa6b1ef19 100644 --- a/readthedocs/rtd_tests/tests/test_project_views.py +++ b/readthedocs/rtd_tests/tests/test_project_views.py @@ -24,40 +24,33 @@ @mock.patch('readthedocs.projects.tasks.builds.update_docs_task', mock.MagicMock()) class TestImportProjectBannedUser(RequestFactoryTestMixin, TestCase): - wizard_class_slug = 'import_wizard_view' - url = '/dashboard/import/manual/' + wizard_class_slug = "import_wizard_view" + url = "/dashboard/import/manual/" def setUp(self): super().setUp() data = { - 'basics': { - 'name': 'foobar', - 'repo': 'http://example.com/foobar', - 'repo_type': 'git', + "basics": { + "name": "foobar", + "repo": "http://example.com/foobar", + "repo_type": "git", }, - 'extra': { - 'description': 'Describe foobar', - 'language': 'en', - 'documentation_type': 'sphinx', + "extra": { + "description": "Describe foobar", + "language": "en", + "documentation_type": "sphinx", }, } self.data = {} for key in data: - self.data.update({('{}-{}'.format(key, k), v) - for (k, v) in list(data[key].items())}) - self.data['{}-current_step'.format(self.wizard_class_slug)] = 'extra' - - def test_not_banned_user(self): - """User without profile and isn't banned.""" - req = self.request(method='post', path='/projects/import', data=self.data) - req.user = get(User, profile=None) - resp = ImportWizardView.as_view()(req) - self.assertEqual(resp.status_code, 302) - self.assertEqual(resp['location'], '/projects/foobar/') + self.data.update( + {("{}-{}".format(key, k), v) for (k, v) in list(data[key].items())} + ) + self.data["{}-current_step".format(self.wizard_class_slug)] = "extra" def test_banned_user(self): """User is banned.""" - req = self.request(method='post', path='/projects/import', data=self.data) + req = self.request(method="post", path=self.url, data=self.data) req.user = get(User) req.user.profile.banned = True req.user.profile.save() @@ -81,6 +74,9 @@ def setUp(self): 'repo': 'http://example.com/foobar', 'repo_type': 'git', } + self.step_data["config"] = { + "confirm": True, + } def tearDown(self): Project.objects.filter(slug='foobar').delete() @@ -114,6 +110,10 @@ def test_form_import_from_remote_repo(self): def test_form_pass(self): """Only submit the basics.""" resp = self.post_step('basics') + self.assertEqual(resp.status_code, 200) + + resp = self.post_step("config", session=list(resp._request.session.items())) + self.assertIsInstance(resp, HttpResponseRedirect) self.assertEqual(resp.status_code, 302) self.assertEqual(resp['location'], '/projects/foobar/') @@ -135,6 +135,9 @@ def test_remote_repository_is_added(self): ) self.step_data['basics']['remote_repository'] = remote_repo.pk resp = self.post_step('basics') + self.assertEqual(resp.status_code, 200) + + resp = self.post_step("config", session=list(resp._request.session.items())) self.assertIsInstance(resp, HttpResponseRedirect) self.assertEqual(resp.status_code, 302) self.assertEqual(resp['location'], '/projects/foobar/') @@ -184,7 +187,10 @@ class TestAdvancedForm(TestBasicsForm): def setUp(self): super().setUp() - self.step_data['basics']['advanced'] = True + self.step_data["basics"]["advanced"] = True + self.step_data["config"] = { + "confirm": True, + } self.step_data['extra'] = { 'description': 'Describe foobar', 'language': 'en', @@ -197,6 +203,9 @@ def test_initial_params(self): 'description': 'An amazing project', 'project_url': "https://foo.bar", } + config_initial = { + "confirm": True, + } basic_initial = { 'name': 'foobar', 'repo': 'https://github.com/foo/bar', @@ -204,7 +213,7 @@ def test_initial_params(self): 'default_branch': 'main', 'remote_repository': '', } - initial = dict(**extra_initial, **basic_initial) + initial = dict(**extra_initial, **config_initial, **basic_initial) self.client.force_login(self.user) # User selects a remote repo to import. @@ -223,14 +232,21 @@ def test_initial_params(self): step_data[f'{self.wizard_class_slug}-current_step'] = 'basics' resp = self.client.post(self.url, step_data) + step_data = {f"config-{k}": v for k, v in config_initial.items()} + step_data[f"{self.wizard_class_slug}-current_step"] = "config" + resp = self.client.post(self.url, step_data) + # The correct initial data for the advanced form is set. form = resp.context_data['form'] self.assertEqual(form.initial, extra_initial) def test_form_pass(self): """Test all forms pass validation.""" - resp = self.post_step('basics') - self.assertWizardResponse(resp, 'extra') + resp = self.post_step("basics") + self.assertWizardResponse(resp, "config") + resp = self.post_step("config", session=list(resp._request.session.items())) + self.assertWizardResponse(resp, "extra") + self.assertEqual(resp.status_code, 200) resp = self.post_step('extra', session=list(resp._request.session.items())) self.assertIsInstance(resp, HttpResponseRedirect) self.assertEqual(resp.status_code, 302) @@ -254,9 +270,11 @@ def test_form_missing_extra(self): # Remove extra data to trigger validation errors self.step_data['extra'] = {} - resp = self.post_step('basics') - self.assertWizardResponse(resp, 'extra') - resp = self.post_step('extra', session=list(resp._request.session.items())) + resp = self.post_step("basics") + self.assertWizardResponse(resp, "config") + resp = self.post_step("config", session=list(resp._request.session.items())) + self.assertWizardResponse(resp, "extra") + resp = self.post_step("extra", session=list(resp._request.session.items())) self.assertWizardFailure(resp, 'language') self.assertWizardFailure(resp, 'documentation_type') @@ -270,10 +288,12 @@ def test_remote_repository_is_added(self): user=self.user, account=socialaccount ) - self.step_data['basics']['remote_repository'] = remote_repo.pk - resp = self.post_step('basics') - self.assertWizardResponse(resp, 'extra') - resp = self.post_step('extra', session=list(resp._request.session.items())) + self.step_data["basics"]["remote_repository"] = remote_repo.pk + resp = self.post_step("basics") + self.assertWizardResponse(resp, "config") + resp = self.post_step("config", session=list(resp._request.session.items())) + self.assertWizardResponse(resp, "extra") + resp = self.post_step("extra", session=list(resp._request.session.items())) self.assertIsInstance(resp, HttpResponseRedirect) self.assertEqual(resp.status_code, 302) self.assertEqual(resp['location'], '/projects/foobar/') diff --git a/readthedocs/templates/projects/import_config.html b/readthedocs/templates/projects/import_config.html new file mode 100644 index 00000000000..5489d24bb85 --- /dev/null +++ b/readthedocs/templates/projects/import_config.html @@ -0,0 +1,53 @@ +{% extends "projects/import_base.html" %} +{% load i18n %} + +{% block content %} +

{% trans "Project configuration file (.readthedocs.yaml)" %}

+ +

+ {% blocktrans trimmed %} + Make sure your project has a .readthedocs.yaml at the root of your repository. This file is required by Read the Docs to be able to build your documentation. You can read more about this in our documentation. + {% endblocktrans %} +

+ +

+ Here you have an example for a common Sphinx project: + +

+        # .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the OS, Python version and other tools you might need
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+    # You can also specify other tool versions:
+    # nodejs: "19"
+    # rust: "1.64"
+    # golang: "1.19"
+
+# Build documentation in the "docs/" directory with Sphinx
+sphinx:
+   configuration: docs/conf.py
+
+# Optionally build your docs in additional formats such as PDF and ePub
+# formats:
+#    - pdf
+#    - epub
+
+# Optionally but recommended, declare the Python requirements required
+# to build your documentation
+# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
+# python:
+#    install:
+#    - requirements: docs/requirements.txt
+    
+

+ +{{ block.super }} +{% endblock %}