Skip to content

Commit f29401f

Browse files
committed
Unify feature check for organization/project
1 parent fc4b63d commit f29401f

File tree

6 files changed

+83
-49
lines changed

6 files changed

+83
-49
lines changed

readthedocs/organizations/views/private.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from readthedocs.audit.models import AuditLog
1313
from readthedocs.core.history import UpdateChangeReasonPostView
1414
from readthedocs.core.mixins import PrivateViewMixin
15-
from readthedocs.core.utils.extend import SettingsOverrideObject
1615
from readthedocs.organizations.forms import (
1716
OrganizationSignupForm,
1817
OrganizationTeamProjectForm,
@@ -26,6 +25,7 @@
2625
OrganizationView,
2726
)
2827
from readthedocs.projects.utils import get_csv_file
28+
from readthedocs.subscriptions.models import PlanFeature
2929

3030

3131
# Organization views
@@ -170,12 +170,13 @@ def post(self, request, *args, **kwargs):
170170
return resp
171171

172172

173-
class OrganizationSecurityLogBase(PrivateViewMixin, OrganizationMixin, ListView):
173+
class OrganizationSecurityLog(PrivateViewMixin, OrganizationMixin, ListView):
174174

175175
"""Display security logs related to this organization."""
176176

177177
model = AuditLog
178178
template_name = 'organizations/security_log.html'
179+
feature_type = PlanFeature.TYPE_AUDIT_LOGS
179180

180181
def get(self, request, *args, **kwargs):
181182
download_data = request.GET.get('download', False)
@@ -273,14 +274,16 @@ def get_queryset(self):
273274
)
274275
return self.filter.qs
275276

276-
def _get_retention_days_limit(self, organization): # noqa
277-
"""From how many days we need to show data for this project?"""
278-
return settings.RTD_AUDITLOGS_DEFAULT_RETENTION_DAYS
279-
280-
def _is_enabled(self, organization): # noqa
281-
"""Should we show audit logs for this organization?"""
282-
return True
283-
277+
def _is_enabled(self, organization):
278+
return PlanFeature.objects.has_feature(
279+
organization,
280+
type=self.feature_type,
281+
)
284282

285-
class OrganizationSecurityLog(SettingsOverrideObject):
286-
_default_class = OrganizationSecurityLogBase
283+
def _get_retention_days_limit(self, organization):
284+
"""From how many days we need to show data for this organization?"""
285+
return PlanFeature.objects.get_feature_value(
286+
organization,
287+
type=self.feature_type,
288+
default=settings.RTD_AUDITLOGS_DEFAULT_RETENTION_DAYS,
289+
)

readthedocs/projects/querysets.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def max_concurrent_builds(self, project):
9797
9898
- project
9999
- organization
100+
- plan
100101
- default setting
101102
102103
:param project: project to be checked
@@ -105,6 +106,8 @@ def max_concurrent_builds(self, project):
105106
:returns: number of max concurrent builds for the project
106107
:rtype: int
107108
"""
109+
from readthedocs.subscriptions.models import PlanFeature
110+
108111
max_concurrent_organization = None
109112
organization = project.organizations.first()
110113
if organization:
@@ -113,7 +116,11 @@ def max_concurrent_builds(self, project):
113116
return (
114117
project.max_concurrent_builds or
115118
max_concurrent_organization or
116-
settings.RTD_MAX_CONCURRENT_BUILDS
119+
PlanFeature.objects.get_feature_value(
120+
project,
121+
type=PlanFeature.TYPE_CONCURRENT_BUILDS,
122+
default=settings.RTD_MAX_CONCURRENT_BUILDS,
123+
)
117124
)
118125

119126
def prefetch_latest_build(self):

readthedocs/projects/views/private.py

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Project views for authenticated users."""
22

