Skip to content

Subscriptions: use djstripe for products/features #10238

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Aug 10, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5570a73
Subscriptions: use djstripe for products/features
stsewd Apr 12, 2023
703cd65
Fix tests
stsewd Apr 12, 2023
efde180
Updates
stsewd Apr 12, 2023
d207565
Linter
stsewd Apr 12, 2023
b7f9c85
More updates
stsewd Apr 12, 2023
4e11f6f
Linter
stsewd Apr 13, 2023
a1cb32e
Merge branch 'main' into use-djstripe-for-features
stsewd Apr 13, 2023
27f9003
Default
stsewd Apr 13, 2023
2aa04f8
Default listed to False
stsewd Apr 13, 2023
356f18d
Fix tests
stsewd Apr 13, 2023
3c65b4b
Black
stsewd Apr 13, 2023
2fc1cbb
Merge branch 'main' into use-djstripe-for-features
stsewd Apr 18, 2023
e432063
Fixes
stsewd Apr 18, 2023
6462892
Fix
stsewd Apr 18, 2023
cd25bba
Merge branch 'main' into use-djstripe-for-features
stsewd May 10, 2023
9864e44
Merge branch 'main' into use-djstripe-for-features
stsewd Jul 25, 2023
aeadfb6
Migrate new tests
stsewd Jul 25, 2023
90f6928
Fix linter
stsewd Jul 26, 2023
8409136
Migrate more features
stsewd Jul 26, 2023
91284b9
Small fixes
stsewd Jul 26, 2023
52a36c5
Document local testing
stsewd Jul 26, 2023
3d9ea19
Include in index
stsewd Jul 26, 2023
29faab5
Filter by active prices
stsewd Jul 26, 2023
f9b060c
Better description for page views logs
stsewd Jul 27, 2023
2399515
Support for multiple products
stsewd Jul 31, 2023
a8950d3
Migration
stsewd Jul 31, 2023
3084035
format
stsewd Jul 31, 2023
45a1574
More docs
stsewd Jul 31, 2023
8589904
Merge branch 'main' into use-djstripe-for-features
stsewd Jul 31, 2023
9f6e3bf
Use new logic to get subscription
stsewd Jul 31, 2023
03d5139
Fix
stsewd Aug 1, 2023
df0a7d5
Invert
stsewd Aug 1, 2023
dd42fd9
Linter
stsewd Aug 1, 2023
b89f7aa
Fix test on .com
stsewd Aug 1, 2023
f1486f6
Quantity
stsewd Aug 1, 2023
6c38514
Tests
stsewd Aug 1, 2023
95eb6f1
More tests
stsewd Aug 1, 2023
19dd947
Fix test on .com
stsewd Aug 1, 2023
87d1cd9
Merge branch 'main' into use-djstripe-for-features
stsewd Aug 3, 2023
62f29d3
Avoid nesting
stsewd Aug 3, 2023
c0ac743
Merge branch 'main' into use-djstripe-for-features
stsewd Aug 8, 2023
aa92198
Merge branch 'main' into use-djstripe-for-features
stsewd Aug 10, 2023
83e26d1
Update from review
stsewd Aug 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions readthedocs/api/v3/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.subscriptions.constants import TYPE_EMBED_API
from readthedocs.subscriptions.models import PlanFeature
from readthedocs.subscriptions.products import get_feature


class HasEmbedAPIAccess(BasePermission):
Expand All @@ -24,7 +24,7 @@ class HasEmbedAPIAccess(BasePermission):
def has_permission(self, request, view):
project = view._get_project()
# The project is None when the is requesting a section from an external site.
if project and not PlanFeature.objects.has_feature(project, TYPE_EMBED_API):
if project and not get_feature(project, feature_type=TYPE_EMBED_API):
return False
return True

