initial commit
This commit is contained in:
commit
b02b9e1811
0
api/__init__.py
Executable file
0
api/__init__.py
Executable file
BIN
api/__pycache__/__init__.cpython-311.pyc
Executable file
BIN
api/__pycache__/__init__.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/admin.cpython-311.pyc
Executable file
BIN
api/__pycache__/admin.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/api_errors.cpython-311.pyc
Executable file
BIN
api/__pycache__/api_errors.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/api_methods.cpython-311.pyc
Normal file
BIN
api/__pycache__/api_methods.cpython-311.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/api_params.cpython-311.pyc
Executable file
BIN
api/__pycache__/api_params.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/api_phone_verificator.cpython-311.pyc
Normal file
BIN
api/__pycache__/api_phone_verificator.cpython-311.pyc
Normal file
Binary file not shown.
BIN
api/__pycache__/api_utils.cpython-311.pyc
Executable file
BIN
api/__pycache__/api_utils.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/apps.cpython-311.pyc
Executable file
BIN
api/__pycache__/apps.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/models.cpython-311.pyc
Executable file
BIN
api/__pycache__/models.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/urls.cpython-311.pyc
Executable file
BIN
api/__pycache__/urls.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/__pycache__/views.cpython-311.pyc
Executable file
BIN
api/__pycache__/views.cpython-311.pyc
Executable file
Binary file not shown.
50
api/admin.py
Executable file
50
api/admin.py
Executable file
@ -0,0 +1,50 @@
|
||||
from django.contrib import admin
|
||||
from .models import *
|
||||
|
||||
|
||||
@admin.register(Account)
|
||||
class AccountAdmin(admin.ModelAdmin):
|
||||
# fields = ['name', 'surname', 'phone', 'email', 'register_datetime']
|
||||
list_display = ['id', 'name', 'surname', 'role', 'phone', 'register_datetime']
|
||||
readonly_fields = ['id', 'register_datetime']
|
||||
|
||||
|
||||
@admin.register(ExecutorAccount)
|
||||
class ExecutorAccountAdmin(admin.ModelAdmin):
|
||||
# fields = ['name', 'surname', 'phone', 'email', 'register_datetime']
|
||||
# list_display = ['name', 'surname', 'phone', 'register_datetime']
|
||||
# readonly_fields = ['register_datetime']
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(AccessToken)
|
||||
class AccessTokenAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ['id', 'access_token']
|
||||
list_display = ['id', 'user', 'creation_time', 'small_access_token']
|
||||
fields = ['user', 'creation_time', 'access_token']
|
||||
ordering = ['-creation_time']
|
||||
|
||||
def small_access_token(self, obj):
|
||||
return f"{obj.access_token[:8]}..."
|
||||
|
||||
|
||||
@admin.register(City)
|
||||
class CityAdmin(admin.ModelAdmin):
|
||||
list_display = ['code', 'name']
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
list_display = ['owner', 'phone', 'name', 'create_time', 'moderated', 'published']
|
||||
readonly_fields = ['create_time']
|
||||
#
|
||||
#
|
||||
# @admin.register(OrderImage)
|
||||
# class OrderImageAdmin(admin.ModelAdmin):
|
||||
# pass
|
||||
#
|
||||
#
|
||||
# @admin.register(OrderRespond)
|
||||
# class OrderRespondAdmin(admin.ModelAdmin):
|
||||
# pass
|
97
api/api_errors.py
Executable file
97
api/api_errors.py
Executable file
@ -0,0 +1,97 @@
|
||||
import traceback
|
||||
from .api_phone_verificator import PhoneVerificationService
|
||||
from arka.settings import PHONE_VERIFICATION_RESEND_TIME_SECS
|
||||
|
||||
# как создавать ошибку
|
||||
# raise Exception(API_ERROR_XXX, <related_obj>)
|
||||
|
||||
API_OK_OBJ = {"status": "success"}
|
||||
|
||||
API_ERROR_INTERNAL_ERROR = (100, 'internal error')
|
||||
|
||||
API_ERROR_MULTIPLY_ERRORS = (101, 'multiply errors')
|
||||
API_ERROR_NOT_FOUND = (102, 'object not found')
|
||||
API_ERROR_ACCESS_DENIED = (103, 'you cannot call this method: permission denied')
|
||||
API_ERROR_NEED_COMPLETED_ACCOUNT = (104, 'need completed account')
|
||||
API_ERROR_NOT_ALLOWED = (105, 'operation not allowed')
|
||||
|
||||
API_ERROR_METHOD_NOT_FOUND = (200, 'method not found')
|
||||
API_ERROR_MISSING_ARGUMENT = (201, 'missing argument')
|
||||
API_ERROR_UNKNOWN_ARGUMENT = (202, 'unknown argument')
|
||||
API_ERROR_INVALID_ARGUMENT_TYPE = (203, 'invalid argument type')
|
||||
API_ERROR_INVALID_ARGUMENT_VALUE = (204, 'invalid argument value')
|
||||
|
||||
API_ERROR_TOKEN_CREATION = (500, 'token creation error')
|
||||
|
||||
API_ERROR_INVALID_LOGIN = (501, 'invalid login')
|
||||
API_ERROR_INVALID_PASSWORD = (502, 'invalid password')
|
||||
API_ERROR_INVALID_TOKEN = (503, 'invalid token')
|
||||
|
||||
# времненное решение, позже нужно будет заменить на конкретные ошибки
|
||||
API_ERROR_USER_REGISTER = (510, 'user registration error')
|
||||
API_ERROR_NEED_VERIFY = (511, 'need verification code')
|
||||
API_ERROR_USER_MODIFY = (512, 'user modification error')
|
||||
API_ERROR_OBJECT_VALIDATION = (513, 'object validation error')
|
||||
|
||||
|
||||
API_ERROR_VERIFY_INVALID_CODE = (520, 'invalid code')
|
||||
API_ERROR_VERIFY_MAX_ATTEMPTS = (521, 'max attempts')
|
||||
API_ERROR_CURRENTLY_VERIFIED = (522, 'currently phone is verified')
|
||||
API_ERROR_VERIFY_FAILED = (523, 'cannot be verified')
|
||||
API_ERROR_VERIFY_NOT_READY = (524, 'verification service not ready. call this method later')
|
||||
API_ERROR_VERIFY_NOT_FOUND = (525, 'verification service did not send code. call this method without \'code\'')
|
||||
API_ERROR_VERIFY_RESEND_LIMIT = (526, f'resend verification limit '
|
||||
f'(one verify for {PHONE_VERIFICATION_RESEND_TIME_SECS} secs)')
|
||||
API_ERROR_VERIFY_UNKNOWN = (527, 'unknown verification error')
|
||||
|
||||
API_ERROR_VERIFICATION = {
|
||||
PhoneVerificationService.CHECK_PHONE_INVALID_CODE: API_ERROR_VERIFY_INVALID_CODE,
|
||||
PhoneVerificationService.CHECK_PHONE_MAX_ATTEMPTS: API_ERROR_VERIFY_MAX_ATTEMPTS,
|
||||
PhoneVerificationService.CHECK_PHONE_FAILED: API_ERROR_VERIFY_FAILED,
|
||||
PhoneVerificationService.CHECK_PHONE_NOT_READY: API_ERROR_VERIFY_NOT_READY,
|
||||
PhoneVerificationService.CHECK_PHONE_NOT_FOUND: API_ERROR_VERIFY_NOT_FOUND,
|
||||
PhoneVerificationService.CHECK_PHONE_RESEND_LIMIT: API_ERROR_VERIFY_RESEND_LIMIT,
|
||||
}
|
||||
|
||||
|
||||
def __make_error(ex: Exception):
|
||||
if type(ex.args[0]) != tuple:
|
||||
raise ex
|
||||
|
||||
error = {
|
||||
"code": ex.args[0][0],
|
||||
"message": ex.args[0][1]
|
||||
}
|
||||
|
||||
if len(ex.args) >= 2:
|
||||
error["related"] = ex.args[1]
|
||||
|
||||
return error
|
||||
|
||||
|
||||
def make_error_object(ex: Exception | list):
|
||||
try:
|
||||
data = {
|
||||
"status": "error"
|
||||
}
|
||||
if type(ex) == list:
|
||||
data["error"] = {
|
||||
"code": API_ERROR_MULTIPLY_ERRORS[0],
|
||||
"message": API_ERROR_MULTIPLY_ERRORS[1],
|
||||
}
|
||||
data["related"] = [__make_error(e) for e in ex]
|
||||
else:
|
||||
data["error"] = __make_error(ex)
|
||||
|
||||
return data
|
||||
except BaseException as err:
|
||||
traceback.print_exc()
|
||||
|
||||
return {
|
||||
"status": "error",
|
||||
"error": {
|
||||
"code": API_ERROR_INTERNAL_ERROR[0],
|
||||
"message": API_ERROR_INTERNAL_ERROR[1],
|
||||
"related": f"Exception {type(err)}: {str(err)}"
|
||||
}
|
||||
}
|
653
api/api_methods.py
Executable file
653
api/api_methods.py
Executable file
@ -0,0 +1,653 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from .api_utils import *
|
||||
from .api_params import *
|
||||
from .models import *
|
||||
import time
|
||||
|
||||
|
||||
def _make_model_validation_errors(validation_error: ValidationError, api_err=API_ERROR_OBJECT_VALIDATION):
|
||||
errors = {}
|
||||
for field_name in validation_error.error_dict:
|
||||
err_list = validation_error.error_dict[field_name]
|
||||
print(err_list)
|
||||
obj = []
|
||||
for err in err_list:
|
||||
obj.append(str(err.code))
|
||||
errors[field_name] = obj
|
||||
return make_error_object(Exception(api_err, errors))
|
||||
|
||||
|
||||
class ApiAccount:
|
||||
@staticmethod
|
||||
def __check_phone_code(phone, code):
|
||||
if code is None:
|
||||
res, err_code = PhoneVerificationService.send_verify(phone)
|
||||
if not res:
|
||||
if err_code in API_ERROR_VERIFICATION:
|
||||
raise Exception(API_ERROR_VERIFICATION[err_code])
|
||||
else:
|
||||
raise Exception(API_ERROR_VERIFY_UNKNOWN)
|
||||
|
||||
raise Exception(API_ERROR_NEED_VERIFY)
|
||||
else:
|
||||
res, err_code = PhoneVerificationService.check_code(phone, code)
|
||||
if not res:
|
||||
if err_code in API_ERROR_VERIFICATION:
|
||||
raise Exception(API_ERROR_VERIFICATION[err_code])
|
||||
else:
|
||||
raise Exception(API_ERROR_VERIFY_UNKNOWN)
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def __make_user_json(user: Account):
|
||||
obj = {
|
||||
"id": user.id,
|
||||
"name": user.name,
|
||||
"surname": user.surname,
|
||||
"phone": user.phone,
|
||||
"email": user.email,
|
||||
"about": user.about,
|
||||
"city": {"code": user.city.code, "name": user.city.name} if user.city is not None else None,
|
||||
"register_datetime": int(time.mktime(user.register_datetime.timetuple())),
|
||||
"role": user.role,
|
||||
}
|
||||
if user.role == Account.ROLE_EXECUTOR:
|
||||
obj |= {
|
||||
"executor_type": user.executoraccount.executor_type,
|
||||
"executor_inn": user.executoraccount.inn,
|
||||
"executor_info": user.executoraccount.additional_info
|
||||
}
|
||||
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
@api_method("account.register",
|
||||
doc="Регистрация нового пользователя",
|
||||
params=[
|
||||
ApiParamEnum(name="role", choices=Account.ROLE_CHOICES, description="Роль пользователя: {choices}"),
|
||||
ApiParamPhone(),
|
||||
ApiParamVerifyCode(),
|
||||
ApiParamPassword(required=False,
|
||||
description=f"Пароль пользователя, требуется для завершения регистрации. "
|
||||
f"Если код верификации будет принят, но пароль не передан"
|
||||
f"метод вернет ошибку {API_ERROR_MISSING_ARGUMENT}"),
|
||||
], returns="Аналогично методу <code>account.auth</code> в случае успеха")
|
||||
async def register(role, phone, password, code):
|
||||
user = Account.create_user(
|
||||
phone=phone,
|
||||
password=(password if password is not None else ""),
|
||||
role=role
|
||||
)
|
||||
|
||||
try:
|
||||
await sync_to_async(user.full_clean)()
|
||||
except ValidationError as validation_error:
|
||||
# traceback.print_exc()
|
||||
return _make_model_validation_errors(validation_error, API_ERROR_USER_REGISTER)
|
||||
|
||||
if ApiAccount.__check_phone_code(user.phone, code):
|
||||
if password is None:
|
||||
raise Exception(API_ERROR_MISSING_ARGUMENT, "password")
|
||||
|
||||
user = await Account.objects.acreate(phone=phone, password=password, role=role)
|
||||
if role == Account.ROLE_EXECUTOR:
|
||||
await ExecutorAccount.objects.acreate(account=user)
|
||||
|
||||
# удаляем код, типа завершение транзакции
|
||||
PhoneVerificationService.check_code(phone, code, auto_pop=True)
|
||||
|
||||
try:
|
||||
token = await AccessToken.create_token(user)
|
||||
return api_make_response({"access_token": token.access_token})
|
||||
except Exception as ex:
|
||||
# если вдруг токен нельзя создать
|
||||
user.delete()
|
||||
raise ex
|
||||
|
||||
@staticmethod
|
||||
@api_method("account.auth",
|
||||
doc="Аутентификация пользователя",
|
||||
params=[
|
||||
ApiParamPhone(name="login", description="Логин пользователя"),
|
||||
ApiParamPassword(),
|
||||
],
|
||||
returns="В случае правильных логина и пароля <code>access_token</code>. "
|
||||
"В противном случае объект ошибки.")
|
||||
async def auth(login, password):
|
||||
return api_make_response({"access_token": (await AccessToken.auth(login, password)).access_token})
|
||||
|
||||
@staticmethod
|
||||
@api_method("account.deauth",
|
||||
doc="Удаление токена, дальшейшие вызовы API с этим токеном вернут ошибку невалидного токена",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
], returns="В случае успеха стандартный код успеха")
|
||||
async def deauth(access_token):
|
||||
await AccessToken.deauth(access_token.access_token)
|
||||
return api_make_response({})
|
||||
|
||||
@staticmethod
|
||||
@api_method("account.delete",
|
||||
doc='Удаление аккаунта пользователя <bold style="color:red">БЕЗ ВОЗМОЖНОСТИ ВОССТАНОВЛЕНИЯ</bold>. '
|
||||
'Так же будут удалены <bold style="color:red">ВСЕ</bold> связанные с аккаунтом данные.',
|
||||
params=[ApiParamAccessToken()],
|
||||
returns="Стандартный ответ успеха, в случае успеха")
|
||||
async def delete(access_token):
|
||||
user = access_token.user
|
||||
await sync_to_async(user.delete)()
|
||||
return api_make_response({})
|
||||
|
||||
@staticmethod
|
||||
@api_method("account.get",
|
||||
doc="Получение информации о пользователе",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamInt(name="user_id", required=False, value_min=0,
|
||||
description="ID пользователя, аккаунт которого нужно вернуть. "
|
||||
"Если не указывать, вернет аккаунт владельца.")
|
||||
],
|
||||
returns="Поля пользователя (name, surname, email, phone и прочие).")
|
||||
async def get(access_token, user_id):
|
||||
if user_id is None:
|
||||
user = access_token.user
|
||||
else:
|
||||
user = await access_token.user.get_by_id(user_id)
|
||||
if user is None:
|
||||
return make_error_object(Exception(API_ERROR_NOT_FOUND, {"user": user_id}))
|
||||
|
||||
return api_make_response(ApiAccount.__make_user_json(user))
|
||||
|
||||
@staticmethod
|
||||
@api_method("account.edit",
|
||||
doc="Редактирование основной информации о пользователе. "
|
||||
"Будут изменены только те данные, которые будут переданы в запросе, остальные не будут изменены.",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamStr(name="name", description="Имя пользователя",
|
||||
min_length=2, max_length=60, required=False),
|
||||
ApiParamStr(name="surname", description="Фамилия пользователя",
|
||||
min_length=2, max_length=60, required=False),
|
||||
ApiParamStr(name="about", description="Текст о себе. максимум - 1000 символов",
|
||||
max_length=1000, required=False),
|
||||
ApiParamEnum(name="executor_type", choices=ExecutorAccount.EXECUTOR_TYPE_CHOICES, required=False,
|
||||
description="Тип исполнителя (только для роли исполнитель): {choices}"),
|
||||
ApiParamStr(name="executor_inn", required=False, regex="^(\\d{12}|\\d{10})$",
|
||||
description="ИНН исполнителя (только для роли исполнитель): "
|
||||
"12 цифр если исполнитель - физ.лицо и 10 цифр если это юр. лицо"),
|
||||
ApiParamEnum(name="city", description="Город, в котором находится ползователь: {choices}",
|
||||
required=False, choices=City.to_choices)
|
||||
],
|
||||
returns="Вернет основную информацию о пользователе, иначе ошибки")
|
||||
async def edit(access_token, name, surname, about, executor_type, executor_inn, city):
|
||||
user = access_token.user
|
||||
executor_need_save, need_save = False, False
|
||||
|
||||
if name is not None:
|
||||
user.name = name
|
||||
need_save = True
|
||||
|
||||
if surname is not None:
|
||||
user.surname = surname
|
||||
need_save = True
|
||||
|
||||
if about is not None:
|
||||
user.about = about
|
||||
need_save = True
|
||||
|
||||
if city is not None:
|
||||
user.city = city
|
||||
need_save = True
|
||||
|
||||
if user.role == Account.ROLE_EXECUTOR:
|
||||
print("Executor account detected")
|
||||
if executor_type is not None:
|
||||
print("Executor account type detected")
|
||||
user.executoraccount.executor_type = executor_type
|
||||
executor_need_save = True
|
||||
|
||||
if executor_inn is not None:
|
||||
print("Executor account inn detected")
|
||||
t = executor_type
|
||||
if t is None:
|
||||
t = user.executoraccount.executor_type
|
||||
|
||||
if t is None:
|
||||
raise Exception(API_ERROR_MISSING_ARGUMENT, "executor_type")
|
||||
|
||||
if t == ExecutorAccount.EXECUTOR_TYPE_SELF_EMPLOYED:
|
||||
if re.match("^\\d{12}$", executor_inn) is None:
|
||||
raise Exception(API_ERROR_INVALID_ARGUMENT_VALUE, "executor_inn must be defined as 12 digits")
|
||||
|
||||
if t == ExecutorAccount.EXECUTOR_TYPE_LEGAL_ENTITY:
|
||||
if re.match("^\\d{10}$", executor_inn) is None:
|
||||
raise Exception(API_ERROR_INVALID_ARGUMENT_VALUE, "executor_inn must be defined as 10 digits")
|
||||
|
||||
user.executoraccount.inn = executor_inn
|
||||
executor_need_save = True
|
||||
|
||||
if need_save:
|
||||
try:
|
||||
await sync_to_async(user.full_clean)()
|
||||
except ValidationError as ve:
|
||||
return _make_model_validation_errors(ve, API_ERROR_USER_MODIFY)
|
||||
|
||||
await sync_to_async(user.save)()
|
||||
|
||||
if executor_need_save:
|
||||
try:
|
||||
await sync_to_async(user.executoraccount.full_clean)()
|
||||
except ValidationError as ve:
|
||||
return _make_model_validation_errors(ve, API_ERROR_USER_MODIFY)
|
||||
|
||||
await sync_to_async(user.executoraccount.save)()
|
||||
|
||||
return api_make_response(ApiAccount.__make_user_json(user))
|
||||
|
||||
@staticmethod
|
||||
@api_method("account.changePhone",
|
||||
doc="Смена пароля. Для подтверждения требуется старый пароль.",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamPassword(description="Пароль пользователя, нужен для подтверждения владельца аккаунта"),
|
||||
ApiParamPhone(description="Новый телефон пользователя"),
|
||||
ApiParamVerifyCode(description="Код подтверждения операции, придет на новый телефон")
|
||||
],
|
||||
returns="Вернет стандартный объект успеха")
|
||||
async def change_phone(access_token, password, phone, code):
|
||||
user = access_token.user
|
||||
|
||||
if not user.check_password(password):
|
||||
raise Exception(API_ERROR_INVALID_PASSWORD)
|
||||
|
||||
# чекаем телефон
|
||||
user.phone = phone
|
||||
try:
|
||||
await sync_to_async(user.full_clean)()
|
||||
except ValidationError as ve:
|
||||
return _make_model_validation_errors(ve, API_ERROR_USER_MODIFY)
|
||||
|
||||
if ApiAccount.__check_phone_code(user.phone, code):
|
||||
await Account.objects.filter(id=user.id).aupdate(phone=phone)
|
||||
PhoneVerificationService.check_code(user.phone, code, auto_pop=True)
|
||||
|
||||
return api_make_response({})
|
||||
|
||||
@staticmethod
|
||||
@api_method("account.resetPassword",
|
||||
doc="Смена пароля. Для подтверждения действия требуется код с телефона. Так же в случае успешного "
|
||||
"восстановления удалит все существующие сессии пользователя.",
|
||||
params=[
|
||||
ApiParamPhone(description="Телефон, для которого нужно сделать восстановление"),
|
||||
ApiParamPassword(name="new_password", required=False,
|
||||
description="Новый пароль, нужен после того, как будет отправлен код ("
|
||||
"можно передать раньше, тогда он будет проверен сразу)"),
|
||||
ApiParamVerifyCode(description="Код подтверждения операции, придет на телефон")
|
||||
],
|
||||
returns="Вернет стандартный объект успеха")
|
||||
async def reset_password(new_password, phone, code):
|
||||
user = await Account.get_by_natural_key(phone)
|
||||
if new_password is not None:
|
||||
user.password = new_password
|
||||
try:
|
||||
await sync_to_async(user.full_clean)()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
raise Exception(API_ERROR_INVALID_PASSWORD)
|
||||
|
||||
if ApiAccount.__check_phone_code(phone, code):
|
||||
if code is not None and new_password is None:
|
||||
raise Exception(API_ERROR_MISSING_ARGUMENT, "new_password")
|
||||
|
||||
await sync_to_async(user.save)()
|
||||
await AccessToken.delete_sessions(user)
|
||||
PhoneVerificationService.check_code(phone, code, auto_pop=True)
|
||||
|
||||
return api_make_response({})
|
||||
|
||||
|
||||
class ApiSecurity:
|
||||
@staticmethod
|
||||
@api_method("security.listSessions",
|
||||
doc="Получение сиписка сессий (кроме текущей)",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamPassword(description="Пароль пользователя, нужен для подтверждения личности")
|
||||
],
|
||||
returns="Вернет sessions: [{id: int, name: str, created: unix_timestamp}]")
|
||||
async def list_sessions(access_token, password):
|
||||
sessions = await access_token.list_sessions()
|
||||
|
||||
if not access_token.user.check_password(password):
|
||||
raise Exception(API_ERROR_INVALID_PASSWORD)
|
||||
|
||||
return api_make_response({
|
||||
"current": {
|
||||
"id": access_token.id,
|
||||
"created": int(time.mktime(access_token.creation_time.timetuple())),
|
||||
},
|
||||
"sessions": [
|
||||
{
|
||||
"id": s.id,
|
||||
"name": f"{s.access_token[:8]}...",
|
||||
"created": int(time.mktime(s.creation_time.timetuple())),
|
||||
} for s in sessions
|
||||
]
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
@api_method("security.removeOtherSessions",
|
||||
doc="Получение сиписка сессий (кроме текущей)",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamPassword(description="Пароль пользователя, нужен для подтверждения личности")
|
||||
],
|
||||
returns="Вернет sessions: [{id: int, name: str, created: unix_timestamp}]")
|
||||
async def remove_other_sessions(access_token, password):
|
||||
if not access_token.user.check_password(password):
|
||||
raise Exception(API_ERROR_INVALID_PASSWORD)
|
||||
|
||||
sessions = await access_token.list_sessions()
|
||||
response = {
|
||||
"current": {
|
||||
"id": access_token.id,
|
||||
"created": int(time.mktime(access_token.creation_time.timetuple())),
|
||||
},
|
||||
"removed": [
|
||||
{
|
||||
"id": s.id,
|
||||
"name": f"{s.access_token[:8]}...",
|
||||
"created": int(time.mktime(s.creation_time.timetuple())),
|
||||
} for s in sessions
|
||||
]
|
||||
}
|
||||
await access_token.delete_sessions_without_current()
|
||||
|
||||
return api_make_response(response)
|
||||
|
||||
@staticmethod
|
||||
@api_method("security.removeSession",
|
||||
doc="Удалит сессию с указанным id. Не удаляет текущую сессию, даже если указан ее ID "
|
||||
"(будет возвращена ошибка NOT FOUND)",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamPassword(description="Пароль пользователя, нужен для подтверждения личности"),
|
||||
ApiParamInt(name="session", description="ID сессии, которую надо деактивировать", value_min=0)
|
||||
],
|
||||
returns="Вернет стандартный отъект в случае успеха")
|
||||
async def remove_session(access_token, password, session):
|
||||
if not access_token.user.check_password(password):
|
||||
raise Exception(API_ERROR_INVALID_PASSWORD)
|
||||
|
||||
await access_token.delete_session(session)
|
||||
|
||||
return api_make_response({})
|
||||
|
||||
@staticmethod
|
||||
@api_method("security.changePassword",
|
||||
doc="Смена пароля. Для подтверждения требуется старый пароль.",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamPassword(name="old_password", description="Старый пароль пользователя"),
|
||||
ApiParamPassword(description="Новый пароль пользователя"),
|
||||
],
|
||||
returns="Вернет стандартный объект успеха")
|
||||
async def change_password(access_token, old_password, password):
|
||||
user = access_token.user
|
||||
|
||||
if not user.check_password(old_password):
|
||||
raise Exception(API_ERROR_INVALID_PASSWORD, "old_password")
|
||||
|
||||
user.password = password
|
||||
|
||||
await sync_to_async(user.save)()
|
||||
|
||||
return api_make_response({})
|
||||
|
||||
|
||||
class ApiOrder:
|
||||
@staticmethod
|
||||
@api_method("order.getForm",
|
||||
doc="Получение формы создания заказа в виде полей, которые нужно показать пользователю",
|
||||
params=[],
|
||||
returns="<code>JSON</code> объект, содержащий данные формы. Структура пока не определена")
|
||||
def get_form():
|
||||
return api_make_response({
|
||||
"fields": [
|
||||
# name = models.CharField(max_length=200, verbose_name="Название заказа")
|
||||
{
|
||||
"name": "name",
|
||||
"label": "Название заказа",
|
||||
"widget": "text",
|
||||
"attrs": {
|
||||
"max_len": 200
|
||||
},
|
||||
"required": True
|
||||
},
|
||||
|
||||
# square = models.DecimalField(max_digits=7, decimal_places=2, blank=False, verbose_name="Площадь в м²")
|
||||
{
|
||||
"name": "square",
|
||||
"label": "Площадь в м²",
|
||||
"widget": "decimal",
|
||||
"attrs": {
|
||||
"max_digits": 7,
|
||||
"decimal_places": 2
|
||||
},
|
||||
"required": True
|
||||
},
|
||||
|
||||
# work_time = models.CharField(max_length=100, blank=True, verbose_name="Рабочее время")
|
||||
{
|
||||
"name": "work_time",
|
||||
"label": "Рабочее время",
|
||||
"widget": "text",
|
||||
"attrs": {
|
||||
"max_len": 100
|
||||
},
|
||||
"required": False
|
||||
},
|
||||
|
||||
# type_of_renovation = models.CharField(max_length=10, choices=TYPE_OF_RENOVATION_CHOICES,
|
||||
# default=CHOICE_UNDEFINED, blank=True, verbose_name="Тип ремонта")
|
||||
{
|
||||
"name": "type_of_renovation",
|
||||
"label": "Тип ремонта",
|
||||
"widget": "choice",
|
||||
"attrs": {
|
||||
"default": None,
|
||||
"choices": "Order.TYPE_OF_RENOVATION_CHOICES"
|
||||
},
|
||||
"required": False
|
||||
},
|
||||
|
||||
# type_of_room = models.CharField(max_length=10, choices=TYPE_OF_ROOM_CHOICES,
|
||||
# blank=True, default=CHOICE_UNDEFINED, verbose_name="Тип квартиры")
|
||||
{
|
||||
"name": "type_of_room",
|
||||
"label": "Тип квартиры",
|
||||
"widget": "radio",
|
||||
"attrs": {
|
||||
"default": None,
|
||||
"choices": "Order.TYPE_OF_ROOM_CHOICES"
|
||||
},
|
||||
"required": False
|
||||
},
|
||||
|
||||
# флажок
|
||||
{
|
||||
"name": "is_with_warranty",
|
||||
"label": "С гарантией",
|
||||
"widget": "checkbox",
|
||||
"attrs": {
|
||||
"default": True
|
||||
},
|
||||
"required": False
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
@api_method("order.create",
|
||||
doc="Создание заказа",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamStr(name='name', max_length=200, description="Название заказа"),
|
||||
ApiParamStr(name='description', max_length=1000, description="Описание заказа",
|
||||
required=False, default=""),
|
||||
ApiParamFloat(name='square', value_max=99999.99, value_min=1.0,
|
||||
description='Площадь в м²'),
|
||||
ApiParamStr(name='work_time', max_length=100, description="Рабочее время",
|
||||
required=False, default=""),
|
||||
|
||||
ApiParamEnum(name='type_of_renovation', choices=Order.TYPE_OF_RENOVATION_CHOICES, required=False,
|
||||
default=Order.CHOICE_UNDEFINED,
|
||||
description="Тип ремонта: {choices}"),
|
||||
ApiParamEnum(name='type_of_house', choices=Order.TYPE_OF_HOUSE_CHOICES, required=False,
|
||||
default=Order.CHOICE_UNDEFINED,
|
||||
description="Тип дома: {choices}"),
|
||||
ApiParamEnum(name='type_of_room', choices=Order.TYPE_OF_ROOM_CHOICES, required=False,
|
||||
default=Order.CHOICE_UNDEFINED,
|
||||
description="Тип квартиры: {choices}"),
|
||||
ApiParamEnum(name='purchase_of_material', choices=Order.PURCHASE_OF_MATERIAL_CHOICES,
|
||||
required=False, default=Order.CHOICE_UNDEFINED,
|
||||
description="Закуп материала: {choices}"),
|
||||
ApiParamEnum(name='type_of_executor', choices=Order.TYPE_OF_EXECUTOR_CHOICES,
|
||||
required=False, default=Order.CHOICE_UNDEFINED,
|
||||
description="Тип исполнителя: {choices}"),
|
||||
|
||||
# дальше отдельные флаги
|
||||
ApiParamBoolean(name="is_with_warranty", required=False, default=True,
|
||||
description="С гарантией"),
|
||||
ApiParamBoolean(name="is_with_contract", required=False, default=False,
|
||||
description="Работа по договору"),
|
||||
ApiParamBoolean(name="is_with_trade", required=False, default=False,
|
||||
description="Возможен торг"),
|
||||
ApiParamBoolean(name="is_with_cleaning", required=False, default=False,
|
||||
description="С уборкой"),
|
||||
ApiParamBoolean(name="is_with_garbage_removal", required=False, default=False,
|
||||
description="С вывозом мусора"),
|
||||
ApiParamBoolean(name="is_require_design", required=False, default=False,
|
||||
description="Требуется дизайн проект"),
|
||||
|
||||
# примерная цена
|
||||
ApiParamFloat(name='approximate_price', value_max=9999999999.99, value_min=1.0,
|
||||
description='Примерная цена'),
|
||||
|
||||
# date_start = models.DateField(null=True, blank=True, default=None, verbose_name="Дата начала")
|
||||
# date_end = models.DateField(null=True, blank=True, default=None, verbose_name="Дата окончания")
|
||||
|
||||
ApiParamEnum(name="address_city", description="Город: {choices}", choices=City.to_choices),
|
||||
|
||||
ApiParamStr(name='address_text', max_length=70, description="Улица, дом",
|
||||
required=False, default="")
|
||||
# email = models.EmailField(null=True, blank=True, verbose_name="Email")
|
||||
# phone = models.CharField(null=True, blank=True, max_length=16, verbose_name="Телефон")
|
||||
],
|
||||
returns="ID созданного заказа, иначе одну из ошибок")
|
||||
async def create(**kwargs):
|
||||
access_token = kwargs.pop('access_token')
|
||||
ApiOrder._check_modify_permissions(access_token)
|
||||
|
||||
city = await City.get_by_code(kwargs.pop('address_city'))
|
||||
|
||||
try:
|
||||
order = await Order.objects.acreate(owner=access_token.user, address_city=city, **kwargs)
|
||||
except ValidationError as ve:
|
||||
return _make_model_validation_errors(ve, API_ERROR_USER_MODIFY)
|
||||
return api_make_response({"order_id": order.id})
|
||||
|
||||
@staticmethod
|
||||
@api_method("order.setPublished",
|
||||
doc="Установка статуса публикации заказа",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamInt(name='order_id', value_min=0, description='ID заказа'),
|
||||
ApiParamBoolean(name='value', description='Значение поля "опубликован"')
|
||||
],
|
||||
returns="Обновленный объект заказа")
|
||||
async def set_published(access_token, order_id, value):
|
||||
ApiOrder._check_modify_permissions(access_token)
|
||||
query = Order.objects.filter(id=order_id)
|
||||
order = await query.afirst()
|
||||
if order.owner_id != access_token.user.id:
|
||||
raise Exception(API_ERROR_ACCESS_DENIED, 'edit operation allowed only for owner')
|
||||
|
||||
await query.aupdate(published=value)
|
||||
|
||||
# вернем в зад обновленный объект
|
||||
order.published = value
|
||||
return api_make_response(ApiOrder._order_to_json(order))
|
||||
|
||||
ORDER_GET_ORDERING = [
|
||||
('create_time', 'время создания, сначала новые записи'),
|
||||
('-create_time', 'время создания, сначала старые записи'),
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _order_to_json(order):
|
||||
return order.__dict__
|
||||
|
||||
@staticmethod
|
||||
@api_method("order.get",
|
||||
doc="Получение объектов заказа, применит все фильтры",
|
||||
params=[
|
||||
ApiParamAccessToken(),
|
||||
ApiParamInt(name='order_id', required=False, value_min=0,
|
||||
description='ID заказа, который нужно вернуть, вернет один заказ, является фильтром'),
|
||||
ApiParamInt(name='user_id', required=False, value_min=0,
|
||||
description='Показать заказы для конкретного пользователя'),
|
||||
ApiParamEnum(name='ordering', choices=ORDER_GET_ORDERING, required=False,
|
||||
description='Сортировка заказов. Возможные значения: {choices} '
|
||||
'Не имеет эффекта если ответ состоит из одного заказа.'),
|
||||
|
||||
],
|
||||
returns="Массив заказов, соответсвующий всем указанным фильтрам.")
|
||||
async def get(access_token, order_id, user_id, ordering):
|
||||
query = Order.objects
|
||||
if order_id is not None:
|
||||
res = await query.aget(id=order_id)
|
||||
if user_id is not None:
|
||||
if access_token.user.id == res.owner_id or (res.published and res.moderated):
|
||||
return api_make_response([ApiOrder._order_to_json(res)])
|
||||
else:
|
||||
raise Exception(API_ERROR_NOT_ALLOWED, 'attempt access to closed order')
|
||||
|
||||
if user_id is not None:
|
||||
user = await access_token.user.get_by_id(user_id)
|
||||
if user is None:
|
||||
raise Exception(API_ERROR_NOT_FOUND, 'user')
|
||||
if user.role != Account.ROLE_CUSTOMER:
|
||||
raise Exception(API_ERROR_NOT_ALLOWED, 'target user is not customer')
|
||||
query = query.filter(owner_id=user_id)
|
||||
|
||||
if ordering is not None:
|
||||
query = query.order_by(ordering)
|
||||
|
||||
return api_make_response([ApiOrder._order_to_json(item) async for item in query.all()])
|
||||
|
||||
@staticmethod
|
||||
def _check_modify_permissions(access_token):
|
||||
if not access_token.user.is_completed():
|
||||
raise Exception(API_ERROR_NEED_COMPLETED_ACCOUNT)
|
||||
if access_token.user.role != Account.ROLE_CUSTOMER:
|
||||
raise Exception(API_ERROR_NOT_ALLOWED, 'you must be a customer')
|
||||
|
||||
|
||||
async def api_call_method(method_name, params: dict):
|
||||
if method_name in api_methods_dict:
|
||||
return await api_methods_dict[method_name]["func"](**params)
|
||||
else:
|
||||
return make_error_object(Exception(API_ERROR_METHOD_NOT_FOUND))
|
||||
|
||||
|
||||
def api_get_documentation():
|
||||
out = []
|
||||
for m in api_methods_dict:
|
||||
out.append({
|
||||
"name": m,
|
||||
"doc": api_methods_dict[m]["doc"],
|
||||
"returns": api_methods_dict[m]["returns"],
|
||||
"params": [p.to_json() for p in api_methods_dict[m]["params"]]
|
||||
})
|
||||
return out
|
272
api/api_params.py
Executable file
272
api/api_params.py
Executable file
@ -0,0 +1,272 @@
|
||||
import asyncio
|
||||
|
||||
from .api_errors import *
|
||||
from .models import AccessToken
|
||||
import re
|
||||
|
||||
|
||||
def _make_invalid_argument_type_error(name, value, except_type):
|
||||
related = {"param_name": name, "excepted_type": except_type, "value": value}
|
||||
raise Exception(API_ERROR_INVALID_ARGUMENT_TYPE, related)
|
||||
|
||||
|
||||
def _make_invalid_argument_value_error(name, value, err_code, message):
|
||||
related = {"param_name": name, "value": value, "error_code": err_code, "message": message}
|
||||
raise Exception(API_ERROR_INVALID_ARGUMENT_VALUE, related)
|
||||
|
||||
|
||||
class ApiParam:
|
||||
def __init__(self, name, description, required=True, default=None):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.required = required
|
||||
self.default = default
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Валидация параметра, в случае ошибки нужно выбросить исключение, соответствующее ошибке
|
||||
в случае успеха нужно вернуть объект, который и будет передан в метод в качестве параметра
|
||||
"""
|
||||
raise Exception(API_ERROR_INTERNAL_ERROR, f"param {self.name} have invalid definition (defined as super class)")
|
||||
|
||||
def get_doc(self):
|
||||
return self.description
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def is_required(self):
|
||||
return self.required
|
||||
|
||||
def get_type_name(self):
|
||||
return f"{type(self)}"
|
||||
|
||||
def to_json(self):
|
||||
return {
|
||||
"name": self.get_name(),
|
||||
"type": self.get_type_name(),
|
||||
"description": self.get_doc(),
|
||||
"required": self.is_required()
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self)}: name={self.name}"
|
||||
|
||||
|
||||
class ApiParamStr(ApiParam):
|
||||
def __init__(self, regex=None, max_length=None, min_length=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.regex = None if regex is None else re.compile(regex)
|
||||
self.max_length = max_length
|
||||
self.min_length = min_length
|
||||
|
||||
def validate(self, value):
|
||||
if value is None:
|
||||
if self.required:
|
||||
raise Exception(API_ERROR_MISSING_ARGUMENT, self.name)
|
||||
else:
|
||||
# вернуть значение по умолчанию без дополнительной проверки
|
||||
return self.default
|
||||
|
||||
if self.regex is not None:
|
||||
if not self.regex.match(value):
|
||||
_make_invalid_argument_value_error(self.name, value, "invalid",
|
||||
f"expected string like this python regex: '{self.regex.pattern}'")
|
||||
if self.max_length is not None:
|
||||
if len(value) > self.max_length:
|
||||
_make_invalid_argument_value_error(self.name, value, "long",
|
||||
f"too long string. max size is '{self.max_length}' char(s)")
|
||||
if self.min_length is not None:
|
||||
if len(value) < self.min_length:
|
||||
_make_invalid_argument_value_error(self.name, value, "short",
|
||||
f"too short string. min size is '{self.max_length}' char(s)")
|
||||
|
||||
return value
|
||||
|
||||
def get_type_name(self):
|
||||
return "String"
|
||||
|
||||
|
||||
class ApiParamInt(ApiParam):
|
||||
def __init__(self, value_min=None, value_max=None, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.value_max = value_max
|
||||
self.value_min = value_min
|
||||
|
||||
def _check_min_max(self, value):
|
||||
if self.value_min is not None:
|
||||
if self.value_min > value:
|
||||
_make_invalid_argument_value_error(self.name, value, 'out_of_range',
|
||||
f'the minimum value for this param is {self.value_min}')
|
||||
if self.value_max is not None:
|
||||
if self.value_max < value:
|
||||
_make_invalid_argument_value_error(self.name, value, 'out_of_range',
|
||||
f'the maximum value for this param is {self.value_max}')
|
||||
|
||||
def validate(self, value):
|
||||
if value is None:
|
||||
if self.required:
|
||||
raise Exception(API_ERROR_MISSING_ARGUMENT, self.name)
|
||||
else:
|
||||
# вернуть значение по умолчанию без дополнительной проверки
|
||||
return self.default
|
||||
|
||||
try:
|
||||
value = int(value)
|
||||
except Exception:
|
||||
_make_invalid_argument_type_error(self.name, value, self.get_type_name())
|
||||
|
||||
self._check_min_max(value)
|
||||
|
||||
return value
|
||||
|
||||
def get_doc(self):
|
||||
return super().get_doc().replace('{min}', str(self.value_min)).replace('{max}', str(self.value_max))
|
||||
|
||||
def get_type_name(self):
|
||||
return "Int"
|
||||
|
||||
|
||||
class ApiParamEnum(ApiParam):
|
||||
def __init__(self, choices, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if not callable(choices):
|
||||
self.choices = []
|
||||
for c in choices:
|
||||
self.choices.append(
|
||||
(c[0], c[1])
|
||||
)
|
||||
else:
|
||||
if asyncio.iscoroutinefunction(choices):
|
||||
loop = asyncio.get_event_loop()
|
||||
coroutine = choices()
|
||||
loop.run_until_complete(coroutine)
|
||||
else:
|
||||
self.choices = choices()
|
||||
|
||||
async def validate(self, value):
|
||||
if value is None:
|
||||
if self.required:
|
||||
raise Exception(API_ERROR_MISSING_ARGUMENT, self.name)
|
||||
else:
|
||||
# вернуть значение по умолчанию без дополнительной проверки
|
||||
return self.default
|
||||
|
||||
for choice in self.choices:
|
||||
cmp = choice[0] if type(choice[0]) is str else str(choice[0])
|
||||
if cmp == value:
|
||||
return choice[0]
|
||||
|
||||
_make_invalid_argument_value_error(self.name, value, 'out_of_range',
|
||||
f'expected value one of {[c[0] for c in self.choices]}')
|
||||
|
||||
def get_doc(self):
|
||||
doc = super().get_doc()
|
||||
ch = "<ul>"
|
||||
for c in self.choices:
|
||||
if c[0] != '':
|
||||
ch += f"<li>{c[0]} - {c[1]}</li>"
|
||||
ch += "</ul>"
|
||||
return doc.replace('{choices}', ch)
|
||||
|
||||
def get_type_name(self):
|
||||
return "Enum"
|
||||
|
||||
|
||||
class ApiParamBoolean(ApiParam):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__( **kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
if value is None:
|
||||
if self.required:
|
||||
raise Exception(API_ERROR_MISSING_ARGUMENT, self.name)
|
||||
else:
|
||||
# вернуть значение по умолчанию без дополнительной проверки
|
||||
return self.default
|
||||
|
||||
if value == 'true' or value == '1' or value == 'y':
|
||||
return True
|
||||
elif value == 'false' or value == '0' or value == 'n':
|
||||
return False
|
||||
else:
|
||||
_make_invalid_argument_value_error(self.name, value, "invalid", "expected (true|y|1) or (false|n|0)")
|
||||
|
||||
def get_type_name(self):
|
||||
return "Boolean"
|
||||
|
||||
|
||||
class ApiParamFloat(ApiParamInt):
|
||||
def validate(self, value):
|
||||
if value is None:
|
||||
if self.required:
|
||||
raise Exception(API_ERROR_MISSING_ARGUMENT, self.name)
|
||||
else:
|
||||
# вернуть значение по умолчанию без дополнительной проверки
|
||||
return self.default
|
||||
|
||||
try:
|
||||
value = float(value)
|
||||
except Exception:
|
||||
_make_invalid_argument_type_error(self.name, value, self.get_type_name())
|
||||
|
||||
self._check_min_max(value)
|
||||
|
||||
return value
|
||||
|
||||
def get_doc(self):
|
||||
return super().get_doc().replace('{min}', str(self.value_min)).replace('{max}', str(self.value_max))
|
||||
|
||||
def get_type_name(self):
|
||||
return "Float"
|
||||
|
||||
|
||||
class ApiParamAccessToken(ApiParam):
|
||||
def __init__(self, regex=None, name="access_token",
|
||||
description="<i>Токен</i>, выданный методом <code>account.auth</code>", **kwargs):
|
||||
super().__init__(name=name, description=description, **kwargs)
|
||||
self.regex = (lambda: None if regex is None else re.compile(regex))
|
||||
|
||||
async def validate(self, value):
|
||||
if value is None:
|
||||
if self.required:
|
||||
raise Exception(API_ERROR_MISSING_ARGUMENT, self.name)
|
||||
else:
|
||||
# вернуть None, потому что параметра нет
|
||||
return None
|
||||
|
||||
return await AccessToken.get_by_token(value)
|
||||
|
||||
def get_type_name(self):
|
||||
return "AccessToken"
|
||||
|
||||
|
||||
class ApiParamPassword(ApiParamStr):
|
||||
def __init__(self, name="password", description="Пароль пользователя", **kwargs):
|
||||
super().__init__(name=name, description=description, regex="^[ -~а-яА-Я]{6,}$", **kwargs)
|
||||
|
||||
|
||||
class ApiParamPhone(ApiParamStr):
|
||||
def __init__(self, name="phone", description="Телефон в формате <code>[[+]7]1112223333</code> "
|
||||
"<i>(в квадратных скобках необязательная часть)</i>", **kwargs):
|
||||
super().__init__(name=name, description=description, regex="^((\\+7)|(7)|)[0-9]{10}$", **kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
value = super(ApiParamPhone, self).validate(value)
|
||||
if value is not None:
|
||||
# Гоша попросил запилить фичу, чтобы принимались номера:
|
||||
# +79991112233
|
||||
# 79991112233
|
||||
# 9991112233
|
||||
if re.match("^[0-9]{10}$", value) is not None:
|
||||
return f"+7{value}"
|
||||
elif re.match("^7[0-9]{10}$", value) is not None:
|
||||
return f"+{value}"
|
||||
return value
|
||||
|
||||
|
||||
class ApiParamVerifyCode(ApiParamInt):
|
||||
def __init__(self, name="code",
|
||||
description="Код верификации (требуется если клиенту будет отправлена "
|
||||
"одна из ошибок <i>верификации</i>)", **kwargs):
|
||||
super().__init__(name=name, required=False, value_min=0, value_max=9999, description=description, **kwargs)
|
162
api/api_phone_verificator.py
Executable file
162
api/api_phone_verificator.py
Executable file
@ -0,0 +1,162 @@
|
||||
# TODO адаптировать под работу с базой данных вместо локального словаря
|
||||
|
||||
from arka.settings import PHONE_VERIFICATION_ENABLE, PHONE_VERIFICATION_ATTEMPTS,\
|
||||
PHONE_VERIFICATION_ACCESS_KEY, PHONE_VERIFICATION_RESEND_TIME_SECS
|
||||
|
||||
from threading import Thread, Lock, Event
|
||||
import random
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
import requests
|
||||
|
||||
|
||||
class PhoneVerificationService:
|
||||
__lock = Lock()
|
||||
__event = Event()
|
||||
|
||||
# номера, для которых отправлен запрос верификации (на внешний ресурс)
|
||||
# имеет структуру:
|
||||
# "{phone}": {"code": None|int|"FAILED", "attempts": int}
|
||||
# None в code означает что ответ еще не пришел, остальное вроде понятно
|
||||
# attempts - сколько попыток верификации осталось (по умолчанию 5)
|
||||
__codes = {}
|
||||
|
||||
# очередь номеров, которые требуют верификации
|
||||
__to_verify = []
|
||||
__instance = None
|
||||
|
||||
@staticmethod
|
||||
def __service_run():
|
||||
while True:
|
||||
try:
|
||||
PhoneVerificationService.__event.wait()
|
||||
phones = None
|
||||
with PhoneVerificationService.__lock:
|
||||
if PhoneVerificationService.__event.is_set():
|
||||
PhoneVerificationService.__event.clear()
|
||||
phones = PhoneVerificationService.__to_verify.copy()
|
||||
PhoneVerificationService.__to_verify.clear()
|
||||
|
||||
if phones is not None:
|
||||
for phone in phones:
|
||||
# тут должна быть проверка, есть ли телефон в списке кодов
|
||||
|
||||
obj = {
|
||||
"code": None,
|
||||
"attempts": PHONE_VERIFICATION_ATTEMPTS,
|
||||
"time": datetime.now()
|
||||
}
|
||||
with PhoneVerificationService.__lock:
|
||||
PhoneVerificationService.__codes[phone] = obj
|
||||
request_success = False
|
||||
|
||||
if PHONE_VERIFICATION_ENABLE:
|
||||
try:
|
||||
# параметры для sms
|
||||
|
||||
params = {
|
||||
"phone": lambda: phone[1:] if phone.startswith("+") else phone,
|
||||
"ip": -1,
|
||||
"api_id": PHONE_VERIFICATION_ACCESS_KEY
|
||||
}
|
||||
res = requests.get("https://sms.ru/code/call", params=params, timeout=5)
|
||||
|
||||
res_json = res.json()
|
||||
request_success = True
|
||||
|
||||
if res_json["status"] == "OK":
|
||||
with PhoneVerificationService.__lock:
|
||||
PhoneVerificationService.__codes[phone]["code"] = res_json["code"]
|
||||
print(f"Verify code for {phone}: {res_json['code']} (sms)")
|
||||
else:
|
||||
with PhoneVerificationService.__lock:
|
||||
PhoneVerificationService.__codes[phone]["code"] = "FAILED"
|
||||
print(f"Verify code for {phone}: FAILED (sms)")
|
||||
except:
|
||||
if not request_success:
|
||||
with PhoneVerificationService.__lock:
|
||||
PhoneVerificationService.__codes[phone]["code"] = "FAILED"
|
||||
traceback.print_exc()
|
||||
else:
|
||||
try:
|
||||
# для бота vk
|
||||
code = random.randint(1000, 9999)
|
||||
|
||||
params = {
|
||||
"v": 5.131,
|
||||
# "user_ids": '352634831,405800248,280108789', # Гоша, Влад, Норик
|
||||
"chat_id": '1', # конфа
|
||||
"access_token": PHONE_VERIFICATION_ACCESS_KEY,
|
||||
"message": f"Верификация для номера {phone}<br>Код: {code}",
|
||||
"random_id": random.randint(1000, 100000000)
|
||||
}
|
||||
res = requests.get("https://api.vk.com/method/messages.send", params=params, timeout=5)
|
||||
# print(res.content)
|
||||
request_success = True
|
||||
obj["code"] = code
|
||||
with PhoneVerificationService.__lock:
|
||||
PhoneVerificationService.__codes[phone] = obj
|
||||
print(f"Verify code for {phone}: {obj['code']} (vk)")
|
||||
except:
|
||||
if not request_success:
|
||||
with PhoneVerificationService.__lock:
|
||||
PhoneVerificationService.__codes[phone]["code"] = "FAILED"
|
||||
traceback.print_exc()
|
||||
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
@staticmethod
|
||||
def create():
|
||||
if PhoneVerificationService.__instance is None:
|
||||
PhoneVerificationService.__instance = Thread(target=PhoneVerificationService.__service_run, daemon=True)
|
||||
PhoneVerificationService.__instance.start()
|
||||
|
||||
CHECK_PHONE_NOT_FOUND = "not-found"
|
||||
CHECK_PHONE_NOT_READY = "not-ready"
|
||||
CHECK_PHONE_FAILED = "failed"
|
||||
CHECK_PHONE_INVALID_CODE = "invalid"
|
||||
CHECK_PHONE_MAX_ATTEMPTS = "max-attempts"
|
||||
CHECK_PHONE_RESEND_LIMIT = "resend"
|
||||
|
||||
@staticmethod
|
||||
def check_code(phone: str, code: int, auto_pop=False):
|
||||
with PhoneVerificationService.__lock:
|
||||
if phone not in PhoneVerificationService.__codes:
|
||||
return False, PhoneVerificationService.CHECK_PHONE_NOT_FOUND
|
||||
c = PhoneVerificationService.__codes[phone]
|
||||
print(f"verify struct: {c}")
|
||||
|
||||
if c["code"] is None:
|
||||
# print(f"[PhoneVerificationService] for phone {phone} code not received yet")
|
||||
return False, PhoneVerificationService.CHECK_PHONE_NOT_READY
|
||||
|
||||
if c["code"] == "FAILED":
|
||||
if auto_pop:
|
||||
PhoneVerificationService.__codes.pop(phone)
|
||||
return False, PhoneVerificationService.CHECK_PHONE_FAILED
|
||||
|
||||
if c["attempts"] > 0:
|
||||
if c["code"] == code:
|
||||
if auto_pop:
|
||||
PhoneVerificationService.__codes.pop(phone)
|
||||
return True, None
|
||||
|
||||
else:
|
||||
# print(f"invalid code! attempts: {c['attempts']}")
|
||||
PhoneVerificationService.__codes[phone]["attempts"] -= 1
|
||||
return False, PhoneVerificationService.CHECK_PHONE_INVALID_CODE
|
||||
else:
|
||||
return False, PhoneVerificationService.CHECK_PHONE_MAX_ATTEMPTS
|
||||
|
||||
@staticmethod
|
||||
def send_verify(phone: str):
|
||||
with PhoneVerificationService.__lock:
|
||||
if phone in PhoneVerificationService.__codes:
|
||||
c = PhoneVerificationService.__codes[phone]
|
||||
if (datetime.now() - c["time"]).total_seconds() <= PHONE_VERIFICATION_RESEND_TIME_SECS:
|
||||
return False, PhoneVerificationService.CHECK_PHONE_RESEND_LIMIT
|
||||
|
||||
PhoneVerificationService.__to_verify.append(phone)
|
||||
PhoneVerificationService.__event.set()
|
||||
return True, None
|
83
api/api_utils.py
Executable file
83
api/api_utils.py
Executable file
@ -0,0 +1,83 @@
|
||||
import asyncio
|
||||
from .api_errors import *
|
||||
from .api_params import ApiParam
|
||||
|
||||
# TODO запилить класс для параметров: телефон, пароль, почта
|
||||
|
||||
|
||||
api_methods_dict = {}
|
||||
|
||||
|
||||
def api_make_response(response):
|
||||
return API_OK_OBJ | {"response": response}
|
||||
|
||||
|
||||
# def api_make_param(name, arg_class, description, required=True):
|
||||
# return {
|
||||
# "name": name,
|
||||
# "type": arg_class,
|
||||
# "description": description,
|
||||
# "required": required
|
||||
# }
|
||||
|
||||
# API_PARAM_ACCESS_TOKEN = api_make_param(
|
||||
# "access_token",
|
||||
# AccessToken,
|
||||
# "<i>Токен</i>, выданный методом <code>account.auth</code>"
|
||||
# )
|
||||
|
||||
|
||||
def api_method(func_name, doc="", params: list or None = None, returns=""):
|
||||
"""
|
||||
Декоратор для методов API, автоматически валидирует и передает параметры методам
|
||||
"""
|
||||
def actual_decorator(func):
|
||||
async def wrapper(**kwargs):
|
||||
print(f"> call method {func_name} with params {kwargs}. method params: {params}")
|
||||
|
||||
errors = []
|
||||
func_args = {}
|
||||
for p in params:
|
||||
try:
|
||||
if not isinstance(p, ApiParam):
|
||||
raise Exception(API_ERROR_INTERNAL_ERROR, f"param {p} is not instance of ApiParam class")
|
||||
|
||||
name = p.get_name()
|
||||
value = kwargs[name] if name in kwargs else None
|
||||
|
||||
if asyncio.iscoroutinefunction(p.validate):
|
||||
func_args[name] = await p.validate(value)
|
||||
else:
|
||||
func_args[name] = p.validate(value)
|
||||
|
||||
except Exception as ex:
|
||||
errors.append(ex)
|
||||
# print(f"errors: {errors}, args: {func_args}")
|
||||
if len(errors) > 0:
|
||||
if len(errors) == 1:
|
||||
return make_error_object(errors[0])
|
||||
else:
|
||||
return make_error_object(errors)
|
||||
else:
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(func):
|
||||
out = await func(**func_args)
|
||||
else:
|
||||
out = func(**func_args)
|
||||
except Exception as ex:
|
||||
return make_error_object(ex)
|
||||
|
||||
if out is None:
|
||||
return make_error_object(Exception(API_ERROR_INTERNAL_ERROR, "method returned null object"))
|
||||
return out
|
||||
|
||||
api_methods_dict[func_name] = {
|
||||
"doc": doc,
|
||||
"params": params,
|
||||
"func": wrapper,
|
||||
"returns": returns
|
||||
}
|
||||
return wrapper
|
||||
|
||||
return actual_decorator
|
||||
|
10
api/apps.py
Executable file
10
api/apps.py
Executable file
@ -0,0 +1,10 @@
|
||||
from django.apps import AppConfig
|
||||
from .api_phone_verificator import PhoneVerificationService
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'api'
|
||||
|
||||
def ready(self):
|
||||
PhoneVerificationService.create()
|
39
api/migrations/0001_initial.py
Executable file
39
api/migrations/0001_initial.py
Executable file
@ -0,0 +1,39 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-18 17:43
|
||||
|
||||
import datetime
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('surname', models.CharField(max_length=60, verbose_name='Фамилия')),
|
||||
('name', models.CharField(max_length=60, verbose_name='Имя')),
|
||||
('phone', models.CharField(max_length=16, unique=True, validators=[django.core.validators.RegexValidator(regex='^\\+7[0-9]{10}$')], verbose_name='Телефон')),
|
||||
('password', models.CharField(max_length=64, verbose_name='Хеш пароля')),
|
||||
('role', models.IntegerField(choices=[(0, 'Заказчик'), (1, 'Исполнитель'), (2, 'Модератор'), (3, 'Админ')])),
|
||||
('is_staff', models.BooleanField(default=False, verbose_name='Разрешение на вход в админку')),
|
||||
('register_datetime', models.DateTimeField(default=datetime.datetime.now, editable=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AccessToken',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('access_token', models.CharField(editable=False, max_length=128, unique=True)),
|
||||
('creation_time', models.DateTimeField(default=datetime.datetime.now)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.account')),
|
||||
],
|
||||
),
|
||||
]
|
31
api/migrations/0002_alter_account_role_executoraccount.py
Executable file
31
api/migrations/0002_alter_account_role_executoraccount.py
Executable file
@ -0,0 +1,31 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-20 10:54
|
||||
|
||||
import api.models
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='role',
|
||||
field=models.SmallIntegerField(choices=[(0, 'Заказчик'), (1, 'Исполнитель'), (2, 'Модератор'), (3, 'Админ')]),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExecutorAccount',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('executor_type', models.SmallIntegerField(choices=[(0, 'самозанятый'), (1, 'юр. лицо')], verbose_name='Тип исполнителя')),
|
||||
('inn', models.CharField(max_length=10, validators=[django.core.validators.RegexValidator(regex='^[0-9]{10}$')], verbose_name='ИНН')),
|
||||
('additional_info', models.JSONField(default=api.models._executor_additional_info_default, verbose_name='Дополнительные данные')),
|
||||
('account', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.account')),
|
||||
],
|
||||
),
|
||||
]
|
23
api/migrations/0003_remove_account_is_staff_and_more.py
Executable file
23
api/migrations/0003_remove_account_is_staff_and_more.py
Executable file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-20 11:03
|
||||
|
||||
import api.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0002_alter_account_role_executoraccount'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='account',
|
||||
name='is_staff',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='executoraccount',
|
||||
name='additional_info',
|
||||
field=models.JSONField(blank=True, default=api.models._executor_additional_info_default, verbose_name='Дополнительные данные'),
|
||||
),
|
||||
]
|
35
api/migrations/0004_remove_executoraccount_id_and_more.py
Executable file
35
api/migrations/0004_remove_executoraccount_id_and_more.py
Executable file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-20 11:28
|
||||
|
||||
import api.models
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0003_remove_account_is_staff_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='executoraccount',
|
||||
name='id',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='executoraccount',
|
||||
name='account',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='api.account'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='executoraccount',
|
||||
name='additional_info',
|
||||
field=models.JSONField(blank=True, default=api.models._executor_additional_info_default, null=True, verbose_name='Дополнительные данные'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='executoraccount',
|
||||
name='inn',
|
||||
field=models.CharField(blank=True, max_length=10, null=True, validators=[django.core.validators.RegexValidator(regex='^[0-9]{10}$')], verbose_name='ИНН'),
|
||||
),
|
||||
]
|
23
api/migrations/0005_account_about_alter_account_role.py
Executable file
23
api/migrations/0005_account_about_alter_account_role.py
Executable file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-21 21:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0004_remove_executoraccount_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='about',
|
||||
field=models.CharField(blank=True, default='', max_length=1000, verbose_name='О себе'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='role',
|
||||
field=models.SmallIntegerField(choices=[(0, 'Заказчик'), (1, 'Исполнитель')]),
|
||||
),
|
||||
]
|
39
api/migrations/0006_alter_account_name_alter_account_password_and_more.py
Executable file
39
api/migrations/0006_alter_account_name_alter_account_password_and_more.py
Executable file
@ -0,0 +1,39 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-26 17:20
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0005_account_about_alter_account_role'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, default='', max_length=60, verbose_name='Имя'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='password',
|
||||
field=models.CharField(max_length=64, validators=[django.core.validators.RegexValidator(regex='^[\\wа-яА-Я]{6,}$')], verbose_name='Хеш пароля'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='role',
|
||||
field=models.SmallIntegerField(blank=True, choices=[(0, 'Заказчик'), (1, 'Исполнитель')], null=True, verbose_name='Роль'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='surname',
|
||||
field=models.CharField(blank=True, default='', max_length=60, verbose_name='Фамилия'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='executoraccount',
|
||||
name='inn',
|
||||
field=models.CharField(blank=True, max_length=12, null=True, validators=[django.core.validators.RegexValidator(regex='^[0-9]{10}$|^[0-9]{12}$')], verbose_name='ИНН'),
|
||||
),
|
||||
]
|
24
api/migrations/0007_alter_account_role_and_more.py
Executable file
24
api/migrations/0007_alter_account_role_and_more.py
Executable file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 4.1.2 on 2022-10-26 17:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0006_alter_account_name_alter_account_password_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='role',
|
||||
field=models.SmallIntegerField(choices=[(0, 'Заказчик'), (1, 'Исполнитель')], default=0, verbose_name='Роль'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='executoraccount',
|
||||
name='executor_type',
|
||||
field=models.SmallIntegerField(blank=True, choices=[(0, 'самозанятый'), (1, 'юр. лицо')], null=True, verbose_name='Тип исполнителя'),
|
||||
),
|
||||
]
|
18
api/migrations/0008_account_email.py
Executable file
18
api/migrations/0008_account_email.py
Executable file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.2 on 2022-11-04 19:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0007_alter_account_role_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, default=None, max_length=254, null=True, unique=True, verbose_name='Почта'),
|
||||
),
|
||||
]
|
27
api/migrations/0009_city_account_city.py
Executable file
27
api/migrations/0009_city_account_city.py
Executable file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.1.2 on 2022-11-06 13:47
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0008_account_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='City',
|
||||
fields=[
|
||||
('code', models.CharField(max_length=20, primary_key=True, serialize=False, validators=[django.core.validators.RegexValidator(regex='^[0-9a-zA-Z_]*$')], verbose_name='Код города')),
|
||||
('name', models.CharField(max_length=50, unique=True, verbose_name='Название города')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='city',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.city'),
|
||||
),
|
||||
]
|
48
api/migrations/0010_order.py
Executable file
48
api/migrations/0010_order.py
Executable file
@ -0,0 +1,48 @@
|
||||
# Generated by Django 4.1.2 on 2022-11-06 15:33
|
||||
|
||||
import datetime
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0009_city_account_city'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='Название заказа')),
|
||||
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||
('square', models.DecimalField(decimal_places=2, max_digits=7, verbose_name='Площадь в м²')),
|
||||
('work_time', models.CharField(blank=True, max_length=100, verbose_name='Рабочее время')),
|
||||
('type_of_renovation', models.CharField(blank=True, choices=[('', 'Не определено'), ('overhaul', 'Капитальный'), ('partial', 'Частичный'), ('redecor', 'Косметический'), ('premium', 'Премиальный'), ('design', 'Дизайнерский')], default='', max_length=10, verbose_name='Тип ремонта')),
|
||||
('type_of_house', models.CharField(blank=True, choices=[('block', 'Блочный'), ('brick', 'Кирпичный'), ('monolith', 'Монолит'), ('panel', 'Панельный')], default='', max_length=10, verbose_name='Тип дома')),
|
||||
('type_of_room', models.CharField(blank=True, choices=[('primary', 'Первичка'), ('secondary', 'Вторичка')], default='', max_length=10, verbose_name='Тип квартиры')),
|
||||
('purchase_of_material', models.CharField(blank=True, choices=[('executor', 'Исполнитель'), ('customer', 'Заказчик')], default='', max_length=10, verbose_name='Закуп материала')),
|
||||
('type_of_executor', models.CharField(blank=True, choices=[('individual', 'Самозанятый/бригада'), ('company', 'Компания')], default='', max_length=10, verbose_name='Тип исполнителя')),
|
||||
('is_with_warranty', models.BooleanField(default=True, verbose_name='С гарантией')),
|
||||
('is_with_contract', models.BooleanField(default=False, verbose_name='Работа по договору')),
|
||||
('is_require_design', models.BooleanField(default=False, verbose_name='Требуется дизайн проект')),
|
||||
('is_with_trade', models.BooleanField(default=False, verbose_name='Возможен торг')),
|
||||
('is_with_cleaning', models.BooleanField(default=False, verbose_name='С уборкой')),
|
||||
('is_with_garbage_removal', models.BooleanField(default=False, verbose_name='С вывозом мусора')),
|
||||
('approximate_price', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Цена')),
|
||||
('date_start', models.DateField(blank=True, default=None, null=True, verbose_name='Дата начала')),
|
||||
('date_end', models.DateField(blank=True, default=None, null=True, verbose_name='Дата окончания')),
|
||||
('address_text', models.CharField(blank=True, max_length=70, verbose_name='Улица, дом')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
||||
('phone', models.CharField(blank=True, max_length=16, null=True, validators=[django.core.validators.RegexValidator(regex='^\\+7[0-9]{10}$')], verbose_name='Телефон')),
|
||||
('create_time', models.DateTimeField(default=datetime.datetime.now, editable=False, verbose_name='Время создания')),
|
||||
('moderated', models.BooleanField(default=True, verbose_name='Модерирован')),
|
||||
('published', models.BooleanField(default=False, verbose_name='Опубликован')),
|
||||
('address_city', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='address_city', to='api.city', verbose_name='Город')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='owner', to='api.account', verbose_name='Владелец')),
|
||||
],
|
||||
),
|
||||
]
|
0
api/migrations/__init__.py
Executable file
0
api/migrations/__init__.py
Executable file
BIN
api/migrations/__pycache__/0001_initial.cpython-311.pyc
Executable file
BIN
api/migrations/__pycache__/0001_initial.cpython-311.pyc
Executable file
Binary file not shown.
Binary file not shown.
BIN
api/migrations/__pycache__/0003_remove_account_is_staff_and_more.cpython-311.pyc
Executable file
BIN
api/migrations/__pycache__/0003_remove_account_is_staff_and_more.cpython-311.pyc
Executable file
Binary file not shown.
Binary file not shown.
BIN
api/migrations/__pycache__/0005_account_about_alter_account_role.cpython-311.pyc
Executable file
BIN
api/migrations/__pycache__/0005_account_about_alter_account_role.cpython-311.pyc
Executable file
Binary file not shown.
Binary file not shown.
BIN
api/migrations/__pycache__/0007_alter_account_role_and_more.cpython-311.pyc
Executable file
BIN
api/migrations/__pycache__/0007_alter_account_role_and_more.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/migrations/__pycache__/0008_account_email.cpython-311.pyc
Executable file
BIN
api/migrations/__pycache__/0008_account_email.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/migrations/__pycache__/0009_city_account_city.cpython-311.pyc
Executable file
BIN
api/migrations/__pycache__/0009_city_account_city.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/migrations/__pycache__/0010_order.cpython-311.pyc
Executable file
BIN
api/migrations/__pycache__/0010_order.cpython-311.pyc
Executable file
Binary file not shown.
BIN
api/migrations/__pycache__/__init__.cpython-311.pyc
Executable file
BIN
api/migrations/__pycache__/__init__.cpython-311.pyc
Executable file
Binary file not shown.
402
api/models.py
Executable file
402
api/models.py
Executable file
@ -0,0 +1,402 @@
|
||||
from datetime import datetime
|
||||
|
||||
from asgiref.sync import sync_to_async
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
|
||||
from hashlib import sha512, sha256
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
from .api_errors import *
|
||||
import re
|
||||
|
||||
|
||||
class City(models.Model):
|
||||
code = models.CharField(primary_key=True, max_length=20, verbose_name="Код города", validators=[
|
||||
RegexValidator(regex="^[0-9a-zA-Z_]*$"),
|
||||
])
|
||||
name = models.CharField(unique=True, max_length=50, verbose_name="Название города")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.code})"
|
||||
|
||||
@staticmethod
|
||||
async def to_choices_async():
|
||||
return [item async for item in City.objects.order_by('name').values_list('code', 'name')]
|
||||
|
||||
@staticmethod
|
||||
def to_choices():
|
||||
return list(City.objects.order_by('name').values_list('code', 'name'))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_code(code):
|
||||
if code is None:
|
||||
return None
|
||||
return await City.objects.aget(code=code)
|
||||
|
||||
|
||||
class Account(models.Model):
|
||||
surname = models.CharField(max_length=60, verbose_name="Фамилия", blank=True, default="")
|
||||
name = models.CharField(max_length=60, verbose_name="Имя", blank=True, default="")
|
||||
phone = models.CharField(unique=True, max_length=16, verbose_name="Телефон", validators=[
|
||||
RegexValidator(regex="^\\+7[0-9]{10}$"),
|
||||
])
|
||||
email = models.EmailField(unique=True, verbose_name="Почта", blank=True, null=True, default=None)
|
||||
|
||||
password = models.CharField(max_length=64, blank=True, verbose_name="Хеш пароля")
|
||||
|
||||
# роль
|
||||
ROLE_CUSTOMER, ROLE_EXECUTOR, ROLE_MODER, ROLE_ADMIN = range(4)
|
||||
|
||||
ROLE_CHOICES = [
|
||||
(ROLE_CUSTOMER, 'Заказчик'),
|
||||
(ROLE_EXECUTOR, 'Исполнитель'),
|
||||
# TODO добавить модератора и админа в API
|
||||
# (ROLE_MODER, 'Модератор'),
|
||||
# (ROLE_ADMIN, 'Админ')
|
||||
]
|
||||
role = models.SmallIntegerField(choices=ROLE_CHOICES, verbose_name="Роль", editable=False)
|
||||
|
||||
about = models.CharField(max_length=1000, blank=True, default="", verbose_name="О себе")
|
||||
|
||||
city = models.ForeignKey(City, on_delete=models.SET_NULL, default=None, null=True, blank=True)
|
||||
|
||||
register_datetime = models.DateTimeField(default=datetime.now, editable=False)
|
||||
|
||||
@staticmethod
|
||||
def create_user(phone: str, **kvargs):
|
||||
return Account(phone=Account.normalize_phone(phone), **kvargs)
|
||||
|
||||
@staticmethod
|
||||
def normalize_phone(phone):
|
||||
# Гоша попросил запилить фичу, чтобы принимались номера:
|
||||
# +79991112233
|
||||
# 79991112233
|
||||
# 9991112233
|
||||
if re.match("^[0-9]{10}$", phone) is not None:
|
||||
return f"+7{phone}"
|
||||
elif re.match("^7[0-9]{10}$", phone) is not None:
|
||||
return f"+{phone}"
|
||||
else:
|
||||
return phone
|
||||
|
||||
@staticmethod
|
||||
async def get_by_natural_key(key):
|
||||
try:
|
||||
return await Account.objects.aget(phone=Account.normalize_phone(key))
|
||||
except:
|
||||
raise Exception(API_ERROR_NOT_FOUND, "user")
|
||||
|
||||
async def get_by_id(self, user_id: int):
|
||||
u = Account.objects.filter(id=user_id).select_related('executoraccount', 'city')
|
||||
if self.role == Account.ROLE_EXECUTOR or self.role == Account.ROLE_CUSTOMER:
|
||||
u.filter(role__in=[Account.ROLE_EXECUTOR, Account.ROLE_CUSTOMER])
|
||||
|
||||
return await u.afirst()
|
||||
|
||||
def save(self, **kvargs):
|
||||
if len(self.password) != 64:
|
||||
self.password = sha256(self.password.encode('utf-8')).hexdigest()
|
||||
|
||||
if self.role != Account.ROLE_EXECUTOR and hasattr(self, 'executoraccount'):
|
||||
self.executoraccount.delete()
|
||||
|
||||
super(Account, self).save(**kvargs)
|
||||
|
||||
def check_password(self, password):
|
||||
return sha256(password.encode('utf-8')).hexdigest() == self.password
|
||||
|
||||
def __str__(self):
|
||||
r = None if self.role is None else ExecutorAccount.EXECUTOR_TYPE_CHOICES[self.role][1]
|
||||
return f"{self.name} {self.surname}: {self.phone} ({r})"
|
||||
|
||||
def is_completed(self):
|
||||
return self.name != '' and self.surname != '' and self.city_id is not None
|
||||
|
||||
|
||||
def _executor_additional_info_default():
|
||||
return {}
|
||||
|
||||
|
||||
class ExecutorAccount(models.Model):
|
||||
account = models.OneToOneField(Account, on_delete=models.CASCADE, primary_key=True)
|
||||
|
||||
# тип исполнителя
|
||||
EXECUTOR_TYPE_SELF_EMPLOYED, EXECUTOR_TYPE_LEGAL_ENTITY = range(2)
|
||||
EXECUTOR_TYPE_CHOICES = [
|
||||
(EXECUTOR_TYPE_SELF_EMPLOYED, "самозанятый"),
|
||||
(EXECUTOR_TYPE_LEGAL_ENTITY, "юр. лицо"),
|
||||
]
|
||||
executor_type = models.SmallIntegerField(choices=EXECUTOR_TYPE_CHOICES, verbose_name="Тип исполнителя",
|
||||
blank=True, null=True)
|
||||
|
||||
# ИНН
|
||||
inn = models.CharField(max_length=12, null=True, blank=True, validators=[
|
||||
RegexValidator(regex="^[0-9]{10}$|^[0-9]{12}$"),
|
||||
], verbose_name="ИНН")
|
||||
|
||||
# дополнительная информация
|
||||
additional_info = models.JSONField(default=_executor_additional_info_default, null=True, blank=True,
|
||||
verbose_name="Дополнительные данные")
|
||||
|
||||
def __str__(self):
|
||||
e_t = None if self.executor_type is None else ExecutorAccount.EXECUTOR_TYPE_CHOICES[self.executor_type][1]
|
||||
return f"{self.account}: {e_t}"
|
||||
|
||||
|
||||
class AccessToken(models.Model):
|
||||
user = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||
access_token = models.CharField(max_length=128, editable=False, unique=True)
|
||||
creation_time = models.DateTimeField(default=datetime.now)
|
||||
|
||||
@staticmethod
|
||||
async def create_token(user: Account):
|
||||
token = AccessToken(user=user, access_token=AccessToken._generate_token(user))
|
||||
print(f"created token {token.access_token[:16]}...")
|
||||
await sync_to_async(token.save)()
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
async def auth(login: str, password: str):
|
||||
try:
|
||||
user = await Account.get_by_natural_key(login)
|
||||
except Exception:
|
||||
raise Exception(API_ERROR_INVALID_LOGIN)
|
||||
|
||||
if not user.check_password(password):
|
||||
raise Exception(API_ERROR_INVALID_PASSWORD)
|
||||
|
||||
return await AccessToken.create_token(user)
|
||||
|
||||
@staticmethod
|
||||
async def deauth(token: str):
|
||||
t = await AccessToken.get_by_token(token)
|
||||
await sync_to_async(t.delete)()
|
||||
|
||||
@staticmethod
|
||||
async def get_user_by_token(token: str):
|
||||
return (await AccessToken.get_by_token(token)).user
|
||||
|
||||
@staticmethod
|
||||
async def get_by_token(token: str):
|
||||
t = await AccessToken.objects.filter(access_token=token)\
|
||||
.select_related('user', 'user__executoraccount', 'user__city').afirst()
|
||||
if t is None:
|
||||
raise Exception(API_ERROR_INVALID_TOKEN)
|
||||
return t
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.name} {self.user.surname} ({self.user.phone}): {self.access_token[:8]}..."
|
||||
|
||||
@staticmethod
|
||||
def _generate_token(user: Account):
|
||||
return sha512(bytearray(user.phone + user.password + str(datetime.now()), 'utf-8')).hexdigest()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.access_token is None or len(self.access_token) == 0:
|
||||
self.access_token = AccessToken._generate_token(self.user)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
async def list_sessions(self):
|
||||
sessions = AccessToken.objects.filter(~Q(access_token=self.access_token), user=self.user)
|
||||
return [item async for item in sessions]
|
||||
|
||||
async def delete_sessions_without_current(self):
|
||||
await sync_to_async(AccessToken.objects.filter(~Q(access_token=self.access_token), user=self.user).delete)()
|
||||
|
||||
async def delete_session(self, session_id: int):
|
||||
session = await AccessToken.objects.filter(~Q(access_token=self.access_token),
|
||||
user=self.user, id=session_id).afirst()
|
||||
if session is None:
|
||||
raise Exception(API_ERROR_NOT_FOUND, "session")
|
||||
|
||||
await sync_to_async(session.delete)()
|
||||
|
||||
@staticmethod
|
||||
async def delete_sessions(user: Account):
|
||||
await sync_to_async(AccessToken.objects.filter(user=user).delete)()
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
# основные поля: название и описание
|
||||
name = models.CharField(max_length=200, verbose_name="Название заказа")
|
||||
description = models.TextField(blank=True, verbose_name="Описание")
|
||||
|
||||
# площадь в квадратных метрах
|
||||
square = models.DecimalField(max_digits=7, decimal_places=2, blank=False, verbose_name="Площадь в м²")
|
||||
|
||||
work_time = models.CharField(max_length=100, blank=True, verbose_name="Рабочее время")
|
||||
|
||||
# дальше вид дома, тип ремонта, тип квартиры, требуется дизайн проект, закуп материала, тип исполнителя
|
||||
|
||||
CHOICE_UNDEFINED = ''
|
||||
|
||||
# тип ремонта
|
||||
TYPE_OF_RENOVATION_OVERHAUL = 'overhaul'
|
||||
TYPE_OF_RENOVATION_PARTIAL = 'partial'
|
||||
TYPE_OF_RENOVATION_REDECOR = 'redecor'
|
||||
TYPE_OF_RENOVATION_PREMIUM = 'premium'
|
||||
TYPE_OF_RENOVATION_DESIGN = 'design'
|
||||
|
||||
TYPE_OF_RENOVATION_CHOICES = [
|
||||
(CHOICE_UNDEFINED, 'Не определено'),
|
||||
(TYPE_OF_RENOVATION_OVERHAUL, 'Капитальный'),
|
||||
(TYPE_OF_RENOVATION_PARTIAL, 'Частичный'),
|
||||
(TYPE_OF_RENOVATION_REDECOR, 'Косметический'),
|
||||
(TYPE_OF_RENOVATION_PREMIUM, 'Премиальный'),
|
||||
(TYPE_OF_RENOVATION_DESIGN, 'Дизайнерский'),
|
||||
]
|
||||
type_of_renovation = models.CharField(max_length=10, choices=TYPE_OF_RENOVATION_CHOICES, default=CHOICE_UNDEFINED,
|
||||
blank=True, verbose_name="Тип ремонта")
|
||||
|
||||
# тип дома
|
||||
TYPE_OF_HOUSE_BLOCK = 'block'
|
||||
TYPE_OF_HOUSE_BRICK = 'brick'
|
||||
TYPE_OF_HOUSE_MONOLITH = 'monolith'
|
||||
TYPE_OF_HOUSE_PANEL = 'panel'
|
||||
|
||||
TYPE_OF_HOUSE_CHOICES = [
|
||||
(TYPE_OF_HOUSE_BLOCK, 'Блочный'),
|
||||
(TYPE_OF_HOUSE_BRICK, 'Кирпичный'),
|
||||
(TYPE_OF_HOUSE_MONOLITH, 'Монолит'),
|
||||
(TYPE_OF_HOUSE_PANEL, 'Панельный'),
|
||||
]
|
||||
type_of_house = models.CharField(max_length=10, choices=TYPE_OF_HOUSE_CHOICES, blank=True, default=CHOICE_UNDEFINED,
|
||||
verbose_name="Тип дома")
|
||||
|
||||
# тип квартиры
|
||||
TYPE_OF_ROOM_PRIMARY = 'primary'
|
||||
TYPE_OF_ROOM_SECONDARY = 'secondary'
|
||||
|
||||
TYPE_OF_ROOM_CHOICES = [
|
||||
(TYPE_OF_ROOM_PRIMARY, 'Первичка'),
|
||||
(TYPE_OF_ROOM_SECONDARY, 'Вторичка')
|
||||
]
|
||||
type_of_room = models.CharField(max_length=10, choices=TYPE_OF_ROOM_CHOICES, blank=True, default=CHOICE_UNDEFINED,
|
||||
verbose_name="Тип квартиры")
|
||||
|
||||
# закуп материала
|
||||
PURCHASE_OF_MATERIAL_EXECUTOR = 'executor'
|
||||
PURCHASE_OF_MATERIAL_CUSTOMER = 'customer'
|
||||
|
||||
PURCHASE_OF_MATERIAL_CHOICES = [
|
||||
(PURCHASE_OF_MATERIAL_EXECUTOR, 'Исполнитель'),
|
||||
(PURCHASE_OF_MATERIAL_CUSTOMER, 'Заказчик')
|
||||
]
|
||||
purchase_of_material = models.CharField(max_length=10, choices=PURCHASE_OF_MATERIAL_CHOICES,
|
||||
blank=True, default=CHOICE_UNDEFINED, verbose_name="Закуп материала")
|
||||
|
||||
# тип исполнителя
|
||||
TYPE_OF_EXECUTOR_INDIVIDUAL = 'individual'
|
||||
TYPE_OF_EXECUTOR_COMPANY = 'company'
|
||||
|
||||
TYPE_OF_EXECUTOR_CHOICES = [
|
||||
(TYPE_OF_EXECUTOR_INDIVIDUAL, 'Самозанятый/бригада'),
|
||||
(TYPE_OF_EXECUTOR_COMPANY, 'Компания')
|
||||
]
|
||||
type_of_executor = models.CharField(max_length=10, choices=TYPE_OF_EXECUTOR_CHOICES,
|
||||
blank=True, default=CHOICE_UNDEFINED, verbose_name="Тип исполнителя")
|
||||
|
||||
# дальше отдельные параметры
|
||||
is_with_warranty = models.BooleanField(default=True, verbose_name="С гарантией")
|
||||
is_with_contract = models.BooleanField(default=False, verbose_name="Работа по договору")
|
||||
is_require_design = models.BooleanField(default=False, verbose_name="Требуется дизайн проект")
|
||||
is_with_trade = models.BooleanField(default=False, verbose_name="Возможен торг")
|
||||
is_with_cleaning = models.BooleanField(default=False, verbose_name="С уборкой")
|
||||
is_with_garbage_removal = models.BooleanField(default=False, verbose_name="С вывозом мусора")
|
||||
|
||||
# примерная цена
|
||||
approximate_price = models.DecimalField(max_digits=12, decimal_places=2, blank=False, verbose_name="Цена")
|
||||
|
||||
date_start = models.DateField(null=True, blank=True, default=None, verbose_name="Дата начала")
|
||||
date_end = models.DateField(null=True, blank=True, default=None, verbose_name="Дата окончания")
|
||||
|
||||
address_city = models.ForeignKey(City, on_delete=models.CASCADE, blank=False, related_name="address_city",
|
||||
verbose_name="Город")
|
||||
address_text = models.CharField(max_length=70, blank=True, verbose_name="Улица, дом")
|
||||
|
||||
owner = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="owner", verbose_name="Владелец")
|
||||
|
||||
email = models.EmailField(null=True, blank=True, verbose_name="Email")
|
||||
phone = models.CharField(null=True, blank=True, max_length=16, verbose_name="Телефон", validators=[
|
||||
RegexValidator(regex="^\\+7[0-9]{10}$")
|
||||
])
|
||||
|
||||
create_time = models.DateTimeField(default=datetime.now, editable=False, verbose_name="Время создания")
|
||||
|
||||
moderated = models.BooleanField(default=True, verbose_name="Модерирован")
|
||||
published = models.BooleanField(default=False, verbose_name="Опубликован")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@staticmethod
|
||||
def get_all_for_user(user):
|
||||
if user.is_staff:
|
||||
return Order.objects.filter().order_by('create_time')
|
||||
else:
|
||||
return Order.objects.filter(published=True, moderated=True).order_by('create_time')
|
||||
|
||||
@staticmethod
|
||||
def get_for_user_by_id(user, order_id):
|
||||
q = Order.get_all_for_user(user).filter(id=order_id)
|
||||
if len(q) == 0:
|
||||
return None
|
||||
else:
|
||||
return q[0]
|
||||
|
||||
|
||||
|
||||
# def _upload_image_filename(instance, filename):
|
||||
# name, ext = os.path.splitext(filename)
|
||||
# fn = sha256((str(datetime.now()) + name).encode('utf-8')).hexdigest() + ext
|
||||
# return "order-images/" + fn
|
||||
#
|
||||
#
|
||||
# # TOFIX сделать удаление ненужных картинок из файловой системы
|
||||
#
|
||||
# class OrderImage(models.Model):
|
||||
# MAX_IMAGES = 10
|
||||
#
|
||||
# order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="order", verbose_name="Заказ")
|
||||
# image = models.ImageField(upload_to=_upload_image_filename, verbose_name="Картинка")
|
||||
#
|
||||
# def __str__(self):
|
||||
# return f"{self.id}: {self.order}"
|
||||
#
|
||||
# def save(self, *args, **kwargs):
|
||||
# super(OrderImage, self).save(*args, **kwargs)
|
||||
# img = Image.open(self.image.path)
|
||||
#
|
||||
# if img.width > 1920 or img.height > 1080:
|
||||
# # нужно урезать фортку так, чтобы пропорции уменьшились пропорционально
|
||||
# output_size = (1920, 1080)
|
||||
# img.thumbnail(output_size)
|
||||
# img.save(self.image.path)
|
||||
#
|
||||
#
|
||||
# class OrderRespond(models.Model):
|
||||
# create_time = models.DateTimeField(default=datetime.now, editable=False, verbose_name="Время отклика")
|
||||
# user = models.ForeignKey(SiteUser, on_delete=models.CASCADE, related_name="respond_user", verbose_name="Пользователь")
|
||||
# order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="respond_order", verbose_name="Заказ")
|
||||
#
|
||||
# class Meta:
|
||||
# constraints = [
|
||||
# models.UniqueConstraint(
|
||||
# fields=['user', 'order'], name='unique_order_respond_user_order'
|
||||
# )
|
||||
# ]
|
||||
#
|
||||
# def __str__(self):
|
||||
# return f"{self.order}: {self.user}"
|
||||
#
|
||||
# def save(self, *args, **kwargs):
|
||||
# if Order.objects.get(id=self.order.id) == self.user.id:
|
||||
# raise Exception("User can't respond to self order")
|
||||
#
|
||||
# self.full_clean()
|
||||
#
|
||||
# super().save(*args, **kwargs)
|
||||
|
3
api/tests.py
Executable file
3
api/tests.py
Executable file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
24
api/urls.py
Executable file
24
api/urls.py
Executable file
@ -0,0 +1,24 @@
|
||||
"""api URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.view_methods, name='view_methods'),
|
||||
path('methods/<str:method_name>', views.call_method, name='call_method')
|
||||
]
|
||||
|
37
api/views.py
Executable file
37
api/views.py
Executable file
@ -0,0 +1,37 @@
|
||||
import json
|
||||
import traceback
|
||||
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from .api_methods import api_call_method, api_get_documentation
|
||||
|
||||
|
||||
def view_methods(request):
|
||||
methods = api_get_documentation()
|
||||
return render(request, 'index.html', {'api_methods': methods})
|
||||
|
||||
|
||||
def _default_serializer(obj):
|
||||
try:
|
||||
return obj.to_json()
|
||||
except Exception:
|
||||
return str(obj)
|
||||
|
||||
|
||||
async def call_method(request, method_name):
|
||||
if request.method == "GET":
|
||||
params = request.GET
|
||||
elif request.method == "POST":
|
||||
params = request.POST
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
api_params = {}
|
||||
for p in params:
|
||||
# защита от нескольких параметров с одним именем
|
||||
api_params[p] = params[p]
|
||||
|
||||
out = await api_call_method(method_name, api_params)
|
||||
|
||||
response = HttpResponse(json.dumps(out, default=_default_serializer, ensure_ascii=False, indent=4))
|
||||
response.headers["Content-type"] = "application/json; charset=utf-8"
|
||||
return response
|
0
arka/__init__.py
Executable file
0
arka/__init__.py
Executable file
BIN
arka/__pycache__/__init__.cpython-311.pyc
Executable file
BIN
arka/__pycache__/__init__.cpython-311.pyc
Executable file
Binary file not shown.
BIN
arka/__pycache__/asgi.cpython-311.pyc
Executable file
BIN
arka/__pycache__/asgi.cpython-311.pyc
Executable file
Binary file not shown.
BIN
arka/__pycache__/settings.cpython-311.pyc
Executable file
BIN
arka/__pycache__/settings.cpython-311.pyc
Executable file
Binary file not shown.
BIN
arka/__pycache__/urls.cpython-311.pyc
Executable file
BIN
arka/__pycache__/urls.cpython-311.pyc
Executable file
Binary file not shown.
BIN
arka/__pycache__/wsgi.cpython-311.pyc
Executable file
BIN
arka/__pycache__/wsgi.cpython-311.pyc
Executable file
Binary file not shown.
16
arka/asgi.py
Executable file
16
arka/asgi.py
Executable file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for arka 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/4.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'arka.settings')
|
||||
|
||||
application = get_asgi_application()
|
149
arka/settings.py
Executable file
149
arka/settings.py
Executable file
@ -0,0 +1,149 @@
|
||||
"""
|
||||
Django settings for stall project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.2.10.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import dotenv
|
||||
|
||||
dotenv.load_dotenv()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
PROJECT_ROOT = os.path.dirname(__file__)
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
|
||||
|
||||
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ["arka.topserv4824.duckdns.org", "192.168.0.160", "localhost"]
|
||||
CSRF_TRUSTED_ORIGINS = ['https://arka.topserv4824.duckdns.org']
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'api.apps.ApiConfig',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# 'django_extensions',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'arka.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'arka.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': os.getenv('DB_NAME'),
|
||||
'USER': os.getenv('DB_USERNAME'),
|
||||
'PASSWORD': os.getenv('DB_PASSWORD'),
|
||||
'HOST': os.getenv('DB_HOST', 'localhost'),
|
||||
'PORT': os.getenv('DB_PORT', ''),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
||||
|
||||
# AUTH_USER_MODEL = 'api.Account'
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'ru-RU'
|
||||
|
||||
TIME_ZONE = 'Europe/Moscow'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = False
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.1/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
|
||||
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
# Настройки сервиса верификации телефонов
|
||||
|
||||
PHONE_VERIFICATION_ENABLE = int(os.getenv('PHONE_VERIFICATION_ENABLE', "1")) != 0
|
||||
PHONE_VERIFICATION_ATTEMPTS = int(os.getenv('PHONE_VERIFICATION_ATTEMPTS', "5"))
|
||||
PHONE_VERIFICATION_RESEND_TIME_SECS = int(os.getenv('PHONE_VERIFICATION_RESEND_TIME_SECS', "180"))
|
||||
PHONE_VERIFICATION_ACCESS_KEY = os.getenv('PHONE_VERIFICATION_ACCESS_KEY', "EMPTY_ACCESS_KEY")
|
24
arka/urls.py
Executable file
24
arka/urls.py
Executable file
@ -0,0 +1,24 @@
|
||||
"""arka URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.1/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf.urls.static import static
|
||||
from django.conf import settings
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('api.urls')),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
16
arka/wsgi.py
Executable file
16
arka/wsgi.py
Executable file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for arka project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'arka.settings')
|
||||
|
||||
application = get_wsgi_application()
|
22
manage.py
Executable file
22
manage.py
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'arka.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
4
requirements.txt
Executable file
4
requirements.txt
Executable file
@ -0,0 +1,4 @@
|
||||
psycopg2
|
||||
django==4.1.7
|
||||
requests==2.28.2
|
||||
python-dotenv
|
79
static/css/style.css
Executable file
79
static/css/style.css
Executable file
@ -0,0 +1,79 @@
|
||||
/* TODO исправить стили, тут верхней навигации вообще нет */
|
||||
|
||||
|
||||
/* ========== THEME ========== */
|
||||
body {
|
||||
--text-color: #111;
|
||||
--brand-color: #231765;
|
||||
--bkg-color-blue: #0066e3;
|
||||
|
||||
--bkg-color: #fff;
|
||||
--bkg-color2: #ccc;
|
||||
--bkg-color3: #aaa;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* defaults to dark theme */
|
||||
body {
|
||||
--text-color: #eee;
|
||||
--brand-color: #654dea;
|
||||
--bkg-color-blue: #003aac;
|
||||
|
||||
--bkg-color: #121212;
|
||||
--bkg-color2: #202020;
|
||||
--bkg-color3: #353435;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
background: transparent;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bkg-color);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin: 1em 3em;
|
||||
}
|
||||
|
||||
/* ========== MAIN STYLES ========== */
|
||||
|
||||
#header-wrapper {
|
||||
display: flex;
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
#header-wrapper * {
|
||||
color: var(--brand-color);
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header > * {
|
||||
margin: auto 0.5em;
|
||||
text-decoration: none;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
header > div > * {
|
||||
display: block;
|
||||
margin: 0.1em 0;
|
||||
}
|
||||
|
||||
#logo-text {
|
||||
font-weight: bolder;
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
#logo-image {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
BIN
static/favicon.webp
Executable file
BIN
static/favicon.webp
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
static/images/ex1.jpg
Normal file
BIN
static/images/ex1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 387 KiB |
BIN
static/images/ex2.jpg
Normal file
BIN
static/images/ex2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
BIN
static/images/ex3.jpg
Normal file
BIN
static/images/ex3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 540 KiB |
BIN
static/images/profile.png
Normal file
BIN
static/images/profile.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 132 KiB |
12
static/js/main.js
Executable file
12
static/js/main.js
Executable file
@ -0,0 +1,12 @@
|
||||
// скрипт...
|
||||
|
||||
// Listen for a click on the button
|
||||
const prefersDarkScheme = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
document.getElementById("theme-switcher").addEventListener("click", function () {
|
||||
if (prefersDarkScheme.matches) {
|
||||
document.body.classList.toggle("light-theme");
|
||||
} else {
|
||||
document.body.classList.toggle("dark-theme");
|
||||
}
|
||||
});
|
BIN
static/test/Untitled.ogg
Normal file
BIN
static/test/Untitled.ogg
Normal file
Binary file not shown.
41
static/test/index.html
Normal file
41
static/test/index.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<style>
|
||||
@keyframes blinking {
|
||||
0% {
|
||||
background-color: #ff3d50;
|
||||
}
|
||||
50% {
|
||||
background-color: #55d66b;
|
||||
}
|
||||
75% {
|
||||
background-color: #d0b91d;
|
||||
}
|
||||
100% {
|
||||
background-color: #222291;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
animation: blinking 1s infinite;
|
||||
}
|
||||
|
||||
a {
|
||||
color: aliceblue;
|
||||
font-size: 100px;
|
||||
font-size: 100px;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
<meta charset="UTF-8">
|
||||
<title> title </title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a href="saratov.html"> не все кто перешол по сылке смагли вернутса взад </a>
|
||||
|
||||
</body>
|
||||
</html>
|
BIN
static/test/saratov.gif
Normal file
BIN
static/test/saratov.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
21
static/test/saratov.html
Normal file
21
static/test/saratov.html
Normal file
@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title> SARATOV </title>
|
||||
</head>
|
||||
<style>
|
||||
html {
|
||||
background-color: black;
|
||||
justify-content: center;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<iframe src="Untitled.ogg" type="audio/ogg" allow="autoplay" id="audio" style="display:none"></iframe>
|
||||
<img src="saratov.gif">
|
||||
</body>
|
||||
</html>
|
27
templates/base.html
Executable file
27
templates/base.html
Executable file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title> {% block title %} Арка {% endblock %} </title>
|
||||
{% load static %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/style.css' %}">
|
||||
{% block styles %} {% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="header-wrapper">
|
||||
<header>
|
||||
<img id="logo-image" src="{% static 'favicon.webp' %}" alt="logo image">
|
||||
<div>
|
||||
<span id="logo-text">АРКА</span>
|
||||
<span>API & Документация</span>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<main id="content">
|
||||
{% block content %} тут должен быть контент {% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
230
templates/index.html
Executable file
230
templates/index.html
Executable file
@ -0,0 +1,230 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %} Арка | API Docs {% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
<style>
|
||||
.table-wrapper, .constructor-result > pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td, th {
|
||||
border: var(--brand-color) solid 1px;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--brand-color);
|
||||
}
|
||||
|
||||
details > div {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-left: 0.5em;
|
||||
padding-left: 1em;
|
||||
border-left: var(--brand-color) solid 1px;
|
||||
}
|
||||
|
||||
.method-div {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.method-div summary {
|
||||
font-weight: bolder;
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
|
||||
.constructor-wrapper {
|
||||
background: var(--bkg-color2);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.constructor-fields {
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.constructor-param {
|
||||
padding: 0.5em 0;
|
||||
display: block;
|
||||
margin: 0.2em 0;
|
||||
}
|
||||
|
||||
.constructor-param > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.constructor-param > input, .constructor-param > button {
|
||||
border: var(--brand-color) solid 2px;
|
||||
border-radius: 5px;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.constructor-result {
|
||||
min-width: 200px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="page-header"> Список методов API </h1>
|
||||
<div>
|
||||
<p> Ну а пока ты ждешь рабочего API для заказов и портфолио, можно послушать музычку </p>
|
||||
<audio preload="none" controls src="/m.mp3"></audio>
|
||||
<p> А еще можно попробовать сделать запросы в конструкторе, он есть для каждого метода </p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getAccessToken(new_value) {
|
||||
if (new_value === undefined || new_value === null || new_value === "") {
|
||||
let res = localStorage.getItem("access_token")
|
||||
if (res === null) {
|
||||
return ""
|
||||
}
|
||||
return res
|
||||
} else {
|
||||
console.log(`Storing ${new_value} as token`)
|
||||
localStorage.setItem("access_token", new_value)
|
||||
document.getElementById('current_access_token').innerText = new_value
|
||||
return new_value
|
||||
}
|
||||
}
|
||||
async function sendRequest(method, params) {
|
||||
let url = `/methods/${method}`
|
||||
if (params !== undefined && params !== null) {
|
||||
url += "?" + new URLSearchParams(params)
|
||||
}
|
||||
return await fetch(url)
|
||||
}
|
||||
|
||||
async function makeRequest(view, method, inputs) {
|
||||
let params = {}
|
||||
for (let k in inputs) {
|
||||
let element = document.getElementById(inputs[k])
|
||||
const name = element.name
|
||||
{#let val = encodeURIComponent(element.value)#}
|
||||
let val = element.value
|
||||
if (name === "access_token") {
|
||||
val = getAccessToken(val)
|
||||
}
|
||||
if (val.length > 0)
|
||||
params[name] = val
|
||||
}
|
||||
let res = await sendRequest(method, params)
|
||||
const text = await res.text()
|
||||
document.getElementById(view).innerText = text
|
||||
|
||||
// чтобы запоминался токен
|
||||
try {
|
||||
let j = JSON.parse(text)
|
||||
getAccessToken(j["response"]["access_token"])
|
||||
} catch (e) {}
|
||||
}
|
||||
</script>
|
||||
|
||||
{% for method in api_methods %}
|
||||
<div class="method-div">
|
||||
<details>
|
||||
<summary>{{ method.name }}</summary>
|
||||
|
||||
<div>
|
||||
<h3>Описание</h3>
|
||||
<p>
|
||||
{{ method.doc | safe }}
|
||||
</p>
|
||||
|
||||
<h3>Параметры</h3>
|
||||
{% if method.params %}
|
||||
<div class="table-wrapper"><table>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
<th>Тип</th>
|
||||
<th>Описание</th>
|
||||
<th>Обязательный</th>
|
||||
</tr>
|
||||
|
||||
{% for param in method.params %}
|
||||
<tr>
|
||||
<td>{{ param.name }}</td>
|
||||
<td>{{ param.type }}</td>
|
||||
<td>{{ param.description | safe }}</td>
|
||||
<td>{{ param.required }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table></div>
|
||||
{% else %}
|
||||
<p>
|
||||
Этот метод не принимает параметров.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<h3>Результат</h3>
|
||||
<p>
|
||||
{{ method.returns | safe }}
|
||||
</p>
|
||||
<p>
|
||||
Ссылка на метод (без параметров): <a href="/methods/{{ method.name }}">{{ method.name }}</a>
|
||||
</p>
|
||||
<details>
|
||||
<summary>Конструктор</summary>
|
||||
<div class="constructor-wrapper" id="view-{{ method.name }}">
|
||||
<div class="constructor-fields">
|
||||
<div style="">
|
||||
<h3>Параметры</h3>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
{% if method.params %}
|
||||
{% for param in method.params %}
|
||||
<div class="constructor-param">
|
||||
<label for="param-{{ method.name }}-{{ param.name }}">{{ param.name }}</label>
|
||||
<input type="text" name="{{ param.name }}" id="param-{{ method.name }}-{{ param.name }}">
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="constructor-param">
|
||||
<p>
|
||||
Этот метод не принимает параметров.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="constructor-param">
|
||||
<button onclick="makeRequest('result-{{ method.name }}', '{{ method.name }}',
|
||||
[{% for param in method.params %}'param-{{ method.name }}-{{ param.name }}', {% endfor %}])">Выполнить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="constructor-result">
|
||||
<h3>Результат</h3>
|
||||
<hr>
|
||||
<pre id="result-{{ method.name }}"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div style="text-align: center; background: var(--bkg-color2); margin: 0; margin-top: 3em; padding: 2em; overflow-wrap: break-word;">
|
||||
Перейти в <a href="/admin">админку</a>.
|
||||
<div>
|
||||
Текущий токен: <i id="current_access_token"></i><br><a onclick="localStorage.clear(); document.getElementById('current_access_token').innerText = ''">Сбросить</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.onload = (event) => {
|
||||
const at = localStorage.getItem("access_token")
|
||||
if (at !== null) {
|
||||
document.getElementById('current_access_token').innerText = at
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user