Skip to content

Commit

Permalink
prevents deleted subscribers to show in subscribers API results
Browse files Browse the repository at this point in the history
  • Loading branch information
smirolo committed Mar 1, 2024
1 parent 3c347f0 commit df27e8e
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 42 deletions.
103 changes: 82 additions & 21 deletions saas/api/organizations.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from ..mixins import (DateRangeContextMixin, OrganizationMixin,
OrganizationSearchOrderListMixin, OrganizationSmartListMixin,
ProviderMixin, OrganizationDecorateMixin)
from ..models import get_broker
from ..models import get_broker, Subscription
from ..utils import (build_absolute_uri, datetime_or_now,
get_organization_model, get_role_model, get_picture_storage,
handle_uniq_error)
Expand Down Expand Up @@ -215,6 +215,7 @@ def destroy(self, request, *args, **kwargs): #pylint:disable=unused-argument
Archive the organization. We don't to loose the subscriptions
and transactions history.
"""
at_time = datetime_or_now()
obj = self.get_object()
user = obj.attached_user()
email = obj.email
Expand All @@ -231,6 +232,8 @@ def destroy(self, request, *args, **kwargs): #pylint:disable=unused-argument
# Removes all roles on the organization such that the organization
# is not picked up inadvertently.
get_role_model().objects.filter(organization=obj).delete()
Subscription.objects.filter(
organization=obj, ends_at__gt=at_time).update(ends_at=at_time)
obj.slug = slug
obj.email = email
obj.is_active = False
Expand Down Expand Up @@ -351,21 +354,23 @@ def paginate_queryset(self, queryset):
return page


class SubscribersQuerysetMixin(OrganizationDecorateMixin, ProviderMixin):
class ProviderAccessiblesQuerysetMixin(OrganizationDecorateMixin,
ProviderMixin):

def get_queryset(self):
queryset = get_organization_model().objects.filter(
subscribes_to__organization=self.provider)
queryset = get_organization_model().objects.filter(is_active=True,
subscribes_to__organization=self.provider).distinct()
return queryset

def paginate_queryset(self, queryset):
page = super(SubscribersQuerysetMixin, self).paginate_queryset(queryset)
page = super(ProviderAccessiblesQuerysetMixin, self).paginate_queryset(
queryset)
page = self.decorate_personal(page)
return page


class ProviderAccessiblesAPIView(OrganizationSmartListMixin,
SubscribersQuerysetMixin, ListAPIView):
ProviderAccessiblesQuerysetMixin, ListAPIView):
"""
Lists subscribers
Expand All @@ -385,6 +390,70 @@ class ProviderAccessiblesAPIView(OrganizationSmartListMixin,
**Examples**
.. code-block:: http
GET /api/profile/cowork/subscribers/all?o=created_at&ot=desc HTTP/1.1
responds
.. code-block:: json
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"slug": "xia",
"printable_name": "Xia Lee",
"picture": null,
"type": "personal",
"credentials": true
}
]
}
"""
serializer_class = OrganizationSerializer


class ActiveSubscribersQuerysetMixin(DateRangeContextMixin,
OrganizationDecorateMixin, ProviderMixin):

def get_queryset(self):
ends_at = datetime_or_now(self.ends_at)
queryset = get_organization_model().objects.filter(is_active=True,
subscribes_to__organization=self.provider,
subscriptions__ends_at__gt=ends_at).distinct()
return queryset

def paginate_queryset(self, queryset):
page = super(ActiveSubscribersQuerysetMixin, self).paginate_queryset(
queryset)
page = self.decorate_personal(page)
return page


class ActiveSubscribersAPIView(OrganizationSmartListMixin,
ActiveSubscribersQuerysetMixin, ListAPIView):
"""
Lists active subscribers
Returns a list of {{PAGE_SIZE}} subscribers which have an active
subscription to a plan of the specified provider {profile}.
The queryset can be filtered for at least one field to match a search
term (``q``) and/or intersects a period (``start_at``, ``ends_at``).
Returned results can be ordered by natural fields (``o``) in either
ascending or descending order by using the minus sign ('-') in front
of the ordering field name.
The API is typically used in search forms linked to providers.
**Tags**: list, provider, profilemodel, subscriptions
**Examples**
.. code-block:: http
GET /api/profile/cowork/subscribers?o=created_at&ot=desc HTTP/1.1
Expand Down Expand Up @@ -440,8 +509,7 @@ class EngagedSubscribersSmartListMixin(object):
# of `EngagedSubscribersQuerysetMixin`.