Expand Down
7 changes: 4 additions & 3 deletions readthedocs/builds/tests/test_build_queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@
from readthedocs.organizations.models import Organization
from readthedocs.projects.models import Project
from readthedocs.subscriptions.constants import TYPE_CONCURRENT_BUILDS
from readthedocs.subscriptions.products import RTDProductFeature


@pytest.mark.django_db
class TestBuildQuerySet:
@pytest.fixture(autouse=True)
def setup_method(self, settings):
settings.RTD_DEFAULT_FEATURES = {
TYPE_CONCURRENT_BUILDS: 4,
}
settings.RTD_DEFAULT_FEATURES = dict(
[RTDProductFeature(type=TYPE_CONCURRENT_BUILDS, value=4).to_item()]
)

def test_concurrent_builds(self):
project = fixture.get(
Expand Down
4 changes: 2 additions & 2 deletions readthedocs/core/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.core.utils.url import unsafe_join_url_path
from readthedocs.subscriptions.constants import TYPE_CNAME
from readthedocs.subscriptions.models import PlanFeature
from readthedocs.subscriptions.products import get_feature

log = structlog.get_logger(__name__)

Expand Down Expand Up @@ -406,7 +406,7 @@ def _use_subdomain(self):

def _use_cname(self, project):
"""Test if to allow direct serving for project on CNAME."""
return PlanFeature.objects.has_feature(project, type=TYPE_CNAME)
return bool(get_feature(project, feature_type=TYPE_CNAME))


class Resolver(SettingsOverrideObject):
Expand Down
7 changes: 4 additions & 3 deletions readthedocs/organizations/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from readthedocs.projects.models import Project
from readthedocs.rtd_tests.base import RequestFactoryTestMixin
from readthedocs.subscriptions.constants import TYPE_AUDIT_LOGS
from readthedocs.subscriptions.products import RTDProductFeature


@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
Expand Down Expand Up @@ -150,9 +151,9 @@ def test_add_owner(self):

@override_settings(
RTD_ALLOW_ORGANIZATIONS=True,
RTD_DEFAULT_FEATURES={
TYPE_AUDIT_LOGS: 90,
},
RTD_DEFAULT_FEATURES=dict(
[RTDProductFeature(type=TYPE_AUDIT_LOGS, value=90).to_item()]
),
)
class OrganizationSecurityLogTests(TestCase):

Expand Down
30 changes: 10 additions & 20 deletions readthedocs/organizations/views/private.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
)
from readthedocs.projects.utils import get_csv_file
from readthedocs.subscriptions.constants import TYPE_AUDIT_LOGS
from readthedocs.subscriptions.models import PlanFeature
from readthedocs.subscriptions.products import get_feature


# Organization views
Expand Down Expand Up @@ -236,8 +236,9 @@ def _get_csv_data(self):
def get_context_data(self, **kwargs):
organization = self.get_organization()
context = super().get_context_data(**kwargs)
context["enabled"] = self._is_feature_enabled(organization)
context["days_limit"] = self._get_retention_days_limit(organization)
feature = self._get_feature(organization)
context["enabled"] = bool(feature)
context["days_limit"] = feature.value if feature else 0
context["filter"] = self.filter
context["AuditLog"] = AuditLog
return context
Expand All @@ -246,18 +247,17 @@ def _get_start_date(self):
"""Get the date to show logs from."""
organization = self.get_organization()
creation_date = organization.pub_date.date()
retention_limit = self._get_retention_days_limit(organization)
if retention_limit in [None, -1]:
# Unlimited.
feature = self._get_feature(organization)
if feature.unlimited:
return creation_date
start_date = timezone.now().date() - timezone.timedelta(days=retention_limit)
start_date = timezone.now().date() - timezone.timedelta(days=feature.value)
# The max we can go back is to the creation of the organization.
return max(start_date, creation_date)

