Compare commits
80 commits
FEATURE/su
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fa3bca29df | |||
| 0973d08637 | |||
| f6099af0e4 | |||
| 2464eb0de8 | |||
| 586068fe3b | |||
| 86b02ffa73 | |||
| e6ea563d19 | |||
|
|
cc6a76581e | ||
| f4fe506aef | |||
| 55dadd67f4 | |||
| 926e48a5dd | |||
|
|
30f23c8d49 | ||
| 43b5ba9028 | |||
| 706de57427 | |||
| 8859fe4d20 | |||
|
|
1981122f28 | ||
|
|
28ae006b5d | ||
|
|
f400eb6ab4 | ||
|
|
280b8f1c2c | ||
|
|
1f44f80a79 | ||
|
|
c227fda538 | ||
|
|
3dd62a0173 | ||
|
|
c35accc8ac | ||
|
|
3d587fb63d | ||
|
|
5db1c6f070 | ||
|
|
ada0391922 | ||
|
|
3796a50ca8 | ||
|
|
0e9ca49ca0 | ||
|
|
7256578ec7 | ||
|
|
0365d17095 | ||
|
|
492d06b5ba | ||
|
|
9f284fbfff | ||
|
|
5f0684d354 | ||
|
|
9c81505a46 | ||
|
|
be4df13c78 | ||
|
|
c1c55c45a6 | ||
|
|
e1d0339759 | ||
|
|
25c53f3a67 | ||
|
|
014c674961 | ||
|
|
4d5f1983c7 | ||
|
|
2d1079f0c2 | ||
|
|
e1b34b2482 | ||
|
|
0cd2b96963 | ||
|
|
6dbb86d7ee | ||
|
|
b786a957e4 | ||
|
|
29e4b4054c | ||
|
|
c0f65b713f | ||
|
|
4b277bc95f | ||
|
|
b8a57a0ce4 | ||
|
|
ba858baa85 | ||
|
|
dda068c018 | ||
|
|
27faf23bec | ||
|
|
d4bb5ed6b6 | ||
|
|
b01dcdf055 | ||
|
|
2b6d192032 | ||
|
|
9b2dce70df | ||
|
|
5afccf8fc4 | ||
|
|
092125dc31 | ||
|
|
589a64b447 | ||
|
|
2f5284607b | ||
|
|
97b8dfdc21 | ||
|
|
b763d5ad4e | ||
|
|
3f5368cd82 | ||
|
|
1a9397d906 | ||
|
|
f38f98068e | ||
|
|
952bb6107a | ||
|
|
171096f9da | ||
|
|
7b153cd8f6 | ||
|
|
88d7b691a3 | ||
|
|
30b3c93168 | ||
|
|
673f525d30 | ||
|
|
c966dd3171 | ||
|
|
63a6da058e | ||
|
|
a5f5e1162b | ||
|
|
119165f2e1 | ||
|
|
d6fe8a93da | ||
|
|
2fe322241a | ||
|
|
0d59a34e06 | ||
|
|
2cf5ab4cb7 | ||
|
|
f2e8777e6d |
106 changed files with 6840 additions and 322 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,3 +6,5 @@ delme*.py
|
||||||
.env
|
.env
|
||||||
/venv/
|
/venv/
|
||||||
oidc.key
|
oidc.key
|
||||||
|
/log/
|
||||||
|
/logs/*.log
|
||||||
|
|
|
||||||
12
Dockerfile
12
Dockerfile
|
|
@ -1,9 +1,9 @@
|
||||||
FROM debian:12
|
FROM base:debian13
|
||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN date
|
#RUN apt update
|
||||||
RUN apt update
|
#RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip -y
|
||||||
RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip -y
|
#COPY requirements.txt /app/requirements.txt
|
||||||
COPY requirements.txt /app/requirements.txt
|
#RUN pip3 install --break-system-packages -r requirements.txt
|
||||||
RUN pip3 install --break-system-packages -r requirements.txt
|
#RUN pip3 install --break-system-packages setuptools
|
||||||
#ENTRYPOINT ["./run.sh"]
|
#ENTRYPOINT ["./run.sh"]
|
||||||
|
|
|
||||||
8
Dockerfile.build
Normal file
8
Dockerfile.build
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
FROM debian:13
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apt update
|
||||||
|
RUN apt install gnupg2 netcat-traditional libssl-dev libcrypto++-dev lsb-release python3-pip -y
|
||||||
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
RUN pip3 install --break-system-packages -r requirements.txt
|
||||||
|
#ENTRYPOINT ["./run.sh"]
|
||||||
|
|
@ -1,2 +1,17 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import SMSPolicy, Config
|
||||||
|
|
||||||
|
|
||||||
|
class SMSPolicyAdmin(admin.ModelAdmin):
|
||||||
|
def has_add_permission(self, request):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['key', 'value', 'value_type', 'get_value', 'description', 'comment']
|
||||||
|
|
||||||
|
admin.site.register(Config, ConfigAdmin)
|
||||||
|
admin.site.register(SMSPolicy, SMSPolicyAdmin)
|
||||||
|
|
|
||||||
31
apps/core/decorators.py
Normal file
31
apps/core/decorators.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from apps.core.models import Config, ConfigValueChoices
|
||||||
|
from utils.exceptions import ServiceUnavailable
|
||||||
|
|
||||||
|
|
||||||
|
def service_availability(key:str):
|
||||||
|
def with_params(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(self, request, *args, **kwargs):
|
||||||
|
print(key)
|
||||||
|
print(f"Executing {view_func.__name__} action")
|
||||||
|
|
||||||
|
service_is_available = Config.get_value_of(f"SERVICE_IS_AVAILABLE_{key.upper()}", "True", ConfigValueChoices.BOOLEAN)
|
||||||
|
config = Config.objects.get(key=f"SERVICE_IS_AVAILABLE_{key.upper()}")
|
||||||
|
if not service_is_available:
|
||||||
|
raise ServiceUnavailable(
|
||||||
|
detail={
|
||||||
|
# "code": 'service_unavailable_at_now',
|
||||||
|
# "timestamp": timezone.now().isoformat(),
|
||||||
|
"message": config.description or "این سرویس در حال حاضر در دسترس نیست"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return view_func(self, request, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return with_params
|
||||||
31
apps/core/migrations/0001_initial.py
Normal file
31
apps/core/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 6.0.2 on 2026-03-16 12:23
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.OAUTH2_PROVIDER_APPLICATION_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SMSPolicy',
|
||||||
|
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)),
|
||||||
|
('preferred', models.CharField(choices=[('fake', 'fake'), ('payam_sms', 'payam_sms'), ('mobin_sms', 'mobin_sms')], default='fake', max_length=16)),
|
||||||
|
('application', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.OAUTH2_PROVIDER_APPLICATION_MODEL, unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
30
apps/core/migrations/0002_config.py
Normal file
30
apps/core/migrations/0002_config.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 6.0.2 on 2026-04-06 05:59
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Config',
|
||||||
|
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)),
|
||||||
|
('key', models.CharField(db_index=True, max_length=255, unique=True, verbose_name='key')),
|
||||||
|
('value', models.TextField(blank=True, null=True, verbose_name='value')),
|
||||||
|
('value_type', models.CharField(choices=[('INT', 'Integer'), ('FLOAT', 'Float'), ('BOOLEAN', 'Boolean'), ('STRING', 'String'), ('DATE', 'Date'), ('DATETIME', 'DateTime'), ('JSON', 'JSON')], max_length=64)),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='description')),
|
||||||
|
('comment', models.TextField(blank=True, null=True, verbose_name='comment')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,2 +1,106 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db.models import TextChoices
|
||||||
|
|
||||||
|
from apps.gooyal_oauth2.settings import oauth2_settings
|
||||||
|
from utils.clients.sms.fake.client import FakeClient
|
||||||
|
from utils.clients.sms.mobin_sms.client import MobinSMSClient
|
||||||
|
from utils.clients.sms.payam_sms.client import PayamSMSClient
|
||||||
|
from utils.clients.sms.sms import BaseSMSClient
|
||||||
|
from utils.models import BaseModel
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class SMSClienChoises(TextChoices):
|
||||||
|
FAKE = "fake", _("fake")
|
||||||
|
PAYAM_SMS = 'payam_sms', _('payam_sms')
|
||||||
|
MOBIN_SMS = 'mobin_sms', _('mobin_sms')
|
||||||
|
|
||||||
|
|
||||||
|
class SMSPolicy(BaseModel):
|
||||||
|
application = models.ForeignKey(oauth2_settings.APPLICATION_MODEL, on_delete=models.PROTECT,
|
||||||
|
related_name='+', null=True, blank=True, unique=True)
|
||||||
|
preferred = models.CharField(max_length=16, choices=SMSClienChoises.choices, default=SMSClienChoises.FAKE)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.get_preferred_display()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_client() -> BaseSMSClient:
|
||||||
|
policy = SMSPolicy.objects.get_or_create(application=None)[0]
|
||||||
|
if policy.preferred == SMSClienChoises.MOBIN_SMS:
|
||||||
|
return MobinSMSClient()
|
||||||
|
elif policy.preferred == SMSClienChoises.PAYAM_SMS:
|
||||||
|
return PayamSMSClient()
|
||||||
|
else:
|
||||||
|
return FakeClient()
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigValueChoices(models.TextChoices):
|
||||||
|
INT = 'INT', _('Integer')
|
||||||
|
FLOAT = 'FLOAT', _('Float')
|
||||||
|
BOOLEAN = 'BOOLEAN', _('Boolean')
|
||||||
|
STRING = 'STRING', _('String')
|
||||||
|
DATE = 'DATE', _('Date')
|
||||||
|
DATETIME = 'DATETIME', _('DateTime')
|
||||||
|
JSON = 'JSON', _('JSON')
|
||||||
|
|
||||||
|
|
||||||
|
class Config(BaseModel):
|
||||||
|
key = models.CharField(_('key'), max_length=255, unique=True, db_index=True)
|
||||||
|
value = models.TextField(_('value'), null=True, blank=True)
|
||||||
|
value_type = models.CharField(choices=ConfigValueChoices.choices, max_length=64)
|
||||||
|
description = models.TextField(_('description'), null=True, blank=True)
|
||||||
|
comment = models.TextField(_('comment'), null=True, blank=True)
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return Config.get_value_of(self.key)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def type_cast(value, value_type):
|
||||||
|
|
||||||
|
try:
|
||||||
|
if value_type == ConfigValueChoices.STRING.value:
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
elif value_type == ConfigValueChoices.BOOLEAN.value:
|
||||||
|
value = value.lower() == 'true'
|
||||||
|
|
||||||
|
elif value_type == ConfigValueChoices.DATE.value:
|
||||||
|
value = datetime.fromisoformat(value)
|
||||||
|
|
||||||
|
elif value_type == ConfigValueChoices.DATETIME.value:
|
||||||
|
value = datetime.fromisoformat(value)
|
||||||
|
|
||||||
|
elif value_type == ConfigValueChoices.JSON.value:
|
||||||
|
value = json.loads(value)
|
||||||
|
|
||||||
|
elif value_type == ConfigValueChoices.INT.value:
|
||||||
|
value = int(value)
|
||||||
|
|
||||||
|
elif value_type == ConfigValueChoices.FLOAT.value:
|
||||||
|
value = float(value)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
value = None
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_value_of(key, default=None, casting_type=None):
|
||||||
|
# TODO: use cache
|
||||||
|
config, created = Config.objects.get_or_create(key=key, defaults={'value': default})
|
||||||
|
|
||||||
|
if config.value is None:
|
||||||
|
return default
|
||||||
|
|
||||||
|
value = Config.type_cast(config.value, casting_type or config.value_type)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
|
||||||
9
apps/core/serializers.py
Normal file
9
apps/core/serializers.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from .models import Config
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = Config
|
||||||
|
fields = ['key', 'value', 'value_type']
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from .views import HomeView
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
|
from .views import HomeView, TestIpView, HealthcheckView, StatusView
|
||||||
|
|
||||||
app_name = "core"
|
app_name = "core"
|
||||||
|
|
||||||
|
router = DefaultRouter()
|
||||||
|
|
||||||
|
# router.register('api/config', ConfigViewSet, basename='configs')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', HomeView.as_view(), name='home'),
|
path('', HomeView.as_view(), name='home'),
|
||||||
|
path('test-ip', TestIpView.as_view(), name='test-ip'),
|
||||||
|
path('status', StatusView.as_view(), name='status'),
|
||||||
|
path('healthcheck', HealthcheckView.as_view(), name='healthcheck'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,82 @@ 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
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
|
from django.contrib.sessions.models import Session
|
||||||
|
from rest_framework import mixins
|
||||||
|
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.core.decorators import service_availability
|
||||||
|
from apps.core.models import Config
|
||||||
|
from apps.core.serializers import ConfigSerializer
|
||||||
|
from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements
|
||||||
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
# @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'
|
||||||
|
|
||||||
|
# def get(self, request, *args, **kwargs):
|
||||||
|
# session = request.session
|
||||||
|
# print(type(session))
|
||||||
|
# print(session.session_key)
|
||||||
|
# return super(HomeView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class TestIpView(TemplateView):
|
||||||
|
template_name = 'core/home.html'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
print(request.headers)
|
||||||
|
headers_data = {'Host': 'accounts.gooyal.ir',
|
||||||
|
'X-Real-Ip': '212.23.216.131',
|
||||||
|
'X-Forwarded-For': '5.216.121.72, 212.23.216.131',
|
||||||
|
'X-Forwarded-Proto': 'http',
|
||||||
|
'Connection': 'close',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en,en-US;q=0.7,fa;q=0.3',
|
||||||
|
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
||||||
|
'Cookie': 'csrftoken=xor4RVhDDuhQ5l4hf44iI3o2I0FJU8kJ; sessionid=3c0kju13bswh6ufu3mdnl1u38wi7d4b3',
|
||||||
|
'Upgrade-Insecure-Requests': '1',
|
||||||
|
'Sec-Fetch-Dest': 'document',
|
||||||
|
'Sec-Fetch-Mode': 'navigate',
|
||||||
|
'Sec-Fetch-Site': 'none',
|
||||||
|
'Sec-Fetch-User': '?1',
|
||||||
|
'Priority': 'u=0, i'}
|
||||||
|
|
||||||
|
return super(TestIpView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigViewSet(mixins.ListModelMixin, GenericViewSet):
|
||||||
|
queryset = Config.objects.all()
|
||||||
|
serializer_class = ConfigSerializer
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
|
||||||
|
class StatusView(APIView):
|
||||||
|
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||||
|
required_alternate_scopes = {
|
||||||
|
"GET": [["accounts.status:get"]],
|
||||||
|
}
|
||||||
|
|
||||||
|
@service_availability('status')
|
||||||
|
def get(self, request, format=None):
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"status": "ok",
|
||||||
|
}
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
|
||||||
|
class HealthcheckView(APIView):
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
def get(self, request, format=None):
|
||||||
|
data = {
|
||||||
|
"is_healthy": True
|
||||||
|
}
|
||||||
|
return Response(data)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from django.contrib.admin.sites import NotRegistered
|
||||||
|
|
||||||
from oauth2_provider.admin import ApplicationAdmin
|
from oauth2_provider.admin import ApplicationAdmin
|
||||||
|
|
||||||
from .models import Application, Resource, Scope
|
from .models import Application, Scope
|
||||||
from .forms import ApplicationForm
|
from .forms import ApplicationForm
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,14 +23,9 @@ class ApplicationAdmin(ApplicationAdmin):
|
||||||
form = ApplicationForm
|
form = ApplicationForm
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Resource)
|
|
||||||
class ResourceAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("name", "user", "expires")
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Scope)
|
@admin.register(Scope)
|
||||||
class ScopeAdmin(admin.ModelAdmin):
|
class ScopeAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', "resource", 'description', 'is_default')
|
list_display = ('name', 'description', 'is_default')
|
||||||
|
|
||||||
|
|
||||||
# admin.site.register(RestrictedApplication, RestrictedApplicationAdmin)
|
# admin.site.register(RestrictedApplication, RestrictedApplicationAdmin)
|
||||||
|
|
|
||||||
2
apps/gooyal_oauth2/decorators.py
Normal file
2
apps/gooyal_oauth2/decorators.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
def session_limit_count(count=0):
|
||||||
|
pass
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
# Generated by Django 5.1.4 on 2025-11-20 09:05
|
||||||
|
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gooyal_oauth2', '0004_application_avatar'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='refreshtoken',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='application',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='application',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='grant',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='grant',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='refreshtoken',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='refreshtoken',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='resource',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='resource',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='scope',
|
||||||
|
name='created_at',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, db_index=True, default=django.utils.timezone.now),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='scope',
|
||||||
|
name='updated_at',
|
||||||
|
field=models.DateTimeField(auto_now=True, db_index=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='application',
|
||||||
|
name='uuid',
|
||||||
|
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='grant',
|
||||||
|
name='uuid',
|
||||||
|
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='refreshtoken',
|
||||||
|
name='uuid',
|
||||||
|
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='resource',
|
||||||
|
name='uuid',
|
||||||
|
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='scope',
|
||||||
|
name='uuid',
|
||||||
|
field=models.UUIDField(db_index=True, default=uuid.uuid4, primary_key=True, serialize=False, unique=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Generated by Django 6.0.2 on 2026-04-05 13:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gooyal_oauth2', '0005_alter_refreshtoken_unique_together_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='scope',
|
||||||
|
name='resource',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='application',
|
||||||
|
name='resource',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='application',
|
||||||
|
name='max_allowed_session',
|
||||||
|
field=models.PositiveIntegerField(default=0),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='application',
|
||||||
|
name='authorization_grant_type',
|
||||||
|
field=models.CharField(choices=[('authorization-code', 'Authorization code'), ('urn:ietf:params:oauth:grant-type:device_code', 'Device Code'), ('implicit', 'Implicit'), ('password', 'Resource owner password-based'), ('client-credentials', 'Client credentials'), ('openid-hybrid', 'OpenID connect hybrid')], max_length=44),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='Resource',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -16,33 +16,19 @@ from django.conf import settings
|
||||||
from django.db import models, router, transaction
|
from django.db import models, router, transaction
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from utils.models import BaseModel
|
||||||
from .settings import oauth2_settings
|
from .settings import oauth2_settings
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Resource(models.Model):
|
class Application(AbstractApplication, BaseModel):
|
||||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
|
|
||||||
user = models.ForeignKey(
|
|
||||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True,
|
|
||||||
related_name="resources"
|
|
||||||
)
|
|
||||||
expires = models.DateTimeField()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Application(AbstractApplication):
|
|
||||||
"""
|
"""
|
||||||
Application model for use with Django OAuth Toolkit that allows the scopes
|
Application model for use with Django OAuth Toolkit that allows the scopes
|
||||||
available to an application to be restricted on a per-application basis.
|
available to an application to be restricted on a per-application basis.
|
||||||
"""
|
"""
|
||||||
id=None
|
id = None
|
||||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
|
|
||||||
name = models.CharField(max_length=255, blank=False, unique=True)
|
name = models.CharField(max_length=255, blank=False, unique=True)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
|
|
@ -50,15 +36,10 @@ class Application(AbstractApplication):
|
||||||
on_delete=models.PROTECT
|
on_delete=models.PROTECT
|
||||||
)
|
)
|
||||||
allowed_scope = models.TextField(blank=True)
|
allowed_scope = models.TextField(blank=True)
|
||||||
resource = models.OneToOneField(
|
|
||||||
Resource,
|
|
||||||
models.PROTECT,
|
|
||||||
blank=True, null=True,
|
|
||||||
help_text='The resource of application.',
|
|
||||||
related_name='application'
|
|
||||||
)
|
|
||||||
|
|
||||||
avatar = models.ImageField(upload_to='avatars', null=True, blank=True)
|
avatar = models.ImageField(upload_to='avatars', null=True, blank=True)
|
||||||
|
max_allowed_session = models.PositiveIntegerField(default=0)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def allowed_scopes(self):
|
def allowed_scopes(self):
|
||||||
|
|
@ -74,14 +55,12 @@ class Application(AbstractApplication):
|
||||||
allowed_scopes = self.allowed_scopes
|
allowed_scopes = self.allowed_scopes
|
||||||
return Scope.objects.filter(name__in=allowed_scopes).order_by('name')
|
return Scope.objects.filter(name__in=allowed_scopes).order_by('name')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def allows_grant_type(self, *grant_types):
|
def allows_grant_type(self, *grant_types):
|
||||||
# Assume, for this example, that self.authorization_grant_type is set to self.GRANT_AUTHORIZATION_CODE
|
# Assume, for this example, that self.authorization_grant_type is set to self.GRANT_AUTHORIZATION_CODE
|
||||||
return bool(set([self.authorization_grant_type, self.GRANT_CLIENT_CREDENTIALS]) & set(grant_types))
|
return bool(set([self.authorization_grant_type, self.GRANT_CLIENT_CREDENTIALS]) & set(grant_types))
|
||||||
|
|
||||||
|
|
||||||
class Scope(models.Model):
|
class Scope(BaseModel):
|
||||||
"""
|
"""
|
||||||
Django model for an OAuth scope.
|
Django model for an OAuth scope.
|
||||||
"""
|
"""
|
||||||
|
|
@ -89,8 +68,6 @@ class Scope(models.Model):
|
||||||
# NOTE: This is not used to limit access to the scope in any way - we want the
|
# NOTE: This is not used to limit access to the scope in any way - we want the
|
||||||
# scope to be available to other applications in order to request access
|
# scope to be available to other applications in order to request access
|
||||||
# to the resource it protects!
|
# to the resource it protects!
|
||||||
id = None
|
|
||||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
|
|
||||||
application = models.ForeignKey(
|
application = models.ForeignKey(
|
||||||
oauth2_settings.APPLICATION_MODEL,
|
oauth2_settings.APPLICATION_MODEL,
|
||||||
models.CASCADE,
|
models.CASCADE,
|
||||||
|
|
@ -101,13 +78,6 @@ class Scope(models.Model):
|
||||||
help_text='The application to which the scope belongs.',
|
help_text='The application to which the scope belongs.',
|
||||||
related_name='scopes'
|
related_name='scopes'
|
||||||
)
|
)
|
||||||
resource = models.ForeignKey(
|
|
||||||
Resource,
|
|
||||||
models.PROTECT,
|
|
||||||
blank=True, null=True,
|
|
||||||
help_text='The resource of scope.',
|
|
||||||
related_name='scopes'
|
|
||||||
)
|
|
||||||
#: The name of the scope
|
#: The name of the scope
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
|
@ -127,19 +97,10 @@ class Scope(models.Model):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def final_name(self):
|
def final_name(self):
|
||||||
args = []
|
return self.name
|
||||||
if self.resource:
|
|
||||||
args.append(self.resource.name)
|
|
||||||
|
|
||||||
args.append(self.name)
|
|
||||||
return '.'.join(args)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def final_description(self):
|
def final_description(self):
|
||||||
resource_name = self.resource and self.resource.name
|
|
||||||
|
|
||||||
if resource_name:
|
|
||||||
return f"{resource_name} -> {self.description}"
|
|
||||||
return self.description
|
return self.description
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -177,17 +138,15 @@ class Scope(models.Model):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Grant(AbstractGrant):
|
class Grant(AbstractGrant, BaseModel):
|
||||||
id = None
|
id = None
|
||||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
|
|
||||||
application = models.ForeignKey(
|
application = models.ForeignKey(
|
||||||
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='grants'
|
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='grants'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class RefreshToken(AbstractRefreshToken):
|
class RefreshToken(BaseModel, AbstractRefreshToken):
|
||||||
id = None
|
id = None
|
||||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
|
|
||||||
application = models.ForeignKey(
|
application = models.ForeignKey(
|
||||||
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='refresh_tokens')
|
oauth2_settings.APPLICATION_MODEL, on_delete=models.CASCADE, related_name='refresh_tokens')
|
||||||
|
|
||||||
|
|
@ -215,7 +174,6 @@ class RefreshToken(AbstractRefreshToken):
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(AbstractAccessToken):
|
class AccessToken(AbstractAccessToken):
|
||||||
id = None
|
id = None
|
||||||
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
|
uuid = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4, unique=True, db_index=True)
|
||||||
|
|
@ -240,7 +198,7 @@ def clear_expired():
|
||||||
flat_queryset = queryset.values_list("pk", flat=True)[:CLEAR_EXPIRED_TOKENS_BATCH_SIZE]
|
flat_queryset = queryset.values_list("pk", flat=True)[:CLEAR_EXPIRED_TOKENS_BATCH_SIZE]
|
||||||
batch_length = flat_queryset.count()
|
batch_length = flat_queryset.count()
|
||||||
queryset.model.objects.filter(pk__in=list(flat_queryset)).delete()
|
queryset.model.objects.filter(pk__in=list(flat_queryset)).delete()
|
||||||
logger.debug(f"{batch_length} tokens deleted, {current_no-batch_length} left")
|
logger.debug(f"{batch_length} tokens deleted, {current_no - batch_length} left")
|
||||||
queryset = queryset.model.objects.filter(query)
|
queryset = queryset.model.objects.filter(query)
|
||||||
time.sleep(CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL)
|
time.sleep(CLEAR_EXPIRED_TOKENS_BATCH_INTERVAL)
|
||||||
current_no = queryset.count()
|
current_no = queryset.count()
|
||||||
|
|
@ -298,4 +256,3 @@ def clear_expired():
|
||||||
|
|
||||||
grants_deleted_no = batch_delete(grants, grants_query)
|
grants_deleted_no = batch_delete(grants, grants_query)
|
||||||
logger.info("%s Expired grant tokens deleted", grants_deleted_no)
|
logger.info("%s Expired grant tokens deleted", grants_deleted_no)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class Scopes(BaseScopes):
|
||||||
def get_queryset(self, application=None):
|
def get_queryset(self, application=None):
|
||||||
queryset = Scope.objects.all()
|
queryset = Scope.objects.all()
|
||||||
if application:
|
if application:
|
||||||
queryset = queryset.filter(name__in=application.allowed_scopes).order_by('resource__uuid')
|
queryset = queryset.filter(name__in=application.allowed_scopes)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
212
apps/gooyal_oauth2/tests.py
Normal file
212
apps/gooyal_oauth2/tests.py
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
|
from unicodedata import category
|
||||||
|
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
|
||||||
|
class Tmp():
|
||||||
|
uuid = sys_uuid.uuid4()
|
||||||
|
|
||||||
|
data = Tmp()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class GooyalOAuth2Tests(APITestCase):
|
||||||
|
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')
|
||||||
|
client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||||
|
client_secret = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||||
|
|
||||||
|
visitor_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005a')
|
||||||
|
|
||||||
|
expire_datetime = timezone.now() + timedelta(seconds=3600)
|
||||||
|
expire_datetime.isoformat()
|
||||||
|
client = APIClient()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from django.conf import settings
|
||||||
|
settings.SMS_SEND = False
|
||||||
|
|
||||||
|
self.notifications_push_user_success_patcher = patch('apps.users.models.User.notify',
|
||||||
|
mock_notifications_push_user_success)
|
||||||
|
|
||||||
|
self.notifications_push_user_success_patcher.start()
|
||||||
|
self.user_phone_number = '+989100000000'
|
||||||
|
self.user = User.objects.create(pk=self.user_uuid, phone_number=self.user_phone_number)
|
||||||
|
|
||||||
|
scope = Scope.objects.create(name='accounts.status:get', description='accounts.status:get')
|
||||||
|
|
||||||
|
self.application = Application.objects.create(
|
||||||
|
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
authorization_grant_type='password',
|
||||||
|
hash_client_secret=False,
|
||||||
|
uuid=self.application_uuid,
|
||||||
|
user_id=self.user_uuid,
|
||||||
|
max_allowed_session=1,
|
||||||
|
allowed_scope='accounts.status:get',
|
||||||
|
)
|
||||||
|
|
||||||
|
# self.sys_date_patcher = patch('simata_safte.models.get_sys_date', mock_get_sys_date)
|
||||||
|
# self.set_id_patcher = patch('simata_safte.models.set_id', mock_set_id)
|
||||||
|
# self.sign_pdf_patcher = patch('simata_safte.models.sign_pdf', mock_sign_pdf)
|
||||||
|
# self.check_pdf_signature_patcher = patch('simata_safte.models.check_pdf_signature', mock_check_pdf_signature)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _create_authorization_header(self, token):
|
||||||
|
return "Bearer {0}".format(token)
|
||||||
|
|
||||||
|
def test_authentication_allow(self):
|
||||||
|
access_token_1 = AccessToken.objects.create(
|
||||||
|
**{
|
||||||
|
"token": self.access_token_1,
|
||||||
|
"user": self.user,
|
||||||
|
# "client_id": self.client_id,
|
||||||
|
# "client_owner": owner,
|
||||||
|
"application_id": self.application_uuid,
|
||||||
|
"scope": 'accounts.status:get',
|
||||||
|
"expires": self.expire_datetime.isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_1 = self._create_authorization_header(access_token_1.token)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_1)
|
||||||
|
|
||||||
|
self.assertContains(response, 'status')
|
||||||
|
|
||||||
|
access_token_2 = AccessToken.objects.create(
|
||||||
|
**{
|
||||||
|
"token": self.access_token_2,
|
||||||
|
"user": self.user,
|
||||||
|
|
||||||
|
# "client_id": self.client_id,
|
||||||
|
# "client_owner": owner,
|
||||||
|
"application_id": self.application_uuid,
|
||||||
|
"scope": 'accounts.status:get',
|
||||||
|
"expires": self.expire_datetime.isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_2 = self._create_authorization_header(access_token_2.token)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_1)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_2)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
|
||||||
|
|
||||||
|
self.application.max_allowed_session = 0
|
||||||
|
self.application.save()
|
||||||
|
self.application.refresh_from_db()
|
||||||
|
|
||||||
|
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_2)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def basic_auth_string(self, username, password):
|
||||||
|
"""ساخت Basic Auth string"""
|
||||||
|
import base64
|
||||||
|
user_pass = f"{username}:{password}"
|
||||||
|
basic_credentials = base64.b64encode(user_pass.encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
return basic_credentials
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
self.user.set_otp()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"grant_type": "password",
|
||||||
|
"username": self.user_phone_number,
|
||||||
|
"password": '77501',
|
||||||
|
"scope": 'accounts.status:get',
|
||||||
|
"auth_fields": 'phone_number:otp'
|
||||||
|
}
|
||||||
|
self.client.credentials(
|
||||||
|
HTTP_AUTHORIZATION='Basic ' + self.basic_auth_string(self.client_id, self.client_secret)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
||||||
|
access_token = result.json()['access_token']
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + access_token)
|
||||||
|
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def test_loginByOTP_allOK_success(self):
|
||||||
|
print(self.login())
|
||||||
|
response = self.client.get(reverse("core:status"))
|
||||||
|
print(response.status_code)
|
||||||
|
|
||||||
|
def test_revoke_token(self):
|
||||||
|
self.user.set_otp()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"grant_type": "password",
|
||||||
|
"username": self.user_phone_number,
|
||||||
|
"password": '77501',
|
||||||
|
"scope": 'accounts.status:get',
|
||||||
|
"auth_fields": 'phone_number:otp'
|
||||||
|
}
|
||||||
|
self.client.credentials(
|
||||||
|
HTTP_AUTHORIZATION='Basic ' + self.basic_auth_string(self.client_id, self.client_secret)
|
||||||
|
)
|
||||||
|
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
||||||
|
access_token = result.json()['access_token']
|
||||||
|
|
||||||
|
revoke_data = {
|
||||||
|
"token": access_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.client.post(reverse("gooyal_oauth2:revoke-token"), data=revoke_data)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assertEqual(AccessToken.objects.count(), 0)
|
||||||
|
|
||||||
|
self.user.refresh_from_db()
|
||||||
|
self.user.set_otp()
|
||||||
|
self.client.credentials(
|
||||||
|
HTTP_AUTHORIZATION='Basic ' + self.basic_auth_string(self.client_id, self.client_secret)
|
||||||
|
)
|
||||||
|
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
||||||
|
access_token = result.json()['access_token']
|
||||||
|
access_token_object = AccessToken.objects.first()
|
||||||
|
revoke_data = {
|
||||||
|
"token": access_token_object.pk,
|
||||||
|
}
|
||||||
|
result = self.client.post(reverse("gooyal_oauth2:revoke-token"), data=revoke_data)
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
self.assertEqual(AccessToken.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
47
apps/gooyal_oauth2/throttling.py
Normal file
47
apps/gooyal_oauth2/throttling.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from rest_framework.throttling import UserRateThrottle
|
||||||
|
|
||||||
|
from apps.gooyal_oauth2.models import AccessToken
|
||||||
|
|
||||||
|
|
||||||
|
def get_application(request):
|
||||||
|
try:
|
||||||
|
application = request.auth.application
|
||||||
|
except:
|
||||||
|
application = None
|
||||||
|
return application
|
||||||
|
|
||||||
|
|
||||||
|
class TokenLimitThrottle:
|
||||||
|
def allow_request(self, request, view):
|
||||||
|
application = get_application(request)
|
||||||
|
|
||||||
|
if application:
|
||||||
|
if hasattr(view, "max_allowed_session"):
|
||||||
|
max_allowed_session = view.max_allowed_session
|
||||||
|
else:
|
||||||
|
max_allowed_session = application.max_allowed_session
|
||||||
|
|
||||||
|
if max_allowed_session:
|
||||||
|
session_count = AccessToken.objects.filter(application=application, user=request.user).count()
|
||||||
|
if max_allowed_session >= session_count:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
from utils.exceptions import Conflict
|
||||||
|
raise Conflict(code='max_allowed_session_reached')
|
||||||
|
|
||||||
|
active_tokens = AccessToken.objects.filter(
|
||||||
|
application=application, user=request.user
|
||||||
|
).order_by("created")[:max_allowed_session].values_list("token", flat=True)
|
||||||
|
if request.auth.token in active_tokens:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
from utils.exceptions import Conflict
|
||||||
|
raise Conflict(code='max_allowed_session_reached')
|
||||||
|
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
pass
|
||||||
|
|
@ -3,15 +3,17 @@ from django.urls import re_path, path
|
||||||
from oauth2_provider import views
|
from oauth2_provider import views
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from .views.introspect import IntrospectTokenView, IntrospectApplicationView
|
from .views.oauth_views import IntrospectTokenView, IntrospectApplicationView, TokenView, GooyalRevokeTokenView
|
||||||
from .views import apis as api_views
|
from .views import apis as api_views
|
||||||
from .views import pages
|
from .views import pages
|
||||||
app_name = "gooyal_oauth2"
|
app_name = "gooyal_oauth2"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
base_urlpatterns = [
|
base_urlpatterns = [
|
||||||
re_path(r"^authorize/$", views.AuthorizationView.as_view(), name="authorize"),
|
re_path(r"^authorize/$", views.AuthorizationView.as_view(), name="authorize"),
|
||||||
re_path(r"^token/$", views.TokenView.as_view(), name="token"),
|
re_path(r"^token/$", TokenView.as_view(), name="token"),
|
||||||
re_path(r"^revoke_token/$", views.RevokeTokenView.as_view(), name="revoke-token"),
|
re_path(r"^revoke_token/$", views.RevokeTokenView.as_view(), name="revoke-token"),
|
||||||
re_path(r"^introspect/$", IntrospectTokenView.as_view(), name="introspect"),
|
re_path(r"^introspect/$", IntrospectTokenView.as_view(), name="introspect"),
|
||||||
re_path(r"^introspect_application/$", IntrospectApplicationView.as_view(), name="introspect-application"),
|
re_path(r"^introspect_application/$", IntrospectApplicationView.as_view(), name="introspect-application"),
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,34 @@
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from urllib.parse import unquote_plus
|
from urllib.parse import unquote_plus
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
# import service_clients
|
# import service_clients
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from oauth2_provider.models import get_access_token_model
|
from oauth2_provider.exceptions import FatalClientError
|
||||||
|
from oauth2_provider.models import get_access_token_model, get_application_model, get_id_token_model, get_grant_model, \
|
||||||
|
get_refresh_token_model
|
||||||
from oauth2_provider.oauth2_validators import OAuth2Validator as BaseOAuth2Validator
|
from oauth2_provider.oauth2_validators import OAuth2Validator as BaseOAuth2Validator
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
from utils.exceptions import ServiceUnavailable
|
||||||
from .settings import oauth2_settings
|
from .settings import oauth2_settings
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import router, transaction
|
||||||
|
|
||||||
log = logging.getLogger("oauth2_provider")
|
log = logging.getLogger("oauth2_provider")
|
||||||
|
|
||||||
AccessTokenModel = get_access_token_model()
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
|
Application = get_application_model()
|
||||||
|
AccessToken = get_access_token_model()
|
||||||
|
IDToken = get_id_token_model()
|
||||||
|
Grant = get_grant_model()
|
||||||
|
RefreshToken = get_refresh_token_model()
|
||||||
|
|
||||||
|
|
||||||
class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
|
class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
|
||||||
|
|
@ -77,3 +89,199 @@ class OAuth2Validator(BaseOAuth2Validator): # pylint: disable=w0223
|
||||||
|
|
||||||
def get_oidc_issuer_endpoint(self, request):
|
def get_oidc_issuer_endpoint(self, request):
|
||||||
return oauth2_settings.oidc_issuer(request)
|
return oauth2_settings.oidc_issuer(request)
|
||||||
|
|
||||||
|
def save_token(self, token, request, *args, **kwargs):
|
||||||
|
"""Persist the token with a token type specific method.
|
||||||
|
|
||||||
|
Currently, only save_bearer_token is supported.
|
||||||
|
|
||||||
|
:param token: A (Bearer) token dict.
|
||||||
|
:param request: OAuthlib request.
|
||||||
|
:type request: oauthlib.common.Request
|
||||||
|
"""
|
||||||
|
return self.save_bearer_token(token, request, *args, **kwargs)
|
||||||
|
|
||||||
|
def save_bearer_token(self, token, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Save access and refresh token.
|
||||||
|
|
||||||
|
Override _save_bearer_token and not this function when adding custom logic
|
||||||
|
for the storing of these token. This allows the transaction logic to be
|
||||||
|
separate from the token handling.
|
||||||
|
"""
|
||||||
|
# Use the AccessToken's database instead of making the assumption it is in 'default'.
|
||||||
|
with transaction.atomic(using=router.db_for_write(AccessToken)):
|
||||||
|
return self._save_bearer_token(token, request, *args, **kwargs)
|
||||||
|
|
||||||
|
def _save_bearer_token(self, token, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Save access and refresh token.
|
||||||
|
|
||||||
|
If refresh token is issued, remove or reuse old refresh token as in rfc:`6`.
|
||||||
|
|
||||||
|
@see: https://rfc-editor.org/rfc/rfc6749.html#section-6
|
||||||
|
"""
|
||||||
|
|
||||||
|
if "scope" not in token:
|
||||||
|
raise FatalClientError("Failed to renew access token: missing scope")
|
||||||
|
|
||||||
|
# expires_in is passed to Server on initialization
|
||||||
|
# custom server class can have logic to override this
|
||||||
|
expires = timezone.now() + timedelta(
|
||||||
|
seconds=token.get(
|
||||||
|
"expires_in",
|
||||||
|
oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.grant_type == "client_credentials":
|
||||||
|
request.user = None
|
||||||
|
|
||||||
|
# This comes from OAuthLib:
|
||||||
|
# https://github.com/idan/oauthlib/blob/1.0.3/oauthlib/oauth2/rfc6749/tokens.py#L267
|
||||||
|
# Its value is either a new random code; or if we are reusing
|
||||||
|
# refresh tokens, then it is the same value that the request passed in
|
||||||
|
# (stored in `request.refresh_token`)
|
||||||
|
refresh_token_code = token.get("refresh_token", None)
|
||||||
|
|
||||||
|
if refresh_token_code:
|
||||||
|
# an instance of `RefreshToken` that matches the old refresh code.
|
||||||
|
# Set on the request in `validate_refresh_token`
|
||||||
|
refresh_token_instance = getattr(request, "refresh_token_instance", None)
|
||||||
|
|
||||||
|
# If we are to reuse tokens, and we can: do so
|
||||||
|
if (
|
||||||
|
not self.rotate_refresh_token(request)
|
||||||
|
and isinstance(refresh_token_instance, RefreshToken)
|
||||||
|
and refresh_token_instance.access_token
|
||||||
|
):
|
||||||
|
access_token = AccessToken.objects.select_for_update().get(
|
||||||
|
pk=refresh_token_instance.access_token.pk
|
||||||
|
)
|
||||||
|
access_token.user = request.user
|
||||||
|
access_token.scope = token["scope"]
|
||||||
|
access_token.expires = expires
|
||||||
|
access_token.token = token["access_token"]
|
||||||
|
access_token.application = request.client
|
||||||
|
access_token.save()
|
||||||
|
|
||||||
|
# else create fresh with access & refresh tokens
|
||||||
|
else:
|
||||||
|
# revoke existing tokens if possible to allow reuse of grant
|
||||||
|
if isinstance(refresh_token_instance, RefreshToken):
|
||||||
|
# First, to ensure we don't have concurrency issues, we refresh the refresh token
|
||||||
|
# from the db while acquiring a lock on it
|
||||||
|
# We also put it in the "request cache"
|
||||||
|
refresh_token_instance = RefreshToken.objects.select_for_update().get(
|
||||||
|
pk=refresh_token_instance.pk
|
||||||
|
)
|
||||||
|
request.refresh_token_instance = refresh_token_instance
|
||||||
|
|
||||||
|
previous_access_token = AccessToken.objects.filter(
|
||||||
|
source_refresh_token=refresh_token_instance
|
||||||
|
).first()
|
||||||
|
try:
|
||||||
|
refresh_token_instance.revoke()
|
||||||
|
except (AccessToken.DoesNotExist, RefreshToken.DoesNotExist):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
setattr(request, "refresh_token_instance", None)
|
||||||
|
else:
|
||||||
|
previous_access_token = None
|
||||||
|
|
||||||
|
# If the refresh token has already been used to create an
|
||||||
|
# access token (ie it's within the grace period), return that
|
||||||
|
# access token
|
||||||
|
if not previous_access_token:
|
||||||
|
access_token = self._create_access_token(
|
||||||
|
expires,
|
||||||
|
request,
|
||||||
|
token,
|
||||||
|
source_refresh_token=refresh_token_instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._create_refresh_token(
|
||||||
|
request, refresh_token_code, access_token, refresh_token_instance
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# make sure that the token data we're returning matches
|
||||||
|
# the existing token
|
||||||
|
token["access_token"] = previous_access_token.token
|
||||||
|
token["refresh_token"] = (
|
||||||
|
RefreshToken.objects.filter(access_token=previous_access_token).first().token
|
||||||
|
)
|
||||||
|
token["scope"] = previous_access_token.scope
|
||||||
|
|
||||||
|
# No refresh token should be created, just access token
|
||||||
|
else:
|
||||||
|
self._create_access_token(expires, request, token)
|
||||||
|
|
||||||
|
def _create_access_token(self, expires, request, token, source_refresh_token=None):
|
||||||
|
id_token = token.get("id_token", None)
|
||||||
|
if id_token:
|
||||||
|
id_token = self._load_id_token(id_token)
|
||||||
|
headers = {}
|
||||||
|
for header, value in dict(request.headers).items():
|
||||||
|
if type(value) in [str, bool, int, float, tuple, list]:
|
||||||
|
print(f'unserializable header: {header} -> {value}')
|
||||||
|
headers[header] = value
|
||||||
|
|
||||||
|
return AccessToken.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
scope=token["scope"],
|
||||||
|
expires=expires,
|
||||||
|
token=token["access_token"],
|
||||||
|
id_token=id_token,
|
||||||
|
application=request.client,
|
||||||
|
source_refresh_token=source_refresh_token,
|
||||||
|
detail={'headers': headers},
|
||||||
|
)
|
||||||
|
|
||||||
|
# def _get_token_from_authentication_server
|
||||||
|
# def validate_bearer_token(self, token, scopes, request):
|
||||||
|
# result = super().validate_bearer_token(token, scopes, request)
|
||||||
|
# if result:
|
||||||
|
# application = request.client
|
||||||
|
# if hasattr(request, "max_allowed_session"):
|
||||||
|
# max_allowed_session = request.max_allowed_session
|
||||||
|
# else:
|
||||||
|
# max_allowed_session = application.max_allowed_session
|
||||||
|
#
|
||||||
|
# if max_allowed_session:
|
||||||
|
# session_count = AccessToken.objects.filter(application=application, user=request.user).count()
|
||||||
|
# if max_allowed_session >= session_count:
|
||||||
|
# return result
|
||||||
|
# else:
|
||||||
|
# active_tokens = AccessToken.objects.filter(
|
||||||
|
# application=application, user=request.user
|
||||||
|
# ).order_by("created")[:max_allowed_session].values_list("token", flat=True)
|
||||||
|
# if token in active_tokens:
|
||||||
|
# return result
|
||||||
|
# else:
|
||||||
|
# raise ServiceUnavailable(code='max_allowed_session_reached')
|
||||||
|
#
|
||||||
|
# else:
|
||||||
|
# return result
|
||||||
|
|
||||||
|
def revoke_token(self, token, token_type_hint, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Revoke an access or refresh token.
|
||||||
|
|
||||||
|
:param token: The token string.
|
||||||
|
:param token_type_hint: access_token or refresh_token.
|
||||||
|
:param request: The HTTP Request (oauthlib.common.Request)
|
||||||
|
"""
|
||||||
|
if token_type_hint not in ["access_token", "refresh_token"]:
|
||||||
|
token_type_hint = None
|
||||||
|
|
||||||
|
token_types = {
|
||||||
|
"access_token": AccessToken,
|
||||||
|
"refresh_token": RefreshToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
token_type = token_types.get(token_type_hint, AccessToken)
|
||||||
|
|
||||||
|
try:
|
||||||
|
token_type.objects.get(pk=token).revoke()
|
||||||
|
except:
|
||||||
|
token_type.objects.get(token=token).revoke()
|
||||||
|
|
|
||||||
|
|
@ -31,3 +31,9 @@ class ApplicationViewSet(ReadOnlyModelViewSet):
|
||||||
else:
|
else:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
|
# @action(detail=True, permission_classes=[AllowAny])
|
||||||
|
# def tokens(self, request, pk=None):
|
||||||
|
# app: Application = self.get_object()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
|
from django.contrib.auth.decorators import login_not_required
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from oauth2_provider.models import get_access_token_model, get_application_model
|
from oauth2_provider.models import get_access_token_model, get_application_model
|
||||||
|
from oauth2_provider.signals import app_authorized
|
||||||
from oauth2_provider.views.generic import ClientProtectedScopedResourceView
|
from oauth2_provider.views.generic import ClientProtectedScopedResourceView
|
||||||
|
from oauth2_provider.views.mixins import OAuthLibMixin
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_exempt, name="dispatch")
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
|
|
@ -117,3 +121,63 @@ class IntrospectApplicationView(ClientProtectedScopedResourceView):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return self.get_application_response(request.POST.get("client_id", None))
|
return self.get_application_response(request.POST.get("client_id", None))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from django.views.decorators.debug import sensitive_post_parameters
|
||||||
|
from django.views.generic import FormView, View
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
|
@method_decorator(login_not_required, name="dispatch")
|
||||||
|
class TokenView(OAuthLibMixin, View):
|
||||||
|
"""
|
||||||
|
Implements an endpoint to provide access tokens
|
||||||
|
|
||||||
|
The endpoint is used in the following flows:
|
||||||
|
* Authorization code
|
||||||
|
* Password
|
||||||
|
* Client credentials
|
||||||
|
"""
|
||||||
|
|
||||||
|
@method_decorator(sensitive_post_parameters("password", "client_secret"))
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
url, headers, body, status = self.create_token_response(request)
|
||||||
|
content = json.loads(body)
|
||||||
|
for k,v in content.items():
|
||||||
|
_('Invalid credentials given.')
|
||||||
|
_('invalid_grant')
|
||||||
|
|
||||||
|
content[k] = _(v)
|
||||||
|
|
||||||
|
if status == 200:
|
||||||
|
access_token = json.loads(body).get("access_token")
|
||||||
|
if access_token is not None:
|
||||||
|
token_checksum = hashlib.sha256(access_token.encode("utf-8")).hexdigest()
|
||||||
|
token = get_access_token_model().objects.get(token_checksum=token_checksum)
|
||||||
|
app_authorized.send(sender=self, request=request, token=token)
|
||||||
|
response = HttpResponse(content=body, status=status)
|
||||||
|
|
||||||
|
for k, v in headers.items():
|
||||||
|
response[k] = v
|
||||||
|
return response
|
||||||
|
|
||||||
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
|
@method_decorator(login_not_required, name="dispatch")
|
||||||
|
class GooyalRevokeTokenView(OAuthLibMixin, View):
|
||||||
|
"""
|
||||||
|
Implements an endpoint to revoke access or refresh tokens
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
url, headers, body, status = self.create_revocation_response(request)
|
||||||
|
response = HttpResponse(content=body or "", status=status)
|
||||||
|
|
||||||
|
for k, v in headers.items():
|
||||||
|
response[k] = v
|
||||||
|
return response
|
||||||
|
|
@ -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):
|
||||||
|
|
@ -11,10 +11,7 @@ class UserAdmin(admin.ModelAdmin):
|
||||||
'first_name',
|
'first_name',
|
||||||
'last_name',
|
'last_name',
|
||||||
'username',
|
'username',
|
||||||
'email',
|
|
||||||
'phone_number',
|
'phone_number',
|
||||||
'password',
|
|
||||||
'otp',
|
|
||||||
'is_staff',
|
'is_staff',
|
||||||
'is_superuser',
|
'is_superuser',
|
||||||
'groups',
|
'groups',
|
||||||
|
|
@ -24,11 +21,28 @@ class UserAdmin(admin.ModelAdmin):
|
||||||
'last_update',
|
'last_update',
|
||||||
'otp_expire',
|
'otp_expire',
|
||||||
'otp_try',
|
'otp_try',
|
||||||
'balance',
|
|
||||||
]
|
]
|
||||||
readonly_fields = ['last_update', 'uuid']
|
readonly_fields = ['last_update', 'uuid']
|
||||||
list_display = ['pk', 'first_name', 'last_name', 'phone_number']
|
list_display = ['pk', 'date_joined', 'last_update']
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
MAX_OTP_TRY = 3
|
MAX_OTP_TRY = 3
|
||||||
DEVELOPMENT_PHONE_NUMBERS = ['+989999999999', '+989999999998']
|
DEVELOPMENT_PHONE_NUMBERS = []
|
||||||
MAX_OTP_VALID_DURATION = 120
|
MAX_OTP_VALID_DURATION = 120
|
||||||
|
|
||||||
class GenderChoices(models.TextChoices):
|
class GenderChoices(models.TextChoices):
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
import django_filters
|
import django_filters
|
||||||
from .models import User
|
from .models import User
|
||||||
|
|
||||||
|
class ListUUIDFilter(django_filters.BaseInFilter, django_filters.UUIDFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserFilter(django_filters.FilterSet):
|
class UserFilter(django_filters.FilterSet):
|
||||||
|
uuid_in = ListUUIDFilter(field_name='uuid', required=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = {
|
fields = {
|
||||||
'uuid': ['in'],
|
# 'uuid': ['in'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
60
apps/users/management/commands/send_sms_by_uuid_list.py
Normal file
60
apps/users/management/commands/send_sms_by_uuid_list.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
from apps.users.models import *
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "send sms by uuid list"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('message_file', type=str, help='Message as a file')
|
||||||
|
parser.add_argument('uuids_file', type=str, help='File with one UUID per line')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
print(options['message_file'])
|
||||||
|
print(options['uuids_file'])
|
||||||
|
message_file_path = options['message_file']
|
||||||
|
uuids_file_path = options['uuids_file']
|
||||||
|
|
||||||
|
if not os.path.exists(message_file_path):
|
||||||
|
raise CommandError(f'File "{message_file_path}" does not exist')
|
||||||
|
|
||||||
|
if not os.path.exists(uuids_file_path):
|
||||||
|
raise CommandError(f'File "{uuids_file_path}" does not exist')
|
||||||
|
|
||||||
|
uuids = []
|
||||||
|
invalid_lines = []
|
||||||
|
message: str = ''''''
|
||||||
|
|
||||||
|
with open(message_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
message = f.read()
|
||||||
|
|
||||||
|
with open(uuids_file_path, 'r', encoding='utf-8') as f:
|
||||||
|
for line_num, line in enumerate(f, 1):
|
||||||
|
line = line.strip()
|
||||||
|
if not line: # Skip empty lines
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
# Validate and normalize UUID
|
||||||
|
valid_uuid = str(uuid.UUID(line))
|
||||||
|
uuids.append(valid_uuid)
|
||||||
|
self.stdout.write(f'Line {line_num}: ✓ {valid_uuid}')
|
||||||
|
except (ValueError, AttributeError, TypeError):
|
||||||
|
invalid_lines.append((line_num, line))
|
||||||
|
self.stdout.write(
|
||||||
|
self.style.ERROR(f'Line {line_num}: ✗ Invalid UUID "{line}"')
|
||||||
|
)
|
||||||
|
|
||||||
|
sms_client = SMSPolicy.get_client()
|
||||||
|
users = User.objects.filter(uuid__in=uuids)
|
||||||
|
empty_phone_numbers = list(filter(lambda u: not u.phone_number, users))
|
||||||
|
if empty_phone_numbers:
|
||||||
|
logger.error(f'not exist phone numbers for following uuids')
|
||||||
|
print(empty_phone_numbers)
|
||||||
|
logger.error(f'end of not exist phone numbers')
|
||||||
|
return
|
||||||
|
for user in users:
|
||||||
|
logger.info(f'try to send message for: {user.uuid} and {user.phone_number}')
|
||||||
|
sms_client.send_sms(user.phone_number.strip('+'), message)
|
||||||
|
logger.info(f'sent message for: {user.uuid} and {user.phone_number}')
|
||||||
18
apps/users/migrations/0007_alter_user_otp.py
Normal file
18
apps/users/migrations/0007_alter_user_otp.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.1.4 on 2025-11-18 11:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0006_alter_user_avatar'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='otp',
|
||||||
|
field=models.CharField(blank=True, max_length=128, null=True, verbose_name='otp'),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
apps/users/migrations/0008_alter_user_options.py
Normal file
17
apps/users/migrations/0008_alter_user_options.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 6.0.2 on 2026-03-16 12:23
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0007_alter_user_otp'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='user',
|
||||||
|
options={'ordering': ['-date_joined'], 'verbose_name': 'user', 'verbose_name_plural': 'users'},
|
||||||
|
),
|
||||||
|
]
|
||||||
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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import logging
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
@ -5,6 +6,7 @@ from datetime import timedelta
|
||||||
# import service_clients
|
# import service_clients
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.base_user import BaseUserManager
|
from django.contrib.auth.base_user import BaseUserManager
|
||||||
|
from django.contrib.auth.hashers import make_password, check_password
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.contrib.auth.validators import UnicodeUsernameValidator
|
from django.contrib.auth.validators import UnicodeUsernameValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
@ -13,13 +15,16 @@ from django.utils.translation import gettext_lazy as _
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from django_minio_backend import MinioBackend, iso_date_prefix
|
from django_minio_backend import MinioBackend, iso_date_prefix
|
||||||
|
from apps.core.models import SMSPolicy
|
||||||
from utils.clients.payam_sms import send_sms
|
|
||||||
from .provinces_and_cities import state
|
from .provinces_and_cities import state
|
||||||
# from .tasks import send_notification
|
# from .tasks import send_notification
|
||||||
|
|
||||||
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__)
|
||||||
|
|
||||||
|
|
||||||
class UserManager(BaseUserManager):
|
class UserManager(BaseUserManager):
|
||||||
|
|
@ -34,7 +39,7 @@ class UserManager(BaseUserManager):
|
||||||
# TODO: validate phone number
|
# TODO: validate phone number
|
||||||
user = self.model(phone_number=phone_number, username=username, email=email, **extra_fields)
|
user = self.model(phone_number=phone_number, username=username, email=email, **extra_fields)
|
||||||
user.set_password(password)
|
user.set_password(password)
|
||||||
user.set_otp()
|
# user.set_otp()
|
||||||
user.date_joined = timezone.now()
|
user.date_joined = timezone.now()
|
||||||
user.save(using=self._db)
|
user.save(using=self._db)
|
||||||
|
|
||||||
|
|
@ -113,7 +118,7 @@ class User(AbstractUser):
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
phone_number = models.CharField(_('phone number'), max_length=30, blank=True, null=True, unique=True)
|
phone_number = models.CharField(_('phone number'), max_length=30, blank=True, null=True, unique=True)
|
||||||
otp = models.CharField(_('otp'), max_length=6, blank=True, null=True)
|
otp = models.CharField(_('otp'), max_length=128, blank=True, null=True)
|
||||||
otp_expire = models.DateTimeField(_('otp expire'), blank=True, null=True)
|
otp_expire = models.DateTimeField(_('otp expire'), blank=True, null=True)
|
||||||
otp_try = models.IntegerField(_('otp try'), blank=True, null=True, default=0)
|
otp_try = models.IntegerField(_('otp try'), blank=True, null=True, default=0)
|
||||||
|
|
||||||
|
|
@ -139,6 +144,12 @@ class User(AbstractUser):
|
||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("user")
|
||||||
|
verbose_name_plural = _("users")
|
||||||
|
ordering = ['-date_joined']
|
||||||
|
|
||||||
|
|
||||||
def get_display_name(self):
|
def get_display_name(self):
|
||||||
return str(self.name or self.get_full_name() or self.username or self.email or self.phone_number or self.pk)
|
return str(self.name or self.get_full_name() or self.username or self.email or self.phone_number or self.pk)
|
||||||
|
|
||||||
|
|
@ -151,16 +162,17 @@ class User(AbstractUser):
|
||||||
return state.get_province_by_id(self.province)
|
return state.get_province_by_id(self.province)
|
||||||
|
|
||||||
def set_otp(self):
|
def set_otp(self):
|
||||||
self.otp = '77501'
|
if not settings.SMS_SEND:
|
||||||
# if settings.DEBUG or self.phone_number in DEVELOPMENT_PHONE_NUMBERS:
|
self._otp = '77501'
|
||||||
# self.otp = '77501'
|
elif self.phone_number in settings.TEST_PHONENUMBERS:
|
||||||
#
|
self._otp = '88555'
|
||||||
# else:
|
else:
|
||||||
# self.otp = ''.join(random.choice('0123456789') for _ in range(5))
|
self._otp = ''.join(random.choice('0123456789') for _ in range(5))
|
||||||
# self.otp = ''.join(random.choice('0123456789') for _ in range(5))
|
|
||||||
|
|
||||||
|
self.otp = make_password(self._otp)
|
||||||
self.otp_expire = timezone.now() + timedelta(seconds=MAX_OTP_VALID_DURATION)
|
self.otp_expire = timezone.now() + timedelta(seconds=MAX_OTP_VALID_DURATION)
|
||||||
self.otp_try = 0
|
self.otp_try = 0
|
||||||
|
self.save()
|
||||||
|
|
||||||
def set_ott(self):
|
def set_ott(self):
|
||||||
self.ott = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
|
self.ott = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
|
||||||
|
|
@ -168,25 +180,33 @@ class User(AbstractUser):
|
||||||
self.ott_expire = timezone.now() + timedelta(seconds=MAX_OTP_VALID_DURATION)
|
self.ott_expire = timezone.now() + timedelta(seconds=MAX_OTP_VALID_DURATION)
|
||||||
|
|
||||||
def otp_is_valid(self):
|
def otp_is_valid(self):
|
||||||
return bool(self.otp and timezone.now() <= self.otp_expire)
|
return bool(self.otp and self.otp_expire and timezone.now() <= self.otp_expire)
|
||||||
|
|
||||||
def ott_is_valid(self):
|
def ott_is_valid(self):
|
||||||
return bool(self.ott and timezone.now() <= self.ott_expire)
|
return bool(self.ott and self.ott_expire and timezone.now() <= self.ott_expire)
|
||||||
|
|
||||||
def check_otp(self, otp):
|
def check_otp(self, otp):
|
||||||
if self.otp_try <= MAX_OTP_TRY:
|
if self.otp_try <= MAX_OTP_TRY:
|
||||||
result = otp and self.otp == otp and self.otp_is_valid()
|
try:
|
||||||
|
result = otp and check_password(otp, self.otp) and self.otp_is_valid()
|
||||||
|
except Exception as e:
|
||||||
|
result = False
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self.otp = None
|
self.otp = None
|
||||||
self.date_joined = timezone.now()
|
self.otp_expire = None
|
||||||
|
|
||||||
|
if not self.date_joined:
|
||||||
|
self.date_joined = timezone.now()
|
||||||
else:
|
else:
|
||||||
self.otp_try += 1
|
self.otp_try += 1
|
||||||
|
self.save()
|
||||||
else:
|
else:
|
||||||
self.otp = None
|
|
||||||
result = False
|
result = False
|
||||||
|
self.otp = None
|
||||||
|
self.otp_expire = None
|
||||||
|
self.save()
|
||||||
|
|
||||||
self.save()
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def check_ott(self, ott: str):
|
def check_ott(self, ott: str):
|
||||||
|
|
@ -221,18 +241,22 @@ class User(AbstractUser):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def send_otp(self):
|
def send_otp(self):
|
||||||
print(self.otp)
|
print(self._otp)
|
||||||
self.notify(body=str(self.otp), title='OTP')
|
self.notify(body=str(self._otp), title='OTP')
|
||||||
|
|
||||||
def notify(self, body, title=None, notification_type='sms'):
|
def notify(self, body, title=None, notification_type='sms'):
|
||||||
message = ("وینسو"
|
message = ("وینسو"
|
||||||
"\n"
|
"\n"
|
||||||
f"رمزیکبارمصرف: {body}"
|
f"رمز یکبار مصرف: {body}"
|
||||||
# "\n"
|
# "\n"
|
||||||
# # f"code is: {body}\n"
|
# # f"code is: {body}\n"
|
||||||
# f"{settings.SMS_OTP_SIGNITURE}"
|
# f"{settings.SMS_OTP_SIGNITURE}"
|
||||||
)
|
)
|
||||||
send_sms(self.phone_number.strip('+'), message)
|
|
||||||
|
sms_client = SMSPolicy.get_client()
|
||||||
|
sms_client.send_sms(self.phone_number.strip('+'), message)
|
||||||
|
|
||||||
|
logger.info(f'otp for: {self.phone_number} is {message}')
|
||||||
return
|
return
|
||||||
# TODO: enable celery
|
# TODO: enable celery
|
||||||
send_notification(
|
send_notification(
|
||||||
|
|
@ -245,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}'
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
|
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 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'."
|
||||||
|
|
@ -59,6 +63,10 @@ class RequestOTPSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
read_only_fields = ['otp_expire', 'ttl']
|
read_only_fields = ['otp_expire', 'ttl']
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs['phone_number'] = unidecode(attrs['phone_number'])
|
||||||
|
return attrs
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
# TODO: move it to query set
|
# TODO: move it to query set
|
||||||
validated_data = self.validated_data
|
validated_data = self.validated_data
|
||||||
|
|
@ -69,9 +77,7 @@ class RequestOTPSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
if not user.otp_is_valid():
|
if not user.otp_is_valid():
|
||||||
user.set_otp()
|
user.set_otp()
|
||||||
user.save()
|
user.send_otp()
|
||||||
|
|
||||||
user.send_otp()
|
|
||||||
|
|
||||||
self.instance = user
|
self.instance = user
|
||||||
return user
|
return user
|
||||||
|
|
@ -126,3 +132,66 @@ class ChangePasswordSerializer(serializers.Serializer):
|
||||||
old_password = serializers.CharField(required=True)
|
old_password = serializers.CharField(required=True)
|
||||||
old_password_field = serializers.CharField(default='password')
|
old_password_field = serializers.CharField(default='password')
|
||||||
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()
|
||||||
|
class SessionSerializer(serializers.ModelSerializer):
|
||||||
|
is_current = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_is_current(self, obj):
|
||||||
|
request = self.context.get('request')
|
||||||
|
token = request and request.auth and request.auth.token
|
||||||
|
if token == obj.token:
|
||||||
|
result = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AccessToken
|
||||||
|
fields = ('uuid', 'created', "detail", "is_current")
|
||||||
|
read_only_fields = ['uuid', 'created', "detail", "is_current" ]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,271 @@
|
||||||
from django.test import TestCase
|
import base64
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
|
from unicodedata import category
|
||||||
|
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.test import APIClient, APITestCase
|
||||||
|
|
||||||
|
|
||||||
|
from apps.core.models import Config
|
||||||
|
from apps.gooyal_oauth2.models import Scope
|
||||||
|
from apps.users.models import User, InviteRequest
|
||||||
|
|
||||||
|
AccessToken = get_access_token_model()
|
||||||
|
Application = get_application_model()
|
||||||
|
|
||||||
|
|
||||||
|
class UserTests(APITestCase):
|
||||||
|
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')
|
||||||
|
client_id = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||||
|
client_secret = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||||
|
|
||||||
|
visitor_uuid = uuid.UUID('b14e8b86-8f4a-44d9-b29d-badceb47005a')
|
||||||
|
|
||||||
|
expire_datetime = timezone.now() + timedelta(seconds=3600)
|
||||||
|
expire_datetime.isoformat()
|
||||||
|
client = APIClient()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from django.conf import settings
|
||||||
|
settings.SMS_SEND = False
|
||||||
|
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')
|
||||||
|
|
||||||
|
self.application = Application.objects.create(
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
authorization_grant_type='password',
|
||||||
|
hash_client_secret=False,
|
||||||
|
uuid=self.application_uuid,
|
||||||
|
user_id=self.user_uuid,
|
||||||
|
max_allowed_session=1,
|
||||||
|
allowed_scope='accounts.status:get accounts.account:retrieve',
|
||||||
|
)
|
||||||
|
|
||||||
|
# self.sys_date_patcher = patch('simata_safte.models.get_sys_date', mock_get_sys_date)
|
||||||
|
# self.set_id_patcher = patch('simata_safte.models.set_id', mock_set_id)
|
||||||
|
# self.sign_pdf_patcher = patch('simata_safte.models.sign_pdf', mock_sign_pdf)
|
||||||
|
# self.check_pdf_signature_patcher = patch('simata_safte.models.check_pdf_signature', mock_check_pdf_signature)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
|
||||||
|
def _create_authorization_header(self, token):
|
||||||
|
return "Bearer {0}".format(token)
|
||||||
|
|
||||||
|
def test_authentication_allow(self):
|
||||||
|
access_token_1 = AccessToken.objects.create(
|
||||||
|
**{
|
||||||
|
"token": self.access_token_1,
|
||||||
|
"user": self.user,
|
||||||
|
# "client_id": self.client_id,
|
||||||
|
# "client_owner": owner,
|
||||||
|
"application_id": self.application_uuid,
|
||||||
|
"scope": 'accounts.status:get accounts.account:retrieve',
|
||||||
|
"expires": self.expire_datetime.isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_1 = self._create_authorization_header(access_token_1.token)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_1)
|
||||||
|
|
||||||
|
self.assertContains(response, 'status')
|
||||||
|
|
||||||
|
access_token_2 = AccessToken.objects.create(
|
||||||
|
**{
|
||||||
|
"token": self.access_token_2,
|
||||||
|
"user": self.user,
|
||||||
|
|
||||||
|
# "client_id": self.client_id,
|
||||||
|
# "client_owner": owner,
|
||||||
|
"application_id": self.application_uuid,
|
||||||
|
"scope": 'accounts.status:get',
|
||||||
|
"expires": self.expire_datetime.isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_2 = self._create_authorization_header(access_token_2.token)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_1)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_2)
|
||||||
|
self.assertEqual(response.status_code, 503)
|
||||||
|
|
||||||
|
self.application.max_allowed_session = 0
|
||||||
|
self.application.save()
|
||||||
|
self.application.refresh_from_db()
|
||||||
|
|
||||||
|
response = self.client.get(reverse("core:status"), HTTP_AUTHORIZATION=auth_2)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def basic_auth_string(self, username, password):
|
||||||
|
"""ساخت Basic Auth string"""
|
||||||
|
import base64
|
||||||
|
user_pass = f"{username}:{password}"
|
||||||
|
basic_credentials = base64.b64encode(user_pass.encode('utf-8')).decode('utf-8')
|
||||||
|
|
||||||
|
return basic_credentials
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
self.user.set_otp()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"grant_type": "password",
|
||||||
|
"username": self.user_phone_number,
|
||||||
|
"password": '77501',
|
||||||
|
"scope": 'accounts.status:get accounts.account:retrieve',
|
||||||
|
"auth_fields": 'phone_number:otp'
|
||||||
|
}
|
||||||
|
self.client.credentials(
|
||||||
|
HTTP_AUTHORIZATION='Basic ' + self.basic_auth_string(self.client_id, self.client_secret)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self.client.post(reverse("gooyal_oauth2:token"), data=data)
|
||||||
|
access_token = result.json()['access_token']
|
||||||
|
self.client.credentials(HTTP_AUTHORIZATION='Bearer ' + access_token)
|
||||||
|
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def test_getSessions_allOK_success(self):
|
||||||
|
self.login()
|
||||||
|
response = self.client.get(reverse("users:user_sessions_api"))
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,18 @@ 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
|
UserDetailedRetrieveView, UserSessionListView, UserProfileView, InviteRequestView
|
||||||
|
|
||||||
app_name = "users"
|
app_name = "users"
|
||||||
|
|
||||||
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'),
|
||||||
path('account/', ProfileDetailView.as_view(), name='account_detail'),
|
path('account/', ProfileDetailView.as_view(), name='account_detail'),
|
||||||
path('account/update/', ProfileUpdateView.as_view(), name='account_update'),
|
path('account/update/', ProfileUpdateView.as_view(), name='account_update'),
|
||||||
path('api/account/', AccountView.as_view(), name='account_api'),
|
path('api/account/', AccountView.as_view(), name='account_api'),
|
||||||
path('api/users/', UserListView.as_view(), name='user_list_api'),
|
path('api/users/all', UserListView.as_view(), name='user_list_api'),
|
||||||
|
path('api/users/', UserProfileView.as_view(), name='user_profile_list_api'),
|
||||||
|
path('api/sessions/', UserSessionListView.as_view(), name='user_sessions_api'),
|
||||||
path('api/users/<uuid>/', UserPublicRetrieveView.as_view(), name='user_public_retrieve_api'),
|
path('api/users/<uuid>/', UserPublicRetrieveView.as_view(), name='user_public_retrieve_api'),
|
||||||
path('api/users/<uuid>/avatar', UserCurrentAvatarUrlView.as_view(), name='user_avatar_api'),
|
path('api/users/<uuid>/avatar', UserCurrentAvatarUrlView.as_view(), name='user_avatar_api'),
|
||||||
path('api/users/<uuid>/details', UserDetailedRetrieveView.as_view(), name='user_detailed_retrieve_api'),
|
path('api/users/<uuid>/details', UserDetailedRetrieveView.as_view(), name='user_detailed_retrieve_api'),
|
||||||
|
|
@ -20,4 +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,26 +17,35 @@ 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
|
ChangePasswordSerializer, UserInquirySerializer, SessionSerializer, InviteRequestSerializer
|
||||||
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle
|
from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle, NumberedRequestOTPDayRateThrottle, NumberedRequestOTPMinRateThrottle
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
class UserListView(generics.ListAPIView):
|
class BaseUserListView(generics.ListAPIView):
|
||||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = PublicUserSerializer
|
serializer_class = PublicUserSerializer
|
||||||
filter_backends = (DjangoFilterBackend,)
|
filter_backends = (DjangoFilterBackend,)
|
||||||
filterset_class = UserFilter
|
|
||||||
|
|
||||||
|
# Make filterset_class abstract - must be defined in subclasses
|
||||||
|
filterset_class = None
|
||||||
|
|
||||||
|
class UserProfileView(BaseUserListView):
|
||||||
|
filterset_class = UserFilter
|
||||||
required_alternate_scopes = {
|
required_alternate_scopes = {
|
||||||
"GET": [["accounts.profile:list"]],
|
"GET": [["accounts.profile:list"]],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class UserListView(BaseUserListView):
|
||||||
|
# filterset_class = UserFilter
|
||||||
|
required_alternate_scopes = {
|
||||||
|
"GET": [["accounts.users:list"]],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserPublicRetrieveView(generics.RetrieveAPIView):
|
class UserPublicRetrieveView(generics.RetrieveAPIView):
|
||||||
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||||
|
|
@ -90,7 +99,10 @@ class RequestOTPView(generics.CreateAPIView):
|
||||||
permission_classes = []
|
permission_classes = []
|
||||||
serializer_class = RequestOTPSerializer
|
serializer_class = RequestOTPSerializer
|
||||||
required_scopes = []
|
required_scopes = []
|
||||||
throttle_classes = [RequestOTPMinRateThrottle, RequestOTPDayRateThrottle]
|
throttle_classes = [RequestOTPMinRateThrottle,
|
||||||
|
RequestOTPDayRateThrottle,
|
||||||
|
NumberedRequestOTPDayRateThrottle,
|
||||||
|
NumberedRequestOTPMinRateThrottle]
|
||||||
|
|
||||||
class RequestOTTView(generics.CreateAPIView):
|
class RequestOTTView(generics.CreateAPIView):
|
||||||
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
permission_classes = [IsAuthenticatedOrTokenHasScope]
|
||||||
|
|
@ -115,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
|
||||||
|
|
@ -180,3 +201,20 @@ class ProfileUpdateView(UpdateView):
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSessionListView(generics.ListAPIView):
|
||||||
|
permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements]
|
||||||
|
serializer_class = SessionSerializer
|
||||||
|
filter_backends = (DjangoFilterBackend,)
|
||||||
|
|
||||||
|
max_allowed_session = 0
|
||||||
|
|
||||||
|
required_alternate_scopes = {
|
||||||
|
"GET": [["accounts.account:retrieve"]],
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
from apps.gooyal_oauth2.models import AccessToken
|
||||||
|
return AccessToken.objects.filter(user=self.request.user).all()
|
||||||
|
|
||||||
|
|
|
||||||
32
client.py
32
client.py
|
|
@ -3,10 +3,10 @@ import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
OAUTH_CLIENT_ID = 'xrFXKf53jrxVOygbLEoqrtOlUwBP00jIQ4zVpzc6'
|
OAUTH_CLIENT_ID = '4INGOCMoulE0fNY1SQlTbPtsWqqxGj2DdqjADq6u'
|
||||||
OAUTH_CLIENT_SECRET = 'qelUdRCdEEalVdko5aHRojxsC3CaL29yjwxmc6FeWs39SeVSb6aM9mNrYQixMJNTYQK7s2wffwPW94JPPbP6jTdd9dSf1mlqYcr6hW2COuayFUk4jP33OWu4taUYP2F5'
|
OAUTH_CLIENT_SECRET = 'KPc4dMSztNwAIB3vii3geXrzC1mKUIsAztz3t2ylC3HlrgJudJWhdrtoY6XeRJIuadTAREYYsXk9XtFHUDfPVcJvyfHMpNkz2VvghwrijhVprok0VYV7XrOirJ5nFUxD'
|
||||||
|
|
||||||
API_URI = 'https://accounts.gooyal.com'
|
API_URI = 'https://accounts.gooyal.ir'
|
||||||
|
|
||||||
|
|
||||||
# API_URI = 'http://127.0.0.1:8000'
|
# API_URI = 'http://127.0.0.1:8000'
|
||||||
|
|
@ -27,7 +27,7 @@ class ApiClient():
|
||||||
"grant_type": "password",
|
"grant_type": "password",
|
||||||
"username": phone_number,
|
"username": phone_number,
|
||||||
"password": password,
|
"password": password,
|
||||||
"scope": 'accounts.account:request_ott accounts.account:change_password accounts.profile:inquiry accounts.account:update accounts.account:retrieve accounts.profile:retrieve accounts.profile:list introspection wallet.wallet:get_balance',
|
"scope": 'introspection',
|
||||||
"auth_fields": 'phone_number:otp'
|
"auth_fields": 'phone_number:otp'
|
||||||
}
|
}
|
||||||
auth = (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET)
|
auth = (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET)
|
||||||
|
|
@ -51,7 +51,8 @@ class ApiClient():
|
||||||
"grant_type": "password",
|
"grant_type": "password",
|
||||||
"username": phone_number,
|
"username": phone_number,
|
||||||
"password": token,
|
"password": token,
|
||||||
"scope": 'introspection education.course:retrieve education.course:submit superapp.applications:list accounts.account:request_ott accounts.profile:list accounts.profile:retrieve accounts.account:retrieve accounts.account:update accounts.profile:inquiry accounts.account:change_password wallet.transaction:list wallet.invoice:create wallet.transaction:retrieve wallet.invoice:pay wallet.invoice:receipt wallet.deposit:submit wallet.deposit:verify wallet.withdraw:submit wallet.withdraw:verify ',
|
# "scope": 'introspection education.course:retrieve education.course:submit superapp.applications:list accounts.account:request_ott accounts.profile:list accounts.profile:retrieve accounts.account:retrieve accounts.account:update accounts.profile:inquiry accounts.account:change_password wallet.transaction:list wallet.invoice:create wallet.transaction:retrieve wallet.invoice:pay wallet.invoice:receipt wallet.deposit:submit wallet.deposit:verify wallet.withdraw:submit wallet.withdraw:verify ',
|
||||||
|
"scope": 'introspection',
|
||||||
"auth_fields": 'phone_number:ott'
|
"auth_fields": 'phone_number:ott'
|
||||||
}
|
}
|
||||||
auth = (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET)
|
auth = (OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET)
|
||||||
|
|
@ -156,6 +157,7 @@ class ApiClient():
|
||||||
}
|
}
|
||||||
url = f'{API_URI}/{path}'
|
url = f'{API_URI}/{path}'
|
||||||
response = self._request(url, method='PUT', data=data)
|
response = self._request(url, method='PUT', data=data)
|
||||||
|
print(response)
|
||||||
return response and 'code' in response, response
|
return response and 'code' in response, response
|
||||||
|
|
||||||
def introspect_account(self, token):
|
def introspect_account(self, token):
|
||||||
|
|
@ -185,19 +187,29 @@ class ApiClient():
|
||||||
|
|
||||||
return self._request(url=url)
|
return self._request(url=url)
|
||||||
|
|
||||||
def get_wallet_balance(self):
|
def get_application_preferences(self):
|
||||||
url = 'http://127.0.0.1:8000/wallet/api/wallet/default/balance'
|
url = 'https://preferences.gooyal.com/data/application/'
|
||||||
|
|
||||||
return self._request(url=url)
|
return self._request(url=url)
|
||||||
|
|
||||||
|
|
||||||
|
auth_data = {'access_token': 'DJ1mycH9r5FCwaNgDA9nB9uU3KWt9m', 'expires_in': 36000, 'token_type': 'Bearer', 'scope': 'accounts.account:change_password accounts.account:update accounts.account:retrieve wallet.wallet:get_balance billboard_merchant.billboard:retrieve billboard_merchant.billboard:update billboard_merchant.billboard:create wallet.user:transaction_list data_crud.data:retrieve data_crud.data:update data_crud.data:delete', 'refresh_token': 'VF4P11tfcnGv9tc2u0qH6035OW5qU4'}
|
||||||
|
|
||||||
client = ApiClient('+989106853582')
|
client = ApiClient('+989106853582')
|
||||||
|
# client.auth_data = auth_data
|
||||||
# print(client.login_as_client_credentials())
|
# print(client.login_as_client_credentials())
|
||||||
print(client.request_otp())
|
print(client.request_otp())
|
||||||
print(client.otp_login('12345'))
|
import time
|
||||||
print(client.introspect_account('V3LUQ9OryHOy6q5iWwLBRAtRBm80ex'))
|
start = time.time()
|
||||||
print(client.get_wallet_balance())
|
print(client.otp_login('77502'))
|
||||||
|
# end = time.time()
|
||||||
|
# print(end - start)
|
||||||
|
# print(client.get_application_preferences())
|
||||||
|
# exit()
|
||||||
|
# print(client.introspect_account('V3LUQ9OryHOy6q5iWwLBRAtRBm80ex'))
|
||||||
|
# print(client.get_wallet_balance())
|
||||||
# ott = client.get_application_token()[1]['ott']
|
# ott = client.get_application_token()[1]['ott']
|
||||||
# print(client.ott_login(ott))
|
# print(client.ott_login(ott))
|
||||||
# print(ott)
|
# print(ott)
|
||||||
|
client.update_account('test')
|
||||||
|
|
||||||
|
|
|
||||||
BIN
locale/ar/LC_MESSAGES/django.mo
Normal file
BIN
locale/ar/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
346
locale/ar/LC_MESSAGES/django.po
Normal file
346
locale/ar/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-02-03 10:43+0000\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
|
||||||
|
"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
|
||||||
|
|
||||||
|
#: accounts/settings.py:222
|
||||||
|
msgid "English"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:223
|
||||||
|
msgid "Persian"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:224
|
||||||
|
msgid "Arabic"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:225
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/gooyal_oauth2/views/introspect.py:153
|
||||||
|
msgid "Invalid credentials given."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/gooyal_oauth2/views/introspect.py:154
|
||||||
|
msgid "invalid_grant"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:9
|
||||||
|
msgid "male"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:10
|
||||||
|
msgid "female"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:14
|
||||||
|
msgid "Primary school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:15
|
||||||
|
msgid "Middle school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:16
|
||||||
|
msgid "High school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:17
|
||||||
|
msgid "Associate degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:18
|
||||||
|
msgid "Bachelors degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:19
|
||||||
|
msgid "Masters degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:20
|
||||||
|
msgid "Doctorate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:16 apps/users/models.py:117
|
||||||
|
msgid "otp"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:23
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"Please enter a correct %(phone_number)s and otp. Note that both fields may "
|
||||||
|
"be case-sensitive."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:26
|
||||||
|
msgid "This account is inactive."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:72
|
||||||
|
msgid "name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:73
|
||||||
|
msgid "first name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:74
|
||||||
|
msgid "last name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:75
|
||||||
|
msgid "email address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:84
|
||||||
|
msgid "Province"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:85
|
||||||
|
msgid "City"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:86
|
||||||
|
msgid "postal_code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:87
|
||||||
|
msgid "address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:90
|
||||||
|
msgid "staff status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:92
|
||||||
|
msgid "Designates whether the user can log into this admin site."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:95
|
||||||
|
msgid "active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:98
|
||||||
|
msgid ""
|
||||||
|
"Designates whether this user should be treated as active. Unselect this "
|
||||||
|
"instead of deleting accounts."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:102
|
||||||
|
msgid "password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:105
|
||||||
|
msgid "username"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:108
|
||||||
|
msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:111
|
||||||
|
msgid "A user with that username already exists."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:116
|
||||||
|
msgid "phone number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:118 apps/users/models.py:122 apps/users/models.py:124
|
||||||
|
msgid "otp expire"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:119
|
||||||
|
msgid "otp try"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:121
|
||||||
|
msgid "one time token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:125
|
||||||
|
msgid "balance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:131
|
||||||
|
msgid "gender"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:132
|
||||||
|
msgid "education degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:134
|
||||||
|
msgid "education field"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:135
|
||||||
|
msgid "birthdate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:137
|
||||||
|
msgid "last update"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:138
|
||||||
|
msgid "last login"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:139
|
||||||
|
msgid "date joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:156
|
||||||
|
msgid "otp expire time not reached."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:250
|
||||||
|
msgid "no name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:11
|
||||||
|
msgid "Phone number must be entered in the format: '+989999999999'."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:76
|
||||||
|
msgid "otp expire time not reached"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:81
|
||||||
|
msgid "otp expire time not reached yet"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:6
|
||||||
|
msgid "Are you sure to delete the application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:12
|
||||||
|
#: templates/oauth2_provider/authorize.html:29
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:13
|
||||||
|
#: templates/oauth2_provider/application_detail.html:53
|
||||||
|
#: templates/oauth2_provider/authorized-token-delete.html:7
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:10
|
||||||
|
msgid "Client id"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:15
|
||||||
|
msgid "Client secret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:20
|
||||||
|
msgid "Hash client secret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:21
|
||||||
|
msgid "yes,no"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:25
|
||||||
|
msgid "Client type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:30
|
||||||
|
msgid "Authorization Grant Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:35
|
||||||
|
msgid "Redirect Uris"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:40
|
||||||
|
msgid "Post Logout Redirect Uris"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:45
|
||||||
|
msgid "Allowed Origins"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:51
|
||||||
|
#: templates/oauth2_provider/application_form.html:35
|
||||||
|
msgid "Go Back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:52
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_form.html:9
|
||||||
|
msgid "Edit application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_form.html:37
|
||||||
|
msgid "Save"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:6
|
||||||
|
msgid "Your applications"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:14
|
||||||
|
msgid "New Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "No applications defined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "Click here"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "if you want to register a new one"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_registration_form.html:5
|
||||||
|
msgid "Register a new application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorize.html:8
|
||||||
|
#: templates/oauth2_provider/authorize.html:30
|
||||||
|
msgid "Authorize"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorize.html:17
|
||||||
|
msgid "Application requires the following permissions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-token-delete.html:6
|
||||||
|
msgid "Are you sure you want to delete this token?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:6
|
||||||
|
msgid "Tokens"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:11
|
||||||
|
msgid "revoke"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:19
|
||||||
|
msgid "There are no authorized tokens yet."
|
||||||
|
msgstr ""
|
||||||
BIN
locale/en/LC_MESSAGES/django.mo
Normal file
BIN
locale/en/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
345
locale/en/LC_MESSAGES/django.po
Normal file
345
locale/en/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-02-03 10:43+0000\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#: accounts/settings.py:222
|
||||||
|
msgid "English"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:223
|
||||||
|
msgid "Persian"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:224
|
||||||
|
msgid "Arabic"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:225
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/gooyal_oauth2/views/introspect.py:153
|
||||||
|
msgid "Invalid credentials given."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/gooyal_oauth2/views/introspect.py:154
|
||||||
|
msgid "invalid_grant"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:9
|
||||||
|
msgid "male"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:10
|
||||||
|
msgid "female"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:14
|
||||||
|
msgid "Primary school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:15
|
||||||
|
msgid "Middle school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:16
|
||||||
|
msgid "High school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:17
|
||||||
|
msgid "Associate degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:18
|
||||||
|
msgid "Bachelors degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:19
|
||||||
|
msgid "Masters degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:20
|
||||||
|
msgid "Doctorate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:16 apps/users/models.py:117
|
||||||
|
msgid "otp"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:23
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"Please enter a correct %(phone_number)s and otp. Note that both fields may "
|
||||||
|
"be case-sensitive."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:26
|
||||||
|
msgid "This account is inactive."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:72
|
||||||
|
msgid "name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:73
|
||||||
|
msgid "first name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:74
|
||||||
|
msgid "last name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:75
|
||||||
|
msgid "email address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:84
|
||||||
|
msgid "Province"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:85
|
||||||
|
msgid "City"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:86
|
||||||
|
msgid "postal_code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:87
|
||||||
|
msgid "address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:90
|
||||||
|
msgid "staff status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:92
|
||||||
|
msgid "Designates whether the user can log into this admin site."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:95
|
||||||
|
msgid "active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:98
|
||||||
|
msgid ""
|
||||||
|
"Designates whether this user should be treated as active. Unselect this "
|
||||||
|
"instead of deleting accounts."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:102
|
||||||
|
msgid "password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:105
|
||||||
|
msgid "username"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:108
|
||||||
|
msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:111
|
||||||
|
msgid "A user with that username already exists."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:116
|
||||||
|
msgid "phone number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:118 apps/users/models.py:122 apps/users/models.py:124
|
||||||
|
msgid "otp expire"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:119
|
||||||
|
msgid "otp try"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:121
|
||||||
|
msgid "one time token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:125
|
||||||
|
msgid "balance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:131
|
||||||
|
msgid "gender"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:132
|
||||||
|
msgid "education degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:134
|
||||||
|
msgid "education field"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:135
|
||||||
|
msgid "birthdate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:137
|
||||||
|
msgid "last update"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:138
|
||||||
|
msgid "last login"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:139
|
||||||
|
msgid "date joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:156
|
||||||
|
msgid "otp expire time not reached."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:250
|
||||||
|
msgid "no name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:11
|
||||||
|
msgid "Phone number must be entered in the format: '+989999999999'."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:76
|
||||||
|
msgid "otp expire time not reached"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:81
|
||||||
|
msgid "otp expire time not reached yet"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:6
|
||||||
|
msgid "Are you sure to delete the application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:12
|
||||||
|
#: templates/oauth2_provider/authorize.html:29
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:13
|
||||||
|
#: templates/oauth2_provider/application_detail.html:53
|
||||||
|
#: templates/oauth2_provider/authorized-token-delete.html:7
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:10
|
||||||
|
msgid "Client id"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:15
|
||||||
|
msgid "Client secret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:20
|
||||||
|
msgid "Hash client secret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:21
|
||||||
|
msgid "yes,no"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:25
|
||||||
|
msgid "Client type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:30
|
||||||
|
msgid "Authorization Grant Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:35
|
||||||
|
msgid "Redirect Uris"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:40
|
||||||
|
msgid "Post Logout Redirect Uris"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:45
|
||||||
|
msgid "Allowed Origins"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:51
|
||||||
|
#: templates/oauth2_provider/application_form.html:35
|
||||||
|
msgid "Go Back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:52
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_form.html:9
|
||||||
|
msgid "Edit application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_form.html:37
|
||||||
|
msgid "Save"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:6
|
||||||
|
msgid "Your applications"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:14
|
||||||
|
msgid "New Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "No applications defined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "Click here"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "if you want to register a new one"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_registration_form.html:5
|
||||||
|
msgid "Register a new application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorize.html:8
|
||||||
|
#: templates/oauth2_provider/authorize.html:30
|
||||||
|
msgid "Authorize"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorize.html:17
|
||||||
|
msgid "Application requires the following permissions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-token-delete.html:6
|
||||||
|
msgid "Are you sure you want to delete this token?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:6
|
||||||
|
msgid "Tokens"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:11
|
||||||
|
msgid "revoke"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:19
|
||||||
|
msgid "There are no authorized tokens yet."
|
||||||
|
msgstr ""
|
||||||
BIN
locale/fa/LC_MESSAGES/django.mo
Normal file
BIN
locale/fa/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
345
locale/fa/LC_MESSAGES/django.po
Normal file
345
locale/fa/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-02-03 10:43+0000\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
|
||||||
|
#: accounts/settings.py:222
|
||||||
|
msgid "English"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:223
|
||||||
|
msgid "Persian"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:224
|
||||||
|
msgid "Arabic"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:225
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/gooyal_oauth2/views/introspect.py:153
|
||||||
|
msgid "Invalid credentials given."
|
||||||
|
msgstr "اطلاعات وارد شده برای اعتبار سنجی نادرست است."
|
||||||
|
|
||||||
|
#: apps/gooyal_oauth2/views/introspect.py:154
|
||||||
|
msgid "invalid_grant"
|
||||||
|
msgstr "دسترسی اشتباه"
|
||||||
|
|
||||||
|
#: apps/users/constans.py:9
|
||||||
|
msgid "male"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:10
|
||||||
|
msgid "female"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:14
|
||||||
|
msgid "Primary school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:15
|
||||||
|
msgid "Middle school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:16
|
||||||
|
msgid "High school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:17
|
||||||
|
msgid "Associate degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:18
|
||||||
|
msgid "Bachelors degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:19
|
||||||
|
msgid "Masters degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:20
|
||||||
|
msgid "Doctorate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:16 apps/users/models.py:117
|
||||||
|
msgid "otp"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:23
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"Please enter a correct %(phone_number)s and otp. Note that both fields may "
|
||||||
|
"be case-sensitive."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:26
|
||||||
|
msgid "This account is inactive."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:72
|
||||||
|
msgid "name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:73
|
||||||
|
msgid "first name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:74
|
||||||
|
msgid "last name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:75
|
||||||
|
msgid "email address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:84
|
||||||
|
msgid "Province"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:85
|
||||||
|
msgid "City"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:86
|
||||||
|
msgid "postal_code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:87
|
||||||
|
msgid "address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:90
|
||||||
|
msgid "staff status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:92
|
||||||
|
msgid "Designates whether the user can log into this admin site."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:95
|
||||||
|
msgid "active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:98
|
||||||
|
msgid ""
|
||||||
|
"Designates whether this user should be treated as active. Unselect this "
|
||||||
|
"instead of deleting accounts."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:102
|
||||||
|
msgid "password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:105
|
||||||
|
msgid "username"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:108
|
||||||
|
msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:111
|
||||||
|
msgid "A user with that username already exists."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:116
|
||||||
|
msgid "phone number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:118 apps/users/models.py:122 apps/users/models.py:124
|
||||||
|
msgid "otp expire"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:119
|
||||||
|
msgid "otp try"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:121
|
||||||
|
msgid "one time token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:125
|
||||||
|
msgid "balance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:131
|
||||||
|
msgid "gender"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:132
|
||||||
|
msgid "education degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:134
|
||||||
|
msgid "education field"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:135
|
||||||
|
msgid "birthdate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:137
|
||||||
|
msgid "last update"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:138
|
||||||
|
msgid "last login"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:139
|
||||||
|
msgid "date joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:156
|
||||||
|
msgid "otp expire time not reached."
|
||||||
|
msgstr "رمز یکبار مصرف قبلی هنوز معتبر است."
|
||||||
|
|
||||||
|
#: apps/users/models.py:250
|
||||||
|
msgid "no name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:11
|
||||||
|
msgid "Phone number must be entered in the format: '+989999999999'."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:76
|
||||||
|
msgid "otp expire time not reached"
|
||||||
|
msgstr "رمز یکبار مصرف قبلی هنوز معتبر است."
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:81
|
||||||
|
msgid "otp expire time not reached yet"
|
||||||
|
msgstr "رمز یکبار مصرف قبلی هنوز معتبر است."
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:6
|
||||||
|
msgid "Are you sure to delete the application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:12
|
||||||
|
#: templates/oauth2_provider/authorize.html:29
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:13
|
||||||
|
#: templates/oauth2_provider/application_detail.html:53
|
||||||
|
#: templates/oauth2_provider/authorized-token-delete.html:7
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:10
|
||||||
|
msgid "Client id"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:15
|
||||||
|
msgid "Client secret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:20
|
||||||
|
msgid "Hash client secret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:21
|
||||||
|
msgid "yes,no"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:25
|
||||||
|
msgid "Client type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:30
|
||||||
|
msgid "Authorization Grant Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:35
|
||||||
|
msgid "Redirect Uris"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:40
|
||||||
|
msgid "Post Logout Redirect Uris"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:45
|
||||||
|
msgid "Allowed Origins"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:51
|
||||||
|
#: templates/oauth2_provider/application_form.html:35
|
||||||
|
msgid "Go Back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:52
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_form.html:9
|
||||||
|
msgid "Edit application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_form.html:37
|
||||||
|
msgid "Save"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:6
|
||||||
|
msgid "Your applications"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:14
|
||||||
|
msgid "New Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "No applications defined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "Click here"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "if you want to register a new one"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_registration_form.html:5
|
||||||
|
msgid "Register a new application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorize.html:8
|
||||||
|
#: templates/oauth2_provider/authorize.html:30
|
||||||
|
msgid "Authorize"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorize.html:17
|
||||||
|
msgid "Application requires the following permissions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-token-delete.html:6
|
||||||
|
msgid "Are you sure you want to delete this token?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:6
|
||||||
|
msgid "Tokens"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:11
|
||||||
|
msgid "revoke"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:19
|
||||||
|
msgid "There are no authorized tokens yet."
|
||||||
|
msgstr ""
|
||||||
BIN
locale/tr/LC_MESSAGES/django.mo
Normal file
BIN
locale/tr/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
345
locale/tr/LC_MESSAGES/django.po
Normal file
345
locale/tr/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-02-03 10:43+0000\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
|
|
||||||
|
#: accounts/settings.py:222
|
||||||
|
msgid "English"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:223
|
||||||
|
msgid "Persian"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:224
|
||||||
|
msgid "Arabic"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: accounts/settings.py:225
|
||||||
|
msgid "Turkish"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/gooyal_oauth2/views/introspect.py:153
|
||||||
|
msgid "Invalid credentials given."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/gooyal_oauth2/views/introspect.py:154
|
||||||
|
msgid "invalid_grant"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:9
|
||||||
|
msgid "male"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:10
|
||||||
|
msgid "female"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:14
|
||||||
|
msgid "Primary school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:15
|
||||||
|
msgid "Middle school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:16
|
||||||
|
msgid "High school"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:17
|
||||||
|
msgid "Associate degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:18
|
||||||
|
msgid "Bachelors degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:19
|
||||||
|
msgid "Masters degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/constans.py:20
|
||||||
|
msgid "Doctorate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:16 apps/users/models.py:117
|
||||||
|
msgid "otp"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:23
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"Please enter a correct %(phone_number)s and otp. Note that both fields may "
|
||||||
|
"be case-sensitive."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/forms.py:26
|
||||||
|
msgid "This account is inactive."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:72
|
||||||
|
msgid "name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:73
|
||||||
|
msgid "first name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:74
|
||||||
|
msgid "last name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:75
|
||||||
|
msgid "email address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:84
|
||||||
|
msgid "Province"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:85
|
||||||
|
msgid "City"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:86
|
||||||
|
msgid "postal_code"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:87
|
||||||
|
msgid "address"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:90
|
||||||
|
msgid "staff status"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:92
|
||||||
|
msgid "Designates whether the user can log into this admin site."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:95
|
||||||
|
msgid "active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:98
|
||||||
|
msgid ""
|
||||||
|
"Designates whether this user should be treated as active. Unselect this "
|
||||||
|
"instead of deleting accounts."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:102
|
||||||
|
msgid "password"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:105
|
||||||
|
msgid "username"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:108
|
||||||
|
msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:111
|
||||||
|
msgid "A user with that username already exists."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:116
|
||||||
|
msgid "phone number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:118 apps/users/models.py:122 apps/users/models.py:124
|
||||||
|
msgid "otp expire"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:119
|
||||||
|
msgid "otp try"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:121
|
||||||
|
msgid "one time token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:125
|
||||||
|
msgid "balance"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:131
|
||||||
|
msgid "gender"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:132
|
||||||
|
msgid "education degree"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:134
|
||||||
|
msgid "education field"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:135
|
||||||
|
msgid "birthdate"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:137
|
||||||
|
msgid "last update"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:138
|
||||||
|
msgid "last login"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:139
|
||||||
|
msgid "date joined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:156
|
||||||
|
msgid "otp expire time not reached."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/models.py:250
|
||||||
|
msgid "no name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:11
|
||||||
|
msgid "Phone number must be entered in the format: '+989999999999'."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:76
|
||||||
|
msgid "otp expire time not reached"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: apps/users/serializers.py:81
|
||||||
|
msgid "otp expire time not reached yet"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:6
|
||||||
|
msgid "Are you sure to delete the application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:12
|
||||||
|
#: templates/oauth2_provider/authorize.html:29
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_confirm_delete.html:13
|
||||||
|
#: templates/oauth2_provider/application_detail.html:53
|
||||||
|
#: templates/oauth2_provider/authorized-token-delete.html:7
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:10
|
||||||
|
msgid "Client id"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:15
|
||||||
|
msgid "Client secret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:20
|
||||||
|
msgid "Hash client secret"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:21
|
||||||
|
msgid "yes,no"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:25
|
||||||
|
msgid "Client type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:30
|
||||||
|
msgid "Authorization Grant Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:35
|
||||||
|
msgid "Redirect Uris"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:40
|
||||||
|
msgid "Post Logout Redirect Uris"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:45
|
||||||
|
msgid "Allowed Origins"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:51
|
||||||
|
#: templates/oauth2_provider/application_form.html:35
|
||||||
|
msgid "Go Back"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_detail.html:52
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_form.html:9
|
||||||
|
msgid "Edit application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_form.html:37
|
||||||
|
msgid "Save"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:6
|
||||||
|
msgid "Your applications"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:14
|
||||||
|
msgid "New Application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "No applications defined"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "Click here"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_list.html:17
|
||||||
|
msgid "if you want to register a new one"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/application_registration_form.html:5
|
||||||
|
msgid "Register a new application"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorize.html:8
|
||||||
|
#: templates/oauth2_provider/authorize.html:30
|
||||||
|
msgid "Authorize"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorize.html:17
|
||||||
|
msgid "Application requires the following permissions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-token-delete.html:6
|
||||||
|
msgid "Are you sure you want to delete this token?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:6
|
||||||
|
msgid "Tokens"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:11
|
||||||
|
msgid "revoke"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/oauth2_provider/authorized-tokens.html:19
|
||||||
|
msgid "There are no authorized tokens yet."
|
||||||
|
msgstr ""
|
||||||
16
main/asgi.py
Normal file
16
main/asgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for accounts project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import os
|
import os
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
|
|
||||||
from utils.broker import get_rpc_broker_consumer
|
from utils.broker import get_rpc_broker_consumer
|
||||||
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
# from django.conf import settings
|
# from django.conf import settings
|
||||||
#
|
#
|
||||||
# # set the default Django settings module for the 'celery' program.
|
# # set the default Django settings module for the 'celery' program.
|
||||||
# # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
# # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
#
|
#
|
||||||
# app = Celery('notification_service')
|
# app = Celery('notification_service')
|
||||||
#
|
#
|
||||||
0
main/other_settings/__init__.py
Normal file
0
main/other_settings/__init__.py
Normal file
141
main/other_settings/logging.py
Normal file
141
main/other_settings/logging.py
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
def get_settings(key):
|
||||||
|
from django.conf import settings
|
||||||
|
return getattr(settings, 'BASE_DIR')
|
||||||
|
|
||||||
|
BASE_DIR = get_settings('BASE_DIR')
|
||||||
|
|
||||||
|
LOG_DIR = BASE_DIR / 'log'
|
||||||
|
if not os.path.exists(LOG_DIR):
|
||||||
|
os.makedirs(LOG_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'verbose': {
|
||||||
|
'format': '{asctime} [{levelname}] {name} {module} {process:d} {thread:d} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
'standard': {
|
||||||
|
'format': '{asctime} [{levelname}] {name}: {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
'simple': {
|
||||||
|
'format': '{levelname} {message}',
|
||||||
|
'style': '{',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'filters': {
|
||||||
|
'require_debug_true': {
|
||||||
|
'()': 'django.utils.log.RequireDebugTrue',
|
||||||
|
},
|
||||||
|
'require_debug_false': {
|
||||||
|
'()': 'django.utils.log.RequireDebugFalse',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
# هندلر برای نوشتن در فایل
|
||||||
|
'file': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': os.path.join(LOG_DIR, 'accounts.log'),
|
||||||
|
# 'maxBytes': 1024 * 1024 * 10, # 10 MB
|
||||||
|
'when': 'midnight',
|
||||||
|
'interval': 5,
|
||||||
|
'backupCount': 30,
|
||||||
|
'formatter': 'verbose',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
},
|
||||||
|
# هندلر برای خطاها در فایل جداگانه
|
||||||
|
'error_file': {
|
||||||
|
'level': 'ERROR',
|
||||||
|
'class': 'logging.handlers.TimedRotatingFileHandler',
|
||||||
|
'filename': os.path.join(LOG_DIR, 'errors.log'),
|
||||||
|
# 'maxBytes': 1024 * 1024 * 10,
|
||||||
|
'when': 'midnight',
|
||||||
|
'interval': 5,
|
||||||
|
'backupCount': 30,
|
||||||
|
'formatter': 'verbose',
|
||||||
|
'encoding': 'utf-8',
|
||||||
|
},
|
||||||
|
# هندلر برای کنسول (فقط در حالت DEBUG)
|
||||||
|
'console': {
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'filters': ['require_debug_true'],
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'simple',
|
||||||
|
},
|
||||||
|
# هندلر برای کنسول همیشه فعال (حتی در production)
|
||||||
|
'console_always': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'standard',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
# لاگر ریشه
|
||||||
|
'': { # empty string = root logger
|
||||||
|
'handlers': ['console', 'file', 'error_file'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': True,
|
||||||
|
},
|
||||||
|
# لاگر اختصاصی برای Django
|
||||||
|
'django': {
|
||||||
|
'handlers': ['console', 'file'],
|
||||||
|
'level': 'INFO',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
# لاگر اختصاصی برای درخواستها
|
||||||
|
'django.request': {
|
||||||
|
'handlers': ['file', 'error_file', 'console'],
|
||||||
|
'level': 'ERROR',
|
||||||
|
'propagate': False,
|
||||||
|
},
|
||||||
|
# # لاگر اختصاصی برای برنامه خودتان
|
||||||
|
# 'root': {
|
||||||
|
# 'handlers': ['console', 'file', 'error_file'],
|
||||||
|
# 'level': 'DEBUG',
|
||||||
|
# 'propagate': False,
|
||||||
|
# },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
|
||||||
|
# {
|
||||||
|
# 'version': 1,
|
||||||
|
# 'disable_existing_loggers': False,
|
||||||
|
# 'formatters': {
|
||||||
|
# 'loki': {
|
||||||
|
# 'class': 'utils.logs.LokiFormatter', # required
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
#
|
||||||
|
# 'handlers': {
|
||||||
|
# 'loki': {
|
||||||
|
# 'level': 'DEBUG', # Log level. Required
|
||||||
|
# 'class': 'utils.logs.LokiHandler', # Required
|
||||||
|
# 'formatter': 'loki', # Loki formatter. Required
|
||||||
|
# 'timeout': 2, # Post request timeout, default is 0.5. Optional
|
||||||
|
# 'url': f'{LOKI_BASE_PUBLIC_URL}/loki/api/v1/push', # Loki url. Defaults to localhost. Optional.
|
||||||
|
# # 'auth': ("user", "password"), # Basic auth to authenticate with loki. Default is None (i.e. no auth). Optional
|
||||||
|
# 'tags': {"app": "accounts"}, # Tags / Labels to attach to the log. Optional, but strongly encoraged to use.
|
||||||
|
# 'mode': 'thread',
|
||||||
|
# # Push mode. Can be 'sync' or 'thread'. Sync is blocking, thread is non-blocking. Defaults to sync. Optional.
|
||||||
|
# },
|
||||||
|
# 'console': {
|
||||||
|
# 'level': 'DEBUG',
|
||||||
|
# 'class': 'logging.StreamHandler',
|
||||||
|
# # 'formatter': 'verbose',
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# 'loggers': {
|
||||||
|
# '': {
|
||||||
|
# 'handlers': ['console', 'loki'],
|
||||||
|
# 'level': 'INFO',
|
||||||
|
# 'propagate': True,
|
||||||
|
# },
|
||||||
|
# },
|
||||||
|
# }
|
||||||
|
|
@ -11,12 +11,12 @@ https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from decouple import config
|
from decouple import config, Csv
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
|
@ -48,14 +48,14 @@ INSTALLED_APPS = [
|
||||||
'crispy_forms',
|
'crispy_forms',
|
||||||
'crispy_bootstrap5',
|
'crispy_bootstrap5',
|
||||||
'django_filters',
|
'django_filters',
|
||||||
'jalali_date',
|
# 'jalali_date',
|
||||||
'django_minio_backend.apps.DjangoMinioBackendConfig',
|
'django_minio_backend.apps.DjangoMinioBackendConfig',
|
||||||
|
|
||||||
# local apps
|
# local apps
|
||||||
'apps.core',
|
'apps.core',
|
||||||
'apps.users',
|
'apps.users',
|
||||||
'apps.gooyal_oauth2',
|
'apps.gooyal_oauth2',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
|
@ -63,7 +63,7 @@ MIDDLEWARE = [
|
||||||
'corsheaders.middleware.CorsMiddleware',
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.locale.LocaleMiddleware',
|
'django.middleware.locale.LocaleMiddleware',
|
||||||
'utils.i18n.ForceAdminLanguageMiddleware',
|
# 'utils.i18n.ForceAdminLanguageMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
|
@ -119,9 +119,16 @@ REST_FRAMEWORK = {
|
||||||
# 'DEFAULT_THROTTLE_CLASSES': [
|
# 'DEFAULT_THROTTLE_CLASSES': [
|
||||||
# 'rest_framework.throttling.AnonRateThrottle',
|
# 'rest_framework.throttling.AnonRateThrottle',
|
||||||
# ],
|
# ],
|
||||||
|
'DEFAULT_THROTTLE_CLASSES': [
|
||||||
|
'apps.gooyal_oauth2.throttling.TokenLimitThrottle',
|
||||||
|
# 'rest_framework.throttling.AnonRateThrottle',
|
||||||
|
# 'rest_framework.throttling.UserRateThrottle'
|
||||||
|
],
|
||||||
'DEFAULT_THROTTLE_RATES': {
|
'DEFAULT_THROTTLE_RATES': {
|
||||||
'otp_min': '2/min',
|
'otp_min': '2/min',
|
||||||
'otp_day': '50/day',
|
'otp_day': '50/day',
|
||||||
|
'numbered_otp_min': '1/min',
|
||||||
|
'numbered_otp_day': '10/day',
|
||||||
},
|
},
|
||||||
'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler',
|
'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler',
|
||||||
}
|
}
|
||||||
|
|
@ -135,8 +142,7 @@ SPECTACULAR_SETTINGS = {
|
||||||
"PARSER_WHITELIST": ["rest_framework.parsers.JSONParser"],
|
"PARSER_WHITELIST": ["rest_framework.parsers.JSONParser"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'main.urls'
|
||||||
ROOT_URLCONF = 'accounts.urls'
|
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
|
|
@ -160,7 +166,7 @@ AUTHENTICATION_BACKENDS = (
|
||||||
'django.contrib.auth.backends.ModelBackend',
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
)
|
)
|
||||||
|
|
||||||
WSGI_APPLICATION = 'accounts.wsgi.application'
|
WSGI_APPLICATION = 'main.wsgi.application'
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||||
|
|
@ -180,7 +186,12 @@ DATABASES = {
|
||||||
'PASSWORD': config('DB_PASSWORD'),
|
'PASSWORD': config('DB_PASSWORD'),
|
||||||
'HOST': config('DB_HOST', '127.0.0.1'),
|
'HOST': config('DB_HOST', '127.0.0.1'),
|
||||||
'PORT': config('DB_PORT', ''),
|
'PORT': config('DB_PORT', ''),
|
||||||
}
|
"CONN_MAX_AGE": 0,
|
||||||
|
"OPTIONS": {
|
||||||
|
"pool": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
|
|
@ -201,11 +212,10 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en'
|
LANGUAGE_CODE = 'fa'
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
|
@ -215,11 +225,17 @@ USE_L10N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
LANGUAGES = [
|
||||||
|
('en', _('English')),
|
||||||
|
('fa', _('Persian')),
|
||||||
|
('ar', _('Arabic')),
|
||||||
|
('tr', _('Turkish')),
|
||||||
|
]
|
||||||
|
|
||||||
LOCALE_PATHS = [
|
LOCALE_PATHS = [
|
||||||
BASE_DIR / 'locale',
|
BASE_DIR / 'locale',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||||
|
|
||||||
|
|
@ -229,23 +245,18 @@ MEDIA_ROOT = BASE_DIR / 'media'
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
|
||||||
if DEBUG == True:
|
if DEBUG == True:
|
||||||
STATICFILES_DIRS = (
|
STATICFILES_DIRS = (
|
||||||
BASE_DIR / 'static',
|
BASE_DIR / 'static',
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
STATIC_ROOT = BASE_DIR / 'static'
|
STATIC_ROOT = BASE_DIR / 'static'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Default primary key field type
|
# Default primary key field type
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
LOGIN_URL = '/users/login/'
|
LOGIN_URL = '/users/login/'
|
||||||
# LOGIN_DEFAULT_REDIRECT_URL = '/'
|
# LOGIN_DEFAULT_REDIRECT_URL = '/'
|
||||||
|
|
||||||
|
|
@ -255,7 +266,7 @@ CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||||
AUTH_USER_MODEL = 'users.User'
|
AUTH_USER_MODEL = 'users.User'
|
||||||
CORS_ORIGIN_ALLOW_ALL = True
|
CORS_ORIGIN_ALLOW_ALL = True
|
||||||
CORS_ALLOW_ALL_ORIGINS = True
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
CSRF_TRUSTED_ORIGINS = ['http://*.gooyal.com', 'https://*.gooyal.com']
|
CSRF_TRUSTED_ORIGINS = ['http://*.gooyal.com', 'https://*.gooyal.com', 'http://*.gooyal.ir', 'https://*.gooyal.ir']
|
||||||
|
|
||||||
JALALI_DATE_DEFAULTS = {
|
JALALI_DATE_DEFAULTS = {
|
||||||
'Strftime': {
|
'Strftime': {
|
||||||
|
|
@ -269,8 +280,10 @@ PAYAM_SMS_USERNAME = config('PAYAM_SMS_USERNAME')
|
||||||
PAYAM_SMS_PASSWORD = config('PAYAM_SMS_PASSWORD')
|
PAYAM_SMS_PASSWORD = config('PAYAM_SMS_PASSWORD')
|
||||||
PAYAM_SMS_CLIENT_ID = config('PAYAM_SMS_CLIENT_ID')
|
PAYAM_SMS_CLIENT_ID = config('PAYAM_SMS_CLIENT_ID')
|
||||||
PAYAM_SMS_CLIENT_SECRET = config('PAYAM_SMS_CLIENT_SECRET')
|
PAYAM_SMS_CLIENT_SECRET = config('PAYAM_SMS_CLIENT_SECRET')
|
||||||
SMS_OTP_SIGNITURE = config('SMS_OTP_SIGNITURE', '')
|
|
||||||
|
|
||||||
|
SMS_OTP_SIGNITURE = config('SMS_OTP_SIGNITURE', '')
|
||||||
|
SMS_SEND = config('SMS_SEND', True, cast=bool)
|
||||||
|
TEST_PHONENUMBERS = config('TEST_PHONENUMBERS', [], cast=Csv(post_process=list))
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {
|
"default": {
|
||||||
|
|
@ -282,12 +295,42 @@ CACHES = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOKI_BASE_PUBLIC_URL = config('LOKI_BASE_PUBLIC_URL', default=None, cast=str)
|
||||||
|
|
||||||
|
from main.other_settings.logging import LOGGING
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
STORAGES = {
|
STORAGES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django_minio_backend.models.MinioBackend",
|
"BACKEND": "django_minio_backend.models.MinioBackend",
|
||||||
|
"OPTIONS": {
|
||||||
|
"MINIO_ENDPOINT": config('MINIO_ENDPOINT', default='drive.gooyal.ir'),
|
||||||
|
"MINIO_USE_HTTPS": config('MINIO_USE_HTTPS', default=True, cast=bool),
|
||||||
|
|
||||||
|
"MINIO_EXTERNAL_ENDPOINT": config('MINIO_EXTERNAL_ENDPOINT', default='drive.gooyal.ir'),
|
||||||
|
# Default is same as MINIO_ENDPOINT
|
||||||
|
"MINIO_EXTERNAL_ENDPOINT_USE_HTTPS": config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True, cast=bool),
|
||||||
|
# Default is same as MINIO_USE_HTTPS
|
||||||
|
"MINIO_REGION": None, # Default is set to None
|
||||||
|
"MINIO_ACCESS_KEY": config('MINIO_ACCESS_KEY'),
|
||||||
|
"MINIO_SECRET_KEY": config('MINIO_SECRET_KEY'),
|
||||||
|
"MINIO_URL_EXPIRY_HOURS": timedelta(days=1), # Default is 7 days (longest) if not defined
|
||||||
|
"MINIO_CONSISTENCY_CHECK_ON_START": True,
|
||||||
|
"MINIO_DEFAULT_BUCKET": config('MINIO_MEDIA_FILES_BUCKET'), # replacement for MEDIA_ROOT
|
||||||
|
# MINIO_STATIC_FILES_BUCKET = 'my-static-files-bucket' # replacement for STATIC_ROOT
|
||||||
|
|
||||||
|
"MINIO_PRIVATE_BUCKETS": [
|
||||||
|
'default',
|
||||||
|
],
|
||||||
|
"MINIO_PUBLIC_BUCKETS": [
|
||||||
|
'default-public',
|
||||||
|
config('MINIO_MEDIA_FILES_BUCKET')
|
||||||
|
],
|
||||||
|
"MINIO_POLICY_HOOKS": [],
|
||||||
|
"MINIO_BUCKET_CHECK_ON_SAVE": True # Default: True // Creates bucket if missing, then save
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"staticfiles": {
|
"staticfiles": {
|
||||||
# "BACKEND": "django_minio_backend.models.MinioBackendStatic",
|
# "BACKEND": "django_minio_backend.models.MinioBackendStatic",
|
||||||
|
|
@ -296,29 +339,6 @@ STORAGES = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
MINIO_ENDPOINT = config('MINIO_ENDPOINT', default='drive.gooyal.com')
|
|
||||||
MINIO_USE_HTTPS = config('MINIO_USE_HTTPS', default=True, cast=bool)
|
|
||||||
|
|
||||||
MINIO_EXTERNAL_ENDPOINT = config('MINIO_EXTERNAL_ENDPOINT', default='drive.gooyal.com') # Default is same as MINIO_ENDPOINT
|
|
||||||
MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True, cast=bool) # Default is same as MINIO_USE_HTTPS
|
|
||||||
MINIO_REGION = None # Default is set to None
|
|
||||||
MINIO_ACCESS_KEY = config('MINIO_ACCESS_KEY')
|
|
||||||
MINIO_SECRET_KEY = config('MINIO_SECRET_KEY')
|
|
||||||
MINIO_URL_EXPIRY_HOURS = timedelta(days=1) # Default is 7 days (longest) if not defined
|
|
||||||
MINIO_CONSISTENCY_CHECK_ON_START = True
|
|
||||||
MINIO_MEDIA_FILES_BUCKET = config('MINIO_MEDIA_FILES_BUCKET') # replacement for MEDIA_ROOT
|
|
||||||
# MINIO_STATIC_FILES_BUCKET = 'my-static-files-bucket' # replacement for STATIC_ROOT
|
|
||||||
|
|
||||||
MINIO_PRIVATE_BUCKETS = [
|
|
||||||
'default',
|
|
||||||
]
|
|
||||||
MINIO_PUBLIC_BUCKETS = [
|
|
||||||
'default-public',
|
|
||||||
MINIO_MEDIA_FILES_BUCKET
|
|
||||||
]
|
|
||||||
MINIO_POLICY_HOOKS: List[Tuple[str, dict]] = []
|
|
||||||
MINIO_BUCKET_CHECK_ON_SAVE = True # Default: True // Creates bucket if missing, then save
|
|
||||||
|
|
||||||
# Custom HTTP Client (OPTIONAL)
|
# Custom HTTP Client (OPTIONAL)
|
||||||
import os
|
import os
|
||||||
import certifi
|
import certifi
|
||||||
|
|
@ -337,3 +357,30 @@ MINIO_HTTP_CLIENT: urllib3.poolmanager.PoolManager = urllib3.PoolManager(
|
||||||
status_forcelist=[500, 502, 503, 504]
|
status_forcelist=[500, 502, 503, 504]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
MINIO_ENDPOINT = config('MINIO_ENDPOINT', default='drive.gooyal.ir')
|
||||||
|
MINIO_USE_HTTPS = config('MINIO_USE_HTTPS', default=True, cast=bool)
|
||||||
|
|
||||||
|
MINIO_EXTERNAL_ENDPOINT = config('MINIO_EXTERNAL_ENDPOINT',
|
||||||
|
default='drive.gooyal.ir') # Default is same as MINIO_ENDPOINT
|
||||||
|
MINIO_EXTERNAL_ENDPOINT_USE_HTTPS = config('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS', default=True,
|
||||||
|
cast=bool) # Default is same as MINIO_USE_HTTPS
|
||||||
|
MINIO_REGION = None # Default is set to None
|
||||||
|
MINIO_ACCESS_KEY = config('MINIO_ACCESS_KEY')
|
||||||
|
MINIO_SECRET_KEY = config('MINIO_SECRET_KEY')
|
||||||
|
MINIO_URL_EXPIRY_HOURS = timedelta(days=1) # Default is 7 days (longest) if not defined
|
||||||
|
MINIO_CONSISTENCY_CHECK_ON_START = False
|
||||||
|
MINIO_MEDIA_FILES_BUCKET = config('MINIO_MEDIA_FILES_BUCKET') # replacement for MEDIA_ROOT
|
||||||
|
# MINIO_STATIC_FILES_BUCKET = 'my-static-files-bucket' # replacement for STATIC_ROOT
|
||||||
|
|
||||||
|
MINIO_PRIVATE_BUCKETS = [
|
||||||
|
'default',
|
||||||
|
]
|
||||||
|
MINIO_PUBLIC_BUCKETS = [
|
||||||
|
'default-public',
|
||||||
|
MINIO_MEDIA_FILES_BUCKET
|
||||||
|
]
|
||||||
|
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='')
|
||||||
|
|
@ -11,6 +11,6 @@ import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
|
@ -5,7 +5,7 @@ import sys
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'accounts.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings')
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ django-crispy-forms
|
||||||
crispy_bootstrap5
|
crispy_bootstrap5
|
||||||
django-jalali-date
|
django-jalali-date
|
||||||
django-betterforms
|
django-betterforms
|
||||||
psycopg[binary]
|
psycopg[binary,pool]
|
||||||
django_redis
|
django_redis
|
||||||
django_minio_backend
|
django_minio_backend
|
||||||
|
daphne
|
||||||
|
Twisted[tls,http2]
|
||||||
|
packaging==25.0
|
||||||
|
unidecode
|
||||||
198
requirements.txt
198
requirements.txt
|
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.11
|
# This file is autogenerated by pip-compile with Python 3.13
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile
|
# pip-compile
|
||||||
|
|
@ -8,33 +8,41 @@ amqp==5.3.1
|
||||||
# via kombu
|
# via kombu
|
||||||
argon2-cffi==25.1.0
|
argon2-cffi==25.1.0
|
||||||
# via minio
|
# via minio
|
||||||
argon2-cffi-bindings==21.2.0
|
argon2-cffi-bindings==25.1.0
|
||||||
# via argon2-cffi
|
# via argon2-cffi
|
||||||
asgiref==3.8.1
|
asgiref==3.11.1
|
||||||
# via
|
# via
|
||||||
|
# daphne
|
||||||
# django
|
# django
|
||||||
# django-cors-headers
|
# django-cors-headers
|
||||||
async-timeout==5.0.1
|
attrs==25.4.0
|
||||||
# via redis
|
|
||||||
attrs==24.3.0
|
|
||||||
# via
|
# via
|
||||||
# jsonschema
|
# jsonschema
|
||||||
# referencing
|
# referencing
|
||||||
billiard==4.2.1
|
# service-identity
|
||||||
|
# twisted
|
||||||
|
autobahn==25.12.2
|
||||||
|
# via daphne
|
||||||
|
automat==25.4.16
|
||||||
|
# via twisted
|
||||||
|
billiard==4.2.4
|
||||||
# via celery
|
# via celery
|
||||||
celery==5.4.0
|
cbor2==5.8.0
|
||||||
|
# via autobahn
|
||||||
|
celery==5.6.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
certifi==2024.12.14
|
certifi==2026.1.4
|
||||||
# via
|
# via
|
||||||
# minio
|
# minio
|
||||||
# requests
|
# requests
|
||||||
cffi==1.17.1
|
cffi==2.0.0
|
||||||
# via
|
# via
|
||||||
# argon2-cffi-bindings
|
# argon2-cffi-bindings
|
||||||
|
# autobahn
|
||||||
# cryptography
|
# cryptography
|
||||||
charset-normalizer==3.4.0
|
charset-normalizer==3.4.4
|
||||||
# via requests
|
# via requests
|
||||||
click==8.1.7
|
click==8.3.1
|
||||||
# via
|
# via
|
||||||
# celery
|
# celery
|
||||||
# click-didyoumean
|
# click-didyoumean
|
||||||
|
|
@ -42,15 +50,23 @@ click==8.1.7
|
||||||
# click-repl
|
# click-repl
|
||||||
click-didyoumean==0.3.1
|
click-didyoumean==0.3.1
|
||||||
# via celery
|
# via celery
|
||||||
click-plugins==1.1.1
|
click-plugins==1.1.1.2
|
||||||
# via celery
|
# via celery
|
||||||
click-repl==0.3.0
|
click-repl==0.3.0
|
||||||
# via celery
|
# via celery
|
||||||
crispy-bootstrap5==2024.10
|
constantly==23.10.4
|
||||||
|
# via twisted
|
||||||
|
crispy-bootstrap5==2025.6
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
cryptography==44.0.0
|
cryptography==46.0.5
|
||||||
# via jwcrypto
|
# via
|
||||||
django==5.1.4
|
# autobahn
|
||||||
|
# jwcrypto
|
||||||
|
# pyopenssl
|
||||||
|
# service-identity
|
||||||
|
daphne==4.2.1
|
||||||
|
# via -r requirements.in
|
||||||
|
django==6.0.2
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# crispy-bootstrap5
|
# crispy-bootstrap5
|
||||||
|
|
@ -64,108 +80,156 @@ django==5.1.4
|
||||||
# django-redis
|
# django-redis
|
||||||
# djangorestframework
|
# djangorestframework
|
||||||
# drf-spectacular
|
# drf-spectacular
|
||||||
django-betterforms==2.0.0
|
django-betterforms==3.0.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-cors-headers==4.6.0
|
django-cors-headers==4.9.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-crispy-forms==2.3
|
django-crispy-forms==2.5
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# crispy-bootstrap5
|
# crispy-bootstrap5
|
||||||
django-filter==24.3
|
django-filter==25.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-jalali-date==1.1.3
|
django-jalali-date==2.0.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-minio-backend==3.8.0
|
django-minio-backend==4.5.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-oauth-toolkit==3.0.1
|
django-oauth-toolkit==3.2.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-redis==6.0.0
|
django-redis==6.0.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
djangorestframework==3.15.2
|
djangorestframework==3.16.1
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# drf-spectacular
|
# drf-spectacular
|
||||||
drf-spectacular==0.28.0
|
drf-spectacular==0.29.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
gevent==24.11.1
|
gevent==25.9.1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
greenlet==3.1.1
|
greenlet==3.3.2
|
||||||
# via gevent
|
# via gevent
|
||||||
gunicorn==23.0.0
|
gunicorn==25.1.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
idna==3.10
|
h2==4.3.0
|
||||||
# via requests
|
# via twisted
|
||||||
|
hpack==4.1.0
|
||||||
|
# via h2
|
||||||
|
hyperframe==6.1.0
|
||||||
|
# via h2
|
||||||
|
hyperlink==21.0.0
|
||||||
|
# via
|
||||||
|
# autobahn
|
||||||
|
# twisted
|
||||||
|
idna==3.11
|
||||||
|
# via
|
||||||
|
# hyperlink
|
||||||
|
# requests
|
||||||
|
# twisted
|
||||||
|
incremental==24.11.0
|
||||||
|
# via twisted
|
||||||
inflection==0.5.1
|
inflection==0.5.1
|
||||||
# via drf-spectacular
|
# via drf-spectacular
|
||||||
jalali-core==1.0.0
|
jalali-core==1.0.0
|
||||||
# via jdatetime
|
# via jdatetime
|
||||||
jdatetime==5.0.0
|
jdatetime==5.2.0
|
||||||
# via django-jalali-date
|
# via django-jalali-date
|
||||||
jsonschema==4.23.0
|
jsonschema==4.26.0
|
||||||
# via drf-spectacular
|
# via drf-spectacular
|
||||||
jsonschema-specifications==2024.10.1
|
jsonschema-specifications==2025.9.1
|
||||||
# via jsonschema
|
# via jsonschema
|
||||||
jwcrypto==1.5.6
|
jwcrypto==1.5.6
|
||||||
# via django-oauth-toolkit
|
# via django-oauth-toolkit
|
||||||
kombu==5.4.2
|
kombu==5.6.2
|
||||||
# via celery
|
# via celery
|
||||||
markdown==3.7
|
markdown==3.10.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
minio==7.2.16
|
minio==7.2.20
|
||||||
# via django-minio-backend
|
# via django-minio-backend
|
||||||
oauthlib==3.2.2
|
msgpack==1.1.2
|
||||||
|
# via autobahn
|
||||||
|
oauthlib==3.3.1
|
||||||
# via django-oauth-toolkit
|
# via django-oauth-toolkit
|
||||||
packaging==24.2
|
packaging==25.0
|
||||||
# via gunicorn
|
# via
|
||||||
pillow==11.0.0
|
# -r requirements.in
|
||||||
# via -r requirements.in
|
# gunicorn
|
||||||
prompt-toolkit==3.0.48
|
# incremental
|
||||||
|
# kombu
|
||||||
|
pillow==12.1.1
|
||||||
|
# via
|
||||||
|
# -r requirements.in
|
||||||
|
# django-minio-backend
|
||||||
|
priority==1.3.0
|
||||||
|
# via twisted
|
||||||
|
prompt-toolkit==3.0.52
|
||||||
# via click-repl
|
# via click-repl
|
||||||
psycopg[binary]==3.2.3
|
psycopg[binary,pool]==3.3.3
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
psycopg-binary==3.2.3
|
psycopg-binary==3.3.3
|
||||||
# via psycopg
|
# via psycopg
|
||||||
pycparser==2.22
|
psycopg-pool==3.3.0
|
||||||
|
# via psycopg
|
||||||
|
py-ubjson==0.16.1
|
||||||
|
# via autobahn
|
||||||
|
pyasn1==0.6.2
|
||||||
|
# via
|
||||||
|
# pyasn1-modules
|
||||||
|
# service-identity
|
||||||
|
pyasn1-modules==0.4.2
|
||||||
|
# via service-identity
|
||||||
|
pycparser==3.0
|
||||||
# via cffi
|
# via cffi
|
||||||
pycryptodome==3.23.0
|
pycryptodome==3.23.0
|
||||||
# via minio
|
# via minio
|
||||||
|
pyopenssl==25.3.0
|
||||||
|
# via twisted
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
# via celery
|
# via celery
|
||||||
python-decouple==3.8
|
python-decouple==3.8
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pyyaml==6.0.2
|
pyyaml==6.0.3
|
||||||
# via drf-spectacular
|
# via drf-spectacular
|
||||||
redis==5.2.1
|
redis==7.2.0
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# django-redis
|
# django-redis
|
||||||
referencing==0.35.1
|
referencing==0.37.0
|
||||||
# via
|
# via
|
||||||
# jsonschema
|
# jsonschema
|
||||||
# jsonschema-specifications
|
# jsonschema-specifications
|
||||||
requests==2.32.3
|
requests==2.32.5
|
||||||
# via django-oauth-toolkit
|
# via django-oauth-toolkit
|
||||||
rpds-py==0.22.3
|
rpds-py==0.30.0
|
||||||
# via
|
# via
|
||||||
# jsonschema
|
# jsonschema
|
||||||
# referencing
|
# referencing
|
||||||
|
service-identity==24.2.0
|
||||||
|
# via twisted
|
||||||
six==1.17.0
|
six==1.17.0
|
||||||
# via python-dateutil
|
# via python-dateutil
|
||||||
sqlparse==0.5.3
|
sqlparse==0.5.5
|
||||||
# via django
|
# via django
|
||||||
typing-extensions==4.12.2
|
twisted[http2,tls]==25.5.0
|
||||||
|
# via
|
||||||
|
# -r requirements.in
|
||||||
|
# daphne
|
||||||
|
txaio==25.12.2
|
||||||
|
# via autobahn
|
||||||
|
typing-extensions==4.15.0
|
||||||
# via
|
# via
|
||||||
# jwcrypto
|
# jwcrypto
|
||||||
# minio
|
# minio
|
||||||
# psycopg
|
# psycopg-pool
|
||||||
tzdata==2024.2
|
# twisted
|
||||||
# via
|
tzdata==2025.3
|
||||||
# celery
|
# via kombu
|
||||||
# kombu
|
tzlocal==5.3.1
|
||||||
uritemplate==4.1.1
|
# via celery
|
||||||
|
ujson==5.11.0
|
||||||
|
# via autobahn
|
||||||
|
uritemplate==4.2.0
|
||||||
# via drf-spectacular
|
# via drf-spectacular
|
||||||
urllib3==2.2.3
|
urllib3==2.6.3
|
||||||
# via
|
# via
|
||||||
# minio
|
# minio
|
||||||
# requests
|
# requests
|
||||||
|
|
@ -174,12 +238,12 @@ vine==5.1.0
|
||||||
# amqp
|
# amqp
|
||||||
# celery
|
# celery
|
||||||
# kombu
|
# kombu
|
||||||
wcwidth==0.2.13
|
wcwidth==0.6.0
|
||||||
# via prompt-toolkit
|
# via prompt-toolkit
|
||||||
zope-event==5.0
|
zope-event==6.1
|
||||||
# via gevent
|
# via gevent
|
||||||
zope-interface==7.2
|
zope-interface==8.2
|
||||||
# via gevent
|
# via
|
||||||
|
# gevent
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# twisted
|
||||||
# setuptools
|
unidecode==1.4.0
|
||||||
|
|
|
||||||
11
run.sh
11
run.sh
|
|
@ -1,8 +1,9 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
while ! nc -z $DB_HOST 5432 ; do
|
#while ! nc -z $DB_HOST 5432 ; do
|
||||||
echo "APP Waiting for the DB Server"
|
# echo "APP Waiting for the DB Server"
|
||||||
sleep 3
|
# sleep 3
|
||||||
done
|
#done
|
||||||
#python3 manage.py collectstatic --noinput
|
#python3 manage.py collectstatic --noinput
|
||||||
python3 manage.py migrate
|
python3 manage.py migrate
|
||||||
gunicorn accounts.wsgi:application --bind 0.0.0.0:8000 -w 4
|
gunicorn main.wsgi:application --bind 0.0.0.0:8000 -w 4
|
||||||
|
#daphne -b 0.0.0.0 -p 8000 main.asgi:application
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
|
|
||||||
{% block title %}Accounts: Home{% endblock %}
|
{% block title %}Accounts: Home{% endblock %}
|
||||||
{% block page_title %}Accounts: Home{% endblock %}
|
{% block page_title %}Accounts: Home{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
|
|
||||||
{% block title %}Application Register{% endblock %}
|
{% block title %}Application Register{% endblock %}
|
||||||
{% block page_title %}Application Register{% endblock %}
|
{% block page_title %}Application Register{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
|
|
||||||
{% block title %}Application Detail: {{ object }}{% endblock %}
|
{% block title %}Application Detail: {{ object }}{% endblock %}
|
||||||
{% block page_title %}Application Detail: {{ object }}{% endblock %}
|
{% block page_title %}Application Detail: {{ object }}{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load tags %}
|
{% load tags %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
|
|
||||||
{% block title %}Application Scope Register{% endblock %}
|
{% block title %}Application Scope Register{% endblock %}
|
||||||
{% block page_title %}Application Scope Register{% endblock %}
|
{% block page_title %}Application Scope Register{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
|
|
||||||
{% block title %}Application Scope Detail: {{ object.name }}{% endblock %}
|
{% block title %}Application Scope Detail: {{ object.name }}{% endblock %}
|
||||||
{% block page_title %}Application Scope Detail: {{ object.name }}{% endblock %}
|
{% block page_title %}Application Scope Detail: {{ object.name }}{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load tags %}
|
{% load tags %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
|
|
||||||
{% block title %}Application Update: {{ object.name }}{% endblock %}
|
{% block title %}Application Update: {{ object.name }}{% endblock %}
|
||||||
{% block page_title %}Application Update: {{ object.name }}{% endblock %}
|
{% block page_title %}Application Update: {{ object.name }}{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
|
|
||||||
{% block title %}Application Update: {{ object.name }}{% endblock %}
|
{% block title %}Application Update: {{ object.name }}{% endblock %}
|
||||||
{% block page_title %}Application Update: {{ object.name }}{% endblock %}
|
{% block page_title %}Application Update: {{ object.name }}{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
|
|
||||||
{% block title %}user account{% endblock %}
|
{% block title %}user account{% endblock %}
|
||||||
{% block page_title %}user account{% endblock %}
|
{% block page_title %}user account{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load jalali_tags %}
|
{#{% load jalali_tags %}#}
|
||||||
|
|
||||||
{% block title %}user account{% endblock %}
|
{% block title %}user account{% endblock %}
|
||||||
{% block page_title %}user account{% endblock %}
|
{% block page_title %}user account{% endblock %}
|
||||||
|
|
|
||||||
8
utils/clients/gooyal_promotions_client/__init__.py
Normal file
8
utils/clients/gooyal_promotions_client/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
"""A client library for accessing service"""
|
||||||
|
|
||||||
|
from .client import AuthenticatedClient, Client
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"AuthenticatedClient",
|
||||||
|
"Client",
|
||||||
|
)
|
||||||
1
utils/clients/gooyal_promotions_client/api/__init__.py
Normal file
1
utils/clients/gooyal_promotions_client/api/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Contains methods for accessing the API"""
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
"""Contains endpoint functions for accessing the API"""
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ... import errors
|
||||||
|
from ...client import AuthenticatedClient, Client
|
||||||
|
from ...models.event import Event
|
||||||
|
from ...types import Response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kwargs(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
body: Event,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
headers: dict[str, Any] = {}
|
||||||
|
|
||||||
|
_kwargs: dict[str, Any] = {
|
||||||
|
"method": "post",
|
||||||
|
"url": "/api/v2/promotions/application/{user_uuid}/event/".format(
|
||||||
|
user_uuid=quote(str(user_uuid), safe=""),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
_kwargs["json"] = body.to_dict()
|
||||||
|
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
_kwargs["headers"] = headers
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Event | None:
|
||||||
|
if response.status_code == 201:
|
||||||
|
response_201 = Event.from_dict(response.json())
|
||||||
|
|
||||||
|
return response_201
|
||||||
|
|
||||||
|
if client.raise_on_unexpected_status:
|
||||||
|
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Event]:
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus(response.status_code),
|
||||||
|
content=response.content,
|
||||||
|
headers=response.headers,
|
||||||
|
parsed=_parse_response(client=client, response=response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_detailed(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Event,
|
||||||
|
) -> Response[Event]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
body (Event):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Event]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_httpx_client().request(
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Event,
|
||||||
|
) -> Event | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
body (Event):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Event
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sync_detailed(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
).parsed
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio_detailed(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Event,
|
||||||
|
) -> Response[Event]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
body (Event):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Event]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.get_async_httpx_client().request(**kwargs)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Event,
|
||||||
|
) -> Event | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
body (Event):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Event
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
await asyncio_detailed(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
).parsed
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ... import errors
|
||||||
|
from ...client import AuthenticatedClient, Client
|
||||||
|
from ...models.paginated_user_plan_list import PaginatedUserPlanList
|
||||||
|
from ...types import UNSET, Response, Unset
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kwargs(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
|
||||||
|
params["limit"] = limit
|
||||||
|
|
||||||
|
params["offset"] = offset
|
||||||
|
|
||||||
|
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
|
||||||
|
|
||||||
|
_kwargs: dict[str, Any] = {
|
||||||
|
"method": "get",
|
||||||
|
"url": "/api/v2/promotions/application/{user_uuid}/plan/".format(
|
||||||
|
user_uuid=quote(str(user_uuid), safe=""),
|
||||||
|
),
|
||||||
|
"params": params,
|
||||||
|
}
|
||||||
|
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> PaginatedUserPlanList | None:
|
||||||
|
if response.status_code == 200:
|
||||||
|
response_200 = PaginatedUserPlanList.from_dict(response.json())
|
||||||
|
|
||||||
|
return response_200
|
||||||
|
|
||||||
|
if client.raise_on_unexpected_status:
|
||||||
|
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(
|
||||||
|
*, client: AuthenticatedClient | Client, response: httpx.Response
|
||||||
|
) -> Response[PaginatedUserPlanList]:
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus(response.status_code),
|
||||||
|
content=response.content,
|
||||||
|
headers=response.headers,
|
||||||
|
parsed=_parse_response(client=client, response=response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_detailed(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> Response[PaginatedUserPlanList]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
limit (int | Unset):
|
||||||
|
offset (int | Unset):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[PaginatedUserPlanList]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_httpx_client().request(
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> PaginatedUserPlanList | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
limit (int | Unset):
|
||||||
|
offset (int | Unset):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PaginatedUserPlanList
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sync_detailed(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
client=client,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
).parsed
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio_detailed(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> Response[PaginatedUserPlanList]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
limit (int | Unset):
|
||||||
|
offset (int | Unset):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[PaginatedUserPlanList]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.get_async_httpx_client().request(**kwargs)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio(
|
||||||
|
user_uuid: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> PaginatedUserPlanList | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
limit (int | Unset):
|
||||||
|
offset (int | Unset):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PaginatedUserPlanList
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
await asyncio_detailed(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
client=client,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
).parsed
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ... import errors
|
||||||
|
from ...client import AuthenticatedClient, Client
|
||||||
|
from ...models.promote import Promote
|
||||||
|
from ...types import Response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kwargs(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
body: Promote,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
headers: dict[str, Any] = {}
|
||||||
|
|
||||||
|
_kwargs: dict[str, Any] = {
|
||||||
|
"method": "post",
|
||||||
|
"url": "/api/v2/promotions/application/{user_uuid}/plan/{id}/promote/".format(
|
||||||
|
user_uuid=quote(str(user_uuid), safe=""),
|
||||||
|
id=quote(str(id), safe=""),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
_kwargs["json"] = body.to_dict()
|
||||||
|
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
_kwargs["headers"] = headers
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promote | None:
|
||||||
|
if response.status_code == 200:
|
||||||
|
response_200 = Promote.from_dict(response.json())
|
||||||
|
|
||||||
|
return response_200
|
||||||
|
|
||||||
|
if client.raise_on_unexpected_status:
|
||||||
|
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promote]:
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus(response.status_code),
|
||||||
|
content=response.content,
|
||||||
|
headers=response.headers,
|
||||||
|
parsed=_parse_response(client=client, response=response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_detailed(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Response[Promote]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
id (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Promote]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
id=id,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_httpx_client().request(
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Promote | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
id (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Promote
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sync_detailed(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
id=id,
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
).parsed
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio_detailed(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Response[Promote]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
id (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Promote]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
id=id,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.get_async_httpx_client().request(**kwargs)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Promote | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
id (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Promote
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
await asyncio_detailed(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
id=id,
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
).parsed
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ... import errors
|
||||||
|
from ...client import AuthenticatedClient, Client
|
||||||
|
from ...models.user_plan import UserPlan
|
||||||
|
from ...types import Response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kwargs(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
|
||||||
|
_kwargs: dict[str, Any] = {
|
||||||
|
"method": "get",
|
||||||
|
"url": "/api/v2/promotions/application/{user_uuid}/plan/{id}/".format(
|
||||||
|
user_uuid=quote(str(user_uuid), safe=""),
|
||||||
|
id=quote(str(id), safe=""),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> UserPlan | None:
|
||||||
|
if response.status_code == 200:
|
||||||
|
response_200 = UserPlan.from_dict(response.json())
|
||||||
|
|
||||||
|
return response_200
|
||||||
|
|
||||||
|
if client.raise_on_unexpected_status:
|
||||||
|
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[UserPlan]:
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus(response.status_code),
|
||||||
|
content=response.content,
|
||||||
|
headers=response.headers,
|
||||||
|
parsed=_parse_response(client=client, response=response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_detailed(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
) -> Response[UserPlan]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
id (str):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[UserPlan]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
id=id,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_httpx_client().request(
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
) -> UserPlan | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
id (str):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UserPlan
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sync_detailed(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
id=id,
|
||||||
|
client=client,
|
||||||
|
).parsed
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio_detailed(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
) -> Response[UserPlan]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
id (str):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[UserPlan]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
id=id,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.get_async_httpx_client().request(**kwargs)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio(
|
||||||
|
user_uuid: str,
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
) -> UserPlan | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
user_uuid (str):
|
||||||
|
id (str):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UserPlan
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
await asyncio_detailed(
|
||||||
|
user_uuid=user_uuid,
|
||||||
|
id=id,
|
||||||
|
client=client,
|
||||||
|
)
|
||||||
|
).parsed
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
"""Contains endpoint functions for accessing the API"""
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ... import errors
|
||||||
|
from ...client import AuthenticatedClient, Client
|
||||||
|
from ...models.promote import Promote
|
||||||
|
from ...types import Response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kwargs(
|
||||||
|
*,
|
||||||
|
body: Promote,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
headers: dict[str, Any] = {}
|
||||||
|
|
||||||
|
_kwargs: dict[str, Any] = {
|
||||||
|
"method": "post",
|
||||||
|
"url": "/promotions/api/v1/events/submit",
|
||||||
|
}
|
||||||
|
|
||||||
|
_kwargs["json"] = body.to_dict()
|
||||||
|
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
_kwargs["headers"] = headers
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promote | None:
|
||||||
|
if response.status_code == 201:
|
||||||
|
response_201 = Promote.from_dict(response.json())
|
||||||
|
|
||||||
|
return response_201
|
||||||
|
|
||||||
|
if client.raise_on_unexpected_status:
|
||||||
|
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promote]:
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus(response.status_code),
|
||||||
|
content=response.content,
|
||||||
|
headers=response.headers,
|
||||||
|
parsed=_parse_response(client=client, response=response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_detailed(
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Response[Promote]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Promote]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_httpx_client().request(
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Promote | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Promote
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sync_detailed(
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
).parsed
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio_detailed(
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Response[Promote]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Promote]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.get_async_httpx_client().request(**kwargs)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio(
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Promote | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Promote
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
await asyncio_detailed(
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
).parsed
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ... import errors
|
||||||
|
from ...client import AuthenticatedClient, Client
|
||||||
|
from ...models.promote import Promote
|
||||||
|
from ...types import Response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kwargs(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
body: Promote,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
headers: dict[str, Any] = {}
|
||||||
|
|
||||||
|
_kwargs: dict[str, Any] = {
|
||||||
|
"method": "post",
|
||||||
|
"url": "/promotions/api/v1/plans/{plan}/".format(
|
||||||
|
plan=quote(str(plan), safe=""),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
_kwargs["json"] = body.to_dict()
|
||||||
|
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
_kwargs["headers"] = headers
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promote | None:
|
||||||
|
if response.status_code == 201:
|
||||||
|
response_201 = Promote.from_dict(response.json())
|
||||||
|
|
||||||
|
return response_201
|
||||||
|
|
||||||
|
if client.raise_on_unexpected_status:
|
||||||
|
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promote]:
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus(response.status_code),
|
||||||
|
content=response.content,
|
||||||
|
headers=response.headers,
|
||||||
|
parsed=_parse_response(client=client, response=response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_detailed(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Response[Promote]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plan (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Promote]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
plan=plan,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_httpx_client().request(
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Promote | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plan (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Promote
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sync_detailed(
|
||||||
|
plan=plan,
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
).parsed
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio_detailed(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Response[Promote]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plan (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Promote]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
plan=plan,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.get_async_httpx_client().request(**kwargs)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Promote | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plan (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Promote
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
await asyncio_detailed(
|
||||||
|
plan=plan,
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
).parsed
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ... import errors
|
||||||
|
from ...client import AuthenticatedClient, Client
|
||||||
|
from ...models.paginated_promotion_list import PaginatedPromotionList
|
||||||
|
from ...types import UNSET, Response, Unset
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kwargs(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
|
||||||
|
params: dict[str, Any] = {}
|
||||||
|
|
||||||
|
params["limit"] = limit
|
||||||
|
|
||||||
|
params["offset"] = offset
|
||||||
|
|
||||||
|
params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
|
||||||
|
|
||||||
|
_kwargs: dict[str, Any] = {
|
||||||
|
"method": "get",
|
||||||
|
"url": "/promotions/api/v1/plans/{plan}/promotins".format(
|
||||||
|
plan=quote(str(plan), safe=""),
|
||||||
|
),
|
||||||
|
"params": params,
|
||||||
|
}
|
||||||
|
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> PaginatedPromotionList | None:
|
||||||
|
if response.status_code == 200:
|
||||||
|
response_200 = PaginatedPromotionList.from_dict(response.json())
|
||||||
|
|
||||||
|
return response_200
|
||||||
|
|
||||||
|
if client.raise_on_unexpected_status:
|
||||||
|
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(
|
||||||
|
*, client: AuthenticatedClient | Client, response: httpx.Response
|
||||||
|
) -> Response[PaginatedPromotionList]:
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus(response.status_code),
|
||||||
|
content=response.content,
|
||||||
|
headers=response.headers,
|
||||||
|
parsed=_parse_response(client=client, response=response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_detailed(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> Response[PaginatedPromotionList]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plan (str):
|
||||||
|
limit (int | Unset):
|
||||||
|
offset (int | Unset):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[PaginatedPromotionList]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
plan=plan,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_httpx_client().request(
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> PaginatedPromotionList | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plan (str):
|
||||||
|
limit (int | Unset):
|
||||||
|
offset (int | Unset):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PaginatedPromotionList
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sync_detailed(
|
||||||
|
plan=plan,
|
||||||
|
client=client,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
).parsed
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio_detailed(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> Response[PaginatedPromotionList]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plan (str):
|
||||||
|
limit (int | Unset):
|
||||||
|
offset (int | Unset):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[PaginatedPromotionList]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
plan=plan,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.get_async_httpx_client().request(**kwargs)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio(
|
||||||
|
plan: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
limit: int | Unset = UNSET,
|
||||||
|
offset: int | Unset = UNSET,
|
||||||
|
) -> PaginatedPromotionList | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
plan (str):
|
||||||
|
limit (int | Unset):
|
||||||
|
offset (int | Unset):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PaginatedPromotionList
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
await asyncio_detailed(
|
||||||
|
plan=plan,
|
||||||
|
client=client,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
).parsed
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ... import errors
|
||||||
|
from ...client import AuthenticatedClient, Client
|
||||||
|
from ...models.promote import Promote
|
||||||
|
from ...types import Response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kwargs(
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
body: Promote,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
headers: dict[str, Any] = {}
|
||||||
|
|
||||||
|
_kwargs: dict[str, Any] = {
|
||||||
|
"method": "post",
|
||||||
|
"url": "/promotions/api/v2/plans/{id}/promote/".format(
|
||||||
|
id=quote(str(id), safe=""),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
_kwargs["json"] = body.to_dict()
|
||||||
|
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
|
_kwargs["headers"] = headers
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promote | None:
|
||||||
|
if response.status_code == 200:
|
||||||
|
response_200 = Promote.from_dict(response.json())
|
||||||
|
|
||||||
|
return response_200
|
||||||
|
|
||||||
|
if client.raise_on_unexpected_status:
|
||||||
|
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promote]:
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus(response.status_code),
|
||||||
|
content=response.content,
|
||||||
|
headers=response.headers,
|
||||||
|
parsed=_parse_response(client=client, response=response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_detailed(
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Response[Promote]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
id (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Promote]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
id=id,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_httpx_client().request(
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Promote | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
id (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Promote
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sync_detailed(
|
||||||
|
id=id,
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
).parsed
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio_detailed(
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Response[Promote]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
id (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[Promote]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
id=id,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.get_async_httpx_client().request(**kwargs)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio(
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
body: Promote,
|
||||||
|
) -> Promote | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
id (str):
|
||||||
|
body (Promote):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Promote
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
await asyncio_detailed(
|
||||||
|
id=id,
|
||||||
|
client=client,
|
||||||
|
body=body,
|
||||||
|
)
|
||||||
|
).parsed
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ... import errors
|
||||||
|
from ...client import AuthenticatedClient, Client
|
||||||
|
from ...models.user_plan import UserPlan
|
||||||
|
from ...types import Response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_kwargs(
|
||||||
|
id: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
|
||||||
|
_kwargs: dict[str, Any] = {
|
||||||
|
"method": "get",
|
||||||
|
"url": "/promotions/api/v2/plans/{id}/".format(
|
||||||
|
id=quote(str(id), safe=""),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return _kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> UserPlan | None:
|
||||||
|
if response.status_code == 200:
|
||||||
|
response_200 = UserPlan.from_dict(response.json())
|
||||||
|
|
||||||
|
return response_200
|
||||||
|
|
||||||
|
if client.raise_on_unexpected_status:
|
||||||
|
raise errors.UnexpectedStatus(response.status_code, response.content)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[UserPlan]:
|
||||||
|
return Response(
|
||||||
|
status_code=HTTPStatus(response.status_code),
|
||||||
|
content=response.content,
|
||||||
|
headers=response.headers,
|
||||||
|
parsed=_parse_response(client=client, response=response),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_detailed(
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
) -> Response[UserPlan]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
id (str):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[UserPlan]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
id=id,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.get_httpx_client().request(
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
def sync(
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
) -> UserPlan | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
id (str):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UserPlan
|
||||||
|
"""
|
||||||
|
|
||||||
|
return sync_detailed(
|
||||||
|
id=id,
|
||||||
|
client=client,
|
||||||
|
).parsed
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio_detailed(
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
) -> Response[UserPlan]:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
id (str):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response[UserPlan]
|
||||||
|
"""
|
||||||
|
|
||||||
|
kwargs = _get_kwargs(
|
||||||
|
id=id,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.get_async_httpx_client().request(**kwargs)
|
||||||
|
|
||||||
|
return _build_response(client=client, response=response)
|
||||||
|
|
||||||
|
|
||||||
|
async def asyncio(
|
||||||
|
id: str,
|
||||||
|
*,
|
||||||
|
client: AuthenticatedClient,
|
||||||
|
) -> UserPlan | None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
id (str):
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
||||||
|
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UserPlan
|
||||||
|
"""
|
||||||
|
|
||||||
|
return (
|
||||||
|
await asyncio_detailed(
|
||||||
|
id=id,
|
||||||
|
client=client,
|
||||||
|
)
|
||||||
|
).parsed
|
||||||
268
utils/clients/gooyal_promotions_client/client.py
Normal file
268
utils/clients/gooyal_promotions_client/client.py
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
import ssl
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from attrs import define, evolve, field
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class Client:
|
||||||
|
"""A class for keeping track of data related to the API
|
||||||
|
|
||||||
|
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
|
||||||
|
|
||||||
|
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
|
||||||
|
|
||||||
|
``cookies``: A dictionary of cookies to be sent with every request
|
||||||
|
|
||||||
|
``headers``: A dictionary of headers to be sent with every request
|
||||||
|
|
||||||
|
``timeout``: The maximum amount of a time a request can take. API functions will raise
|
||||||
|
httpx.TimeoutException if this is exceeded.
|
||||||
|
|
||||||
|
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
|
||||||
|
but can be set to False for testing purposes.
|
||||||
|
|
||||||
|
``follow_redirects``: Whether or not to follow redirects. Default value is False.
|
||||||
|
|
||||||
|
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
|
||||||
|
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
|
||||||
|
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
|
||||||
|
argument to the constructor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
|
||||||
|
_base_url: str = field(alias="base_url")
|
||||||
|
_cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
|
||||||
|
_headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
|
||||||
|
_timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
|
||||||
|
_verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl")
|
||||||
|
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
|
||||||
|
_httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
|
||||||
|
_client: httpx.Client | None = field(default=None, init=False)
|
||||||
|
_async_client: httpx.AsyncClient | None = field(default=None, init=False)
|
||||||
|
|
||||||
|
def with_headers(self, headers: dict[str, str]) -> "Client":
|
||||||
|
"""Get a new client matching this one with additional headers"""
|
||||||
|
if self._client is not None:
|
||||||
|
self._client.headers.update(headers)
|
||||||
|
if self._async_client is not None:
|
||||||
|
self._async_client.headers.update(headers)
|
||||||
|
return evolve(self, headers={**self._headers, **headers})
|
||||||
|
|
||||||
|
def with_cookies(self, cookies: dict[str, str]) -> "Client":
|
||||||
|
"""Get a new client matching this one with additional cookies"""
|
||||||
|
if self._client is not None:
|
||||||
|
self._client.cookies.update(cookies)
|
||||||
|
if self._async_client is not None:
|
||||||
|
self._async_client.cookies.update(cookies)
|
||||||
|
return evolve(self, cookies={**self._cookies, **cookies})
|
||||||
|
|
||||||
|
def with_timeout(self, timeout: httpx.Timeout) -> "Client":
|
||||||
|
"""Get a new client matching this one with a new timeout configuration"""
|
||||||
|
if self._client is not None:
|
||||||
|
self._client.timeout = timeout
|
||||||
|
if self._async_client is not None:
|
||||||
|
self._async_client.timeout = timeout
|
||||||
|
return evolve(self, timeout=timeout)
|
||||||
|
|
||||||
|
def set_httpx_client(self, client: httpx.Client) -> "Client":
|
||||||
|
"""Manually set the underlying httpx.Client
|
||||||
|
|
||||||
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
||||||
|
"""
|
||||||
|
self._client = client
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_httpx_client(self) -> httpx.Client:
|
||||||
|
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
|
||||||
|
if self._client is None:
|
||||||
|
self._client = httpx.Client(
|
||||||
|
base_url=self._base_url,
|
||||||
|
cookies=self._cookies,
|
||||||
|
headers=self._headers,
|
||||||
|
timeout=self._timeout,
|
||||||
|
verify=self._verify_ssl,
|
||||||
|
follow_redirects=self._follow_redirects,
|
||||||
|
**self._httpx_args,
|
||||||
|
)
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
def __enter__(self) -> "Client":
|
||||||
|
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
|
||||||
|
self.get_httpx_client().__enter__()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
|
||||||
|
self.get_httpx_client().__exit__(*args, **kwargs)
|
||||||
|
|
||||||
|
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client":
|
||||||
|
"""Manually set the underlying httpx.AsyncClient
|
||||||
|
|
||||||
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
||||||
|
"""
|
||||||
|
self._async_client = async_client
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_async_httpx_client(self) -> httpx.AsyncClient:
|
||||||
|
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
|
||||||
|
if self._async_client is None:
|
||||||
|
self._async_client = httpx.AsyncClient(
|
||||||
|
base_url=self._base_url,
|
||||||
|
cookies=self._cookies,
|
||||||
|
headers=self._headers,
|
||||||
|
timeout=self._timeout,
|
||||||
|
verify=self._verify_ssl,
|
||||||
|
follow_redirects=self._follow_redirects,
|
||||||
|
**self._httpx_args,
|
||||||
|
)
|
||||||
|
return self._async_client
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "Client":
|
||||||
|
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
|
||||||
|
await self.get_async_httpx_client().__aenter__()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
|
||||||
|
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class AuthenticatedClient:
|
||||||
|
"""A Client which has been authenticated for use on secured endpoints
|
||||||
|
|
||||||
|
The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
|
||||||
|
|
||||||
|
``base_url``: The base URL for the API, all requests are made to a relative path to this URL
|
||||||
|
|
||||||
|
``cookies``: A dictionary of cookies to be sent with every request
|
||||||
|
|
||||||
|
``headers``: A dictionary of headers to be sent with every request
|
||||||
|
|
||||||
|
``timeout``: The maximum amount of a time a request can take. API functions will raise
|
||||||
|
httpx.TimeoutException if this is exceeded.
|
||||||
|
|
||||||
|
``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
|
||||||
|
but can be set to False for testing purposes.
|
||||||
|
|
||||||
|
``follow_redirects``: Whether or not to follow redirects. Default value is False.
|
||||||
|
|
||||||
|
``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
|
||||||
|
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a
|
||||||
|
status code that was not documented in the source OpenAPI document. Can also be provided as a keyword
|
||||||
|
argument to the constructor.
|
||||||
|
token: The token to use for authentication
|
||||||
|
prefix: The prefix to use for the Authorization header
|
||||||
|
auth_header_name: The name of the Authorization header
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise_on_unexpected_status: bool = field(default=False, kw_only=True)
|
||||||
|
_base_url: str = field(alias="base_url")
|
||||||
|
_cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
|
||||||
|
_headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
|
||||||
|
_timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
|
||||||
|
_verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl")
|
||||||
|
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
|
||||||
|
_httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
|
||||||
|
_client: httpx.Client | None = field(default=None, init=False)
|
||||||
|
_async_client: httpx.AsyncClient | None = field(default=None, init=False)
|
||||||
|
|
||||||
|
token: str
|
||||||
|
prefix: str = "Bearer"
|
||||||
|
auth_header_name: str = "Authorization"
|
||||||
|
|
||||||
|
def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient":
|
||||||
|
"""Get a new client matching this one with additional headers"""
|
||||||
|
if self._client is not None:
|
||||||
|
self._client.headers.update(headers)
|
||||||
|
if self._async_client is not None:
|
||||||
|
self._async_client.headers.update(headers)
|
||||||
|
return evolve(self, headers={**self._headers, **headers})
|
||||||
|
|
||||||
|
def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient":
|
||||||
|
"""Get a new client matching this one with additional cookies"""
|
||||||
|
if self._client is not None:
|
||||||
|
self._client.cookies.update(cookies)
|
||||||
|
if self._async_client is not None:
|
||||||
|
self._async_client.cookies.update(cookies)
|
||||||
|
return evolve(self, cookies={**self._cookies, **cookies})
|
||||||
|
|
||||||
|
def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient":
|
||||||
|
"""Get a new client matching this one with a new timeout configuration"""
|
||||||
|
if self._client is not None:
|
||||||
|
self._client.timeout = timeout
|
||||||
|
if self._async_client is not None:
|
||||||
|
self._async_client.timeout = timeout
|
||||||
|
return evolve(self, timeout=timeout)
|
||||||
|
|
||||||
|
def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient":
|
||||||
|
"""Manually set the underlying httpx.Client
|
||||||
|
|
||||||
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
||||||
|
"""
|
||||||
|
self._client = client
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_httpx_client(self) -> httpx.Client:
|
||||||
|
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
|
||||||
|
if self._client is None:
|
||||||
|
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
|
||||||
|
self._client = httpx.Client(
|
||||||
|
base_url=self._base_url,
|
||||||
|
cookies=self._cookies,
|
||||||
|
headers=self._headers,
|
||||||
|
timeout=self._timeout,
|
||||||
|
verify=self._verify_ssl,
|
||||||
|
follow_redirects=self._follow_redirects,
|
||||||
|
**self._httpx_args,
|
||||||
|
)
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
def __enter__(self) -> "AuthenticatedClient":
|
||||||
|
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
|
||||||
|
self.get_httpx_client().__enter__()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
|
||||||
|
self.get_httpx_client().__exit__(*args, **kwargs)
|
||||||
|
|
||||||
|
def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient":
|
||||||
|
"""Manually set the underlying httpx.AsyncClient
|
||||||
|
|
||||||
|
**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
|
||||||
|
"""
|
||||||
|
self._async_client = async_client
|
||||||
|
return self
|
||||||
|
|
||||||
|
def get_async_httpx_client(self) -> httpx.AsyncClient:
|
||||||
|
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
|
||||||
|
if self._async_client is None:
|
||||||
|
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
|
||||||
|
self._async_client = httpx.AsyncClient(
|
||||||
|
base_url=self._base_url,
|
||||||
|
cookies=self._cookies,
|
||||||
|
headers=self._headers,
|
||||||
|
timeout=self._timeout,
|
||||||
|
verify=self._verify_ssl,
|
||||||
|
follow_redirects=self._follow_redirects,
|
||||||
|
**self._httpx_args,
|
||||||
|
)
|
||||||
|
return self._async_client
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "AuthenticatedClient":
|
||||||
|
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
|
||||||
|
await self.get_async_httpx_client().__aenter__()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
|
||||||
|
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
|
||||||
16
utils/clients/gooyal_promotions_client/errors.py
Normal file
16
utils/clients/gooyal_promotions_client/errors.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""Contains shared errors types that can be raised from API functions"""
|
||||||
|
|
||||||
|
|
||||||
|
class UnexpectedStatus(Exception):
|
||||||
|
"""Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True"""
|
||||||
|
|
||||||
|
def __init__(self, status_code: int, content: bytes):
|
||||||
|
self.status_code = status_code
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["UnexpectedStatus"]
|
||||||
21
utils/clients/gooyal_promotions_client/models/__init__.py
Normal file
21
utils/clients/gooyal_promotions_client/models/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
"""Contains all the data models used in inputs/outputs"""
|
||||||
|
|
||||||
|
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
|
||||||
|
from .user_plan import UserPlan
|
||||||
|
from .user_recipient import UserRecipient
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
"Event",
|
||||||
|
"PaginatedPromotionList",
|
||||||
|
"PaginatedUserPlanList",
|
||||||
|
"Promote",
|
||||||
|
"Promotion",
|
||||||
|
"StateEnum",
|
||||||
|
"UserPlan",
|
||||||
|
"UserRecipient",
|
||||||
|
)
|
||||||
120
utils/clients/gooyal_promotions_client/models/event.py
Normal file
120
utils/clients/gooyal_promotions_client/models/event.py
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any, TypeVar, cast
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from attrs import define as _attrs_define
|
||||||
|
from attrs import field as _attrs_field
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="Event")
|
||||||
|
|
||||||
|
|
||||||
|
@_attrs_define
|
||||||
|
class Event:
|
||||||
|
"""
|
||||||
|
Attributes:
|
||||||
|
label (str):
|
||||||
|
data (Any):
|
||||||
|
user (None | UUID):
|
||||||
|
application (None | UUID):
|
||||||
|
"""
|
||||||
|
|
||||||
|
label: str
|
||||||
|
data: Any
|
||||||
|
user: None | UUID
|
||||||
|
application: None | UUID
|
||||||
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
label = self.label
|
||||||
|
|
||||||
|
data = self.data
|
||||||
|
|
||||||
|
user: None | str
|
||||||
|
if isinstance(self.user, UUID):
|
||||||
|
user = str(self.user)
|
||||||
|
else:
|
||||||
|
user = self.user
|
||||||
|
|
||||||
|
application: None | str
|
||||||
|
if isinstance(self.application, UUID):
|
||||||
|
application = str(self.application)
|
||||||
|
else:
|
||||||
|
application = self.application
|
||||||
|
|
||||||
|
field_dict: dict[str, Any] = {}
|
||||||
|
field_dict.update(self.additional_properties)
|
||||||
|
field_dict.update(
|
||||||
|
{
|
||||||
|
"label": label,
|
||||||
|
"data": data,
|
||||||
|
"user": user,
|
||||||
|
"application": application,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return field_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||||
|
d = dict(src_dict)
|
||||||
|
label = d.pop("label")
|
||||||
|
|
||||||
|
data = d.pop("data")
|
||||||
|
|
||||||
|
def _parse_user(data: object) -> None | UUID:
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
try:
|
||||||
|
if not isinstance(data, str):
|
||||||
|
raise TypeError()
|
||||||
|
user_type_0 = UUID(data)
|
||||||
|
|
||||||
|
return user_type_0
|
||||||
|
except (TypeError, ValueError, AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
return cast(None | UUID, data)
|
||||||
|
|
||||||
|
user = _parse_user(d.pop("user"))
|
||||||
|
|
||||||
|
def _parse_application(data: object) -> None | UUID:
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
try:
|
||||||
|
if not isinstance(data, str):
|
||||||
|
raise TypeError()
|
||||||
|
application_type_0 = UUID(data)
|
||||||
|
|
||||||
|
return application_type_0
|
||||||
|
except (TypeError, ValueError, AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
return cast(None | UUID, data)
|
||||||
|
|
||||||
|
application = _parse_application(d.pop("application"))
|
||||||
|
|
||||||
|
event = cls(
|
||||||
|
label=label,
|
||||||
|
data=data,
|
||||||
|
user=user,
|
||||||
|
application=application,
|
||||||
|
)
|
||||||
|
|
||||||
|
event.additional_properties = d
|
||||||
|
return event
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
@ -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.promotion import Promotion
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="PaginatedPromotionList")
|
||||||
|
|
||||||
|
|
||||||
|
@_attrs_define
|
||||||
|
class PaginatedPromotionList:
|
||||||
|
"""
|
||||||
|
Attributes:
|
||||||
|
count (int): Example: 123.
|
||||||
|
results (list[Promotion]):
|
||||||
|
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[Promotion]
|
||||||
|
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.promotion import Promotion
|
||||||
|
|
||||||
|
d = dict(src_dict)
|
||||||
|
count = d.pop("count")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
_results = d.pop("results")
|
||||||
|
for results_item_data in _results:
|
||||||
|
results_item = Promotion.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_promotion_list = cls(
|
||||||
|
count=count,
|
||||||
|
results=results,
|
||||||
|
next_=next_,
|
||||||
|
previous=previous,
|
||||||
|
)
|
||||||
|
|
||||||
|
paginated_promotion_list.additional_properties = d
|
||||||
|
return paginated_promotion_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
|
||||||
|
|
@ -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
|
||||||
91
utils/clients/gooyal_promotions_client/models/promote.py
Normal file
91
utils/clients/gooyal_promotions_client/models/promote.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import TYPE_CHECKING, Any, TypeVar
|
||||||
|
|
||||||
|
from attrs import define as _attrs_define
|
||||||
|
from attrs import field as _attrs_field
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..models.promotion import Promotion
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="Promote")
|
||||||
|
|
||||||
|
|
||||||
|
@_attrs_define
|
||||||
|
class Promote:
|
||||||
|
"""
|
||||||
|
Attributes:
|
||||||
|
promotions (list[Promotion]):
|
||||||
|
label (str):
|
||||||
|
data (Any):
|
||||||
|
"""
|
||||||
|
|
||||||
|
promotions: list[Promotion]
|
||||||
|
label: str
|
||||||
|
data: Any
|
||||||
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
promotions = []
|
||||||
|
for promotions_item_data in self.promotions:
|
||||||
|
promotions_item = promotions_item_data.to_dict()
|
||||||
|
promotions.append(promotions_item)
|
||||||
|
|
||||||
|
label = self.label
|
||||||
|
|
||||||
|
data = self.data
|
||||||
|
|
||||||
|
field_dict: dict[str, Any] = {}
|
||||||
|
field_dict.update(self.additional_properties)
|
||||||
|
field_dict.update(
|
||||||
|
{
|
||||||
|
"promotions": promotions,
|
||||||
|
"label": label,
|
||||||
|
"data": data,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return field_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||||
|
from ..models.promotion import Promotion
|
||||||
|
|
||||||
|
d = dict(src_dict)
|
||||||
|
promotions = []
|
||||||
|
_promotions = d.pop("promotions")
|
||||||
|
for promotions_item_data in _promotions:
|
||||||
|
promotions_item = Promotion.from_dict(promotions_item_data)
|
||||||
|
|
||||||
|
promotions.append(promotions_item)
|
||||||
|
|
||||||
|
label = d.pop("label")
|
||||||
|
|
||||||
|
data = d.pop("data")
|
||||||
|
|
||||||
|
promote = cls(
|
||||||
|
promotions=promotions,
|
||||||
|
label=label,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
promote.additional_properties = d
|
||||||
|
return promote
|
||||||
|
|
||||||
|
@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
|
||||||
129
utils/clients/gooyal_promotions_client/models/promotion.py
Normal file
129
utils/clients/gooyal_promotions_client/models/promotion.py
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from attrs import define as _attrs_define
|
||||||
|
from attrs import field as _attrs_field
|
||||||
|
|
||||||
|
from ..models.state_enum import StateEnum
|
||||||
|
from ..types import UNSET, Unset
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..models.event import Event
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="Promotion")
|
||||||
|
|
||||||
|
|
||||||
|
@_attrs_define
|
||||||
|
class Promotion:
|
||||||
|
"""
|
||||||
|
Attributes:
|
||||||
|
uuid (UUID):
|
||||||
|
event (Event):
|
||||||
|
user (UUID):
|
||||||
|
promotion_amount (int | None | Unset):
|
||||||
|
state (StateEnum | Unset): * `1` - created
|
||||||
|
* `2` - delayed
|
||||||
|
* `3` - pending
|
||||||
|
* `4` - incomplete
|
||||||
|
* `5` - success
|
||||||
|
* `6` - failed
|
||||||
|
* `7` - expected_failure
|
||||||
|
"""
|
||||||
|
|
||||||
|
uuid: UUID
|
||||||
|
event: Event
|
||||||
|
user: UUID
|
||||||
|
promotion_amount: int | None | Unset = UNSET
|
||||||
|
state: StateEnum | Unset = UNSET
|
||||||
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
uuid = str(self.uuid)
|
||||||
|
|
||||||
|
event = self.event.to_dict()
|
||||||
|
|
||||||
|
user = str(self.user)
|
||||||
|
|
||||||
|
promotion_amount: int | None | Unset
|
||||||
|
if isinstance(self.promotion_amount, Unset):
|
||||||
|
promotion_amount = UNSET
|
||||||
|
else:
|
||||||
|
promotion_amount = self.promotion_amount
|
||||||
|
|
||||||
|
state: int | Unset = UNSET
|
||||||
|
if not isinstance(self.state, Unset):
|
||||||
|
state = self.state.value
|
||||||
|
|
||||||
|
field_dict: dict[str, Any] = {}
|
||||||
|
field_dict.update(self.additional_properties)
|
||||||
|
field_dict.update(
|
||||||
|
{
|
||||||
|
"uuid": uuid,
|
||||||
|
"event": event,
|
||||||
|
"user": user,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if promotion_amount is not UNSET:
|
||||||
|
field_dict["promotion_amount"] = promotion_amount
|
||||||
|
if state is not UNSET:
|
||||||
|
field_dict["state"] = state
|
||||||
|
|
||||||
|
return field_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||||
|
from ..models.event import Event
|
||||||
|
|
||||||
|
d = dict(src_dict)
|
||||||
|
uuid = UUID(d.pop("uuid"))
|
||||||
|
|
||||||
|
event = Event.from_dict(d.pop("event"))
|
||||||
|
|
||||||
|
user = UUID(d.pop("user"))
|
||||||
|
|
||||||
|
def _parse_promotion_amount(data: object) -> int | None | Unset:
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
if isinstance(data, Unset):
|
||||||
|
return data
|
||||||
|
return cast(int | None | Unset, data)
|
||||||
|
|
||||||
|
promotion_amount = _parse_promotion_amount(d.pop("promotion_amount", UNSET))
|
||||||
|
|
||||||
|
_state = d.pop("state", UNSET)
|
||||||
|
state: StateEnum | Unset
|
||||||
|
if isinstance(_state, Unset):
|
||||||
|
state = UNSET
|
||||||
|
else:
|
||||||
|
state = StateEnum(_state)
|
||||||
|
|
||||||
|
promotion = cls(
|
||||||
|
uuid=uuid,
|
||||||
|
event=event,
|
||||||
|
user=user,
|
||||||
|
promotion_amount=promotion_amount,
|
||||||
|
state=state,
|
||||||
|
)
|
||||||
|
|
||||||
|
promotion.additional_properties = d
|
||||||
|
return promotion
|
||||||
|
|
||||||
|
@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
|
||||||
14
utils/clients/gooyal_promotions_client/models/state_enum.py
Normal file
14
utils/clients/gooyal_promotions_client/models/state_enum.py
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class StateEnum(IntEnum):
|
||||||
|
VALUE_1 = 1
|
||||||
|
VALUE_2 = 2
|
||||||
|
VALUE_3 = 3
|
||||||
|
VALUE_4 = 4
|
||||||
|
VALUE_5 = 5
|
||||||
|
VALUE_6 = 6
|
||||||
|
VALUE_7 = 7
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return str(self.value)
|
||||||
113
utils/clients/gooyal_promotions_client/models/user_plan.py
Normal file
113
utils/clients/gooyal_promotions_client/models/user_plan.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..models.user_recipient import UserRecipient
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="UserPlan")
|
||||||
|
|
||||||
|
|
||||||
|
@_attrs_define
|
||||||
|
class UserPlan:
|
||||||
|
"""
|
||||||
|
Attributes:
|
||||||
|
title (str):
|
||||||
|
banner (None | str):
|
||||||
|
description (str):
|
||||||
|
description_details (Any):
|
||||||
|
recipients (list[UserRecipient]):
|
||||||
|
"""
|
||||||
|
|
||||||
|
title: str
|
||||||
|
banner: None | str
|
||||||
|
description: str
|
||||||
|
description_details: Any
|
||||||
|
recipients: list[UserRecipient]
|
||||||
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
title = self.title
|
||||||
|
|
||||||
|
banner: None | str
|
||||||
|
banner = self.banner
|
||||||
|
|
||||||
|
description = self.description
|
||||||
|
|
||||||
|
description_details = self.description_details
|
||||||
|
|
||||||
|
recipients = []
|
||||||
|
for recipients_item_data in self.recipients:
|
||||||
|
recipients_item = recipients_item_data.to_dict()
|
||||||
|
recipients.append(recipients_item)
|
||||||
|
|
||||||
|
field_dict: dict[str, Any] = {}
|
||||||
|
field_dict.update(self.additional_properties)
|
||||||
|
field_dict.update(
|
||||||
|
{
|
||||||
|
"title": title,
|
||||||
|
"banner": banner,
|
||||||
|
"description": description,
|
||||||
|
"description_details": description_details,
|
||||||
|
"recipients": recipients,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return field_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||||
|
from ..models.user_recipient import UserRecipient
|
||||||
|
|
||||||
|
d = dict(src_dict)
|
||||||
|
title = d.pop("title")
|
||||||
|
|
||||||
|
def _parse_banner(data: object) -> None | str:
|
||||||
|
if data is None:
|
||||||
|
return data
|
||||||
|
return cast(None | str, data)
|
||||||
|
|
||||||
|
banner = _parse_banner(d.pop("banner"))
|
||||||
|
|
||||||
|
description = d.pop("description")
|
||||||
|
|
||||||
|
description_details = d.pop("description_details")
|
||||||
|
|
||||||
|
recipients = []
|
||||||
|
_recipients = d.pop("recipients")
|
||||||
|
for recipients_item_data in _recipients:
|
||||||
|
recipients_item = UserRecipient.from_dict(recipients_item_data)
|
||||||
|
|
||||||
|
recipients.append(recipients_item)
|
||||||
|
|
||||||
|
user_plan = cls(
|
||||||
|
title=title,
|
||||||
|
banner=banner,
|
||||||
|
description=description,
|
||||||
|
description_details=description_details,
|
||||||
|
recipients=recipients,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_plan.additional_properties = d
|
||||||
|
return user_plan
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
|
from attrs import define as _attrs_define
|
||||||
|
from attrs import field as _attrs_field
|
||||||
|
|
||||||
|
T = TypeVar("T", bound="UserRecipient")
|
||||||
|
|
||||||
|
|
||||||
|
@_attrs_define
|
||||||
|
class UserRecipient:
|
||||||
|
"""
|
||||||
|
Attributes:
|
||||||
|
label (str):
|
||||||
|
base_amount_field (str):
|
||||||
|
"""
|
||||||
|
|
||||||
|
label: str
|
||||||
|
base_amount_field: str
|
||||||
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, Any]:
|
||||||
|
label = self.label
|
||||||
|
|
||||||
|
base_amount_field = self.base_amount_field
|
||||||
|
|
||||||
|
field_dict: dict[str, Any] = {}
|
||||||
|
field_dict.update(self.additional_properties)
|
||||||
|
field_dict.update(
|
||||||
|
{
|
||||||
|
"label": label,
|
||||||
|
"base_amount_field": base_amount_field,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return field_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
||||||
|
d = dict(src_dict)
|
||||||
|
label = d.pop("label")
|
||||||
|
|
||||||
|
base_amount_field = d.pop("base_amount_field")
|
||||||
|
|
||||||
|
user_recipient = cls(
|
||||||
|
label=label,
|
||||||
|
base_amount_field=base_amount_field,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_recipient.additional_properties = d
|
||||||
|
return user_recipient
|
||||||
|
|
||||||
|
@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
utils/clients/gooyal_promotions_client/py.typed
Normal file
1
utils/clients/gooyal_promotions_client/py.typed
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# Marker file for PEP 561
|
||||||
54
utils/clients/gooyal_promotions_client/types.py
Normal file
54
utils/clients/gooyal_promotions_client/types.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
"""Contains some shared types for properties"""
|
||||||
|
|
||||||
|
from collections.abc import Mapping, MutableMapping
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import IO, BinaryIO, Generic, Literal, TypeVar
|
||||||
|
|
||||||
|
from attrs import define
|
||||||
|
|
||||||
|
|
||||||
|
class Unset:
|
||||||
|
def __bool__(self) -> Literal[False]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
UNSET: Unset = Unset()
|
||||||
|
|
||||||
|
# The types that `httpx.Client(files=)` can accept, copied from that library.
|
||||||
|
FileContent = IO[bytes] | bytes | str
|
||||||
|
FileTypes = (
|
||||||
|
# (filename, file (or bytes), content_type)
|
||||||
|
tuple[str | None, FileContent, str | None]
|
||||||
|
# (filename, file (or bytes), content_type, headers)
|
||||||
|
| tuple[str | None, FileContent, str | None, Mapping[str, str]]
|
||||||
|
)
|
||||||
|
RequestFiles = list[tuple[str, FileTypes]]
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class File:
|
||||||
|
"""Contains information for file uploads"""
|
||||||
|
|
||||||
|
payload: BinaryIO
|
||||||
|
file_name: str | None = None
|
||||||
|
mime_type: str | None = None
|
||||||
|
|
||||||
|
def to_tuple(self) -> FileTypes:
|
||||||
|
"""Return a tuple representation that httpx will accept for multipart/form-data"""
|
||||||
|
return self.file_name, self.payload, self.mime_type
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
|
@define
|
||||||
|
class Response(Generic[T]):
|
||||||
|
"""A response from an endpoint"""
|
||||||
|
|
||||||
|
status_code: HTTPStatus
|
||||||
|
content: bytes
|
||||||
|
headers: MutableMapping[str, str]
|
||||||
|
parsed: T | None
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"]
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
# TODO: this is time fourced code. refactor
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
def get_access_token():
|
|
||||||
payam_sms_system_name = settings.PAYAM_SMS_SYSTEM_NAME
|
|
||||||
payam_sms_username = settings.PAYAM_SMS_USERNAME
|
|
||||||
payam_sms_password = settings.PAYAM_SMS_PASSWORD
|
|
||||||
payam_sms_client_id = settings.PAYAM_SMS_CLIENT_ID
|
|
||||||
payam_sms_client_secret = settings.PAYAM_SMS_CLIENT_SECRET
|
|
||||||
auth = (payam_sms_client_id, payam_sms_client_secret)
|
|
||||||
url = f"https://www.payamsms.com/auth/oauth/token?systemName={payam_sms_system_name}&username={payam_sms_username}&password={payam_sms_password}&scope=webservice&grant_type=password"
|
|
||||||
|
|
||||||
response_object = requests.post(url, auth=auth, json={})
|
|
||||||
response = response_object.json()
|
|
||||||
return response["access_token"]
|
|
||||||
|
|
||||||
|
|
||||||
def send_sms(phone_number, message):
|
|
||||||
try:
|
|
||||||
url = "https://www.payamsms.com/panel/webservice/send"
|
|
||||||
|
|
||||||
# access_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJzb2JhbiIsInNjb3BlIjpbIndlYnNlcnZpY2UiXSwiZXhwIjoxNzQ0MTAyMDU5LCJ1c2VyRGV0YWlscyI6eyJpZCI6MjU0MCwidXNlck5hbWUiOiJzb2JhbiIsInJvbGUiOiJVU0VSIiwiZW5hYmxlZCI6dHJ1ZSwic3lzdGVtSWQiOjcyMiwiYWNjZXNzIjp7InJvbGUiOlsiYWRkIiwiZWRpdCIsImdldCIsImRlbGV0ZSJdLCJwaG9uZUJvb2siOlsiYWRkIiwiZWRpdCIsImdldExvY2FsIiwiZ2V0R2xvYmFsIiwiZGVsZXRlIl0sInNtcyI6WyJzaW5nbGUiLCJnZXRMb2NhbCIsImdldEdsb2JhbCIsImJ1bGsiLCJkYiJdLCJyZXBvcnQiOlsiZ2V0TW9HbG9iYWwiLCJnZXREYWlseUxvY2FsIiwiZ2V0TW9Mb2NhbCIsImdldERhaWx5R2xvYmFsIl0sIndzIjpbIndzIl0sInVzZXIiOlsiYWRkIiwiZWRpdCIsImdldExvY2FsIiwiZ2V0IiwiZ2V0R2xvYmFsIiwiZGVsZXRlIl0sInRvb2xzIjpbInJhY2UiLCJwZXJpb2RpYyIsInBvbGxpbmciLCJzZWNyZXRvcnkiLCJhdXRvYW5zd2VyIl0sImZpbmFuY2UiOlsidGRyIiwiY2hhcmdlIl19LCJyb2xlSWQiOjQ0NDAsImNoaWxkcmVuSWRzIjpbXSwic3lzdGVtVHlwZSI6Ik5PUk1BTCIsInN5c3RlbVBheW1lbnRUeXBlIjoiREVCSVQiLCJzeXN0ZW1OYW1lIjoic29iYW4iLCJzZW5kV2l0aE91dFBlcm1pc3Npb25TeXN0ZW0iOmZhbHNlLCJtaXNDdXN0b21lciI6Itio2YbYr9in2LEg2LPZiNio2KfZhiDYs9in2YXYp9mG2YciLCJwYXJlbnRVc2VyTmFtZSI6bnVsbCwicGFyZW50SWQiOm51bGwsImFkbWluIjp0cnVlfSwiYXV0aG9yaXRpZXMiOlsiVVNFUiJdLCJqdGkiOiJNeU5vTml2RURmc3YxUDR3U20tbTR2Y2NHODgiLCJjbGllbnRfaWQiOiJzb2JhbiJ9.9mdneDduK4OVH2zsXvRqvXt2qwpLvs0vCuseLaB-LCo'
|
|
||||||
access_token = get_access_token()
|
|
||||||
|
|
||||||
body = [
|
|
||||||
{
|
|
||||||
"sender": "98200000443295",
|
|
||||||
"recipient": phone_number,
|
|
||||||
"body": message,
|
|
||||||
# "customerId": "1",
|
|
||||||
# "sendDate": "2024-03-04 10:17:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
'Authorization': f'Bearer {access_token}',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
response_object = requests.post(url, headers=headers, json=body, timeout=10)
|
|
||||||
print(response_object.text)
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
|
|
||||||
|
|
||||||
# send_sms("989106853582", "تست")
|
|
||||||
6
utils/clients/sms/fake/client.py
Normal file
6
utils/clients/sms/fake/client.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from utils.clients.sms.sms import BaseSMSClient
|
||||||
|
|
||||||
|
|
||||||
|
class FakeClient(BaseSMSClient):
|
||||||
|
def send_sms(self, phone_number, message):
|
||||||
|
pass
|
||||||
43
utils/clients/sms/mobin_sms/client.py
Normal file
43
utils/clients/sms/mobin_sms/client.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import urllib.parse
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from utils.clients.sms.sms import BaseSMSClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MobinSMSClient(BaseSMSClient):
|
||||||
|
access_token = settings.MOBIN_SMS_TOKEN
|
||||||
|
base_url = 'https://connect.mobinsms.com'
|
||||||
|
sender = '9890008050'
|
||||||
|
|
||||||
|
def send_sms(self, phone_number, message):
|
||||||
|
return self.send_sms_quick([phone_number], message)
|
||||||
|
|
||||||
|
def send_sms_quick(self, phone_number_list, message):
|
||||||
|
try:
|
||||||
|
path = '/v1/send/quick'
|
||||||
|
url = urllib.parse.urljoin(self.base_url, path)
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"from": self.sender,
|
||||||
|
"to": phone_number_list,
|
||||||
|
"body": message,
|
||||||
|
"unique_id": "" # UDH (optional field)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-API-Key": self.access_token,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, headers=headers, json=body, timeout=10)
|
||||||
|
logger.info(response.text)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f'error sending sms to {phone_number_list}: {e}')
|
||||||
|
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "9cc27226-bee8-4b29-a924-a9e14bb61d14",
|
||||||
|
"name": "Connect API",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||||
|
"_exporter_id": "2960438"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "Send by Pattern",
|
||||||
|
"protocolProfileBehavior": {
|
||||||
|
"followRedirects": true,
|
||||||
|
"disableUrlEncoding": false,
|
||||||
|
"disableCookies": false
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{apiKey}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"from\": \"989000xxxx\",\n\t\"to\": [\"989126880345\"],\n\t\"pattern\": \"YOUR_PATTERN_KEY\",\n\t\"data\": {\n\t\t\"0\": \"Var 0\",\n\t\t\"1\": \"Var 1\",\n\t\t\"2\": \"Var 2\",\n\t\t\"3\": \"Var 3\"\n\t}\n}\n",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/v1/send/pattern",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"v1",
|
||||||
|
"send",
|
||||||
|
"pattern"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send Quick",
|
||||||
|
"protocolProfileBehavior": {
|
||||||
|
"followRedirects": true,
|
||||||
|
"disableUrlEncoding": false,
|
||||||
|
"disableCookies": false
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{apiKey}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"from\": \"989000xxxx\",\n\t\"to\": [\"989126880345\", \"989127761851\"],\n\t\"body\": \"Your message\",\n \"unique_id\": \"\" // UDH (optional field)\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/v1/send/quick",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"v1",
|
||||||
|
"send",
|
||||||
|
"quick"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Status",
|
||||||
|
"protocolProfileBehavior": {
|
||||||
|
"disableBodyPruning": true,
|
||||||
|
"followRedirects": true,
|
||||||
|
"disableUrlEncoding": false,
|
||||||
|
"disableCookies": false
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{apiKey}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/v1/message/status?message_id=1893",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"v1",
|
||||||
|
"message",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "unique_id",
|
||||||
|
"value": "20",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "message_id",
|
||||||
|
"value": "1893"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "Delivered Message",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "User-Agent",
|
||||||
|
"value": "insomnia/11.6.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "07e7e9c79652b857aa77b8f3d40cc4314321970e1a5f78b9fc18e497aca2bf40"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"from\": \"9890001777\",\n\t\"to\": [\"989126880345\"],\n\t\"body\": \"Your message\",\n \"unique_id\": \"6\" // UDH (optional field)\n}\n",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/v1/message/status?message_id=1852",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"v1",
|
||||||
|
"message",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"query": [
|
||||||
|
{
|
||||||
|
"key": "message_id",
|
||||||
|
"value": "1852"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "unique_id",
|
||||||
|
"value": "webengage-message-id",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Server",
|
||||||
|
"value": "nginx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Date",
|
||||||
|
"value": "Sun, 09 Nov 2025 16:20:37 GMT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"description": "",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Content-Length",
|
||||||
|
"value": "38"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Connection",
|
||||||
|
"value": "keep-alive"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": 274,\n \"status\": \"2\", // 0: Pending, 1: Sent, 2: Delivered\n \"unique_id\": \"\"\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Webengage",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{apiKey}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"version\": \"2.0\",\n \"smsData\": {\n \"toNumber\": \"989195712939\",\n \"fromNumber\": \"9890006950\",\n \"body\": \"Text message body\"\n },\n \"metadata\": {\n \"campaignType\": \"PROMOTIONAL\",\n \"timestamp\": \"2018-01-25T10:24:16+0000\",\n \"messageId\": \"webengage-message-id222\",\n \"custom\": {\n \"key1\": \"val1\",\n \"key2\": \"val2\"\n },\n \"indiaDLT\": {\n \"contentTemplateId\": \"xyz\",\n \"principalEntityId\": \"abc\",\n \"telemarketerId\": \"tm1,tm2\"\n }\n }\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/v1/integration/webengage",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"v1",
|
||||||
|
"integration",
|
||||||
|
"webengage"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send Bulk",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{apiKey}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"from\": \"989000xxxx\",\n \"on_duplicate\": \"skip\", // Skip to send only unique ones, reject to stop sending any message even if one is not unique\n \"messages\": [\n {\n \"to\": \"989126880345\",\n \"body\": \"Your message\",\n \"unique_id\": \"1\" // UDH: Optional\n },\n {\n \"to\": \"989127865454\",\n \"body\": \"Your message 2\",\n \"unique_id\": \"20\" // UDH: Optional\n }\n ]\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/v1/send/bulk",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"v1",
|
||||||
|
"send",
|
||||||
|
"bulk"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Account Balance",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{apiKey}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/v1/account/balance",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"v1",
|
||||||
|
"account",
|
||||||
|
"balance"
|
||||||
|
],
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Send vOTP",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "x-api-key",
|
||||||
|
"value": "{{apiKey}}",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n\t\"from\": \"9890002999\",\n\t\"to\": \"989126880345\",\n\t\"code\": \"123456\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/v1/send/votp",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"v1",
|
||||||
|
"send",
|
||||||
|
"votp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "baseURL",
|
||||||
|
"value": "",
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "apiKey",
|
||||||
|
"value": "",
|
||||||
|
"type": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
54
utils/clients/sms/payam_sms/client.py
Normal file
54
utils/clients/sms/payam_sms/client.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# TODO: this is time fourced code. refactor
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from utils.clients.sms.sms import BaseSMSClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class PayamSMSClient(BaseSMSClient):
|
||||||
|
def get_access_token(self):
|
||||||
|
payam_sms_system_name = settings.PAYAM_SMS_SYSTEM_NAME
|
||||||
|
payam_sms_username = settings.PAYAM_SMS_USERNAME
|
||||||
|
payam_sms_password = settings.PAYAM_SMS_PASSWORD
|
||||||
|
payam_sms_client_id = settings.PAYAM_SMS_CLIENT_ID
|
||||||
|
payam_sms_client_secret = settings.PAYAM_SMS_CLIENT_SECRET
|
||||||
|
auth = (payam_sms_client_id, payam_sms_client_secret)
|
||||||
|
url = f"https://www.payamsms.com/auth/oauth/token?systemName={payam_sms_system_name}&username={payam_sms_username}&password={payam_sms_password}&scope=webservice&grant_type=password"
|
||||||
|
|
||||||
|
response_object = requests.post(url, auth=auth, json={})
|
||||||
|
response = response_object.json()
|
||||||
|
return response["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def send_sms(self, phone_number, message):
|
||||||
|
try:
|
||||||
|
url = "https://www.payamsms.com/panel/webservice/send"
|
||||||
|
|
||||||
|
# access_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJzb2JhbiIsInNjb3BlIjpbIndlYnNlcnZpY2UiXSwiZXhwIjoxNzQ0MTAyMDU5LCJ1c2VyRGV0YWlscyI6eyJpZCI6MjU0MCwidXNlck5hbWUiOiJzb2JhbiIsInJvbGUiOiJVU0VSIiwiZW5hYmxlZCI6dHJ1ZSwic3lzdGVtSWQiOjcyMiwiYWNjZXNzIjp7InJvbGUiOlsiYWRkIiwiZWRpdCIsImdldCIsImRlbGV0ZSJdLCJwaG9uZUJvb2siOlsiYWRkIiwiZWRpdCIsImdldExvY2FsIiwiZ2V0R2xvYmFsIiwiZGVsZXRlIl0sInNtcyI6WyJzaW5nbGUiLCJnZXRMb2NhbCIsImdldEdsb2JhbCIsImJ1bGsiLCJkYiJdLCJyZXBvcnQiOlsiZ2V0TW9HbG9iYWwiLCJnZXREYWlseUxvY2FsIiwiZ2V0TW9Mb2NhbCIsImdldERhaWx5R2xvYmFsIl0sIndzIjpbIndzIl0sInVzZXIiOlsiYWRkIiwiZWRpdCIsImdldExvY2FsIiwiZ2V0IiwiZ2V0R2xvYmFsIiwiZGVsZXRlIl0sInRvb2xzIjpbInJhY2UiLCJwZXJpb2RpYyIsInBvbGxpbmciLCJzZWNyZXRvcnkiLCJhdXRvYW5zd2VyIl0sImZpbmFuY2UiOlsidGRyIiwiY2hhcmdlIl19LCJyb2xlSWQiOjQ0NDAsImNoaWxkcmVuSWRzIjpbXSwic3lzdGVtVHlwZSI6Ik5PUk1BTCIsInN5c3RlbVBheW1lbnRUeXBlIjoiREVCSVQiLCJzeXN0ZW1OYW1lIjoic29iYW4iLCJzZW5kV2l0aE91dFBlcm1pc3Npb25TeXN0ZW0iOmZhbHNlLCJtaXNDdXN0b21lciI6Itio2YbYr9in2LEg2LPZiNio2KfZhiDYs9in2YXYp9mG2YciLCJwYXJlbnRVc2VyTmFtZSI6bnVsbCwicGFyZW50SWQiOm51bGwsImFkbWluIjp0cnVlfSwiYXV0aG9yaXRpZXMiOlsiVVNFUiJdLCJqdGkiOiJNeU5vTml2RURmc3YxUDR3U20tbTR2Y2NHODgiLCJjbGllbnRfaWQiOiJzb2JhbiJ9.9mdneDduK4OVH2zsXvRqvXt2qwpLvs0vCuseLaB-LCo'
|
||||||
|
access_token = self.get_access_token()
|
||||||
|
|
||||||
|
body = [
|
||||||
|
{
|
||||||
|
"sender": "98200000443295",
|
||||||
|
"recipient": phone_number,
|
||||||
|
"body": message,
|
||||||
|
# "customerId": "1",
|
||||||
|
# "sendDate": "2024-03-04 10:17:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bearer {access_token}',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
response_object = requests.post(url, headers=headers, json=body, timeout=10)
|
||||||
|
logger.info(response_object.text)
|
||||||
|
return response_object
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f'error sending sms to {phone_number}: {e}')
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue