diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py index b4d8b2b9b16..606f444496b 100644 --- a/readthedocs/restapi/serializers.py +++ b/readthedocs/restapi/serializers.py @@ -100,6 +100,8 @@ class BuildSerializer(serializers.ModelSerializer): """Build serializer for user display, doesn't display internal fields.""" commands = BuildCommandSerializer(many=True, read_only=True) + project_slug = serializers.ReadOnlyField(source='project.slug') + version_slug = serializers.ReadOnlyField(source='version.slug') docs_url = serializers.ReadOnlyField(source='version.get_absolute_url') state_display = serializers.ReadOnlyField(source='get_state_display') diff --git a/readthedocs/restapi/templates/restapi/log.txt b/readthedocs/restapi/templates/restapi/log.txt new file mode 100644 index 00000000000..dda62a6e8e7 --- /dev/null +++ b/readthedocs/restapi/templates/restapi/log.txt @@ -0,0 +1,14 @@ +Read the Docs build information +Build id: {{ build.id }} +Project: {{ build.project_slug }} +Version: {{ build.version_slug }} +Commit: {{ build.commit }} +Date: {{ build.date }} +State: {{ build.state }} +Success: {% if build.state == 'finished' %}{{ build.success }}{% else %}Unknown{% endif %} + +{% for command in build.commands %} +[rtd-command-info] start-time: {{ command.start_time }}, end-time: {{ command.end_time }}, duration: {{ command.run_time }}, exit-code: {{ command.exit_code }} +{{ command.command|safe }} +{{ command.output|safe }} +{% endfor %} diff --git a/readthedocs/restapi/views/model_views.py b/readthedocs/restapi/views/model_views.py index 0daf74409d4..fee6153091f 100644 --- a/readthedocs/restapi/views/model_views.py +++ b/readthedocs/restapi/views/model_views.py @@ -9,8 +9,9 @@ from allauth.socialaccount.models import SocialAccount from django.shortcuts import get_object_or_404 from rest_framework import decorators, permissions, status, viewsets +from django.template.loader import render_to_string from rest_framework.decorators import detail_route -from rest_framework.renderers import JSONRenderer +from rest_framework.renderers import BaseRenderer, JSONRenderer from rest_framework.response import Response from readthedocs.builds.constants import BRANCH, TAG @@ -34,6 +35,28 @@ log = logging.getLogger(__name__) +class PlainTextBuildRenderer(BaseRenderer): + + """ + Custom renderer for text/plain format. + + charset is 'utf-8' by default. + """ + + media_type = 'text/plain' + format = 'txt' + + def render(self, data, accepted_media_type=None, renderer_context=None): + renderer_context = renderer_context or {} + response = renderer_context.get('response') + if not response or response.exception: + return data.get('detail', '').encode(self.charset) + data = render_to_string( + 'restapi/log.txt', {'build': data} + ) + return data.encode(self.charset) + + class UserSelectViewSet(viewsets.ModelViewSet): """ @@ -213,7 +236,7 @@ class VersionViewSet(UserSelectViewSet): class BuildViewSetBase(UserSelectViewSet): permission_classes = [APIRestrictedPermission] - renderer_classes = (JSONRenderer,) + renderer_classes = (JSONRenderer, PlainTextBuildRenderer) serializer_class = BuildSerializer admin_serializer_class = BuildAdminSerializer model = Build diff --git a/readthedocs/rtd_tests/tests/test_api.py b/readthedocs/rtd_tests/tests/test_api.py index 726ec8b06fc..365dfff61c3 100644 --- a/readthedocs/rtd_tests/tests/test_api.py +++ b/readthedocs/rtd_tests/tests/test_api.py @@ -17,7 +17,7 @@ from rest_framework import status from rest_framework.test import APIClient -from readthedocs.builds.models import Build, Version +from readthedocs.builds.models import Build, BuildCommandResult, Version from readthedocs.integrations.models import Integration from readthedocs.oauth.models import RemoteOrganization, RemoteRepository from readthedocs.projects.models import Feature, Project @@ -277,6 +277,140 @@ def test_make_build_commands(self): self.assertEqual(build['commands'][0]['run_time'], 5) self.assertEqual(build['commands'][0]['description'], 'foo') + def test_get_raw_log_success(self): + build = get(Build, project_id=1, version_id=1, builder='foo') + get( + BuildCommandResult, + build=build, + command='python setup.py install', + output='Installing dependencies...' + ) + get( + BuildCommandResult, + build=build, + command='git checkout master', + output='Switched to branch "master"' + ) + client = APIClient() + + api_user = get(User, user='test', password='test') + client.force_authenticate(user=api_user) + resp = client.get('/api/v2/build/{0}.txt'.format(build.pk)) + self.assertEqual(resp.status_code, 200) + + self.assertIn('Read the Docs build information', resp.content.decode()) + self.assertIn('Build id: {}'.format(build.id), resp.content.decode()) + self.assertIn('Project: {}'.format(build.project.slug), resp.content.decode()) + self.assertIn('Version: {}'.format(build.version.slug), resp.content.decode()) + self.assertIn('Commit: {}'.format(build.commit), resp.content.decode()) + self.assertIn('Date: ', resp.content.decode()) + self.assertIn('State: finished', resp.content.decode()) + self.assertIn('Success: True', resp.content.decode()) + self.assertIn('[rtd-command-info]', resp.content.decode()) + self.assertIn( + 'python setup.py install\nInstalling dependencies...', + resp.content.decode() + ) + self.assertIn( + 'git checkout master\nSwitched to branch "master"', + resp.content.decode() + ) + + def test_get_raw_log_building(self): + build = get( + Build, project_id=1, version_id=1, + builder='foo', success=False, + exit_code=1, state='building', + ) + get( + BuildCommandResult, + build=build, + command='python setup.py install', + output='Installing dependencies...', + exit_code=1, + ) + get( + BuildCommandResult, + build=build, + command='git checkout master', + output='Switched to branch "master"' + ) + client = APIClient() + + api_user = get(User, user='test', password='test') + client.force_authenticate(user=api_user) + resp = client.get('/api/v2/build/{0}.txt'.format(build.pk)) + self.assertEqual(resp.status_code, 200) + + self.assertIn('Read the Docs build information', resp.content.decode()) + self.assertIn('Build id: {}'.format(build.id), resp.content.decode()) + self.assertIn('Project: {}'.format(build.project.slug), resp.content.decode()) + self.assertIn('Version: {}'.format(build.version.slug), resp.content.decode()) + self.assertIn('Commit: {}'.format(build.commit), resp.content.decode()) + self.assertIn('Date: ', resp.content.decode()) + self.assertIn('State: building', resp.content.decode()) + self.assertIn('Success: Unknow', resp.content.decode()) + self.assertIn('[rtd-command-info]', resp.content.decode()) + self.assertIn( + 'python setup.py install\nInstalling dependencies...', + resp.content.decode() + ) + self.assertIn( + 'git checkout master\nSwitched to branch "master"', + resp.content.decode() + ) + + def test_get_raw_log_failure(self): + build = get( + Build, project_id=1, version_id=1, + builder='foo', success=False, exit_code=1 + ) + get( + BuildCommandResult, + build=build, + command='python setup.py install', + output='Installing dependencies...', + exit_code=1, + ) + get( + BuildCommandResult, + build=build, + command='git checkout master', + output='Switched to branch "master"' + ) + client = APIClient() + + api_user = get(User, user='test', password='test') + client.force_authenticate(user=api_user) + resp = client.get('/api/v2/build/{0}.txt'.format(build.pk)) + self.assertEqual(resp.status_code, 200) + + self.assertIn('Read the Docs build information', resp.content.decode()) + self.assertIn('Build id: {}'.format(build.id), resp.content.decode()) + self.assertIn('Project: {}'.format(build.project.slug), resp.content.decode()) + self.assertIn('Version: {}'.format(build.version.slug), resp.content.decode()) + self.assertIn('Commit: {}'.format(build.commit), resp.content.decode()) + self.assertIn('Date: ', resp.content.decode()) + self.assertIn('State: finished', resp.content.decode()) + self.assertIn('Success: False', resp.content.decode()) + self.assertIn('[rtd-command-info]', resp.content.decode()) + self.assertIn( + 'python setup.py install\nInstalling dependencies...', + resp.content.decode() + ) + self.assertIn( + 'git checkout master\nSwitched to branch "master"', + resp.content.decode() + ) + + def test_get_invalid_raw_log(self): + client = APIClient() + + api_user = get(User, user='test', password='test') + client.force_authenticate(user=api_user) + resp = client.get('/api/v2/build/{0}.txt'.format(404)) + self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) + class APITests(TestCase): fixtures = ['eric.json', 'test_data.json'] diff --git a/readthedocs/templates/builds/build_detail.html b/readthedocs/templates/builds/build_detail.html index 9b07479a795..0111805741e 100644 --- a/readthedocs/templates/builds/build_detail.html +++ b/readthedocs/templates/builds/build_detail.html @@ -61,6 +61,14 @@ + +
+
  • + + {% trans "View raw" %} + +
  • +