FEATURE(account): adding InviteRequestView
adding InviteRequestView
This commit is contained in:
parent
e6ea563d19
commit
86b02ffa73
5 changed files with 165 additions and 7 deletions
|
|
@ -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'), 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,7 +2,7 @@ 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
|
||||||
|
|
@ -134,6 +134,34 @@ 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)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
invite_code = validated_data.pop('invite_code')
|
||||||
|
application = self.context['request'].auth
|
||||||
|
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()
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ 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()
|
||||||
|
|
@ -149,6 +149,100 @@ class UserTests(APITestCase):
|
||||||
self.assertEqual(response.json()['results'][0]['is_current'] , True)
|
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')
|
||||||
|
client_id = 'inviteClientId1234567890123456789012'
|
||||||
|
client_secret = 'inviteClientSecret123456789012345678'
|
||||||
|
mobile_number = '+989120000000'
|
||||||
|
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
|
||||||
|
def basic_auth_header(self):
|
||||||
|
credentials = f'{self.client_id}:{self.client_secret}'.encode('utf-8')
|
||||||
|
return 'Basic ' + base64.b64encode(credentials).decode('utf-8')
|
||||||
|
|
||||||
|
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',
|
||||||
|
HTTP_AUTHORIZATION=self.basic_auth_header(),
|
||||||
|
)
|
||||||
|
|
||||||
|
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',
|
||||||
|
HTTP_AUTHORIZATION=self.basic_auth_header(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertEqual(InviteRequest.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_create_invite_request_invalid_client_credentials(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('users:invite_request_api'),
|
||||||
|
data={
|
||||||
|
'invite_code': str(self.inviter_uuid),
|
||||||
|
'mobile_number': self.mobile_number,
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
HTTP_AUTHORIZATION='Basic ' + base64.b64encode(b'wrong:credentials').decode('utf-8'),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 401)
|
||||||
|
self.assertEqual(InviteRequest.objects.count(), 0)
|
||||||
|
|
||||||
|
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',
|
||||||
|
HTTP_AUTHORIZATION=self.basic_auth_header(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(InviteRequest.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from django.urls import path
|
||||||
from django.contrib.auth.views import LogoutView
|
from django.contrib.auth.views import LogoutView
|
||||||
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
|
UserDetailedRetrieveView, UserSessionListView, UserProfileView, InviteRequestView
|
||||||
|
|
||||||
app_name = "users"
|
app_name = "users"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
@ -21,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'),
|
||||||
|
path('api/invite_requests/', InviteRequestView.as_view(), name='invite_request_api'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,9 @@ from rest_framework.views import APIView
|
||||||
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||||
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
|
ChangePasswordSerializer, UserInquirySerializer, SessionSerializer, InviteRequestSerializer
|
||||||
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle, NumberedRequestOTPDayRateThrottle, NumberedRequestOTPMinRateThrottle
|
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle, NumberedRequestOTPDayRateThrottle, NumberedRequestOTPMinRateThrottle
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
|
|
@ -128,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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue