Skip to content

Commit 23780b9

Browse files
authored
Build: don't show listing or detail view if project is spam (#11633)
* Build: don't show listing or detail view if project is spam Check for `is_show_dashboard_denied` when accessing build listing or detail pages and don't show the dashboard if the project is marked as spam. I tried to write some test cases for this, but it's hard since we don't have `readthedocs-ext` in here. I didn't find any test for similar cases either. We could probably mock most of that code, but I don't think it's worth. I tested this locally and it works as expected at least. Closes readthedocs/readthedocs-ext#554 * Tests for dashboard views on spam projects
1 parent 698f6f0 commit 23780b9

File tree

4 files changed

+72
-5
lines changed

4 files changed

+72
-5
lines changed

readthedocs/builds/tests/test_views.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,35 @@ def _get_project(self, owners, **kwargs):
105105
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
106106
class CancelBuildViewWithOrganizationsTests(CancelBuildViewTests):
107107
pass
108+
109+
110+
class BuildViewsTests(TestCase):
111+
def setUp(self):
112+
self.user = get(User, username="test")
113+
self.project = get(Project, users=[self.user])
114+
self.version = get(Version, project=self.project)
115+
self.build = get(
116+
Build,
117+
project=self.project,
118+
version=self.version,
119+
task_id="1234",
120+
state=BUILD_STATE_INSTALLING,
121+
)
122+
123+
@mock.patch(
124+
"readthedocs.builds.views.BuildList.is_show_dashboard_denied_wrapper",
125+
mock.MagicMock(return_value=True),
126+
)
127+
def test_builds_list_view_spam_project(self):
128+
url = reverse("builds_project_list", args=[self.project.slug])
129+
response = self.client.get(url)
130+
self.assertEqual(response.status_code, 410)
131+
132+
@mock.patch(
133+
"readthedocs.builds.views.BuildDetail.is_show_dashboard_denied_wrapper",
134+
mock.MagicMock(return_value=True),
135+
)
136+
def test_builds_detail_view_spam_project(self):
137+
url = reverse("builds_detail", args=[self.project.slug, self.build.pk])
138+
response = self.client.get(url)
139+
self.assertEqual(response.status_code, 410)

readthedocs/builds/views.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from readthedocs.core.utils import cancel_build, trigger_build
2323
from readthedocs.doc_builder.exceptions import BuildAppError
2424
from readthedocs.projects.models import Project
25+
from readthedocs.projects.views.base import ProjectSpamMixin
2526

2627
log = structlog.get_logger(__name__)
2728

@@ -124,9 +125,20 @@ def _get_versions(self, project):
124125
)
125126

126127

127-
class BuildList(FilterContextMixin, BuildBase, BuildTriggerMixin, ListView):
128+
class BuildList(
129+
FilterContextMixin,
130+
ProjectSpamMixin,
131+
BuildBase,
132+
BuildTriggerMixin,
133+
ListView,
134+
):
128135
filterset_class = BuildListFilter
129136

137+
def get_project(self):
138+
# Call ``.get_queryset()`` to get the current project from ``kwargs``
139+
self.get_queryset()
140+
return self.project
141+
130142
def get_context_data(self, **kwargs):
131143
context = super().get_context_data(**kwargs)
132144

@@ -154,9 +166,12 @@ def get_context_data(self, **kwargs):
154166
return context
155167

156168

157-
class BuildDetail(BuildBase, DetailView):
169+
class BuildDetail(BuildBase, ProjectSpamMixin, DetailView):
158170
pk_url_kwarg = "build_pk"
159171

172+
def get_project(self):
173+
return self.get_object().project
174+
160175
@method_decorator(login_required)
161176
def post(self, request, project_slug, build_pk):
162177
project = get_object_or_404(Project, slug=project_slug)

readthedocs/projects/views/base.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,25 @@ class ProjectSpamMixin:
100100
project's dashboard is denied.
101101
"""
102102

103-
def get(self, request, *args, **kwargs):
103+
def is_show_dashboard_denied_wrapper(self):
104+
"""
105+
Determine if the project has reached dashboard denied treshold.
106+
107+
This function is wrapped just for testing purposes,
108+
so we are able to mock it from outside.
109+
"""
104110
if "readthedocsext.spamfighting" in settings.INSTALLED_APPS:
105111
from readthedocsext.spamfighting.utils import ( # noqa
106112
is_show_dashboard_denied,
107113
)
108114

109115
if is_show_dashboard_denied(self.get_project()):
110-
template_name = "errors/dashboard/spam.html"
111-
return render(request, template_name=template_name, status=410)
116+
return True
117+
return False
118+
119+
def get(self, request, *args, **kwargs):
120+
if self.is_show_dashboard_denied_wrapper():
121+
template_name = "errors/dashboard/spam.html"
122+
return render(request, template_name=template_name, status=410)
112123

113124
return super().get(request, *args, **kwargs)

readthedocs/rtd_tests/tests/test_project_views.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,15 @@ def test_project_versions_only_shows_internal_versons(self):
372372
self.assertNotIn(self.external_version, response.context["active_versions"])
373373
self.assertNotIn(self.external_version, response.context["inactive_versions"])
374374

375+
@mock.patch(
376+
"readthedocs.projects.views.base.ProjectSpamMixin.is_show_dashboard_denied_wrapper",
377+
mock.MagicMock(return_value=True),
378+
)
379+
def test_project_detail_view_spam_project(self):
380+
url = reverse("projects_detail", args=[self.pip.slug])
381+
response = self.client.get(url)
382+
self.assertEqual(response.status_code, 410)
383+
375384

376385
@mock.patch("readthedocs.core.utils.trigger_build", mock.MagicMock())
377386
class TestPrivateViews(TestCase):

0 commit comments

Comments
 (0)