class EngagedSubscribersQuerysetMixin(DateRangeContextMixin,
SubscribersQuerysetMixin):
class EngagedSubscribersQuerysetMixin(ActiveSubscribersQuerysetMixin):

def get_queryset(self):
filter_params = {}
Expand All @@ -454,10 +522,8 @@ def get_queryset(self):
'user__last_login__gte': start_at
})
queryset = get_role_model().objects.filter(
organization__in=get_organization_model().objects.filter(
is_active=True,
subscriptions__plan__organization=self.provider,
subscriptions__ends_at__gt=ends_at),
organization__in=super(
EngagedSubscribersQuerysetMixin, self).get_queryset(),
**filter_params
).select_related('user', 'organization')
return queryset
Expand Down Expand Up @@ -530,10 +596,7 @@ class EngagedSubscribersAPIView(EngagedSubscribersSmartListMixin,
serializer_class = EngagedSubscriberSerializer


class UnengagedSubscribersQuerysetMixin(DateRangeContextMixin,
SubscribersQuerysetMixin):
# Uses `OrganizationSmartListMixin` here because it derives from
# `DateRangeContextMixin` and we need `start_at`/`ends_at` in the query.
class UnengagedSubscribersQuerysetMixin(ActiveSubscribersQuerysetMixin):

def get_queryset(self):
ends_at = datetime_or_now(self.ends_at)
Expand All @@ -546,11 +609,9 @@ def get_queryset(self):
days=settings.INACTIVITY_DAYS)})
one_login_within_period = get_organization_model().objects.filter(
**kwargs)
queryset = get_organization_model().objects.filter(
is_active=True,
subscribes_to__organization=self.provider,
subscriptions__ends_at__gt=ends_at).exclude(
pk__in=one_login_within_period).distinct()
queryset = super(
UnengagedSubscribersQuerysetMixin, self).get_queryset().exclude(
pk__in=one_login_within_period).distinct()
return queryset


Expand Down
15 changes: 6 additions & 9 deletions saas/api/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,13 +775,9 @@ class PlanChurnedSubscribersAPIView(SubscriptionSmartListMixin,
ChurnedInPeriodFilter,)


class AllSubscribersBaseAPIView(ProvidedSubscriptionsMixin, ListAPIView):

pass


class AllSubscribersAPIView(SubscriptionSmartListMixin,
AllSubscribersBaseAPIView):
class AllSubscriberSubscriptionsAPIView(SubscriptionSmartListMixin,
ProvidedSubscriptionsMixin,
ListAPIView):
"""
Lists provider subscriptions
Expand Down Expand Up @@ -843,14 +839,15 @@ class AllSubscribersAPIView(SubscriptionSmartListMixin,



class ActiveSubscribersMixin(SubscriptionSmartListMixin,
class ActiveSubscriberSubscriptionsMixin(SubscriptionSmartListMixin,
ProvidedSubscriptionsMixin):

filter_backends = SubscriptionSmartListMixin.filter_backends + (
ActiveInPeriodFilter,)


class ActiveSubscribersAPIView(ActiveSubscribersMixin, ListAPIView):
class ActiveSubscriberSubscriptionsAPIView(ActiveSubscriberSubscriptionsMixin,
ListAPIView):
"""
Lists provider active subscriptions
Expand Down
3 changes: 2 additions & 1 deletion saas/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -1085,7 +1085,8 @@ class ProvidedSubscriptionsMixin(OrganizationDecorateMixin, ProviderMixin):

