Compare commits

..

6 commits

Author SHA1 Message Date
fa3bca29df FEATURE(account): adding promotions client
adding promotions client
2026-07-26 10:58:59 +03:30
0973d08637 FIX(account): fix duplicated invite code
fix duplicated invite code
2026-07-25 17:16:18 +03:30
f6099af0e4 FIX(account): add InviteRequest admin panel
add InviteRequest admin panel
2026-07-25 16:56:44 +03:30
2464eb0de8 FIX(account): fix application in request
fix application in request
2026-07-25 16:45:53 +03:30
586068fe3b FEATURE(account): adding InviteRequestView
adding InviteRequestView
2026-07-22 17:33:45 +03:30
86b02ffa73 FEATURE(account): adding InviteRequestView
adding InviteRequestView
2026-07-22 17:32:30 +03:30
17 changed files with 453 additions and 320 deletions

View file

@ -1,5 +1,3 @@
import logging
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
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
logger = logging.getLogger(__name__)
# @method_decorator(login_required, name='dispatch')
class HomeView(TemplateView):
template_name = 'core/home.html'

View file

@ -1,93 +1,19 @@
import logging
from django.core.exceptions import ImproperlyConfigured
from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements, OAuth2Authentication
from rest_framework.permissions import (
IsAuthenticated, BasePermission
IsAuthenticated
)
logger = logging.getLogger("oauth2_provider")
log = logging.getLogger("oauth2_provider")
class IsAuthenticatedOrTokenMatchesOASRequirements(TokenMatchesOASRequirements):
def has_permission(self, request, view):
logger.debug(f'try to authenticate {request} for {view} in IsAuthenticatedOrTokenMatchesOASRequirements')
is_authenticated = IsAuthenticated().has_permission(request, view)
logger.debug(f'is_authenticated: {is_authenticated}')
oauth2authenticated = False
if is_authenticated:
oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication)
logger.debug(f'oauth2authenticated: {oauth2authenticated}')
token_has_scope = TokenMatchesOASRequirements()
logger.debug(f'token_has_scope: {token_has_scope}')
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"
)
return (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view)

View file

@ -8,12 +8,19 @@ from unittest.mock import patch
from django.test import TestCase
from django.urls import reverse
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.test import APIClient, APITestCase
from apps.core.models import Config
from apps.gooyal_oauth2.models import Scope
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):
import uuid as sys_uuid
@ -24,12 +31,6 @@ def mock_notifications_push_user_success(user_uuid, title, message, priority=5,
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):
user_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
access_token_1 = 'au4naVsdKCbKNOhnElPyXcrwSnqqFbm'
@ -45,7 +46,6 @@ class GooyalOAuth2Tests(APITestCase):
client = APIClient()
def setUp(self):
from django.conf import settings
settings.SMS_SEND = False
@ -203,3 +203,10 @@ class GooyalOAuth2Tests(APITestCase):
result = self.client.post(reverse("gooyal_oauth2:revoke-token"), data=revoke_data)
self.assertEqual(result.status_code, 200)
self.assertEqual(AccessToken.objects.count(), 0)

View file

@ -1,5 +1,5 @@
from django.contrib import admin
from .models import User
from .models import User, InviteRequest
class UserAdmin(admin.ModelAdmin):
@ -27,4 +27,22 @@ class UserAdmin(admin.ModelAdmin):
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(InviteRequest, InviteRequestAdmin)

View 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'],
},
),
]

View file

@ -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'),
),
]

View file

@ -21,6 +21,8 @@ from .provinces_and_cities import state
from apps.users.constans import MAX_OTP_TRY, DEVELOPMENT_PHONE_NUMBERS, GenderChoices, EducationDegreeChoices, \
MAX_OTP_VALID_DURATION
from oauth2_provider import settings as oauth2_settings
from utils.models import BaseModel
logger = logging.getLogger(__name__)
@ -267,3 +269,28 @@ class User(AbstractUser):
def __str__(self):
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}'

View file

@ -2,12 +2,12 @@ from django.core.validators import RegexValidator
from oauth2_provider.models import get_access_token_model
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.translation import gettext_lazy as _
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 must be entered in the format: '+989999999999'."
@ -134,6 +134,47 @@ class ChangePasswordSerializer(serializers.Serializer):
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()
class SessionSerializer(serializers.ModelSerializer):
is_current = serializers.SerializerMethodField()
@ -154,9 +195,3 @@ class SessionSerializer(serializers.ModelSerializer):
model = AccessToken
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()

View file

