Skip to content

Commit cefb86f

Browse files
authored
Merge pull request #3585 from stsewd/download-raw-log
Download raw build log
2 parents 98d18b8 + 1415270 commit cefb86f

File tree

5 files changed

+184
-3
lines changed

5 files changed

+184
-3
lines changed

readthedocs/restapi/serializers.py

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ class BuildSerializer(serializers.ModelSerializer):
102102
"""Build serializer for user display, doesn't display internal fields."""
103103

104104
commands = BuildCommandSerializer(many=True, read_only=True)
105+
project_slug = serializers.ReadOnlyField(source='project.slug')
106+
version_slug = serializers.ReadOnlyField(source='version.slug')
105107
docs_url = serializers.ReadOnlyField(source='version.get_absolute_url')
106108
state_display = serializers.ReadOnlyField(source='get_state_display')
107109

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Read the Docs build information
2+
Build id: {{ build.id }}
3+
Project: {{ build.project_slug }}
4+
Version: {{ build.version_slug }}
5+
Commit: {{ build.commit }}
6+
Date: {{ build.date }}
7+
State: {{ build.state }}
8+
Success: {% if build.state == 'finished' %}{{ build.success }}{% else %}Unknown{% endif %}
9+
10+
{% for command in build.commands %}
11+
[rtd-command-info] start-time: {{ command.start_time }}, end-time: {{ command.end_time }}, duration: {{ command.run_time }}, exit-code: {{ command.exit_code }}
12+
{{ command.command|safe }}
13+
{{ command.output|safe }}
14+
{% endfor %}

readthedocs/restapi/views/model_views.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
from allauth.socialaccount.models import SocialAccount
1010
from django.shortcuts import get_object_or_404
1111
from rest_framework import decorators, permissions, status, viewsets
12+
from django.template.loader import render_to_string
1213
from rest_framework.decorators import detail_route
13-
from rest_framework.renderers import JSONRenderer
14+
from rest_framework.renderers import BaseRenderer, JSONRenderer
1415
from rest_framework.response import Response
1516

1617
from readthedocs.builds.constants import BRANCH, TAG
@@ -34,6 +35,28 @@
3435
log = logging.getLogger(__name__)
3536

3637

38+
class PlainTextBuildRenderer(BaseRenderer):
39+
40+
"""
41+
Custom renderer for text/plain format.
42+
43+
charset is 'utf-8' by default.
44+
"""
45+
46+
media_type = 'text/plain'
47+
format = 'txt'
48+
49+
def render(self, data, accepted_media_type=None, renderer_context=None):
50+
renderer_context = renderer_context or {}
51+
response = renderer_context.get('response')
52+
if not response or response.exception:
53+
return data.get('detail', '').encode(self.charset)
54+
data = render_to_string(
55+
'restapi/log.txt', {'build': data}
56+
)
57+
return data.encode(self.charset)
58+
59+
3760
class UserSelectViewSet(viewsets.ModelViewSet):
3861

