diff --git a/readthedocs/api/v2/serializers.py b/readthedocs/api/v2/serializers.py index dd1a864028d..dc2f7c04bc6 100644 --- a/readthedocs/api/v2/serializers.py +++ b/readthedocs/api/v2/serializers.py @@ -1,6 +1,9 @@ """Defines serializers for each of our models.""" +import re + from allauth.socialaccount.models import SocialAccount +from django.conf import settings from rest_framework import serializers from readthedocs.builds.models import Build, BuildCommandResult, Version @@ -133,11 +136,49 @@ class Meta: exclude = [] +class BuildCommandUISerializer(BuildCommandSerializer): + """ + Serializer used on GETs to trimm the commands' path. + + Remove unreadable paths from the command outputs when returning it from the API. + We could make this change at build level, but we want to avoid undoable issues from now + and hack a small solution to fix the immediate problem. + + This converts: + $ /usr/src/app/checkouts/readthedocs.org/user_builds/ + //envs//bin/python + $ /home/docs/checkouts/readthedocs.org/user_builds/ + /envs//bin/python + into + $ python + """ + + command = serializers.SerializerMethodField() + + def get_command(self, obj): + project_slug = obj.build.version.project.slug + version_slug = obj.build.version.slug + docroot = settings.DOCROOT.rstrip("/") # remove trailing '/' + + # Remove Docker hash from DOCROOT when running it locally + # DOCROOT contains the Docker container hash (e.g. b7703d1b5854). + # We have to remove it from the DOCROOT it self since it changes each time + # we spin up a new Docker instance locally. + container_hash = "/" + if settings.RTD_DOCKER_COMPOSE: + docroot = re.sub("/[0-9a-z]+/?$", "", settings.DOCROOT, count=1) + container_hash = "/[0-9a-z]+/" + + regex = f"{docroot}{container_hash}{project_slug}/envs/{version_slug}(/bin/)?" + command = re.sub(regex, "", obj.command, count=1) + return command + + class BuildSerializer(serializers.ModelSerializer): """Build serializer for user display, doesn't display internal fields.""" - commands = BuildCommandSerializer(many=True, read_only=True) + commands = BuildCommandUISerializer(many=True, read_only=True) project_slug = serializers.ReadOnlyField(source='project.slug') version_slug = serializers.ReadOnlyField(source='get_version_slug') docs_url = serializers.SerializerMethodField() @@ -162,11 +203,17 @@ class BuildAdminSerializer(BuildSerializer): """Build serializer for display to admin users and build instances.""" + commands = BuildCommandSerializer(many=True, read_only=True) + class Meta(BuildSerializer.Meta): # `_config` should be excluded to avoid conflicts with `config` exclude = ('_config',) +class BuildAdminUISerializer(BuildAdminSerializer): + commands = BuildCommandUISerializer(many=True, read_only=True) + + class SearchIndexSerializer(serializers.Serializer): q = serializers.CharField(max_length=500) project = serializers.CharField(max_length=500, required=False) diff --git a/readthedocs/api/v2/views/model_views.py b/readthedocs/api/v2/views/model_views.py index b8b3eb21cfa..b954f6b014f 100644 --- a/readthedocs/api/v2/views/model_views.py +++ b/readthedocs/api/v2/views/model_views.py @@ -23,6 +23,7 @@ from ..permissions import APIPermission, APIRestrictedPermission, IsOwner from ..serializers import ( BuildAdminSerializer, + BuildAdminUISerializer, BuildCommandSerializer, BuildSerializer, DomainSerializer, @@ -224,11 +225,25 @@ class VersionViewSet(DisableListEndpoint, UserSelectViewSet): class BuildViewSet(DisableListEndpoint, UserSelectViewSet): permission_classes = [APIRestrictedPermission] renderer_classes = (JSONRenderer, PlainTextBuildRenderer) - serializer_class = BuildSerializer - admin_serializer_class = BuildAdminSerializer model = Build filterset_fields = ('project__slug', 'commit') + def get_serializer_class(self): + """ + Return the proper serializer for UI and Admin. + + This ViewSet has a sligtly different pattern since we want to + pre-process the `command` field before returning it to the user, and we + also want to have a specific serializer for admins. + """ + if self.request.user.is_staff: + # Logic copied from `UserSelectViewSet.get_serializer_class` + # and extended to check for GET method + if self.request.method == "GET": + return BuildAdminUISerializer + return BuildAdminSerializer + return BuildSerializer + @decorators.action( detail=False, permission_classes=[permissions.IsAdminUser],