Skip to content

Commit 911234e

Browse files
authored
Project: suggest a simple config file on project import wizard (#10356)
* Project: suggest a simple config file on project import wizard As part of the "Import Wizard" steps, we add an extra step now that shows a simple suggestion for a config file v2 (the same we currently have in our documentation) that uses `build.os: ubuntu-22.04` and `build.tools.python: "3.11"`. This is an initial work to show the value we can add to this wizard with something pretty simple. There is more work required on the copy for this intermediate step and the UX (I added a checkbox for now to force the user to read the message, not ideal, but works for now). Also, it would be good to find a way to highlight the YAML syntaxis using nice colors and add more useful copy to that intermediate page. Related #10352 * Template: update link * Template: add more style to the wizard config suggestion * Template: show the file as code * Form: swap label/help_text * Tests: update wizard to add a new step * Remove checkbox from suggested YAML file page * Suggested YAML file CSS style * Test: re-add `self.data` to test class * Use CSS class to style the YAML shown at import step * Add required variable in tests * Minor style for import config step * Split phrase to avoid scrolling
1 parent be72682 commit 911234e

File tree

5 files changed

+125
-38
lines changed

5 files changed

+125
-38
lines changed

media/css/core.css

+4
Original file line numberDiff line numberDiff line change
@@ -1397,3 +1397,7 @@ div.highlight pre .vc { color: #bb60d5 } /* Name.Variable.Class */
13971397
div.highlight pre .vg { color: #bb60d5 } /* Name.Variable.Global */
13981398
div.highlight pre .vi { color: #bb60d5 } /* Name.Variable.Instance */
13991399
div.highlight pre .il { color: #40a070 } /* Literal.Number.Integer.Long */
1400+
1401+
pre.small {
1402+
font-size: 0.85em;
1403+
}

readthedocs/projects/forms.py

+10
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,16 @@ def clean_tags(self):
191191
return tags
192192

193193

194+
class ProjectConfigForm(forms.Form):
195+
196+
"""Simple intermediate step to communicate about the .readthedocs.yaml file."""
197+
198+
def __init__(self, *args, **kwargs):
199+
# Remove 'user' field since it's not expected by BaseForm.
200+
kwargs.pop("user")
201+
super().__init__(*args, **kwargs)
202+
203+
194204
class ProjectAdvancedForm(ProjectTriggerBuildMixin, ProjectForm):
195205

196206
"""Advanced project option form."""

readthedocs/projects/views/private.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
ProjectAdvancedForm,
5454
ProjectAdvertisingForm,
5555
ProjectBasicsForm,
56+
ProjectConfigForm,
5657
ProjectExtraForm,
5758
ProjectRelationshipForm,
5859
RedirectForm,
@@ -260,8 +261,9 @@ class ImportWizardView(ProjectImportMixin, PrivateViewMixin, SessionWizardView):
260261
"""
261262

262263
form_list = [
263-
('basics', ProjectBasicsForm),
264-
('extra', ProjectExtraForm),
264+
("basics", ProjectBasicsForm),
265+
("config", ProjectConfigForm),
266+
("extra", ProjectExtraForm),
265267
]
266268
condition_dict = {'extra': lambda self: self.is_advanced()}
267269

@@ -315,9 +317,7 @@ def done(self, form_list, **kwargs):
315317
"""
316318
form_data = self.get_all_cleaned_data()
317319
extra_fields = ProjectExtraForm.Meta.fields
318-
# expect the first form; manually wrap in a list in case it's a
319-
# View Object, as it is in Python 3.
320-
basics_form = list(form_list)[0]
320+
basics_form = form_list[0]
321321
# Save the basics form to create the project instance, then alter
322322
# attributes directly from other forms
323323
project = basics_form.save()

readthedocs/rtd_tests/tests/test_project_views.py

+53-33
Original file line numberDiff line numberDiff line change
@@ -24,40 +24,33 @@
2424
@mock.patch('readthedocs.projects.tasks.builds.update_docs_task', mock.MagicMock())
2525
class TestImportProjectBannedUser(RequestFactoryTestMixin, TestCase):
2626

27-
wizard_class_slug = 'import_wizard_view'
28-
url = '/dashboard/import/manual/'
27+
wizard_class_slug = "import_wizard_view"
28+
url = "/dashboard/import/manual/"
2929

3030
def setUp(self):
3131
super().setUp()
3232
data = {
33-
'basics': {
34-
'name': 'foobar',
35-
'repo': 'http://example.com/foobar',
36-
'repo_type': 'git',
33+
"basics": {
34+
"name": "foobar",
35+
"repo": "http://example.com/foobar",
36+
"repo_type": "git",
3737
},
38-
'extra': {
39-
'description': 'Describe foobar',
40-
'language': 'en',
41-
'documentation_type': 'sphinx',
38+
"extra": {
39+
"description": "Describe foobar",
40+
"language": "en",
41+
"documentation_type": "sphinx",
4242
},
4343
}
4444
self.data = {}
4545
for key in data:
46-
self.data.update({('{}-{}'.format(key, k), v)
47-
for (k, v) in list(data[key].items())})
48-
self.data['{}-current_step'.format(self.wizard_class_slug)] = 'extra'
49-
50-
def test_not_banned_user(self):
51-
"""User without profile and isn't banned."""
52-
req = self.request(method='post', path='/projects/import', data=self.data)
53-
req.user = get(User, profile=None)
54-
resp = ImportWizardView.as_view()(req)
55-
self.assertEqual(resp.status_code, 302)
56-
self.assertEqual(resp['location'], '/projects/foobar/')
46+
self.data.update(
47+
{("{}-{}".format(key, k), v) for (k, v) in list(data[key].items())}
48+
)
49+
self.data["{}-current_step".format(self.wizard_class_slug)] = "extra"
5750

5851
def test_banned_user(self):
5952
"""User is banned."""
60-
req = self.request(method='post', path='/projects/import', data=self.data)
53+
req = self.request(method="post", path=self.url, data=self.data)
6154
req.user = get(User)
6255
req.user.profile.banned = True
6356
req.user.profile.save()
@@ -81,6 +74,9 @@ def setUp(self):
8174
'repo': 'http://example.com/foobar',
8275
'repo_type': 'git',
8376
}
77+
self.step_data["config"] = {
78+
"confirm": True,
79+
}
8480

8581
def tearDown(self):
8682
Project.objects.filter(slug='foobar').delete()
@@ -114,6 +110,10 @@ def test_form_import_from_remote_repo(self):
114110
def test_form_pass(self):
115111
"""Only submit the basics."""
116112
resp = self.post_step('basics')
113+
self.assertEqual(resp.status_code, 200)
114+
115+
resp = self.post_step("config", session=list(resp._request.session.items()))
116+
117117
self.assertIsInstance(resp, HttpResponseRedirect)
118118
self.assertEqual(resp.status_code, 302)
119119
self.assertEqual(resp['location'], '/projects/foobar/')
@@ -135,6 +135,9 @@ def test_remote_repository_is_added(self):
135135
)
136136
self.step_data['basics']['remote_repository'] = remote_repo.pk
137137
resp = self.post_step('basics')
138+
self.assertEqual(resp.status_code, 200)
139+
140+
resp = self.post_step("config", session=list(resp._request.session.items()))
138141
self.assertIsInstance(resp, HttpResponseRedirect)
139142
self.assertEqual(resp.status_code, 302)
140143
self.assertEqual(resp['location'], '/projects/foobar/')
@@ -184,7 +187,10 @@ class TestAdvancedForm(TestBasicsForm):
184187

185188
def setUp(self):
186189
super().setUp()
187-
self.step_data['basics']['advanced'] = True
190+
self.step_data["basics"]["advanced"] = True
191+
self.step_data["config"] = {
192+
"confirm": True,
193+
}
188194
self.step_data['extra'] = {
189195
'description': 'Describe foobar',
190196
'language': 'en',
@@ -197,14 +203,17 @@ def test_initial_params(self):
197203
'description': 'An amazing project',
198204
'project_url': "https://foo.bar",
199205
}
206+
config_initial = {
207+
"confirm": True,
208+
}
200209
basic_initial = {
201210
'name': 'foobar',
202211
'repo': 'https://github.com/foo/bar',
203212
'repo_type': 'git',
204213
'default_branch': 'main',
205214
'remote_repository': '',
206215
}
207-
initial = dict(**extra_initial, **basic_initial)
216+
initial = dict(**extra_initial, **config_initial, **basic_initial)
208217
self.client.force_login(self.user)
209218

210219
# User selects a remote repo to import.
@@ -223,14 +232,21 @@ def test_initial_params(self):
223232
step_data[f'{self.wizard_class_slug}-current_step'] = 'basics'
224233
resp = self.client.post(self.url, step_data)
225234

235+
step_data = {f"config-{k}": v for k, v in config_initial.items()}
236+
step_data[f"{self.wizard_class_slug}-current_step"] = "config"
237+
resp = self.client.post(self.url, step_data)
238+
226239
# The correct initial data for the advanced form is set.
227240
form = resp.context_data['form']
228241
self.assertEqual(form.initial, extra_initial)
229242

230243
def test_form_pass(self):
231244
"""Test all forms pass validation."""
232-
resp = self.post_step('basics')
233-
self.assertWizardResponse(resp, 'extra')
245+
resp = self.post_step("basics")
246+
self.assertWizardResponse(resp, "config")
247+
resp = self.post_step("config", session=list(resp._request.session.items()))
248+
self.assertWizardResponse(resp, "extra")
249+
self.assertEqual(resp.status_code, 200)
234250
resp = self.post_step('extra', session=list(resp._request.session.items()))
235251
self.assertIsInstance(resp, HttpResponseRedirect)
236252
self.assertEqual(resp.status_code, 302)
@@ -254,9 +270,11 @@ def test_form_missing_extra(self):
254270
# Remove extra data to trigger validation errors
255271
self.step_data['extra'] = {}
256272

257-
resp = self.post_step('basics')
258-
self.assertWizardResponse(resp, 'extra')
259-
resp = self.post_step('extra', session=list(resp._request.session.items()))
273+
resp = self.post_step("basics")
274+
self.assertWizardResponse(resp, "config")
275+
resp = self.post_step("config", session=list(resp._request.session.items()))
276+
self.assertWizardResponse(resp, "extra")
277+
resp = self.post_step("extra", session=list(resp._request.session.items()))
260278

261279
self.assertWizardFailure(resp, 'language')
262280
self.assertWizardFailure(resp, 'documentation_type')
@@ -270,10 +288,12 @@ def test_remote_repository_is_added(self):
270288
user=self.user,
271289
account=socialaccount
272290
)
273-
self.step_data['basics']['remote_repository'] = remote_repo.pk
274-
resp = self.post_step('basics')
275-
self.assertWizardResponse(resp, 'extra')
276-
resp = self.post_step('extra', session=list(resp._request.session.items()))
291+
self.step_data["basics"]["remote_repository"] = remote_repo.pk
292+
resp = self.post_step("basics")
293+
self.assertWizardResponse(resp, "config")
294+
resp = self.post_step("config", session=list(resp._request.session.items()))
295+
self.assertWizardResponse(resp, "extra")
296+
resp = self.post_step("extra", session=list(resp._request.session.items()))
277297
self.assertIsInstance(resp, HttpResponseRedirect)
278298
self.assertEqual(resp.status_code, 302)
279299
self.assertEqual(resp['location'], '/projects/foobar/')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{% extends "projects/import_base.html" %}
2+
{% load i18n %}
3+
4+
{% block content %}
5+
<h3>{% trans "Project configuration file (<code>.readthedocs.yaml</code>)" %}</h3>
6+
7+
<p class="info">
8+
{% blocktrans trimmed %}
9+
Make sure your project has a <code>.readthedocs.yaml</code> at the root of your repository. This file is required by Read the Docs to be able to build your documentation. You can <a href="https://docs.readthedocs.io/en/stable/config-file/v2.html">read more about this in our documentation</a>.
10+
{% endblocktrans %}
11+
</p>
12+
13+
<p class="info">
14+
Here you have an example for a common Sphinx project:
15+
16+
<pre class="small">
17+
<code># .readthedocs.yaml
18+
# Read the Docs configuration file
19+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
20+
21+
# Required
22+
version: 2
23+
24+
# Set the OS, Python version and other tools you might need
25+
build:
26+
os: ubuntu-22.04
27+
tools:
28+
python: "3.11"
29+
# You can also specify other tool versions:
30+
# nodejs: "19"
31+
# rust: "1.64"
32+
# golang: "1.19"
33+
34+
# Build documentation in the "docs/" directory with Sphinx
35+
sphinx:
36+
configuration: docs/conf.py
37+
38+
# Optionally build your docs in additional formats such as PDF and ePub
39+
# formats:
40+
# - pdf
41+
# - epub
42+
43+
# Optionally but recommended, declare the Python requirements required
44+
# to build your documentation
45+
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
46+
# python:
47+
# install:
48+
# - requirements: docs/requirements.txt</code>
49+
</pre>
50+
</p>
51+
52+
{{ block.super }}
53+
{% endblock %}

0 commit comments

Comments
 (0)