@ -14,15 +14,14 @@ from rest_framework.test import APIClient, APITestCase
from apps.core.models import Config
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()
Application = get_application_model()
class UserTests(APITestCase):
user_1_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
user_2_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005e')
user_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005f')
access_token_1 = 'au4naVsdKCbKNOhnElPyXcrwSnqqFbm'
access_token_2 = 'vu4naVsdKCbKNOhnElPyXcrwSnqqFbm'
application_uuid = uuid.UUID('a14e8b86-8f4a-44d9-b29d-badceb47005f')
@ -38,14 +37,11 @@ class UserTests(APITestCase):
def setUp(self):
from django.conf import settings
settings.SMS_SEND = False
self.user_1_phone_number = '+989100000000'
self.user_2_phone_number = '+989100000001'
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)
self.user_phone_number = '+989100000000'
self.user = User.objects.create(pk=self.user_uuid, phone_number=self.user_phone_number)
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.users.user:submit_referral', description='accounts.users.user:submit_referral')
self.application = Application.objects.create(
client_id=self.client_id,
@ -53,9 +49,9 @@ class UserTests(APITestCase):
authorization_grant_type='password',
hash_client_secret=False,
uuid=self.application_uuid,
user_id=self.user_1_uuid,
user_id=self.user_uuid,
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)
@ -74,7 +70,7 @@ class UserTests(APITestCase):
access_token_1 = AccessToken.objects.create(
**{
"token": self.access_token_1,
"user": self.user_1,
"user": self.user,
# "client_id": self.client_id,
# "client_owner": owner,
"application_id": self.application_uuid,
@ -92,7 +88,7 @@ class UserTests(APITestCase):
access_token_2 = AccessToken.objects.create(
**{
"token": self.access_token_2,
"user": self.user_1,
"user": self.user,
# "client_id": self.client_id,
# "client_owner": owner,
@ -126,13 +122,13 @@ class UserTests(APITestCase):
return basic_credentials
def login(self):
self.user_1.set_otp()
self.user.set_otp()
data = {
"grant_type": "password",
"username": self.user_1_phone_number,
"username": self.user_phone_number,
"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'
}
self.client.credentials(
@ -140,7 +136,6 @@ class UserTests(APITestCase):
)
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
print(result.json())
access_token = result.json()['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"))
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)

View file

@ -1,16 +1,10 @@
from django.urls import path
from django.contrib.auth.views import LogoutView
from rest_framework.routers import DefaultRouter
from .views import UserListView, UserPublicRetrieveView, AccountView, RequestOTPView, ChangePasswordView, \
OTPLoginView, ProfileDetailView, ProfileUpdateView, RequestOTTView, UserCurrentAvatarUrlView, UserInquiryView, \
UserDetailedRetrieveView, UserSessionListView, UserProfileView, UserViewSet
UserDetailedRetrieveView, UserSessionListView, UserProfileView, InviteRequestView
app_name = "users"
router = DefaultRouter()
router.register("user", UserViewSet, basename="user")
urlpatterns = [
path('login/', OTPLoginView.as_view(), name='login'),
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/change_password/', ChangePasswordView.as_view(), name='change_password_api'),
path('api/inquiry/', UserInquiryView.as_view(), name='user_inquiry_api'),
] + router.urls
path('api/invite_requests/', InviteRequestView.as_view(), name='invite_request_api'),
]

View file

@ -1,4 +1,3 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
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_filters.rest_framework import DjangoFilterBackend
from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope
from rest_framework import generics, status, permissions, mixins
from rest_framework.decorators import action
from rest_framework.generics import get_object_or_404
from rest_framework import generics, status, permissions
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
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.throttling import get_application
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
from apps.users.filters import UserFilter
from apps.users.forms import OTPAuthenticationForm, ProfileUpdateForm
from apps.users.models import User
from apps.users.provinces_and_cities import State
from apps.users.models import User, InviteRequest
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.clients.promotions_client import promote_user
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle
from utils import exceptions
UserModel = get_user_model()
@ -65,7 +56,6 @@ class UserPublicRetrieveView(generics.RetrieveAPIView):
"GET": [["accounts.profile:retrieve"]],
}
class UserDetailedRetrieveView(generics.RetrieveAPIView):
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
queryset = User.objects.all()
@ -75,7 +65,6 @@ class UserDetailedRetrieveView(generics.RetrieveAPIView):
"GET": [["accounts.profile:detailed_retrieve"]],
}
class UserCurrentAvatarUrlView(generics.RetrieveAPIView):
permission_classes = [AllowAny]
queryset = User.objects.all()
@ -115,7 +104,6 @@ class RequestOTPView(generics.CreateAPIView):
NumberedRequestOTPDayRateThrottle,
NumberedRequestOTPMinRateThrottle]
class RequestOTTView(generics.CreateAPIView):
permission_classes = [IsAuthenticatedOrTokenHasScope]
required_scopes = ['accounts.account:request_ott']
@ -139,6 +127,15 @@ class UserInquiryView(generics.CreateAPIView):
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):
permission_classes = [IsAuthenticatedOrTokenHasScope]
serializer_class = ChangePasswordSerializer
@ -221,58 +218,3 @@ class UserSessionListView(generics.ListAPIView):
from apps.gooyal_oauth2.models import AccessToken
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)