33
import structlog
4-
54
from allauth.socialaccount.models import SocialAccount
65
from django.conf import settings
76
from django.contrib import messages
@@ -40,7 +39,6 @@
4039
)
4140
from readthedocs.core.history import UpdateChangeReasonPostView
4241
from readthedocs.core.mixins import ListViewWithForm, PrivateViewMixin
43-
from readthedocs.core.utils.extend import SettingsOverrideObject
4442
from readthedocs.integrations.models import HttpExchange, Integration
4543
from readthedocs.oauth.services import registry
4644
from readthedocs.oauth.tasks import attach_webhook
@@ -79,6 +77,8 @@
7977
ProjectRelationListMixin,
8078
)
8179
from readthedocs.search.models import SearchQuery
80+
from readthedocs.subscriptions.models import PlanFeature
81+
8282

8383
log = structlog.get_logger(__name__)
8484

@@ -747,6 +747,7 @@ class DomainMixin(ProjectAdminMixin, PrivateViewMixin):
747747
model = Domain
748748
form_class = DomainForm
749749
lookup_url_kwarg = 'domain_pk'
750+
feature_type = PlanFeature.TYPE_CNAME
750751

751752
def get_success_url(self):
752753
return reverse('projects_domains', args=[self.get_project().slug])
@@ -758,11 +759,13 @@ def get_context_data(self, **kwargs):
758759
return context
759760

760761
def _is_enabled(self, project):
761-
"""Should we allow custom domains for this project?"""
762-
return True
762+
return PlanFeature.objects.has_feature(
763+
project,
764+
type=self.feature_type,
765+
)
763766

764767

765-
class DomainListBase(DomainMixin, ListViewWithForm):
768+
class DomainList(DomainMixin, ListViewWithForm):
766769

767770
def get_context_data(self, **kwargs):
768771
ctx = super().get_context_data(**kwargs)
@@ -777,12 +780,7 @@ def get_context_data(self, **kwargs):
777780
return ctx
778781

779782

780-
class DomainList(SettingsOverrideObject):
781-
782-
_default_class = DomainListBase
783-
784-
785-
class DomainCreateBase(DomainMixin, CreateView):
783+
class DomainCreate(DomainMixin, CreateView):
786784

787785
def post(self, request, *args, **kwargs):
788786
project = self.get_project()
@@ -801,12 +799,7 @@ def get_success_url(self):
801799
)
802800

803801

804-
class DomainCreate(SettingsOverrideObject):
805-
806-
_default_class = DomainCreateBase
807-
808-
809-
class DomainUpdateBase(DomainMixin, UpdateView):
802+
class DomainUpdate(DomainMixin, UpdateView):
810803

811804
def post(self, request, *args, **kwargs):
812805
project = self.get_project()
@@ -815,11 +808,6 @@ def post(self, request, *args, **kwargs):
815808
return HttpResponse('Action not allowed', status=401)
816809

817810

818-
class DomainUpdate(SettingsOverrideObject):
819-
820-
_default_class = DomainUpdateBase
821-
822-
823811
class DomainDelete(DomainMixin, DeleteView):
824812

825813
pass
@@ -1062,10 +1050,11 @@ class RegexAutomationRuleUpdate(RegexAutomationRuleMixin, UpdateView):
10621050
pass
10631051

10641052

1065-
class SearchAnalyticsBase(ProjectAdminMixin, PrivateViewMixin, TemplateView):
1053+
class SearchAnalytics(ProjectAdminMixin, PrivateViewMixin, TemplateView):
10661054

10671055
template_name = 'projects/projects_search_analytics.html'
10681056
http_method_names = ['get']
1057+
feature_type = PlanFeature.TYPE_SEARCH_ANALYTICS
10691058

10701059
def get(self, request, *args, **kwargs):
10711060
download_data = request.GET.get('download', False)
@@ -1149,21 +1138,25 @@ def _get_csv_data(self):
11491138

11501139
def _get_retention_days_limit(self, project):
11511140
"""From how many days we need to show data for this project?"""
1152-
return settings.RTD_ANALYTICS_DEFAULT_RETENTION_DAYS
1141+
return PlanFeature.objects.get_feature_value(
1142+
project,
1143+
type=self.feature_type,
1144+
default=settings.RTD_ANALYTICS_DEFAULT_RETENTION_DAYS,
1145+
)
11531146