def _get_queryset(self):
"""Return the queryset without filters."""
organization = self.get_organization()
if not self._is_feature_enabled(organization):
if not self._get_feature(organization):
return AuditLog.objects.none()
start_date = self._get_start_date()
queryset = AuditLog.objects.filter(
Expand Down Expand Up @@ -286,15 +286,5 @@ def get_queryset(self):
)
return self.filter.qs

def _get_retention_days_limit(self, organization):
"""From how many days we need to show data for this organization?"""
return PlanFeature.objects.get_feature_value(
organization,
type=self.feature_type,
)

def _is_feature_enabled(self, organization):
return PlanFeature.objects.has_feature(
organization,
type=self.feature_type,
)
def _get_feature(self, organization):
return get_feature(organization, self.feature_type)
9 changes: 4 additions & 5 deletions readthedocs/projects/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from readthedocs.core.permissions import AdminPermission
from readthedocs.core.utils.extend import SettingsOverrideObject
from readthedocs.projects import constants
from readthedocs.subscriptions.products import get_feature


class ProjectQuerySetBase(models.QuerySet):
Expand Down Expand Up @@ -105,20 +106,18 @@ def max_concurrent_builds(self, project):
:rtype: int
"""
from readthedocs.subscriptions.constants import TYPE_CONCURRENT_BUILDS
from readthedocs.subscriptions.models import PlanFeature

max_concurrent_organization = None
organization = project.organizations.first()
if organization:
max_concurrent_organization = organization.max_concurrent_builds

feature = get_feature(project, feature_type=TYPE_CONCURRENT_BUILDS)
feature_value = feature.value if feature else 1
return (
project.max_concurrent_builds
or max_concurrent_organization
or PlanFeature.objects.get_feature_value(
project,
type=TYPE_CONCURRENT_BUILDS,
)
or feature_value
)

def prefetch_latest_build(self):
Expand Down
21 changes: 5 additions & 16 deletions readthedocs/projects/tests/test_domain_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
from readthedocs.organizations.models import Organization
from readthedocs.projects.models import Domain, Project
from readthedocs.subscriptions.constants import TYPE_CNAME
from readthedocs.subscriptions.models import Plan, PlanFeature, Subscription
from readthedocs.subscriptions.products import RTDProductFeature


@override_settings(RTD_ALLOW_ORGANIZATIONS=False)
@override_settings(
RTD_ALLOW_ORGANIZATIONS=False,
RTD_DEFAULT_FEATURES=dict([RTDProductFeature(type=TYPE_CNAME).to_item()]),
)
class TestDomainViews(TestCase):
def setUp(self):
self.user = get(User, username="user")
Expand Down Expand Up @@ -103,17 +106,3 @@ def setUp(self):
self.org = get(
Organization, owners=[self.user], projects=[self.project, self.subproject]
)
self.plan = get(
Plan,
published=True,
)
self.subscription = get(
Subscription,
plan=self.plan,
organization=self.org,
)
self.feature = get(
PlanFeature,
plan=self.plan,
feature_type=TYPE_CNAME,
)
93 changes: 33 additions & 60 deletions readthedocs/projects/views/private.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
TYPE_PAGEVIEW_ANALYTICS,
TYPE_SEARCH_ANALYTICS,
)
from readthedocs.subscriptions.models import PlanFeature
from readthedocs.subscriptions.products import get_feature

log = structlog.get_logger(__name__)

Expand Down Expand Up @@ -769,10 +769,7 @@ def get_context_data(self, **kwargs):
return context

def _is_enabled(self, project):
return PlanFeature.objects.has_feature(
project,
type=self.feature_type,
)
return bool(get_feature(project, feature_type=self.feature_type))


class DomainList(DomainMixin, ListViewWithForm):
Expand Down Expand Up @@ -1080,7 +1077,7 @@ def get(self, request, *args, **kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
project = self.get_project()
enabled = self._is_enabled(project)
enabled = bool(self._get_feature(project))
context.update({'enabled': enabled})
if not enabled:
return context
Expand Down Expand Up @@ -1115,28 +1112,27 @@ def _get_csv_data(self):
"""Generate raw csv data of search queries."""
project = self.get_project()
now = timezone.now().date()
retention_limit = self._get_retention_days_limit(project)
if retention_limit in [None, -1]:
# Unlimited.
feature = self._get_feature(project)
if not feature:
raise Http404
if feature.unlimited:
days_ago = project.pub_date.date()
else:
days_ago = now - timezone.timedelta(days=retention_limit)
days_ago = now - timezone.timedelta(days=feature.value)

values = [
('Created Date', 'created'),
('Query', 'query'),
('Total Results', 'total_results'),
]
data = []
if self._is_enabled(project):
data = (
SearchQuery.objects.filter(
project=project,
created__date__gte=days_ago,
)
.order_by('-created')
.values_list(*[value for _, value in values])
data = (
SearchQuery.objects.filter(
project=project,
created__date__gte=days_ago,
)
.order_by("-created")
.values_list(*[value for _, value in values])
)

filename = 'readthedocs_search_analytics_{project_slug}_{start}_{end}.csv'.format(
project_slug=project.slug,
Expand All @@ -1151,19 +1147,8 @@ def _get_csv_data(self):
csv_data.insert(0, [header for header, _ in values])
return get_csv_file(filename=filename, csv_data=csv_data)

def _get_retention_days_limit(self, project):
"""From how many days we need to show data for this project?"""
return PlanFeature.objects.get_feature_value(
project,
type=self.feature_type,
)

def _is_enabled(self, project):
"""Should we show search analytics for this project?"""
return PlanFeature.objects.has_feature(
project,
type=self.feature_type,
)
def _get_feature(self, project):
return get_feature(project, feature_type=self.feature_type)


class TrafficAnalyticsView(ProjectAdminMixin, PrivateViewMixin, TemplateView):
Expand All @@ -1181,7 +1166,7 @@ def get(self, request, *args, **kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
project = self.get_project()
enabled = self._is_enabled(project)
enabled = bool(self._get_feature(project))
context.update({'enabled': enabled})
if not enabled:
return context
Expand Down Expand Up @@ -1217,30 +1202,29 @@ def get_context_data(self, **kwargs):
def _get_csv_data(self):
project = self.get_project()
now = timezone.now().date()
retention_limit = self._get_retention_days_limit(project)
if retention_limit in [None, -1]:
# Unlimited.
feature = self._get_feature(project)
if not feature:
raise Http404
if feature.unlimited:
days_ago = project.pub_date.date()
else:
days_ago = now - timezone.timedelta(days=retention_limit)
days_ago = now - timezone.timedelta(days=feature.value)

values = [
('Date', 'date'),
('Version', 'version__slug'),
('Path', 'path'),
('Views', 'view_count'),
]
data = []
if self._is_enabled(project):
data = (
PageView.objects.filter(
project=project,
date__gte=days_ago,
status=200,
)
.order_by('-date')
.values_list(*[value for _, value in values])
data = (
PageView.objects.filter(
project=project,
date__gte=days_ago,
status=200,
)
.order_by("-date")
.values_list(*[value for _, value in values])
)

filename = 'readthedocs_traffic_analytics_{project_slug}_{start}_{end}.csv'.format(
project_slug=project.slug,
Expand All @@ -1254,16 +1238,5 @@ def _get_csv_data(self):
csv_data.insert(0, [header for header, _ in values])
return get_csv_file(filename=filename, csv_data=csv_data)

def _get_retention_days_limit(self, project):
"""From how many days we need to show data for this project?"""
return PlanFeature.objects.get_feature_value(
project,
type=self.feature_type,
)

def _is_enabled(self, project):
"""Should we show traffic analytics for this project?"""
return PlanFeature.objects.has_feature(
project,
type=self.feature_type,
)
def _get_feature(self, project):
return get_feature(project, feature_type=self.feature_type)
Loading