initial commit

This commit is contained in:
VladislavOstapov 2023-03-06 20:27:57 +03:00
commit b02b9e1811
70 changed files with 2741 additions and 0 deletions

0
api/__init__.py Executable file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

50
api/admin.py Executable file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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')),
],
),
]

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

View 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='Дополнительные данные'),
),
]

View 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='ИНН'),
),
]

View 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, 'Исполнитель')]),
),
]

View 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='ИНН'),
),
]

View 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='Тип исполнителя'),
),
]

View 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='Почта'),
),
]

View 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
View 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
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

402
api/models.py Executable file
View 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
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

24
api/urls.py Executable file
View 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
View 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
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
arka/asgi.py Executable file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
psycopg2
django==4.1.7
requests==2.28.2
python-dotenv

79
static/css/style.css Executable file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
static/images/ex1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 KiB

BIN
static/images/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

12
static/js/main.js Executable file
View 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

Binary file not shown.

41
static/test/index.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

21
static/test/saratov.html Normal file
View 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
View 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
View 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 %}