11541147
def _is_enabled(self, project):
11551148
"""Should we show search analytics for this project?"""
1156-
return True
1157-
1158-
1159-
class SearchAnalytics(SettingsOverrideObject):
1160-
_default_class = SearchAnalyticsBase
1149+
return PlanFeature.objects.has_feature(
1150+
project,
1151+
type=self.feature_type,
1152+
)
11611153

11621154

1163-
class TrafficAnalyticsViewBase(ProjectAdminMixin, PrivateViewMixin, TemplateView):
1155+
class TrafficAnalyticsView(ProjectAdminMixin, PrivateViewMixin, TemplateView):
11641156

11651157
template_name = 'projects/project_traffic_analytics.html'
11661158
http_method_names = ['get']
1159+
feature_type = PlanFeature.TYPE_PAGEVIEW_ANALYTICS
11671160

11681161
def get(self, request, *args, **kwargs):
11691162
download_data = request.GET.get('download', False)
@@ -1239,12 +1232,15 @@ def _get_csv_data(self):
12391232

12401233
def _get_retention_days_limit(self, project):
12411234
"""From how many days we need to show data for this project?"""
1242-
return settings.RTD_ANALYTICS_DEFAULT_RETENTION_DAYS
1235+
return PlanFeature.objects.get_feature_value(
1236+
project,
1237+
type=self.feature_type,
1238+
default=settings.RTD_ANALYTICS_DEFAULT_RETENTION_DAYS,
1239+
)
12431240

12441241
def _is_enabled(self, project):
12451242
"""Should we show traffic analytics for this project?"""
1246-
return True
1247-
1248-
1249-
class TrafficAnalyticsView(SettingsOverrideObject):
1250-
_default_class = TrafficAnalyticsViewBase
1243+
return PlanFeature.objects.has_feature(
1244+
project,
1245+
type=self.feature_type,
1246+
)

readthedocs/proxito/tests/test_full.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,7 @@ def test_sitemap_all_private_versions(self):
985985
ALLOW_PRIVATE_REPOS=True,
986986
PUBLIC_DOMAIN='dev.readthedocs.io',
987987
PUBLIC_DOMAIN_USES_HTTPS=True,
988+
RTD_ALL_FEATURES_ENABLED=True,
988989
)
989990
class TestCDNCache(BaseDocServing):
990991

readthedocs/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ def DOCKER_LIMITS(self):
683683
DEFAULT_PRIVACY_LEVEL = 'public'
684684
DEFAULT_VERSION_PRIVACY_LEVEL = 'public'
685685
ALLOW_ADMIN = True
686+
RTD_ALL_FEATURES_ENABLED = True
686687

687688
# Organization settings
688689
RTD_ALLOW_ORGANIZATIONS = False

readthedocs/subscriptions/managers.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,29 @@ def get_feature(self, obj, type):
218218
plan__subscriptions__organization=organization,
219219
)
220220
return feature.first()
221+
222+
# pylint: disable=redefined-builtin
223+
def get_feature_value(self, obj, type, default=None):
224+
"""
225+
Get the value of the given feature.
226+
227+
Use this function instead of ``get_feature().value``
228+
when you need to respect the ``RTD_ALL_FEATURES_ENABLED`` setting.
229+
"""
230+
if not settings.RTD_ALL_FEATURES_ENABLED:
231+
feature = self.get_feature(obj, type)
232+
if feature:
233+
return feature.value
234+
return default
235+
236+
# pylint: disable=redefined-builtin
237+
def has_feature(self, obj, type):
238+
"""
239+
Get the value of the given feature.
240+
241+
Use this function instead of ``bool(get_feature())``
242+
when you need to respect the ``RTD_ALL_FEATURES_ENABLED`` setting.
243+
"""
244+
if settings.RTD_ALL_FEATURES_ENABLED:
245+
return True
246+
return self.get_feature(obj, type) is not None

0 commit comments

Comments
 (0)