From fd32096652b11fc8ea295de58df141f37e3e0195 Mon Sep 17 00:00:00 2001 From: dojutsu-user Date: Tue, 22 Jan 2019 00:03:51 +0530 Subject: [PATCH 1/6] add custom admin action --- readthedocs/builds/admin.py | 16 ++++- readthedocs/core/utils/general.py | 27 ++++++++ readthedocs/core/views/__init__.py | 15 +---- .../rtd_tests/tests/test_core_utils.py | 31 +++++++++ .../rtd_tests/tests/versions/__init__.py | 0 .../tests/versions/test_admin_actions.py | 66 +++++++++++++++++++ 6 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 readthedocs/core/utils/general.py create mode 100644 readthedocs/rtd_tests/tests/versions/__init__.py create mode 100644 readthedocs/rtd_tests/tests/versions/test_admin_actions.py diff --git a/readthedocs/builds/admin.py b/readthedocs/builds/admin.py index 66c046f9c3e..201f379bfa6 100644 --- a/readthedocs/builds/admin.py +++ b/readthedocs/builds/admin.py @@ -1,8 +1,9 @@ """Django admin interface for `~builds.models.Build` and related models.""" from __future__ import absolute_import -from django.contrib import admin +from django.contrib import admin, messages from readthedocs.builds.models import Build, Version, BuildCommandResult +from readthedocs.core.utils.general import wipe_version_via_slug from guardian.admin import GuardedModelAdmin @@ -29,6 +30,19 @@ class VersionAdmin(GuardedModelAdmin): list_display = ('slug', 'type', 'project', 'privacy_level', 'active', 'built') list_filter = ('type', 'privacy_level', 'active', 'built') raw_id_fields = ('project',) + actions = ['wipe_selected_versions'] + + def wipe_selected_versions(self, request, queryset): + """Wipes the selected versions.""" + for version in queryset: + wipe_version_via_slug(version.slug) + self.message_user( + request, + 'Wiped {}.'.format(version.slug), + level=messages.SUCCESS + ) + + wipe_selected_versions.short_description = 'Wipe selected versions' admin.site.register(Build, BuildAdmin) diff --git a/readthedocs/core/utils/general.py b/readthedocs/core/utils/general.py new file mode 100644 index 00000000000..6b6174606dc --- /dev/null +++ b/readthedocs/core/utils/general.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from __future__ import ( + division, + print_function, + unicode_literals, +) + +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_slug(version_slug): + """Wipes the given version.""" + version = get_object_or_404(Version, slug=version_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 f3c67b56b64..7f536b652b8 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -6,9 +6,7 @@ documentation and header rendering, and server errors. """ -from __future__ import absolute_import -from __future__ import division -import os +from __future__ import absolute_import, division import logging from django.conf import settings @@ -17,10 +15,9 @@ from django.views.generic import TemplateView from readthedocs.builds.models import Version -from readthedocs.core.utils import broadcast from readthedocs.projects.models import Project, ImportedFile -from readthedocs.projects.tasks import remove_dirs from readthedocs.redirects.utils import get_redirect_response +from readthedocs.core.utils.general import wipe_version_via_slug log = logging.getLogger(__name__) @@ -83,13 +80,7 @@ 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_slug(version) 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 85e7d735507..3f6a5599eb0 100644 --- a/readthedocs/rtd_tests/tests/test_core_utils.py +++ b/readthedocs/rtd_tests/tests/test_core_utils.py @@ -2,14 +2,19 @@ """Test core util functions""" from __future__ import absolute_import +import os import mock +from mock import call from django_dynamic_fixture import get from django.test import TestCase +from django.http import Http404 from readthedocs.projects.models import Project from readthedocs.builds.models import Version from readthedocs.core.utils import trigger_build, slugify +from readthedocs.core.utils.general import wipe_version_via_slug +from readthedocs.projects.tasks import remove_dirs class CoreUtilTests(TestCase): @@ -145,3 +150,29 @@ def test_slugify(self): 'a-title-with-separated-parts') self.assertEqual(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_slug(self.version.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_slug('wrong-slug') + mock_broadcast.assert_not_called() 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..6ae3200264d --- /dev/null +++ b/readthedocs/rtd_tests/tests/versions/test_admin_actions.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +from __future__ import ( + division, + print_function, + unicode_literals, +) + +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 + ) From 27e648bf0443a1d463b094f278dfb35132383430 Mon Sep 17 00:00:00 2001 From: dojutsu-user Date: Tue, 22 Jan 2019 13:17:46 +0530 Subject: [PATCH 2/6] remove imports from __future__ --- readthedocs/core/utils/general.py | 6 ------ readthedocs/core/views/__init__.py | 1 - readthedocs/rtd_tests/tests/versions/test_admin_actions.py | 6 ------ 3 files changed, 13 deletions(-) diff --git a/readthedocs/core/utils/general.py b/readthedocs/core/utils/general.py index 6b6174606dc..b2ab2cf2681 100644 --- a/readthedocs/core/utils/general.py +++ b/readthedocs/core/utils/general.py @@ -1,11 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import ( - division, - print_function, - unicode_literals, -) - import os from django.shortcuts import get_object_or_404 diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index 417240a4dd1..57cf59b024f 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -6,7 +6,6 @@ documentation and header rendering, and server errors. """ -from __future__ import absolute_import, division import logging from django.conf import settings diff --git a/readthedocs/rtd_tests/tests/versions/test_admin_actions.py b/readthedocs/rtd_tests/tests/versions/test_admin_actions.py index 6ae3200264d..03dbfd20039 100644 --- a/readthedocs/rtd_tests/tests/versions/test_admin_actions.py +++ b/readthedocs/rtd_tests/tests/versions/test_admin_actions.py @@ -1,11 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import ( - division, - print_function, - unicode_literals, -) - import os import mock From 45c75d327983fc0d6960364909f8e03c6119f8b2 Mon Sep 17 00:00:00 2001 From: dojutsu-user Date: Wed, 30 Jan 2019 20:58:28 +0530 Subject: [PATCH 3/6] add improvements --- readthedocs/builds/admin.py | 7 +++++-- readthedocs/core/utils/general.py | 10 +++++++--- readthedocs/core/views/__init__.py | 7 +++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/readthedocs/builds/admin.py b/readthedocs/builds/admin.py index 94a946cfe33..d344ad8b73d 100644 --- a/readthedocs/builds/admin.py +++ b/readthedocs/builds/admin.py @@ -6,7 +6,7 @@ from guardian.admin import GuardedModelAdmin from readthedocs.builds.models import Build, Version, BuildCommandResult -from readthedocs.core.utils.general import wipe_version_via_slug +from readthedocs.core.utils.general import wipe_version_via_slugs class BuildCommandResultInline(admin.TabularInline): @@ -61,7 +61,10 @@ class VersionAdmin(GuardedModelAdmin): def wipe_selected_versions(self, request, queryset): """Wipes the selected versions.""" for version in queryset: - wipe_version_via_slug(version.slug) + wipe_version_via_slugs( + version_slug=version.slug, + project_slug=version.project.slug + ) self.message_user( request, 'Wiped {}.'.format(version.slug), diff --git a/readthedocs/core/utils/general.py b/readthedocs/core/utils/general.py index b2ab2cf2681..5949666a3d9 100644 --- a/readthedocs/core/utils/general.py +++ b/readthedocs/core/utils/general.py @@ -9,9 +9,13 @@ from readthedocs.builds.models import Version -def wipe_version_via_slug(version_slug): - """Wipes the given version.""" - version = get_object_or_404(Version, slug=version_slug) +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), diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index 7400d9ebe30..9f138616856 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -16,7 +16,7 @@ from readthedocs.builds.models import Version from readthedocs.projects.models import Project, ImportedFile from readthedocs.redirects.utils import get_redirect_response -from readthedocs.core.utils.general import wipe_version_via_slug +from readthedocs.core.utils.general import wipe_version_via_slugs log = logging.getLogger(__name__) @@ -79,7 +79,10 @@ def wipe_version(request, project_slug, version_slug): raise Http404('You must own this project to wipe it.') if request.method == 'POST': - wipe_version_via_slug(version) + wipe_version_via_slugs( + version_slug=version_slug, + project_slug=project_slug + ) return redirect('project_version_list', project_slug) return render( request, From 93000cfef9adc48eb670b9b949cb43c522ed7ab9 Mon Sep 17 00:00:00 2001 From: dojutsu-user Date: Wed, 30 Jan 2019 21:03:23 +0530 Subject: [PATCH 4/6] update test --- readthedocs/rtd_tests/tests/test_core_utils.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/readthedocs/rtd_tests/tests/test_core_utils.py b/readthedocs/rtd_tests/tests/test_core_utils.py index b6e198e7f3a..308312c251f 100644 --- a/readthedocs/rtd_tests/tests/test_core_utils.py +++ b/readthedocs/rtd_tests/tests/test_core_utils.py @@ -10,7 +10,7 @@ from django_dynamic_fixture import get from readthedocs.builds.models import Version -from readthedocs.core.utils.general import wipe_version_via_slug +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 @@ -162,7 +162,10 @@ def test_slugify(self): @mock.patch('readthedocs.core.utils.general.broadcast') def test_wipe_version_via_slug(self, mock_broadcast): - wipe_version_via_slug(self.version.slug) + 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), @@ -181,7 +184,9 @@ def test_wipe_version_via_slug(self, mock_broadcast): @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_slug('wrong-slug') + wipe_version_via_slugs( + version_slug='wrong-slug', + project_slug=self.version.project.slug + ) mock_broadcast.assert_not_called() From 1795bb08e3358a4443b9b04ff3f05c418c164d56 Mon Sep 17 00:00:00 2001 From: dojutsu-user Date: Fri, 8 Feb 2019 10:20:56 +0530 Subject: [PATCH 5/6] import the missing os --- readthedocs/core/views/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/readthedocs/core/views/__init__.py b/readthedocs/core/views/__init__.py index a1a8a95ebbb..2b0a902e017 100644 --- a/readthedocs/core/views/__init__.py +++ b/readthedocs/core/views/__init__.py @@ -6,6 +6,7 @@ documentation and header rendering, and server errors. """ +import os import logging from urllib.parse import urlparse From b206c70285ab91213800f458bd898a333fa2e812 Mon Sep 17 00:00:00 2001 From: dojutsu-user Date: Thu, 21 Feb 2019 23:04:44 +0530 Subject: [PATCH 6/6] add more tests --- readthedocs/core/utils/general.py | 2 +- .../rtd_tests/tests/test_core_utils.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/readthedocs/core/utils/general.py b/readthedocs/core/utils/general.py index 5949666a3d9..9b7c41b21a9 100644 --- a/readthedocs/core/utils/general.py +++ b/readthedocs/core/utils/general.py @@ -14,7 +14,7 @@ def wipe_version_via_slugs(version_slug, project_slug): version = get_object_or_404( Version, slug=version_slug, - project__slug=project_slug + project__slug=project_slug, ) del_dirs = [ os.path.join(version.project.doc_path, 'checkouts', version.slug), diff --git a/readthedocs/rtd_tests/tests/test_core_utils.py b/readthedocs/rtd_tests/tests/test_core_utils.py index 308312c251f..4c7adfb2313 100644 --- a/readthedocs/rtd_tests/tests/test_core_utils.py +++ b/readthedocs/rtd_tests/tests/test_core_utils.py @@ -190,3 +190,27 @@ def test_wipe_version_via_slug_wrong_param(self, mock_broadcast): 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 + )