def get_queryset(self):
# OK to use ``filter`` here since we want to list all subscriptions.
queryset = Subscription.objects.filter(plan__organization=self.provider)
queryset = Subscription.objects.filter(organization__is_active=True,
plan__organization=self.provider)
# `SubscriptionSerializer` will expand `organization` and `plan`.
queryset = queryset.select_related('organization').select_related(
'plan')
Expand Down
23 changes: 15 additions & 8 deletions saas/urls/api/provider/subscribers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2022, DjaoDjin inc.
# Copyright (c) 2024, DjaoDjin inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -27,10 +27,12 @@
"""

from .... import settings
from ....api.organizations import (EngagedSubscribersAPIView,
ProviderAccessiblesAPIView, UnengagedSubscribersAPIView)
from ....api.subscriptions import (ActiveSubscribersAPIView,
AllSubscribersAPIView, ChurnedSubscribersAPIView, PlanAllSubscribersAPIView,
from ....api.organizations import (ActiveSubscribersAPIView,
EngagedSubscribersAPIView, ProviderAccessiblesAPIView,
UnengagedSubscribersAPIView)
from ....api.subscriptions import (ActiveSubscriberSubscriptionsAPIView,
AllSubscriberSubscriptionsAPIView, ChurnedSubscribersAPIView,
PlanAllSubscribersAPIView,
PlanActiveSubscribersAPIView, PlanChurnedSubscribersAPIView,
PlanSubscriptionDetailAPIView, SubscriptionRequestAcceptAPIView)
from ....compat import path, re_path
Expand All @@ -44,14 +46,16 @@
name='saas_api_subscription_grant_accept'),
path('profile/<slug:%s>/subscribers/subscriptions/all' %
settings.PROFILE_URL_KWARG,
AllSubscribersAPIView.as_view(), name='saas_api_subscribers_all'),
AllSubscriberSubscriptionsAPIView.as_view(),
name='saas_api_subscribed_and_churned'),
path('profile/<slug:%s>/subscribers/subscriptions/churned' %
settings.PROFILE_URL_KWARG,
ChurnedSubscribersAPIView.as_view(),
name='saas_api_churned'),
path('profile/<slug:%s>/subscribers/subscriptions' %
settings.PROFILE_URL_KWARG,
ActiveSubscribersAPIView.as_view(), name='saas_api_subscribed'),
ActiveSubscriberSubscriptionsAPIView.as_view(),
name='saas_api_subscribed'),

path('profile/<slug:%s>/subscribers/engaged' %
settings.PROFILE_URL_KWARG,
Expand All @@ -61,9 +65,12 @@
settings.PROFILE_URL_KWARG,
UnengagedSubscribersAPIView.as_view(),
name='saas_api_unengaged_subscribers'),
path('profile/<slug:%s>/subscribers/all' %
settings.PROFILE_URL_KWARG,
ProviderAccessiblesAPIView.as_view(), name='saas_api_subscribers_all'),
path('profile/<slug:%s>/subscribers' %
settings.PROFILE_URL_KWARG,
ProviderAccessiblesAPIView.as_view(), name='saas_api_subscribers'),
ActiveSubscribersAPIView.as_view(), name='saas_api_subscribers'),

path('profile/<slug:%s>/plans/<slug:plan>/subscriptions/all' %
settings.PROFILE_URL_KWARG,
Expand Down
2 changes: 1 addition & 1 deletion saas/views/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
ImportTransactionForm, RedeemCouponForm, VTChargeForm, WithdrawForm)
from ..mixins import (BalanceDueMixin, BalanceAndCartMixin, ChargeMixin,
DateRangeContextMixin, InvoicablesMixin, OrganizationMixin,
ProviderMixin, get_charge_context, product_url, UserMixin)
ProviderMixin, product_url, UserMixin)
from ..models import (CartItem, Charge, Coupon,
Plan, Price, Subscription, Transaction, UseCharge, get_broker)
from ..utils import (get_organization_model, update_context_urls,
Expand Down
5 changes: 3 additions & 2 deletions saas/views/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
from ..api.organizations import (EngagedSubscribersQuerysetMixin,
UnengagedSubscribersQuerysetMixin)
from ..api.serializers import OrganizationSerializer
from ..api.subscriptions import ActiveSubscribersMixin, ChurnedSubscribersMixin
from ..api.subscriptions import (ActiveSubscriberSubscriptionsMixin,
ChurnedSubscribersMixin)
from ..api.transactions import (BillingsQuerysetMixin,
SmartTransactionListMixin, TransactionQuerysetMixin, TransferQuerysetMixin)
from ..api.users import RegisteredQuerysetMixin
Expand Down Expand Up @@ -327,7 +328,7 @@ def queryrow_to_columns(self, record):
]


class ActiveSubscriptionDownloadView(ActiveSubscribersMixin,
class ActiveSubscriptionDownloadView(ActiveSubscriberSubscriptionsMixin,
SubscriptionBaseDownloadView):

subscriber_type = 'active'
Expand Down

0 comments on commit df27e8e

Please sign in to comment.