diff --git a/readthedocs/builds/admin.py b/readthedocs/builds/admin.py index ac20db480b0..af83ba82f12 100644 --- a/readthedocs/builds/admin.py +++ b/readthedocs/builds/admin.py @@ -7,6 +7,7 @@ from readthedocs.builds.models import Build, BuildCommandResult, Version from readthedocs.core.utils import trigger_build +from readthedocs.core.utils.general import wipe_version_via_slugs class BuildCommandResultInline(admin.TabularInline): @@ -57,7 +58,22 @@ class VersionAdmin(GuardedModelAdmin): list_filter = ('type', 'privacy_level', 'active', 'built') search_fields = ('slug', 'project__slug') raw_id_fields = ('project',) - actions = ['build_version'] + actions = ['wipe_selected_versions', 'build_version'] + + def wipe_selected_versions(self, request, queryset): + """Wipes the selected versions.""" + for version in queryset: + wipe_version_via_slugs( + version_slug=version.slug, + project_slug=version.project.slug + ) + self.message_user( + request, + 'Wiped {}.'.format(version.slug), + level=messages.SUCCESS + ) + + wipe_selected_versions.short_description = 'Wipe selected versions' def build_version(self, request, queryset): """Trigger a build for the project version.""" diff --git a/readthedocs/core/utils/general.py b/readthedocs/core/utils/general.py new file mode 100644 index 00000000000..9b7c41b21a9 --- /dev/null +++ b/readthedocs/core/utils/general.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +import os + +from django.shortcuts import get_object_or_404 + +from readthedocs.core.utils import broadcast +from readthedocs.projects.tasks import remove_dirs +from readthedocs.builds.models import Version + + +def wipe_version_via_slugs(version_slug, project_slug): + """Wipes the given version of a given project.""" + version = get_object_or_404( + Version, + slug=version_slug, + project__slug=project_slug, + ) + del_dirs = [ + os.path.join(version.project.doc_path, 'checkouts', version.slug), + os.path.join(version.project.doc_path, 'envs', version.slug), + os.path.join(version.project.doc_path, 'conda', version.slug), + ] + for del_dir in del_dirs: + broadcast(type='build', task=remove_dirs, args=[(del_dir,)]) diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index 60fff9efbad..2b0a902e017 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -6,8 +6,6 @@ documentation and header rendering, and server errors. """ -from __future__ import absolute_import -from __future__ import division import os import logging from urllib.parse import urlparse @@ -19,6 +17,7 @@ from readthedocs.builds.models import Version +from readthedocs.core.utils.general import wipe_version_via_slugs from readthedocs.core.resolver import resolve_path from readthedocs.core.symlink import PrivateSymlink, PublicSymlink from readthedocs.core.utils import broadcast @@ -89,13 +88,10 @@ def wipe_version(request, project_slug, version_slug): raise Http404('You must own this project to wipe it.') if request.method == 'POST': - del_dirs = [ - os.path.join(version.project.doc_path, 'checkouts', version.slug), - os.path.join(version.project.doc_path, 'envs', version.slug), - os.path.join(version.project.doc_path, 'conda', version.slug), - ] - for del_dir in del_dirs: - broadcast(type='build', task=remove_dirs, args=[(del_dir,)]) + wipe_version_via_slugs( + version_slug=version_slug, + project_slug=project_slug + ) return redirect('project_version_list', project_slug) return render( request, diff --git a/readthedocs/rtd_tests/tests/test_core_utils.py b/readthedocs/rtd_tests/tests/test_core_utils.py index e57b46c0b17..4c7adfb2313 100644 --- a/readthedocs/rtd_tests/tests/test_core_utils.py +++ b/readthedocs/rtd_tests/tests/test_core_utils.py @@ -1,11 +1,17 @@ # -*- coding: utf-8 -*- """Test core util functions.""" +import os import mock + +from mock import call +from django.http import Http404 from django.test import TestCase from django_dynamic_fixture import get from readthedocs.builds.models import Version +from readthedocs.core.utils.general import wipe_version_via_slugs +from readthedocs.projects.tasks import remove_dirs from readthedocs.core.utils import slugify, trigger_build from readthedocs.projects.models import Project @@ -153,3 +159,58 @@ def test_slugify(self): slugify('A title_-_with separated parts', dns_safe=False), 'a-title_-_with-separated-parts', ) + + @mock.patch('readthedocs.core.utils.general.broadcast') + def test_wipe_version_via_slug(self, mock_broadcast): + wipe_version_via_slugs( + version_slug=self.version.slug, + project_slug=self.version.project.slug + ) + expected_del_dirs = [ + os.path.join(self.version.project.doc_path, 'checkouts', self.version.slug), + os.path.join(self.version.project.doc_path, 'envs', self.version.slug), + os.path.join(self.version.project.doc_path, 'conda', self.version.slug), + ] + + mock_broadcast.assert_has_calls( + [ + call(type='build', task=remove_dirs, args=[(expected_del_dirs[0],)]), + call(type='build', task=remove_dirs, args=[(expected_del_dirs[1],)]), + call(type='build', task=remove_dirs, args=[(expected_del_dirs[2],)]), + ], + any_order=False + ) + + @mock.patch('readthedocs.core.utils.general.broadcast') + def test_wipe_version_via_slug_wrong_param(self, mock_broadcast): + self.assertFalse(Version.objects.filter(slug='wrong-slug').exists()) + with self.assertRaises(Http404): + wipe_version_via_slugs( + version_slug='wrong-slug', + project_slug=self.version.project.slug + ) + mock_broadcast.assert_not_called() + + @mock.patch('readthedocs.core.utils.general.broadcast') + def test_wipe_version_via_slugs_same_version_slug_with_diff_proj(self, mock_broadcast): + project_2 = get(Project) + version_2 = get(Version, project=project_2, slug=self.version.slug) + wipe_version_via_slugs( + version_slug=version_2.slug, + project_slug=project_2.slug, + ) + + expected_del_dirs = [ + os.path.join(version_2.project.doc_path, 'checkouts', version_2.slug), + os.path.join(version_2.project.doc_path, 'envs', version_2.slug), + os.path.join(version_2.project.doc_path, 'conda', version_2.slug), + ] + + mock_broadcast.assert_has_calls( + [ + call(type='build', task=remove_dirs, args=[(expected_del_dirs[0],)]), + call(type='build', task=remove_dirs, args=[(expected_del_dirs[1],)]), + call(type='build', task=remove_dirs, args=[(expected_del_dirs[2],)]), + ], + any_order=False + ) diff --git a/readthedocs/rtd_tests/tests/versions/__init__.py b/readthedocs/rtd_tests/tests/versions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/readthedocs/rtd_tests/tests/versions/test_admin_actions.py b/readthedocs/rtd_tests/tests/versions/test_admin_actions.py new file mode 100644 index 00000000000..03dbfd20039 --- /dev/null +++ b/readthedocs/rtd_tests/tests/versions/test_admin_actions.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +import os +import mock + +from mock import call +import django_dynamic_fixture as fixture +from django.test import TestCase +from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME +from django.contrib.auth.models import User +from django import urls + +from readthedocs.builds.models import Version +from readthedocs.core.models import UserProfile +from readthedocs.projects.models import Project +from readthedocs.projects.tasks import remove_dirs + + +class VersionAdminActionsTest(TestCase): + + @classmethod + def setUpTestData(cls): + cls.owner = fixture.get(User) + cls.profile = fixture.get(UserProfile, user=cls.owner, banned=False) + cls.admin = fixture.get(User, is_staff=True, is_superuser=True) + cls.project = fixture.get( + Project, + main_language_project=None, + users=[cls.owner], + ) + cls.version = fixture.get(Version, project=cls.project) + + def setUp(self): + self.client.force_login(self.admin) + + @mock.patch('readthedocs.core.utils.general.broadcast') + def test_wipe_selected_version(self, mock_broadcast): + action_data = { + ACTION_CHECKBOX_NAME: [self.version.pk], + 'action': 'wipe_selected_versions', + 'post': 'yes', + } + resp = self.client.post( + urls.reverse('admin:builds_version_changelist'), + action_data + ) + expected_del_dirs = [ + os.path.join(self.version.project.doc_path, 'checkouts', self.version.slug), + os.path.join(self.version.project.doc_path, 'envs', self.version.slug), + os.path.join(self.version.project.doc_path, 'conda', self.version.slug), + ] + + mock_broadcast.assert_has_calls( + [ + call(type='build', task=remove_dirs, args=[(expected_del_dirs[0],)]), + call(type='build', task=remove_dirs, args=[(expected_del_dirs[1],)]), + call(type='build', task=remove_dirs, args=[(expected_del_dirs[2],)]), + ], + any_order=False + )