View file

@ -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
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)

View file

@ -6,7 +6,7 @@ import httpx
from ... import errors
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
@ -36,9 +36,9 @@ def _get_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:
response_200 = PaginatedPromotionList.from_dict(response.json())
response_200 = PaginatedUserPlanList.from_dict(response.json())
return response_200
@ -50,7 +50,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res
def _build_response(
*, client: AuthenticatedClient | Client, response: httpx.Response
) -> Response[PaginatedPromotionList]:
) -> Response[PaginatedUserPlanList]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
@ -65,7 +65,7 @@ def sync_detailed(
client: AuthenticatedClient,
limit: int | Unset = UNSET,
offset: int | Unset = UNSET,
) -> Response[PaginatedPromotionList]:
) -> Response[PaginatedUserPlanList]:
"""
Args:
user_uuid (str):
@ -77,7 +77,7 @@ def sync_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[PaginatedPromotionList]
Response[PaginatedUserPlanList]
"""
kwargs = _get_kwargs(
@ -99,7 +99,7 @@ def sync(
client: AuthenticatedClient,
limit: int | Unset = UNSET,
offset: int | Unset = UNSET,
) -> PaginatedPromotionList | None:
) -> PaginatedUserPlanList | None:
"""
Args:
user_uuid (str):
@ -111,7 +111,7 @@ def sync(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
PaginatedPromotionList
PaginatedUserPlanList
"""
return sync_detailed(
@ -128,7 +128,7 @@ async def asyncio_detailed(
client: AuthenticatedClient,
limit: int | Unset = UNSET,
offset: int | Unset = UNSET,
) -> Response[PaginatedPromotionList]:
) -> Response[PaginatedUserPlanList]:
"""
Args:
user_uuid (str):
@ -140,7 +140,7 @@ async def asyncio_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[PaginatedPromotionList]
Response[PaginatedUserPlanList]
"""
kwargs = _get_kwargs(
@ -160,7 +160,7 @@ async def asyncio(
client: AuthenticatedClient,
limit: int | Unset = UNSET,
offset: int | Unset = UNSET,
) -> PaginatedPromotionList | None:
) -> PaginatedUserPlanList | None:
"""
Args:
user_uuid (str):
@ -172,7 +172,7 @@ async def asyncio(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
PaginatedPromotionList
PaginatedUserPlanList
"""
return (

View file

@ -6,7 +6,7 @@ import httpx
from ... import errors
from ...client import AuthenticatedClient, Client
from ...models.promotion import Promotion
from ...models.user_plan import UserPlan
from ...types import Response
@ -26,9 +26,9 @@ def _get_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:
response_200 = Promotion.from_dict(response.json())
response_200 = UserPlan.from_dict(response.json())
return response_200
@ -38,7 +38,7 @@ def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Res
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(
status_code=HTTPStatus(response.status_code),
content=response.content,
@ -52,7 +52,7 @@ def sync_detailed(
id: str,
*,
client: AuthenticatedClient,
) -> Response[Promotion]:
) -> Response[UserPlan]:
"""
Args:
user_uuid (str):
@ -63,7 +63,7 @@ def sync_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Promotion]
Response[UserPlan]
"""
kwargs = _get_kwargs(
@ -83,7 +83,7 @@ def sync(
id: str,
*,
client: AuthenticatedClient,
) -> Promotion | None:
) -> UserPlan | None:
"""
Args:
user_uuid (str):
@ -94,7 +94,7 @@ def sync(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Promotion
UserPlan
"""
return sync_detailed(
@ -109,7 +109,7 @@ async def asyncio_detailed(
id: str,
*,
client: AuthenticatedClient,
) -> Response[Promotion]:
) -> Response[UserPlan]:
"""
Args:
user_uuid (str):
@ -120,7 +120,7 @@ async def asyncio_detailed(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Response[Promotion]
Response[UserPlan]
"""
kwargs = _get_kwargs(
@ -138,7 +138,7 @@ async def asyncio(
id: str,
*,
client: AuthenticatedClient,
) -> Promotion | None:
) -> UserPlan | None:
"""
Args:
user_uuid (str):
@ -149,7 +149,7 @@ async def asyncio(
httpx.TimeoutException: If the request takes longer than Client.timeout.
Returns:
Promotion
UserPlan
"""
return (

View file

@ -2,6 +2,7 @@
from .event import Event
from .paginated_promotion_list import PaginatedPromotionList
from .paginated_user_plan_list import PaginatedUserPlanList
from .promote import Promote
from .promotion import Promotion
from .state_enum import StateEnum
@ -11,6 +12,7 @@ from .user_recipient import UserRecipient
__all__ = (
"Event",
"PaginatedPromotionList",
"PaginatedUserPlanList",
"Promote",
"Promotion",
"StateEnum",

View file

@ -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

View file

@ -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