3962
"""
@@ -213,7 +236,7 @@ class VersionViewSet(UserSelectViewSet):
213236

214237
class BuildViewSetBase(UserSelectViewSet):
215238
permission_classes = [APIRestrictedPermission]
216-
renderer_classes = (JSONRenderer,)
239+
renderer_classes = (JSONRenderer, PlainTextBuildRenderer)
217240
serializer_class = BuildSerializer
218241
admin_serializer_class = BuildAdminSerializer
219242
model = Build

readthedocs/rtd_tests/tests/test_api.py

+135-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from rest_framework import status
1818
from rest_framework.test import APIClient
1919

20-
from readthedocs.builds.models import Build, Version
20+
from readthedocs.builds.models import Build, BuildCommandResult, Version
2121
from readthedocs.integrations.models import Integration
2222
from readthedocs.oauth.models import RemoteOrganization, RemoteRepository
2323
from readthedocs.projects.models import Feature, Project
@@ -277,6 +277,140 @@ def test_make_build_commands(self):
277277
self.assertEqual(build['commands'][0]['run_time'], 5)
278278
self.assertEqual(build['commands'][0]['description'], 'foo')
279279

280+
def test_get_raw_log_success(self):
281+
build = get(Build, project_id=1, version_id=1, builder='foo')
282+
get(
283+
BuildCommandResult,
284+
build=build,
285+
command='python setup.py install',
286+
output='Installing dependencies...'
287+
)
288+
get(
289+
BuildCommandResult,
290+
build=build,
291+
command='git checkout master',
292+
output='Switched to branch "master"'
293+
)
294+
client = APIClient()
295+
296+
api_user = get(User, user='test', password='test')
297+
client.force_authenticate(user=api_user)
298+
resp = client.get('/api/v2/build/{0}.txt'.format(build.pk))
299+
self.assertEqual(resp.status_code, 200)
300+
301+
self.assertIn('Read the Docs build information', resp.content.decode())
302+
self.assertIn('Build id: {}'.format(build.id), resp.content.decode())
303+
self.assertIn('Project: {}'.format(build.project.slug), resp.content.decode())
304+
self.assertIn('Version: {}'.format(build.version.slug), resp.content.decode())
305+
self.assertIn('Commit: {}'.format(build.commit), resp.content.decode())
306+
self.assertIn('Date: ', resp.content.decode())
307+
self.assertIn('State: finished', resp.content.decode())
308+
self.assertIn('Success: True', resp.content.decode())
309+
self.assertIn('[rtd-command-info]', resp.content.decode())
310+
self.assertIn(
311+
'python setup.py install\nInstalling dependencies...',
312+
resp.content.decode()
313+
)
314+
self.assertIn(
315+
'git checkout master\nSwitched to branch "master"',
316+
resp.content.decode()
317+
)
318+
319+
def test_get_raw_log_building(self):
320+
build = get(
321+
Build, project_id=1, version_id=1,
322+
builder='foo', success=False,
323+
exit_code=1, state='building',
324+
)
325+
get(
326+
BuildCommandResult,
327+
build=build,
328+
command='python setup.py install',
329+
output='Installing dependencies...',
330+
exit_code=1,
331+
)
332+
get(
333+
BuildCommandResult,
334+
build=build,
335+
command='git checkout master',
336+
output='Switched to branch "master"'
337+
)
338+
client = APIClient()
339+
340+
api_user = get(User, user='test', password='test')
341+
client.force_authenticate(user=api_user)
342+
resp = client.get('/api/v2/build/{0}.txt'.format(build.pk))
343+
self.assertEqual(resp.status_code, 200)
344+
345+
self.assertIn('Read the Docs build information', resp.content.decode())
346+
self.assertIn('Build id: {}'.format(build.id), resp.content.decode())
347+
self.assertIn('Project: {}'.format(build.project.slug), resp.content.decode())
348+
self.assertIn('Version: {}'.format(build.version.slug), resp.content.decode())
349+
self.assertIn('Commit: {}'.format(build.commit), resp.content.decode())
350+
self.assertIn('Date: ', resp.content.decode())
351+
self.assertIn('State: building', resp.content.decode())
352+
self.assertIn('Success: Unknow', resp.content.decode())
353+
self.assertIn('[rtd-command-info]', resp.content.decode())
354+
self.assertIn(
355+
'python setup.py install\nInstalling dependencies...',
356+
resp.content.decode()
357+
)
358+
self.assertIn(
359+
'git checkout master\nSwitched to branch "master"',
360+
resp.content.decode()
361+
)
362+
363+
def test_get_raw_log_failure(self):
364+
build = get(
365+
Build, project_id=1, version_id=1,
366+
builder='foo', success=False, exit_code=1
367+
)
368+
get(
369+
BuildCommandResult,
370+
build=build,
371+
command='python setup.py install',
372+
output='Installing dependencies...',
373+
exit_code=1,
374+
)
375+
get(
376+
BuildCommandResult,
377+
build=build,
378+
command='git checkout master',
379+
output='Switched to branch "master"'
380+
)
381+
client = APIClient()
382+
383+
api_user = get(User, user='test', password='test')
384+
client.force_authenticate(user=api_user)
385+
resp = client.get('/api/v2/build/{0}.txt'.format(build.pk))
386+
self.assertEqual(resp.status_code, 200)
387+
388+
self.assertIn('Read the Docs build information', resp.content.decode())
389+
self.assertIn('Build id: {}'.format(build.id), resp.content.decode())
390+
self.assertIn('Project: {}'.format(build.project.slug), resp.content.decode())
391+
self.assertIn('Version: {}'.format(build.version.slug), resp.content.decode())
392+
self.assertIn('Commit: {}'.format(build.commit), resp.content.decode())
393+
self.assertIn('Date: ', resp.content.decode())
394+
self.assertIn('State: finished', resp.content.decode())
395+
self.assertIn('Success: False', resp.content.decode())
396+
self.assertIn('[rtd-command-info]', resp.content.decode())
397+
self.assertIn(
398+
'python setup.py install\nInstalling dependencies...',
399+
resp.content.decode()
400+
)
401+
self.assertIn(
402+
'git checkout master\nSwitched to branch "master"',
403+
resp.content.decode()
404+
)
405+
406+
def test_get_invalid_raw_log(self):
407+
client = APIClient()
408+
409+
api_user = get(User, user='test', password='test')
410+
client.force_authenticate(user=api_user)
411+
resp = client.get('/api/v2/build/{0}.txt'.format(404))
412+
self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND)
413+
280414

281415
class APITests(TestCase):
282416
fixtures = ['eric.json', 'test_data.json']

readthedocs/templates/builds/build_detail.html

+8
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@
6161
</a>
6262
</li>
6363
</div>
64+
65+
<div data-bind="visible: finished()">
66+
<li>
67+
<a href="{% url "build-detail" build.pk "txt" %}">
68+
{% trans "View raw" %}
69+
</a>
70+
</li>
71+
</div>
6472
</ul>
6573

6674
<div class="build-id">

0 commit comments

Comments
 (0)