initial commit
This commit is contained in:
Executable
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
+50
@@ -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
|
||||
Executable
+97
@@ -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)}"
|
||||
}
|
||||
}
|
||||
Executable
+653
@@ -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
|
||||
Executable
+272
@@ -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)
|
||||
Executable
+162
@@ -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
|
||||
Executable
+83
@@ -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
|
||||
|
||||
Executable
+10
@@ -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()
|
||||
Executable
+39
@@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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
@@ -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='Дополнительные данные'),
|
||||
),
|
||||
]
|
||||
@@ -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
@@ -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, 'Исполнитель')]),
|
||||
),
|
||||
]
|
||||
@@ -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
@@ -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='Тип исполнителя'),
|
||||
),
|
||||
]
|
||||
Executable
+18
@@ -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='Почта'),
|
||||
),
|
||||
]
|
||||
Executable
+27
@@ -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'),
|
||||
),
|
||||
]
|
||||
Executable
+48
@@ -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='Владелец')),
|
||||
],
|
||||
),
|
||||
]
|
||||
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
+402
@@ -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)
|
||||
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
Executable
+24
@@ -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')
|
||||
]
|
||||
|
||||
Executable
+37
@@ -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
|
||||
Reference in New Issue
Block a user