Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 9 additions & 9 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pythonpro/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .celery import app # noqa
from .celery import app as celery_app # noqa

__all__ = ('celery_app',)
2 changes: 1 addition & 1 deletion pythonpro/celery.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pythonpro.settings')
app = Celery('pythonpro.celery')
app.config_from_object('django.conf:settings')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
32 changes: 32 additions & 0 deletions pythonpro/memberkit/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ def activate_user(full_name: str, email: str, subscription_type_id: int, expires
return user_detail(email)


@_configure_api_key
def update_user_subscription(memberkit_user_id: int, subscription_type_id: int, status: str, expires_at: date = None, *,
api_key=_ApiKeyNone):
user_json = user_detail(memberkit_user_id, api_key=api_key)
if expires_at is None:
expires_at = date(2200, 1, 1)
data = {
'full_name': user_json['full_name'],
'email': user_json['email'],
'status': status,
'blocked': False,
'membership_level_id': subscription_type_id,
'unlimited': False,
'expires_at': expires_at.strftime('%d/%m/%Y'),
}
response = requests.post(f'{_base_url}/api/v1/users?api_key={api_key}', json=data)
return response.json()


@_configure_api_key
def inactivate_user(memberkit_user_id: int, subscription_type_id: int, *,
api_key=_ApiKeyNone):
Expand All @@ -89,6 +108,19 @@ def inactivate_user(memberkit_user_id: int, subscription_type_id: int, *,
return response.json()


@_configure_api_key
def delete_user(memberkit_user_id: int, *, api_key=_ApiKeyNone):
response = requests.delete(f'{_base_url}/api/v1/users/{memberkit_user_id}?api_key={api_key}')
response.raise_for_status()


@_configure_api_key
def list_users(page=0, *, api_key=_ApiKeyNone):
response = requests.get(f'{_base_url}/api/v1/users?api_key={api_key}&page={page}')
response.raise_for_status()
return response.json()


@_configure_api_key
def generate_token(memberkit_user_id: int, *, api_key=_ApiKeyNone):
user_json = user_detail(memberkit_user_id, api_key=api_key)
Expand Down
65 changes: 63 additions & 2 deletions pythonpro/memberkit/facade.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from builtins import Exception
from datetime import timedelta
from itertools import count
from logging import Logger
from typing import List

from celery import shared_task
from django.utils import timezone

from pythonpro.memberkit import api
from pythonpro.memberkit.models import SubscriptionType, Subscription, YEAR_IN_DAYS
from pythonpro.memberkit.models import SubscriptionType, Subscription, YEAR_IN_DAYS, UserSubscriptionsSummary

_logger = Logger(__file__)


def synchronize_subscription_types() -> List[SubscriptionType]:
Expand Down Expand Up @@ -57,7 +62,8 @@ def create_new_subscription(payment, observation: str = '') -> Subscription:

def activate(subscription, responsible=None, observation=''):
user = subscription.subscriber
subscription.activated_at = timezone.now()
if subscription.status == Subscription.Status.INACTIVE or subscription.activated_at is None:
subscription.activated_at = timezone.now()
for subscription_type in subscription.subscription_types.all():
expires_at = subscription.activated_at + timedelta(days=subscription_type.days_of_access)
if subscription_type.id in IDS_COMUNIDADE_SUBSCRIPTION:
Expand Down Expand Up @@ -103,6 +109,26 @@ def inactivate(subscription, responsible=None, observation=''):
return subscription


def clean_memberkit_users_up():
total = 0
for page in count(1):
memberkit_users = api.list_users(page)
if len(memberkit_users) == 0:
break
for memberkit_user in memberkit_users:
total += 1
memberkit_user_id = int(memberkit_user['id'])
has_active_subscription = Subscription.objects.filter(
memberkit_user_id=memberkit_user_id, status=Subscription.Status.ACTIVE
).exists()
if not has_active_subscription:
api.delete_user(memberkit_user_id)
print(f'Desativado: {memberkit_user_id} ############################################')
else:
print(f'Ativo: {memberkit_user_id}')
return total


class InactiveUserException(Exception):
pass

Expand Down Expand Up @@ -133,3 +159,38 @@ def migrate_when_status_active(user):
).exclude(activated_at__isnull=False)
for subscription in status_active_but_not_activated:
activate(subscription, observation='Migrado automaticamente da plataforma antiga para nova')


@shared_task
def process_expired_subscriptions(user_id):
now = timezone.now()
summary = UserSubscriptionsSummary(user_id)
active_subscriptions = list(summary.active_subscriptions())
for subscription in active_subscriptions:
if subscription.expires_at < now:
subscription.status = Subscription.Status.INACTIVE
subscription.save()
inactive_subscriptions = [s for s in active_subscriptions if s.status == Subscription.Status.INACTIVE]
active_subscriptions = [s for s in active_subscriptions if s.status == Subscription.Status.ACTIVE]
if len(active_subscriptions) == 0:
for memberkit_user_id in summary.memberkit_user_ids():
_logger.info(f'Deleted memberkit account for user_id: {user_id}')
api.delete_user(memberkit_user_id)
else:
for inactive_subscription in inactive_subscriptions:
_logger.info(f'Inactivated {inactive_subscription.name} for user_id: {user_id}')
inactivate(inactive_subscription, observation='Inativada por processo de inativação')
for active_subscription in active_subscriptions:
for subscription_type in active_subscription.subscription_types.all().only('id'):
_logger.info(f'Activated {active_subscription.name} for user_id: {user_id}')
api.update_user_subscription(
active_subscription.memberkit_user_id,
subscription_type,
'activate'
)


def inactivate_expired_subscriptions():
for user_id in UserSubscriptionsSummary.users_with_active_subscriptions().values_list('id', flat=True):
_logger.info(f'Adding task to process subscriptions expiration for user_id: {user_id}')
process_expired_subscriptions.delay(user_id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand

from pythonpro.memberkit import facade


class Command(BaseCommand):
help = 'Busca todas assinaturas ativas e inativa as que estão expiradas'

def add_arguments(self, parser):
pass

def handle(self, *args, **options):
facade.inactivate_expired_subscriptions()
36 changes: 36 additions & 0 deletions pythonpro/memberkit/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,39 @@ def discourse_groups(self):

def __str__(self):
return f'Assinatura: {self.id} de {self.subscriber}'


class UserSubscriptionsSummary:
"""
This class provides summary data about user subscriptions.
This is not a model, but a utility class to handle user subscriptions
"""

def __init__(self, django_user_or_user_id):
if isinstance(django_user_or_user_id, get_user_model()):
self.user = django_user_or_user_id
self.user_id = django_user_or_user_id.id
else:
self.user = None
self.user_id = django_user_or_user_id

def has_active_subscriptions(self):
return self.active_subscriptions().exists()

def active_subscriptions(self):
return Subscription.objects.filter(
subscriber_id=self.user_id, status=Subscription.Status.ACTIVE
)

@classmethod
def users_with_active_subscriptions(cls):
"""
Returns query set with user with at least one active subscription
:return: Django Use Query Set
"""
return get_user_model().objects.filter(subscriptions__status=Subscription.Status.ACTIVE).distinct()

def memberkit_user_ids(self) -> set[int]:
return set(Subscription.objects.filter(
subscriber_id=self.user_id
).values_list('memberkit_user_id', flat=True))
8 changes: 8 additions & 0 deletions pythonpro/memberkit/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,11 @@ def test_subscription_creation_another_subscription_type(role, subscription_type
assert Subscription.objects.count() == 2, 'New Subscription should be created'
management.call_command('create_subscriptions_for_roles')
assert Subscription.objects.count() == 2, 'New Subscription should be created only once'


def test_inactivate_expired_subscriptions(django_user_model, mocker):
process_expired_subscriptions_mock = mocker.patch('pythonpro.memberkit.facade.process_expired_subscriptions.delay')
active_user = baker.make(django_user_model)
baker.make(Subscription, status=Subscription.Status.ACTIVE, subscriber=active_user)
management.call_command('inactivate_expired_subscriptions')
process_expired_subscriptions_mock.assert_called_once_with(active_user.id)
38 changes: 38 additions & 0 deletions pythonpro/memberkit/tests/test_process_expired_subscriptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest
from django.utils import timezone
from model_bakery import baker

from pythonpro.memberkit import facade
from pythonpro.memberkit.models import Subscription


@pytest.mark.freeze_time('2023-03-22')
def test_process_valid_subscriptions(django_user_model):
active_user = baker.make(django_user_model)
now = timezone.now()
subscription = baker.make(
Subscription,
status=Subscription.Status.ACTIVE,
subscriber=active_user,
activated_at=now,
days_of_access=1
)
facade.process_expired_subscriptions(active_user.id)
subscription.refresh_from_db()
assert subscription.status == Subscription.Status.ACTIVE

# @pytest.mark.freeze_time('2023-03-22')
# def test_process_expired_subscriptions(django_user_model):
# active_user = baker.make(django_user_model)
# now = timezone.now()
# tow_days_ago = now - timedelta(days=2)
# subscription = baker.make(
# Subscription,
# status=Subscription.Status.ACTIVE,
# subscriber=active_user,
# activated_at=tow_days_ago,
# days_of_access=1
# )
# facade.process_expired_subscriptions(active_user.id)
# subscription.refresh_from_db()
# assert subscription.status == Subscription.Status.INACTIVE
23 changes: 23 additions & 0 deletions pythonpro/memberkit/tests/test_subscription_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from model_bakery import baker

from pythonpro.memberkit.models import UserSubscriptionsSummary, Subscription


def test_user_summary_with_no_subscriptions(logged_user):
summary = UserSubscriptionsSummary(logged_user)
assert not summary.has_active_subscriptions()


def test_user_with_active_subscriptions(django_user_model):
active_users = baker.make(django_user_model, 5)
for active_user in active_users:
baker.make(Subscription, status=Subscription.Status.ACTIVE, subscriber=active_user)

inactive_users = baker.make(django_user_model, 5)
for inactive_user in inactive_users:
baker.make(Subscription, status=Subscription.Status.INACTIVE, subscriber=inactive_user)

active_user_ids_from_db = set(
UserSubscriptionsSummary.users_with_active_subscriptions().values_list('id', flat=True)
)
assert active_user_ids_from_db == set(active_user.id for active_user in active_users)
3 changes: 1 addition & 2 deletions pythonpro/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,8 @@
# Celery config


BROKER_URL = config('CLOUDAMQP_URL')
CELERY_BROKER_URL = config('CLOUDAMQP_URL')

CELERY_RESULT_BACKEND = f'{REDIS_URL}/0'
CELERY_TASK_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE

Expand Down