Skip to content

Commit 6edc096

Browse files
authored
Unify feature check for organization/project (#8920)
We are overriding classes and skipping tests con .com just because on .com we require a subscription for some features. Now that we have subs on .org we can share more code between both sites. We have a setting to enable a set of features, this is for users that don't have a subscription (.org), on .com this will {} to rely entirely on the features from the subscription.
1 parent f02bc09 commit 6edc096

File tree

19 files changed

+228
-134
lines changed

19 files changed

+228
-134
lines changed

readthedocs/builds/tests/test_build_queryset.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44
from readthedocs.builds.models import Build
55
from readthedocs.organizations.models import Organization
66
from readthedocs.projects.models import Project
7+
from readthedocs.subscriptions.constants import TYPE_CONCURRENT_BUILDS
78

89

910
@pytest.mark.django_db
1011
class TestBuildQuerySet:
12+
@pytest.fixture(autouse=True)
13+
def setup_method(self, settings):
14+
settings.RTD_DEFAULT_FEATURES = {
15+
TYPE_CONCURRENT_BUILDS: 4,
16+
}
1117

1218
def test_concurrent_builds(self):
1319
project = fixture.get(

readthedocs/core/resolver.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from readthedocs.builds.constants import EXTERNAL
88
from readthedocs.core.utils.extend import SettingsOverrideObject
99
from readthedocs.core.utils.url import unsafe_join_url_path
10+
from readthedocs.subscriptions.constants import TYPE_CNAME
11+
from readthedocs.subscriptions.models import PlanFeature
1012

1113
log = structlog.get_logger(__name__)
1214

@@ -404,7 +406,7 @@ def _use_subdomain(self):
404406

405407
def _use_cname(self, project):
406408
"""Test if to allow direct serving for project on CNAME."""
407-
return True
409+
return PlanFeature.objects.has_feature(project, type=TYPE_CNAME)
408410

409411

410412
class Resolver(SettingsOverrideObject):

readthedocs/organizations/tests/test_views.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from readthedocs.organizations.models import Organization, Team
1717
from readthedocs.projects.models import Project
1818
from readthedocs.rtd_tests.base import RequestFactoryTestMixin
19+
from readthedocs.subscriptions.constants import TYPE_AUDIT_LOGS
1920

2021

2122
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
@@ -147,7 +148,12 @@ def test_add_owner(self):
147148
self.assertNotIn(user_b, self.organization.owners.all())
148149

149150

150-
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
151+
@override_settings(
152+
RTD_ALLOW_ORGANIZATIONS=True,
153+
RTD_DEFAULT_FEATURES={
154+
TYPE_AUDIT_LOGS: 90,
155+
},
156+
)
151157
class OrganizationSecurityLogTests(TestCase):
152158

153159
def setUp(self):

readthedocs/organizations/views/private.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from readthedocs.audit.models import AuditLog
1414
from readthedocs.core.history import UpdateChangeReasonPostView
1515
from readthedocs.core.mixins import PrivateViewMixin
16-
from readthedocs.core.utils.extend import SettingsOverrideObject
1716
from readthedocs.invitations.models import Invitation
1817
from readthedocs.organizations.forms import (
1918
OrganizationSignupForm,
@@ -28,6 +27,8 @@
2827
OrganizationView,
2928
)
3029
from readthedocs.projects.utils import get_csv_file
30+
from readthedocs.subscriptions.constants import TYPE_AUDIT_LOGS
31+
from readthedocs.subscriptions.models import PlanFeature
3132

3233

3334
# Organization views
@@ -183,12 +184,13 @@ def post(self, request, *args, **kwargs):
183184
return resp
184185

185186

186-
class OrganizationSecurityLogBase(PrivateViewMixin, OrganizationMixin, ListView):
187+
class OrganizationSecurityLog(PrivateViewMixin, OrganizationMixin, ListView):
187188

188189
"""Display security logs related to this organization."""
189190

190191
model = AuditLog
191192
template_name = 'organizations/security_log.html'
193+
feature_type = TYPE_AUDIT_LOGS
192194

193195
def get(self, request, *args, **kwargs):
194196
download_data = request.GET.get('download', False)
@@ -234,10 +236,10 @@ def _get_csv_data(self):
234236
def get_context_data(self, **kwargs):
235237
organization = self.get_organization()
236238
context = super().get_context_data(**kwargs)
237-
context['enabled'] = self._is_enabled(organization)
238-
context['days_limit'] = self._get_retention_days_limit(organization)
239-
context['filter'] = self.filter
240-
context['AuditLog'] = AuditLog
239+
context["enabled"] = self._is_feature_enabled(organization)
240+
context["days_limit"] = self._get_retention_days_limit(organization)
241+
context["filter"] = self.filter
242+
context["AuditLog"] = AuditLog
241243
return context
242244

243245
def _get_start_date(self):
@@ -255,7 +257,7 @@ def _get_start_date(self):
255257
def _get_queryset(self):
256258
"""Return the queryset without filters."""
257259
organization = self.get_organization()
258-
if not self._is_enabled(organization):
260+
if not self._is_feature_enabled(organization):
259261
return AuditLog.objects.none()
260262
start_date = self._get_start_date()
261263
queryset = AuditLog.objects.filter(
@@ -284,14 +286,15 @@ def get_queryset(self):
284286
)
285287
return self.filter.qs
286288

287-
def _get_retention_days_limit(self, organization): # noqa
288-
"""From how many days we need to show data for this project?"""
289-
return settings.RTD_AUDITLOGS_DEFAULT_RETENTION_DAYS
290-
291-
def _is_enabled(self, organization): # noqa
292-
"""Should we show audit logs for this organization?"""
293-
return True
294-
289+
def _get_retention_days_limit(self, organization):
290+
"""From how many days we need to show data for this organization?"""
291+
return PlanFeature.objects.get_feature_value(
292+
organization,
293+
type=self.feature_type,
294+
)
295295

296-
class OrganizationSecurityLog(SettingsOverrideObject):
297-
_default_class = OrganizationSecurityLogBase
296+
def _is_feature_enabled(self, organization):
297+
return PlanFeature.objects.has_feature(
298+
organization,
299+
type=self.feature_type,
300+
)

readthedocs/projects/querysets.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"""Project model QuerySet classes."""
2-
from django.conf import settings
32
from django.db import models
43
from django.db.models import Count, OuterRef, Prefetch, Q, Subquery
54

@@ -96,6 +95,7 @@ def max_concurrent_builds(self, project):
9695
9796
- project
9897
- organization
98+
- plan
9999
- default setting
100100
101101
:param project: project to be checked
@@ -104,15 +104,21 @@ def max_concurrent_builds(self, project):
104104
:returns: number of max concurrent builds for the project
105105
:rtype: int
106106
"""
107+
from readthedocs.subscriptions.constants import TYPE_CONCURRENT_BUILDS
108+
from readthedocs.subscriptions.models import PlanFeature
109+
107110
max_concurrent_organization = None
108111
organization = project.organizations.first()
109112
if organization:
110113
max_concurrent_organization = organization.max_concurrent_builds
111114

112115
return (
113-
project.max_concurrent_builds or
114-
max_concurrent_organization or
115-
settings.RTD_MAX_CONCURRENT_BUILDS
116+
project.max_concurrent_builds
117+
or max_concurrent_organization
118+
or PlanFeature.objects.get_feature_value(
119+
project,
120+
type=TYPE_CONCURRENT_BUILDS,
121+
)
116122
)
117123

118124
def prefetch_latest_build(self):

readthedocs/projects/tests/test_domain_views.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from readthedocs.organizations.models import Organization
77
from readthedocs.projects.models import Domain, Project
8+
from readthedocs.subscriptions.constants import TYPE_CNAME
89
from readthedocs.subscriptions.models import Plan, PlanFeature, Subscription
910

1011

@@ -114,5 +115,5 @@ def setUp(self):
114115
self.feature = get(
115116
PlanFeature,
116117
plan=self.plan,
117-
feature_type=PlanFeature.TYPE_CNAME,
118+
feature_type=TYPE_CNAME,
118119
)

readthedocs/projects/views/private.py

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
)
4040
from readthedocs.core.history import UpdateChangeReasonPostView
4141
from readthedocs.core.mixins import ListViewWithForm, PrivateViewMixin
42-
from readthedocs.core.utils.extend import SettingsOverrideObject
4342
from readthedocs.integrations.models import HttpExchange, Integration
4443
from readthedocs.invitations.models import Invitation
4544
from readthedocs.oauth.services import registry
@@ -80,6 +79,12 @@
8079
ProjectRelationListMixin,
8180
)
8281
from readthedocs.search.models import SearchQuery
82+
from readthedocs.subscriptions.constants import (
83+
TYPE_CNAME,
84+
TYPE_PAGEVIEW_ANALYTICS,
85+
TYPE_SEARCH_ANALYTICS,
86+
)
87+
from readthedocs.subscriptions.models import PlanFeature
8388

8489
log = structlog.get_logger(__name__)
8590

@@ -752,6 +757,7 @@ class DomainMixin(ProjectAdminMixin, PrivateViewMixin):
752757
model = Domain
753758
form_class = DomainForm
754759
lookup_url_kwarg = 'domain_pk'
760+
feature_type = TYPE_CNAME
755761

756762
def get_success_url(self):
757763
return reverse('projects_domains', args=[self.get_project().slug])
@@ -763,11 +769,13 @@ def get_context_data(self, **kwargs):
763769
return context
764770

765771
def _is_enabled(self, project):
766-
"""Should we allow custom domains for this project?"""
767-
return True
772+
return PlanFeature.objects.has_feature(
773+
project,
774+
type=self.feature_type,
775+
)
768776

769777

770-
class DomainListBase(DomainMixin, ListViewWithForm):
778+
class DomainList(DomainMixin, ListViewWithForm):
771779

772780
def get_context_data(self, **kwargs):
773781
ctx = super().get_context_data(**kwargs)
@@ -782,12 +790,7 @@ def get_context_data(self, **kwargs):
782790
return ctx
783791

784792

785-
class DomainList(SettingsOverrideObject):
786-
787-
_default_class = DomainListBase
788-
789-
790-
class DomainCreateBase(DomainMixin, CreateView):
793+
class DomainCreate(DomainMixin, CreateView):
791794

792795
def post(self, request, *args, **kwargs):
793796
project = self.get_project()
@@ -806,12 +809,7 @@ def get_success_url(self):
806809
)
807810

808811

809-
class DomainCreate(SettingsOverrideObject):
810-
811-
_default_class = DomainCreateBase
812-
813-
814-
class DomainUpdateBase(DomainMixin, UpdateView):
812+
class DomainUpdate(DomainMixin, UpdateView):
815813

816814
def form_valid(self, form):
817815
response = super().form_valid(form)
@@ -825,11 +823,6 @@ def post(self, request, *args, **kwargs):
825823
return HttpResponse('Action not allowed', status=401)
826824

827825

828-
class DomainUpdate(SettingsOverrideObject):
829-
830-
_default_class = DomainUpdateBase
831-
832-
833826
class DomainDelete(DomainMixin, DeleteView):
834827

835828
pass
@@ -1072,10 +1065,11 @@ class RegexAutomationRuleUpdate(RegexAutomationRuleMixin, UpdateView):
10721065
pass
10731066

10741067

1075-
class SearchAnalyticsBase(ProjectAdminMixin, PrivateViewMixin, TemplateView):
1068+
class SearchAnalytics(ProjectAdminMixin, PrivateViewMixin, TemplateView):
10761069

10771070
template_name = 'projects/projects_search_analytics.html'
10781071
http_method_names = ['get']
1072+
feature_type = TYPE_SEARCH_ANALYTICS
10791073

10801074
def get(self, request, *args, **kwargs):
10811075
download_data = request.GET.get('download', False)
@@ -1159,21 +1153,24 @@ def _get_csv_data(self):
11591153

11601154
def _get_retention_days_limit(self, project):
11611155
"""From how many days we need to show data for this project?"""
1162-
return settings.RTD_ANALYTICS_DEFAULT_RETENTION_DAYS
1156+
return PlanFeature.objects.get_feature_value(
1157+
project,
1158+
type=self.feature_type,
1159+
)
11631160

11641161
def _is_enabled(self, project):
11651162
"""Should we show search analytics for this project?"""
1166-
return True
1167-
1168-
1169-
class SearchAnalytics(SettingsOverrideObject):
1170-
_default_class = SearchAnalyticsBase
1163+
return PlanFeature.objects.has_feature(
1164+
project,
1165+
type=self.feature_type,
1166+
)
11711167

11721168

1173-
class TrafficAnalyticsViewBase(ProjectAdminMixin, PrivateViewMixin, TemplateView):
1169+
class TrafficAnalyticsView(ProjectAdminMixin, PrivateViewMixin, TemplateView):
11741170

11751171
template_name = 'projects/project_traffic_analytics.html'
11761172
http_method_names = ['get']
1173+
feature_type = TYPE_PAGEVIEW_ANALYTICS
11771174

11781175
def get(self, request, *args, **kwargs):
11791176
download_data = request.GET.get('download', False)
@@ -1259,12 +1256,14 @@ def _get_csv_data(self):
12591256

12601257
def _get_retention_days_limit(self, project):
12611258
"""From how many days we need to show data for this project?"""
1262-
return settings.RTD_ANALYTICS_DEFAULT_RETENTION_DAYS
1259+
return PlanFeature.objects.get_feature_value(
1260+
project,
1261+
type=self.feature_type,
1262+
)
12631263

12641264
def _is_enabled(self, project):
12651265
"""Should we show traffic analytics for this project?"""
1266-
return True
1267-
1268-
1269-
class TrafficAnalyticsView(SettingsOverrideObject):
1270-
_default_class = TrafficAnalyticsViewBase
1266+
return PlanFeature.objects.has_feature(
1267+
project,
1268+
type=self.feature_type,
1269+
)

readthedocs/proxito/tests/base.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from django.test import TestCase
99

1010
from readthedocs.builds.constants import LATEST
11-
from readthedocs.projects.constants import PUBLIC
11+
from readthedocs.projects.constants import PUBLIC, SSL_STATUS_VALID
1212
from readthedocs.projects.models import Domain, Project
1313
from readthedocs.proxito.views import serve
1414

@@ -80,5 +80,17 @@ def setUp(self):
8080
self.project.add_subproject(self.subproject_alias, alias='this-is-an-alias')
8181

8282
# These can be set to canonical as needed in specific tests
83-
self.domain = fixture.get(Domain, project=self.project, domain='docs1.example.com', https=True)
84-
self.domain2 = fixture.get(Domain, project=self.project, domain='docs2.example.com', https=True)
83+
self.domain = fixture.get(
84+
Domain,
85+
project=self.project,
86+
domain="docs1.example.com",
87+
https=True,
88+
ssl_status=SSL_STATUS_VALID,
89+
)
90+
self.domain2 = fixture.get(
91+
Domain,
92+
project=self.project,
93+
domain="docs2.example.com",
94+
https=True,
95+
ssl_status=SSL_STATUS_VALID,
96+
)

0 commit comments

Comments
 (0)