Compare commits
6 commits
FEATURE/ad
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fa3bca29df | |||
| 0973d08637 | |||
| f6099af0e4 | |||
| 2464eb0de8 | |||
| 586068fe3b | |||
| 86b02ffa73 |
17 changed files with 453 additions and 320 deletions
|
|
@ -1,5 +1,3 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
|
@ -18,8 +16,6 @@ from apps.core.serializers import ConfigSerializer
|
||||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# @method_decorator(login_required, name='dispatch')
|
# @method_decorator(login_required, name='dispatch')
|
||||||
class HomeView(TemplateView):
|
class HomeView(TemplateView):
|
||||||
template_name = 'core/home.html'
|
template_name = 'core/home.html'
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,19 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
|
||||||
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements, OAuth2Authentication
|
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements, OAuth2Authentication
|
||||||
from rest_framework.permissions import (
|
from rest_framework.permissions import (
|
||||||
IsAuthenticated, BasePermission
|
IsAuthenticated
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger("oauth2_provider")
|
log = logging.getLogger("oauth2_provider")
|
||||||
|
|
||||||
|
|
||||||
class IsAuthenticatedOrTokenMatchesOASRequirements(TokenMatchesOASRequirements):
|
class IsAuthenticatedOrTokenMatchesOASRequirements(TokenMatchesOASRequirements):
|
||||||
def has_permission(self, request, view):
|
def has_permission(self, request, view):
|
||||||
logger.debug(f'try to authenticate {request} for {view} in IsAuthenticatedOrTokenMatchesOASRequirements')
|
|
||||||
is_authenticated = IsAuthenticated().has_permission(request, view)
|
is_authenticated = IsAuthenticated().has_permission(request, view)
|
||||||
logger.debug(f'is_authenticated: {is_authenticated}')
|
|
||||||
oauth2authenticated = False
|
oauth2authenticated = False
|
||||||
if is_authenticated:
|
if is_authenticated:
|
||||||
oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication)
|
oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication)
|
||||||
|
|
||||||
logger.debug(f'oauth2authenticated: {oauth2authenticated}')
|
|
||||||
|
|
||||||
token_has_scope = TokenMatchesOASRequirements()
|
token_has_scope = TokenMatchesOASRequirements()
|
||||||
logger.debug(f'token_has_scope: {token_has_scope}')
|
return (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view)
|
||||||
|
|
||||||
result = (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view)
|
|
||||||
logger.debug(f'authentication result: {result}')
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class TokenMatchesViewSetActions(BasePermission):
|
|
||||||
"""
|
|
||||||
:attr:action_required_scopes: dict keyed by view set action name with value: iterable action scope lists
|
|
||||||
|
|
||||||
This fulfills the [Open API Specification (OAS; formerly Swagger)](https://www.openapis.org/)
|
|
||||||
list of alternative Security Requirements Objects for oauth2 or openIdConnect:
|
|
||||||
When a list of Security Requirement Objects is defined on the Open API object or Operation Object,
|
|
||||||
only one of Security Requirement Objects in the list needs to be satisfied to authorize the request.
|
|
||||||
[1](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject)
|
|
||||||
|
|
||||||
For each method, a list of lists of allowed scopes is tried in order and the first to match succeeds.
|
|
||||||
|
|
||||||
@example
|
|
||||||
required_action_scopes = {
|
|
||||||
'list': [['read']],
|
|
||||||
'create': [['create1','scope2'], ['alt-scope3'], ['alt-scope4','alt-scope5']],
|
|
||||||
}
|
|
||||||
|
|
||||||
TODO: DRY: subclass TokenHasScope and iterate over values of required_scope?
|
|
||||||
"""
|
|
||||||
|
|
||||||
def has_permission(self, request, view):
|
|
||||||
token = request.auth
|
|
||||||
|
|
||||||
if not token:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if hasattr(token, "scope"): # OAuth 2
|
|
||||||
required_action_scopes = self.get_required_action_scopes(request, view)
|
|
||||||
|
|
||||||
# TODO: use action map instead to analyze method
|
|
||||||
action_map = view.action_map
|
|
||||||
a = view.action
|
|
||||||
|
|
||||||
if a in required_action_scopes:
|
|
||||||
logger.debug(
|
|
||||||
"Required scopes alternatives to access resource: {0}".format(
|
|
||||||
required_action_scopes[a]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for alt in required_action_scopes[a]:
|
|
||||||
if token.is_valid(alt):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logger.warning("no scope action defined for action {0}".format(a))
|
|
||||||
return False
|
|
||||||
|
|
||||||
assert False, (
|
|
||||||
"TokenMatchesViewSetActions requires the"
|
|
||||||
"`oauth2_provider.rest_framework.OAuth2Authentication` authentication "
|
|
||||||
"class to be used."
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_required_action_scopes(self, request, view):
|
|
||||||
try:
|
|
||||||
return getattr(view, "required_action_scopes")
|
|
||||||
except AttributeError:
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
"TokenMatchesViewSetActions requires the view to"
|
|
||||||
" define the required_action_scopes attribute"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,19 @@ from unittest.mock import patch
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from oauth2_provider.models import get_access_token_model, get_application_model
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APIClient, APITestCase
|
from rest_framework.test import APIClient, APITestCase
|
||||||
|
|
||||||
|
|
||||||
|
from apps.core.models import Config
|
||||||
from apps.gooyal_oauth2.models import Scope
|
from apps.gooyal_oauth2.models import Scope
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
|
|
||||||
|
AccessToken = get_access_token_model()
|
||||||
|
Application = get_application_model()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mock_notifications_push_user_success(user_uuid, title, message, priority=5, extras=None):
|
def mock_notifications_push_user_success(user_uuid, title, message, priority=5, extras=None):
|
||||||
import uuid as sys_uuid
|
import uuid as sys_uuid
|
||||||
|
|
@ -24,12 +31,6 @@ def mock_notifications_push_user_success(user_uuid, title, message, priority=5,
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
from oauth2_provider.models import get_access_token_model, get_application_model
|
|
||||||
|
|
||||||
AccessToken = get_access_token_model()
|
|
||||||
Application = get_application_model()
|
|
||||||
|
|
||||||
|
|
||||||
class GooyalOAuth2Tests(APITestCase):
|
class GooyalOAuth2Tests(APITestCase):
|
||||||
user_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
|
user_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||||
access_token_1 = 'au4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
access_token_1 = 'au4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
||||||
|
|
@ -45,7 +46,6 @@ class GooyalOAuth2Tests(APITestCase):
|
||||||
client = APIClient()
|
client = APIClient()
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
settings.SMS_SEND = False
|
settings.SMS_SEND = False
|
||||||
|
|
||||||
|
|
@ -203,3 +203,10 @@ class GooyalOAuth2Tests(APITestCase):
|
||||||
result = self.client.post(reverse("gooyal_oauth2:revoke-token"), data=revoke_data)
|
result = self.client.post(reverse("gooyal_oauth2:revoke-token"), data=revoke_data)
|
||||||
self.assertEqual(result.status_code, 200)
|
self.assertEqual(result.status_code, 200)
|
||||||
self.assertEqual(AccessToken.objects.count(), 0)
|
self.assertEqual(AccessToken.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import User
|
from .models import User, InviteRequest
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(admin.ModelAdmin):
|
class UserAdmin(admin.ModelAdmin):
|
||||||
|
|
@ -27,4 +27,22 @@ class UserAdmin(admin.ModelAdmin):
|
||||||
search_fields = ['pk', 'first_name', 'last_name', 'phone_number']
|
search_fields = ['pk', 'first_name', 'last_name', 'phone_number']
|
||||||
|
|
||||||
|
|
||||||
|
class InviteRequestAdmin(admin.ModelAdmin):
|
||||||
|
fields = [
|
||||||
|
'uuid',
|
||||||
|
'inviter',
|
||||||
|
'mobile_number',
|
||||||
|
'application',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
]
|
||||||
|
readonly_fields = ['uuid', 'created_at', 'updated_at']
|
||||||
|
list_display = ['uuid', 'mobile_number', 'inviter', 'application', 'created_at']
|
||||||
|
list_filter = ['created_at', 'application']
|
||||||
|
search_fields = ['uuid', 'mobile_number', 'inviter__uuid', 'inviter__phone_number']
|
||||||
|
raw_id_fields = ['inviter', 'application']
|
||||||
|
date_hierarchy = 'created_at'
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(User, UserAdmin)
|
admin.site.register(User, UserAdmin)
|
||||||
|
admin.site.register(InviteRequest, InviteRequestAdmin)
|
||||||
|
|
|
||||||
33
apps/users/migrations/0009_inviterequest.py
Normal file
33
apps/users/migrations/0009_inviterequest.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 5.1.4 on 2026-07-22 13:45
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0008_alter_user_options'),
|
||||||
|
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InviteRequest',
|
||||||
|
fields=[
|
||||||
|
('uuid', models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
|
||||||
|
('mobile_number', models.CharField(db_index=True, max_length=30, verbose_name='mobile number')),
|
||||||
|
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='invite_requests', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL)),
|
||||||
|
('inviter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invite_requests', to=settings.AUTH_USER_MODEL, verbose_name='inviter')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'invite request',
|
||||||
|
'verbose_name_plural': 'invite requests',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.1.4 on 2026-07-25
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0009_inviterequest'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inviterequest',
|
||||||
|
name='mobile_number',
|
||||||
|
field=models.CharField(db_index=True, max_length=30, unique=True, verbose_name='mobile number'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -21,6 +21,8 @@ from .provinces_and_cities import state
|
||||||
|
|
||||||
from apps.users.constans import MAX_OTP_TRY, DEVELOPMENT_PHONE_NUMBERS, GenderChoices, EducationDegreeChoices, \
|
from apps.users.constans import MAX_OTP_TRY, DEVELOPMENT_PHONE_NUMBERS, GenderChoices, EducationDegreeChoices, \
|
||||||
MAX_OTP_VALID_DURATION
|
MAX_OTP_VALID_DURATION
|
||||||
|
from oauth2_provider import settings as oauth2_settings
|
||||||
|
from utils.models import BaseModel
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -267,3 +269,28 @@ class User(AbstractUser):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name or self.get_full_name() or self.username or self.phone_number or self.email or _('no name')
|
return self.name or self.get_full_name() or self.username or self.phone_number or self.email or _('no name')
|
||||||
|
|
||||||
|
|
||||||
|
class InviteRequest(BaseModel):
|
||||||
|
inviter = models.ForeignKey(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='invite_requests',
|
||||||
|
verbose_name=_('inviter'),
|
||||||
|
)
|
||||||
|
mobile_number = models.CharField(_('mobile number'), unique=True, max_length=30, db_index=True)
|
||||||
|
application = models.ForeignKey(
|
||||||
|
oauth2_settings.APPLICATION_MODEL,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='invite_requests',
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('invite request')
|
||||||
|
verbose_name_plural = _('invite requests')
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.mobile_number} -> {self.inviter_id}'
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@ from django.core.validators import RegexValidator
|
||||||
from oauth2_provider.models import get_access_token_model
|
from oauth2_provider.models import get_access_token_model
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from apps.users.models import User
|
from apps.users.models import User, InviteRequest
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from unidecode import unidecode
|
from unidecode import unidecode
|
||||||
|
|
||||||
from utils.exceptions import UnprocessableEntity
|
from utils.exceptions import UnprocessableEntity, Conflict
|
||||||
|
|
||||||
phone_number_validator = RegexValidator(regex=r'^\+98[1-9]\d{9}$', message=_(
|
phone_number_validator = RegexValidator(regex=r'^\+98[1-9]\d{9}$', message=_(
|
||||||
"Phone number must be entered in the format: '+989999999999'."
|
"Phone number must be entered in the format: '+989999999999'."
|
||||||
|
|
@ -134,6 +134,47 @@ class ChangePasswordSerializer(serializers.Serializer):
|
||||||
new_password = serializers.CharField(required=True)
|
new_password = serializers.CharField(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class InviteRequestSerializer(serializers.ModelSerializer):
|
||||||
|
invite_code = serializers.UUIDField(write_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InviteRequest
|
||||||
|
fields = ('uuid', 'invite_code', 'mobile_number', 'created_at')
|
||||||
|
read_only_fields = ('uuid', 'created_at')
|
||||||
|
|
||||||
|
def validate_invite_code(self, value):
|
||||||
|
if not User.objects.filter(uuid=value).exists():
|
||||||
|
raise serializers.ValidationError(_('Invalid invite code.'))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def validate_mobile_number(self, value):
|
||||||
|
value = unidecode(value)
|
||||||
|
phone_number_validator(value)
|
||||||
|
|
||||||
|
if User.objects.filter(phone_number=value).exists():
|
||||||
|
raise Conflict(
|
||||||
|
detail=_('This mobile number is already registered as a user.'),
|
||||||
|
code='user_already_exists',
|
||||||
|
)
|
||||||
|
|
||||||
|
if InviteRequest.objects.filter(mobile_number=value).exists():
|
||||||
|
raise Conflict(
|
||||||
|
detail=_('An invite request for this mobile number already exists.'),
|
||||||
|
code='invite_request_already_exists',
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
invite_code = validated_data.pop('invite_code')
|
||||||
|
application = self.context['request'].auth.application
|
||||||
|
return InviteRequest.objects.create(
|
||||||
|
inviter_id=invite_code,
|
||||||
|
application=application,
|
||||||
|
**validated_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
AccessToken = get_access_token_model()
|
AccessToken = get_access_token_model()
|
||||||
class SessionSerializer(serializers.ModelSerializer):
|
class SessionSerializer(serializers.ModelSerializer):
|
||||||
is_current = serializers.SerializerMethodField()
|
is_current = serializers.SerializerMethodField()
|
||||||
|
|
@ -154,9 +195,3 @@ class SessionSerializer(serializers.ModelSerializer):
|
||||||
model = AccessToken
|
model = AccessToken
|
||||||
fields = ('uuid', 'created', "detail", "is_current")
|
fields = ('uuid', 'created', "detail", "is_current")
|
||||||
read_only_fields = ['uuid', 'created', "detail", "is_current" ]
|
read_only_fields = ['uuid', 'created', "detail", "is_current" ]
|
||||||
|
|
||||||
|
|
||||||
# TODO: better solution
|
|
||||||
# this is a proxy api for submit event for promotion
|
|
||||||
class ReferralSetSerializer(serializers.Serializer):
|
|
||||||
referral = serializers.UUIDField()
|
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,14 @@ from rest_framework.test import APIClient, APITestCase
|
||||||
|
|
||||||
from apps.core.models import Config
|
from apps.core.models import Config
|
||||||
from apps.gooyal_oauth2.models import Scope
|
from apps.gooyal_oauth2.models import Scope
|
||||||
from apps.users.models import User
|
from apps.users.models import User, InviteRequest
|
||||||
|
|
||||||
AccessToken = get_access_token_model()
|
AccessToken = get_access_token_model()
|
||||||
Application = get_application_model()
|
Application = get_application_model()
|
||||||
|
|
||||||
|
|
||||||
class UserTests(APITestCase):
|
class UserTests(APITestCase):
|
||||||
user_1_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
|
user_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||||
user_2_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005e')
|
|
||||||
access_token_1 = 'au4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
access_token_1 = 'au4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
||||||
access_token_2 = 'vu4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
access_token_2 = 'vu4naVsdKCbKNOhnElPyXcrwSnqqFbm'
|
||||||
application_uuid = uuid.UUID('a14e8b86-8f4a-44d9-b29d-badceb47005f')
|
application_uuid = uuid.UUID('a14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||||
|
|
@ -38,14 +37,11 @@ class UserTests(APITestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
settings.SMS_SEND = False
|
settings.SMS_SEND = False
|
||||||
self.user_1_phone_number = '+989100000000'
|
self.user_phone_number = '+989100000000'
|
||||||
self.user_2_phone_number = '+989100000001'
|
self.user = User.objects.create(pk=self.user_uuid, phone_number=self.user_phone_number)
|
||||||
self.user_1 = User.objects.create(pk=self.user_1_uuid, phone_number=self.user_1_phone_number)
|
|
||||||
self.user_2 = User.objects.create(pk=self.user_2_uuid, phone_number=self.user_2_phone_number)
|
|
||||||
|
|
||||||
Scope.objects.create(name='accounts.status:get', description='accounts.status:get')
|
Scope.objects.create(name='accounts.status:get', description='accounts.status:get')
|
||||||
Scope.objects.create(name='accounts.account:retrieve', description='accounts.account:retrieve')
|
Scope.objects.create(name='accounts.account:retrieve', description='accounts.account:retrieve')
|
||||||
Scope.objects.create(name='accounts.users.user:submit_referral', description='accounts.users.user:submit_referral')
|
|
||||||
|
|
||||||
self.application = Application.objects.create(
|
self.application = Application.objects.create(
|
||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
|
|
@ -53,9 +49,9 @@ class UserTests(APITestCase):
|
||||||
authorization_grant_type='password',
|
authorization_grant_type='password',
|
||||||
hash_client_secret=False,
|
hash_client_secret=False,
|
||||||
uuid=self.application_uuid,
|
uuid=self.application_uuid,
|
||||||
user_id=self.user_1_uuid,
|
user_id=self.user_uuid,
|
||||||
max_allowed_session=1,
|
max_allowed_session=1,
|
||||||
allowed_scope='accounts.status:get accounts.account:retrieve accounts.users.user:submit_referral',
|
allowed_scope='accounts.status:get accounts.account:retrieve',
|
||||||
)
|
)
|
||||||
|
|
||||||
# self.sys_date_patcher = patch('simata_safte.models.get_sys_date', mock_get_sys_date)
|
# self.sys_date_patcher = patch('simata_safte.models.get_sys_date', mock_get_sys_date)
|
||||||
|
|
@ -74,7 +70,7 @@ class UserTests(APITestCase):
|
||||||
access_token_1 = AccessToken.objects.create(
|
access_token_1 = AccessToken.objects.create(
|
||||||
**{
|
**{
|
||||||
"token": self.access_token_1,
|
"token": self.access_token_1,
|
||||||
"user": self.user_1,
|
"user": self.user,
|
||||||
# "client_id": self.client_id,
|
# "client_id": self.client_id,
|
||||||
# "client_owner": owner,
|
# "client_owner": owner,
|
||||||
"application_id": self.application_uuid,
|
"application_id": self.application_uuid,
|
||||||
|
|
@ -92,7 +88,7 @@ class UserTests(APITestCase):
|
||||||
access_token_2 = AccessToken.objects.create(
|
access_token_2 = AccessToken.objects.create(
|
||||||
**{
|
**{
|
||||||
"token": self.access_token_2,
|
"token": self.access_token_2,
|
||||||
"user": self.user_1,
|
"user": self.user,
|
||||||
|
|
||||||
# "client_id": self.client_id,
|
# "client_id": self.client_id,
|
||||||
# "client_owner": owner,
|
# "client_owner": owner,
|
||||||
|
|
@ -126,13 +122,13 @@ class UserTests(APITestCase):
|
||||||
return basic_credentials
|
return basic_credentials
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
self.user_1.set_otp()
|
self.user.set_otp()
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"grant_type": "password",
|
"grant_type": "password",
|
||||||
"username": self.user_1_phone_number,
|
"username": self.user_phone_number,
|
||||||
"password": '77501',
|
"password": '77501',
|
||||||
"scope": 'accounts.status:get accounts.account:retrieve accounts.users.user:submit_referral',
|
"scope": 'accounts.status:get accounts.account:retrieve',
|
||||||
"auth_fields": 'phone_number:otp'
|
"auth_fields": 'phone_number:otp'
|
||||||
}
|
}
|
||||||
self.client.credentials(
|
self.client.credentials(
|
||||||
|
|
@ -140,7 +136,6 @@ class UserTests(APITestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
||||||
print(result.json())
|
|
||||||
access_token = result.json()['access_token']
|
access_token = result.json()['access_token']
|
||||||
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + access_token)
|
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + access_token)
|
||||||
|
|
||||||
|
|
@ -153,18 +148,124 @@ class UserTests(APITestCase):
|
||||||
response = self.client.get(reverse("users:user_sessions_api"))
|
response = self.client.get(reverse("users:user_sessions_api"))
|
||||||
self.assertEqual(response.json()['results'][0]['is_current'] , True)
|
self.assertEqual(response.json()['results'][0]['is_current'] , True)
|
||||||
|
|
||||||
def test_referralPromotion_allOK_success(self):
|
|
||||||
self.login()
|
|
||||||
|
|
||||||
submit_referral_url = reverse("users:user-submit-referral")
|
|
||||||
submit_referral_data = {
|
|
||||||
"referral": self.user_2_uuid
|
|
||||||
}
|
|
||||||
response = self.client.post(submit_referral_url, submit_referral_data)
|
|
||||||
print(response.json())
|
|
||||||
# self.assertEqual(response.json()['results'][0]['is_current'] , True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class InviteRequestTests(APITestCase):
|
||||||
|
user_uuid = uuid.UUID('c14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||||
|
inviter_uuid = uuid.UUID('d14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||||
|
application_uuid = uuid.UUID('e14e8b86-8f4a-44d9-b29d-badceb47005f')
|
||||||
|
access_token = 'inviteAccessToken123456789012345678901'
|
||||||
|
client_id = 'inviteClientId1234567890123456789012'
|
||||||
|
client_secret = 'inviteClientSecret123456789012345678'
|
||||||
|
mobile_number = '+989120000000'
|
||||||
|
expire_datetime = timezone.now() + timedelta(seconds=3600)
|
||||||
|
|
||||||
|
client = APIClient()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(pk=self.user_uuid, phone_number='+989100000001')
|
||||||
|
self.inviter = User.objects.create(pk=self.inviter_uuid, phone_number='+989100000002')
|
||||||
|
|
||||||
|
Scope.objects.create(name='accounts.invite:submit', description='accounts.invite:submit')
|
||||||
|
|
||||||
|
self.application = Application.objects.create(
|
||||||
|
name='invite-test-app',
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
client_type=Application.CLIENT_CONFIDENTIAL,
|
||||||
|
authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
|
||||||
|
hash_client_secret=False,
|
||||||
|
uuid=self.application_uuid,
|
||||||
|
user_id=self.user_uuid,
|
||||||
|
allowed_scope='accounts.invite:submit',
|
||||||
|
)
|
||||||
|
|
||||||
|
AccessToken.objects.create(
|
||||||
|
token=self.access_token,
|
||||||
|
application_id=self.application_uuid,
|
||||||
|
scope='accounts.invite:submit',
|
||||||
|
expires=self.expire_datetime,
|
||||||
|
)
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION=f'Bearer {self.access_token}')
|
||||||
|
|
||||||
|
def test_create_invite_request_success(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('users:invite_request_api'),
|
||||||
|
data={
|
||||||
|
'invite_code': str(self.inviter_uuid),
|
||||||
|
'mobile_number': self.mobile_number,
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(InviteRequest.objects.count(), 1)
|
||||||
|
|
||||||
|
invite_request = InviteRequest.objects.get()
|
||||||
|
self.assertEqual(invite_request.inviter_id, self.inviter_uuid)
|
||||||
|
self.assertEqual(invite_request.mobile_number, self.mobile_number)
|
||||||
|
self.assertEqual(invite_request.application_id, self.application_uuid)
|
||||||
|
|
||||||
|
def test_create_invite_request_invalid_invite_code(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('users:invite_request_api'),
|
||||||
|
data={
|
||||||
|
'invite_code': str(uuid.uuid4()),
|
||||||
|
'mobile_number': self.mobile_number,
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(InviteRequest.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_create_invite_request_existing_user(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('users:invite_request_api'),
|
||||||
|
data={
|
||||||
|
'invite_code': str(self.inviter_uuid),
|
||||||
|
'mobile_number': self.inviter.phone_number,
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 409)
|
||||||
|
self.assertEqual(response.json()['details']['error'], 'user_already_exists')
|
||||||
|
self.assertEqual(InviteRequest.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_create_invite_request_duplicate_mobile_number(self):
|
||||||
|
InviteRequest.objects.create(
|
||||||
|
inviter=self.inviter,
|
||||||
|
mobile_number=self.mobile_number,
|
||||||
|
application=self.application,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('users:invite_request_api'),
|
||||||
|
data={
|
||||||
|
'invite_code': str(self.inviter_uuid),
|
||||||
|
'mobile_number': self.mobile_number,
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 409)
|
||||||
|
self.assertEqual(response.json()['details']['error'], 'invite_request_already_exists')
|
||||||
|
self.assertEqual(InviteRequest.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_create_invite_request_missing_scope(self):
|
||||||
|
self.application.allowed_scope = ''
|
||||||
|
self.application.save(update_fields=['allowed_scope'])
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('users:invite_request_api'),
|
||||||
|
data={
|
||||||
|
'invite_code': str(self.inviter_uuid),
|
||||||
|
'mobile_number': self.mobile_number,
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(InviteRequest.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,10 @@
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.contrib.auth.views import LogoutView
|
from django.contrib.auth.views import LogoutView
|
||||||
from rest_framework.routers import DefaultRouter
|
|
||||||
|
|
||||||
from .views import UserListView, UserPublicRetrieveView, AccountView, RequestOTPView, ChangePasswordView, \
|
from .views import UserListView, UserPublicRetrieveView, AccountView, RequestOTPView, ChangePasswordView, \
|
||||||
OTPLoginView, ProfileDetailView, ProfileUpdateView, RequestOTTView, UserCurrentAvatarUrlView, UserInquiryView, \
|
OTPLoginView, ProfileDetailView, ProfileUpdateView, RequestOTTView, UserCurrentAvatarUrlView, UserInquiryView, \
|
||||||
UserDetailedRetrieveView, UserSessionListView, UserProfileView, UserViewSet
|
UserDetailedRetrieveView, UserSessionListView, UserProfileView, InviteRequestView
|
||||||
|
|
||||||
app_name = "users"
|
app_name = "users"
|
||||||
|
|
||||||
router = DefaultRouter()
|
|
||||||
router.register("user", UserViewSet, basename="user")
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('login/', OTPLoginView.as_view(), name='login'),
|
path('login/', OTPLoginView.as_view(), name='login'),
|
||||||
path('logout/', LogoutView.as_view(), name='logout'),
|
path('logout/', LogoutView.as_view(), name='logout'),
|
||||||
|
|
@ -27,5 +21,6 @@ urlpatterns = [
|
||||||
path('api/request_ott/', RequestOTTView.as_view(), name='request_ott_api'),
|
path('api/request_ott/', RequestOTTView.as_view(), name='request_ott_api'),
|
||||||
path('api/change_password/', ChangePasswordView.as_view(), name='change_password_api'),
|
path('api/change_password/', ChangePasswordView.as_view(), name='change_password_api'),
|
||||||
path('api/inquiry/', UserInquiryView.as_view(), name='user_inquiry_api'),
|
path('api/inquiry/', UserInquiryView.as_view(), name='user_inquiry_api'),
|
||||||
] + router.urls
|
path('api/invite_requests/', InviteRequestView.as_view(), name='invite_request_api'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib.auth.views import LoginView
|
||||||
|
|
@ -10,26 +9,18 @@ from django.utils.decorators import method_decorator
|
||||||
from django.views.generic import DetailView, UpdateView
|
from django.views.generic import DetailView, UpdateView
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
|
from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
|
||||||
from rest_framework import generics, status, permissions, mixins
|
from rest_framework import generics, status, permissions
|
||||||
from rest_framework.decorators import action
|
|
||||||
from rest_framework.generics import get_object_or_404
|
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.viewsets import GenericViewSet
|
|
||||||
|
|
||||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements, TokenMatchesViewSetActions
|
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||||
from apps.gooyal_oauth2.throttling import get_application
|
|
||||||
from apps.users.filters import UserFilter
|
from apps.users.filters import UserFilter
|
||||||
from apps.users.forms import OTPAuthenticationForm, ProfileUpdateForm
|
from apps.users.forms import OTPAuthenticationForm, ProfileUpdateForm
|
||||||
from apps.users.models import User
|
from apps.users.models import User, InviteRequest
|
||||||
from apps.users.provinces_and_cities import State
|
|
||||||
from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, RequestOTTSerializer, \
|
from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, RequestOTTSerializer, \
|
||||||
ChangePasswordSerializer, UserInquirySerializer, SessionSerializer, ReferralSetSerializer
|
ChangePasswordSerializer, UserInquirySerializer, SessionSerializer, InviteRequestSerializer
|
||||||
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle, NumberedRequestOTPDayRateThrottle, NumberedRequestOTPMinRateThrottle
|
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle, NumberedRequestOTPDayRateThrottle, NumberedRequestOTPMinRateThrottle
|
||||||
from utils.clients.promotions_client import promote_user
|
|
||||||
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle
|
|
||||||
from utils import exceptions
|
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
|
@ -65,7 +56,6 @@ class UserPublicRetrieveView(generics.RetrieveAPIView):
|
||||||
"GET": [["accounts.profile:retrieve"]],
|
"GET": [["accounts.profile:retrieve"]],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserDetailedRetrieveView(generics.RetrieveAPIView):
|
class UserDetailedRetrieveView(generics.RetrieveAPIView):
|
||||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
|
|
@ -75,7 +65,6 @@ class UserDetailedRetrieveView(generics.RetrieveAPIView):
|
||||||
"GET": [["accounts.profile:detailed_retrieve"]],
|
"GET": [["accounts.profile:detailed_retrieve"]],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserCurrentAvatarUrlView(generics.RetrieveAPIView):
|
class UserCurrentAvatarUrlView(generics.RetrieveAPIView):
|
||||||
permission_classes = [AllowAny]
|
permission_classes = [AllowAny]
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
|
|
@ -115,7 +104,6 @@ class RequestOTPView(generics.CreateAPIView):
|
||||||
NumberedRequestOTPDayRateThrottle,
|
NumberedRequestOTPDayRateThrottle,
|
||||||
NumberedRequestOTPMinRateThrottle]
|
NumberedRequestOTPMinRateThrottle]
|
||||||
|
|
||||||
|
|
||||||
class RequestOTTView(generics.CreateAPIView):
|
class RequestOTTView(generics.CreateAPIView):
|
||||||
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
||||||
required_scopes = ['accounts.account:request_ott']
|
required_scopes = ['accounts.account:request_ott']
|
||||||
|
|
@ -139,6 +127,15 @@ class UserInquiryView(generics.CreateAPIView):
|
||||||
required_scopes = ['accounts.profile:inquiry']
|
required_scopes = ['accounts.profile:inquiry']
|
||||||
|
|
||||||
|
|
||||||
|
class InviteRequestView(generics.CreateAPIView):
|
||||||
|
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||||
|
required_scopes = ['accounts.invite:submit']
|
||||||
|
required_alternate_scopes ={
|
||||||
|
"POST": [["accounts.invite:submit"]],
|
||||||
|
}
|
||||||
|
serializer_class = InviteRequestSerializer
|
||||||
|
queryset = InviteRequest.objects.all()
|
||||||
|
|
||||||
class ChangePasswordView(generics.UpdateAPIView):
|
class ChangePasswordView(generics.UpdateAPIView):
|
||||||
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
||||||
serializer_class = ChangePasswordSerializer
|
serializer_class = ChangePasswordSerializer
|
||||||
|
|
@ -221,58 +218,3 @@ class UserSessionListView(generics.ListAPIView):
|
||||||
from apps.gooyal_oauth2.models import AccessToken
|
from apps.gooyal_oauth2.models import AccessToken
|
||||||
return AccessToken.objects.filter(user=self.request.user).all()
|
return AccessToken.objects.filter(user=self.request.user).all()
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(
|
|
||||||
# mixins.CreateModelMixin,
|
|
||||||
GenericViewSet
|
|
||||||
):
|
|
||||||
serializer_class = AccountSerializer
|
|
||||||
|
|
||||||
permission_classes = [TokenMatchesViewSetActions]
|
|
||||||
required_action_scopes = {
|
|
||||||
"submit_referral": [["accounts.users.user:submit_referral"]],
|
|
||||||
}
|
|
||||||
|
|
||||||
# TODO: better solution
|
|
||||||
# this is a proxy api for submit event in promotion service
|
|
||||||
@action(detail=False, methods=['POST'], serializer_class=ReferralSetSerializer)
|
|
||||||
def submit_referral(self, request, *args, **kwargs):
|
|
||||||
user = self.request.user
|
|
||||||
application = get_application(self.request)
|
|
||||||
serializer = self.get_serializer(data=request.data)
|
|
||||||
serializer.is_valid(raise_exception=True)
|
|
||||||
referral = serializer.validated_data.get("referral")
|
|
||||||
|
|
||||||
if referral == self.request.user.pk:
|
|
||||||
raise exceptions.Conflict('SAME AS USER')
|
|
||||||
|
|
||||||
referral_user = get_object_or_404(User.objects.all(), pk=referral)
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"referral": referral,
|
|
||||||
"application": application.pk,
|
|
||||||
"user": user.pk,
|
|
||||||
}
|
|
||||||
plan = settings.PROMOTIONS_REFERRAL_PLAN
|
|
||||||
|
|
||||||
result = promote_user(user, plan, 'accounts::submit_referral', data)
|
|
||||||
# result_json = {
|
|
||||||
# "promotions": [
|
|
||||||
# {
|
|
||||||
# "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
||||||
# "event": {
|
|
||||||
# "label": "string",
|
|
||||||
# "data": "string",
|
|
||||||
# "user": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
||||||
# "application": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
|
|
||||||
# },
|
|
||||||
# "promotion_amount": 2147483647,
|
|
||||||
# "state": 1,
|
|
||||||
# "user": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
|
|
||||||
# }
|
|
||||||
# ]
|
|
||||||
# }
|
|
||||||
if not result:
|
|
||||||
raise exceptions.Conflict('can not set referral')
|
|
||||||
|
|
||||||
return Response(serializer.validated_data)
|
|
||||||
|
|
|
||||||
|
|
@ -384,7 +384,3 @@ MINIO_POLICY_HOOKS: List[Tuple[str, dict]] = []
|
||||||
MINIO_BUCKET_CHECK_ON_SAVE = True # Default: True // Creates bucket if missing, then save
|
MINIO_BUCKET_CHECK_ON_SAVE = True # Default: True // Creates bucket if missing, then save
|
||||||
|
|
||||||
MOBIN_SMS_TOKEN = config('MOBIN_SMS_TOKEN', default='')
|
MOBIN_SMS_TOKEN = config('MOBIN_SMS_TOKEN', default='')
|
||||||
|
|
||||||
PROMOTIONS_BASE_PUBLIC_URL = config('PROMOTIONS_BASE_PUBLIC_URL', default=None, cast=str)
|
|
||||||
PROMOTIONS_BASE_PRIVATE_URL = config('PROMOTIONS_BASE_PRIVATE_URL', default=None, cast=str)
|
|
||||||
PROMOTIONS_REFERRAL_PLAN = config('PROMOTIONS_REFERRAL_PLAN', default=None, cast=str)
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import httpx
|
||||||
|
|
||||||
from ... import errors
|
from ... import errors
|
||||||
from ...client import AuthenticatedClient, Client
|
from ...client import AuthenticatedClient, Client
|
||||||
from ...models.paginated_promotion_list import PaginatedPromotionList
|
from ...models.paginated_user_plan_list import PaginatedUserPlanList
|
||||||
from ...types import UNSET, Response, Unset
|
from ...types import UNSET, Response, Unset
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -36,9 +36,9 @@ def _get_kwargs(
|
||||||
return _kwargs
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> PaginatedPromotionList | None:
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> PaginatedUserPlanList | None:
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
response_200 = PaginatedPromotionList.from_dict(response.json())
|
response_200 = PaginatedUserPlanList.from_dict(response.json())
|
||||||
|
|
||||||
return response_200
|
return response_200
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res
|
||||||
|
|
||||||
def _build_response(
|
def _build_response(
|
||||||
*, client: AuthenticatedClient | Client, response: httpx.Response
|
*, client: AuthenticatedClient | Client, response: httpx.Response
|
||||||
) -> Response[PaginatedPromotionList]:
|
) -> Response[PaginatedUserPlanList]:
|
||||||
return Response(
|
return Response(
|
||||||
status_code=HTTPStatus(response.status_code),
|
status_code=HTTPStatus(response.status_code),
|
||||||
content=response.content,
|
content=response.content,
|
||||||
|
|
@ -65,7 +65,7 @@ def sync_detailed(
|
||||||
client: AuthenticatedClient,
|
client: AuthenticatedClient,
|
||||||
limit: int | Unset = UNSET,
|
limit: int | Unset = UNSET,
|
||||||
offset: int | Unset = UNSET,
|
offset: int | Unset = UNSET,
|
||||||
) -> Response[PaginatedPromotionList]:
|
) -> Response[PaginatedUserPlanList]:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
user_uuid (str):
|
user_uuid (str):
|
||||||
|
|
@ -77,7 +77,7 @@ def sync_detailed(
|
||||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response[PaginatedPromotionList]
|
Response[PaginatedUserPlanList]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
kwargs = _get_kwargs(
|
kwargs = _get_kwargs(
|
||||||
|
|
@ -99,7 +99,7 @@ def sync(
|
||||||
client: AuthenticatedClient,
|
client: AuthenticatedClient,
|
||||||
limit: int | Unset = UNSET,
|
limit: int | Unset = UNSET,
|
||||||
offset: int | Unset = UNSET,
|
offset: int | Unset = UNSET,
|
||||||
) -> PaginatedPromotionList | None:
|
) -> PaginatedUserPlanList | None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
user_uuid (str):
|
user_uuid (str):
|
||||||
|
|
@ -111,7 +111,7 @@ def sync(
|
||||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
PaginatedPromotionList
|
PaginatedUserPlanList
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return sync_detailed(
|
return sync_detailed(
|
||||||
|
|
@ -128,7 +128,7 @@ async def asyncio_detailed(
|
||||||
client: AuthenticatedClient,
|
client: AuthenticatedClient,
|
||||||
limit: int | Unset = UNSET,
|
limit: int | Unset = UNSET,
|
||||||
offset: int | Unset = UNSET,
|
offset: int | Unset = UNSET,
|
||||||
) -> Response[PaginatedPromotionList]:
|
) -> Response[PaginatedUserPlanList]:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
user_uuid (str):
|
user_uuid (str):
|
||||||
|
|
@ -140,7 +140,7 @@ async def asyncio_detailed(
|
||||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response[PaginatedPromotionList]
|
Response[PaginatedUserPlanList]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
kwargs = _get_kwargs(
|
kwargs = _get_kwargs(
|
||||||
|
|
@ -160,7 +160,7 @@ async def asyncio(
|
||||||
client: AuthenticatedClient,
|
client: AuthenticatedClient,
|
||||||
limit: int | Unset = UNSET,
|
limit: int | Unset = UNSET,
|
||||||
offset: int | Unset = UNSET,
|
offset: int | Unset = UNSET,
|
||||||
) -> PaginatedPromotionList | None:
|
) -> PaginatedUserPlanList | None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
user_uuid (str):
|
user_uuid (str):
|
||||||
|
|
@ -172,7 +172,7 @@ async def asyncio(
|
||||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
PaginatedPromotionList
|
PaginatedUserPlanList
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import httpx
|
||||||
|
|
||||||
from ... import errors
|
from ... import errors
|
||||||
from ...client import AuthenticatedClient, Client
|
from ...client import AuthenticatedClient, Client
|
||||||
from ...models.promotion import Promotion
|
from ...models.user_plan import UserPlan
|
||||||
from ...types import Response
|
from ...types import Response
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -26,9 +26,9 @@ def _get_kwargs(
|
||||||
return _kwargs
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promotion | None:
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> UserPlan | None:
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
response_200 = Promotion.from_dict(response.json())
|
response_200 = UserPlan.from_dict(response.json())
|
||||||
|
|
||||||
return response_200
|
return response_200
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promotion]:
|
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[UserPlan]:
|
||||||
return Response(
|
return Response(
|
||||||
status_code=HTTPStatus(response.status_code),
|
status_code=HTTPStatus(response.status_code),
|
||||||
content=response.content,
|
content=response.content,
|
||||||
|
|
@ -52,7 +52,7 @@ def sync_detailed(
|
||||||
id: str,
|
id: str,
|
||||||
*,
|
*,
|
||||||
client: AuthenticatedClient,
|
client: AuthenticatedClient,
|
||||||
) -> Response[Promotion]:
|
) -> Response[UserPlan]:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
user_uuid (str):
|
user_uuid (str):
|
||||||
|
|
@ -63,7 +63,7 @@ def sync_detailed(
|
||||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response[Promotion]
|
Response[UserPlan]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
kwargs = _get_kwargs(
|
kwargs = _get_kwargs(
|
||||||
|
|
@ -83,7 +83,7 @@ def sync(
|
||||||
id: str,
|
id: str,
|
||||||
*,
|
*,
|
||||||
client: AuthenticatedClient,
|
client: AuthenticatedClient,
|
||||||
) -> Promotion | None:
|
) -> UserPlan | None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
user_uuid (str):
|
user_uuid (str):
|
||||||
|
|
@ -94,7 +94,7 @@ def sync(
|
||||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Promotion
|
UserPlan
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return sync_detailed(
|
return sync_detailed(
|
||||||
|
|
@ -109,7 +109,7 @@ async def asyncio_detailed(
|
||||||
id: str,
|
id: str,
|
||||||
*,
|
*,
|
||||||
client: AuthenticatedClient,
|
client: AuthenticatedClient,
|
||||||
) -> Response[Promotion]:
|
) -> Response[UserPlan]:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
user_uuid (str):
|
user_uuid (str):
|
||||||
|
|
@ -120,7 +120,7 @@ async def asyncio_detailed(
|
||||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Response[Promotion]
|
Response[UserPlan]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
kwargs = _get_kwargs(
|
kwargs = _get_kwargs(
|
||||||
|
|
@ -138,7 +138,7 @@ async def asyncio(
|
||||||
id: str,
|
id: str,
|
||||||
*,
|
*,
|
||||||
client: AuthenticatedClient,
|
client: AuthenticatedClient,
|
||||||
) -> Promotion | None:
|
) -> UserPlan | None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
user_uuid (str):
|
user_uuid (str):
|
||||||
|
|
@ -149,7 +149,7 @@ async def asyncio(
|
||||||
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Promotion
|
UserPlan
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .paginated_promotion_list import PaginatedPromotionList
|
from .paginated_promotion_list import PaginatedPromotionList
|
||||||
|
from .paginated_user_plan_list import PaginatedUserPlanList
|
||||||
from .promote import Promote
|
from .promote import Promote
|
||||||
from .promotion import Promotion
|
from .promotion import Promotion
|
||||||
from .state_enum import StateEnum
|
from .state_enum import StateEnum
|
||||||
|
|
@ -11,6 +12,7 @@ from .user_recipient import UserRecipient
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"Event",
|
"Event",
|
||||||
"PaginatedPromotionList",
|
"PaginatedPromotionList",
|
||||||
|
"PaginatedUserPlanList",
|
||||||
"Promote",
|
"Promote",
|
||||||
"Promotion",
|
"Promotion",
|
||||||
"StateEnum",
|
"StateEnum",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||||
|
|
||||||
|
from attrs import define as _attrs_define
|
||||||
|
from attrs import field as _attrs_field
|
||||||
|
|
||||||
|
from ..types import UNSET, Unset
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..models.user_plan import UserPlan
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="PaginatedUserPlanList")
|
||||||
|
|
||||||
|
|
||||||
|
@_attrs_define
|
||||||
|
class PaginatedUserPlanList:
|
||||||
|
"""
|
||||||
|
Attributes:
|
||||||
|
count (int): Example: 123.
|
||||||
|
results (list[UserPlan]):
|
||||||
|
next_ (None | str | Unset): Example: http://api.example.org/accounts/?offset=400&limit=100.
|
||||||
|
previous (None | str | Unset): Example: http://api.example.org/accounts/?offset=200&limit=100.
|
||||||
|
"""
|
||||||
|
|
||||||
|
count: int
|
||||||
|
results: list[UserPlan]
|
||||||
|
next_: None | str | Unset = UNSET
|
||||||
|
previous: None | str | Unset = UNSET
|
||||||
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
count = self.count
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for results_item_data in self.results:
|
||||||
|
results_item = results_item_data.to_dict()
|
||||||
|
results.append(results_item)
|
||||||
|
|
||||||
|
next_: None | str | Unset
|
||||||
|
if isinstance(self.next_, Unset):
|
||||||
|
next_ = UNSET
|
||||||
|
else:
|
||||||
|
next_ = self.next_
|
||||||
|
|
||||||
|
previous: None | str | Unset
|
||||||
|
if isinstance(self.previous, Unset):
|
||||||
|
previous = UNSET
|
||||||
|
else:
|
||||||
|
previous = self.previous
|
||||||
|
|
||||||
|
field_dict: dict[str, Any] = {}
|
||||||
|
field_dict.update(self.additional_properties)
|
||||||
|
field_dict.update(
|
||||||
|
{
|
||||||
|
"count": count,
|
||||||
|
"results": results,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if next_ is not UNSET:
|
||||||
|
field_dict["next"] = next_
|
||||||
|
if previous is not UNSET:
|
||||||
|
field_dict["previous"] = previous
|
||||||
|
|
||||||
|
return field_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||||
|
from ..models.user_plan import UserPlan
|
||||||
|
|
||||||
|
d = dict(src_dict)
|
||||||
|
count = d.pop("count")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
_results = d.pop("results")
|
||||||
|
for results_item_data in _results:
|
||||||
|
results_item = UserPlan.from_dict(results_item_data)
|
||||||
|
|
||||||
|
results.append(results_item)
|
||||||
|
|
||||||
|
def _parse_next_(data: object) -> None | str | Unset:
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
if isinstance(data, Unset):
|
||||||
|
return data
|
||||||
|
return cast(None | str | Unset, data)
|
||||||
|
|
||||||
|
next_ = _parse_next_(d.pop("next", UNSET))
|
||||||
|
|
||||||
|
def _parse_previous(data: object) -> None | str | Unset:
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
if isinstance(data, Unset):
|
||||||
|
return data
|
||||||
|
return cast(None | str | Unset, data)
|
||||||
|
|
||||||
|
previous = _parse_previous(d.pop("previous", UNSET))
|
||||||
|
|
||||||
|
paginated_user_plan_list = cls(
|
||||||
|
count=count,
|
||||||
|
results=results,
|
||||||
|
next_=next_,
|
||||||
|
previous=previous,
|
||||||
|
)
|
||||||
|
|
||||||
|
paginated_user_plan_list.additional_properties = d
|
||||||
|
return paginated_user_plan_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def additional_keys(self) -> list[str]:
|
||||||
|
return list(self.additional_properties.keys())
|
||||||
|
|
||||||
|
def __getitem__(self, key: str) -> Any:
|
||||||
|
return self.additional_properties[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key: str, value: Any) -> None:
|
||||||
|
self.additional_properties[key] = value
|
||||||
|
|
||||||
|
def __delitem__(self, key: str) -> None:
|
||||||
|
del self.additional_properties[key]
|
||||||
|
|
||||||
|
def __contains__(self, key: str) -> bool:
|
||||||
|
return key in self.additional_properties
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import time
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.cache import cache
|
|
||||||
from django.db import models
|
|
||||||
from httpx import Request
|
|
||||||
|
|
||||||
from utils.clients.gooyal_promotions_client import AuthenticatedClient
|
|
||||||
from utils.clients.gooyal_promotions_client.api.api import (
|
|
||||||
api_v2_promotions_application_plan_promote_create
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def login_as_client_credentials():
|
|
||||||
access_token_cache_key = 'accounts_access_token'
|
|
||||||
access_token = cache.get(access_token_cache_key)
|
|
||||||
# expires_in = self.auth_data.get('expires_in')
|
|
||||||
# created_at = self.auth_data.get('created_at')
|
|
||||||
#
|
|
||||||
# if access_token and expires_in > time.time() - created_at:
|
|
||||||
# return self.auth_data
|
|
||||||
if access_token:
|
|
||||||
logger.debug('token available in cache')
|
|
||||||
return access_token
|
|
||||||
|
|
||||||
logger.debug('no token available in cache')
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"grant_type": 'client_credentials',
|
|
||||||
"scope": settings.OAUTH2_PROVIDER_SCOPES,
|
|
||||||
}
|
|
||||||
auth = (settings.OAUTH2_PROVIDER_CLIENT_ID, settings.OAUTH2_PROVIDER_CLIENT_SECRET)
|
|
||||||
|
|
||||||
response = requests.post(f'{settings.OAUTH2_PROVIDER_BASE_PUBLIC_URL}/token/',
|
|
||||||
data=data,
|
|
||||||
auth=auth)
|
|
||||||
logger.debug(response.content)
|
|
||||||
|
|
||||||
auth_data = response.json()
|
|
||||||
if 'access_token' in auth_data:
|
|
||||||
auth_data['created_at'] = time.time()
|
|
||||||
cache.set(access_token_cache_key, auth_data, timeout=auth_data.get('expires_in') - 5)
|
|
||||||
|
|
||||||
return auth_data
|
|
||||||
|
|
||||||
|
|
||||||
def log_request(request: Request):
|
|
||||||
logger.debug(f'authorization: {request.headers["authorization"]}')
|
|
||||||
logger.debug(
|
|
||||||
f"Request event hook: {request.method} {request.url} {request.method} {request.headers} - Waiting for response")
|
|
||||||
|
|
||||||
|
|
||||||
def log_response(response):
|
|
||||||
request = response.request
|
|
||||||
logger.debug(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
|
|
||||||
# logger.debug(f"Response event hook: {response.read()}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_promotions_client():
|
|
||||||
access_token = login_as_client_credentials()['access_token']
|
|
||||||
client = AuthenticatedClient(base_url=settings.PROMOTIONS_BASE_PUBLIC_URL, token=access_token,
|
|
||||||
httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
|
|
||||||
)
|
|
||||||
return client
|
|
||||||
|
|
||||||
|
|
||||||
def promote_user(user, plan, label, data):
|
|
||||||
class Tmp():
|
|
||||||
def to_dict(self):
|
|
||||||
return {
|
|
||||||
"data": data,
|
|
||||||
"label": label
|
|
||||||
}
|
|
||||||
|
|
||||||
data = Tmp()
|
|
||||||
client = get_promotions_client()
|
|
||||||
response = api_v2_promotions_application_plan_promote_create.sync(
|
|
||||||
user_uuid=user,
|
|
||||||
id=plan,
|
|
||||||
client=client,
|
|
||||||
body=data
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
Loading…
Add table
Reference in a new issue