This repository has been archived on 2024-09-18. You can view files and clone it, but cannot push or open issues or pull requests.
arka-api/api/api_methods.py

910 lines
45 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import random
import time
from datetime import date as dt
import traceback
from .api_media_utils import *
from .api_utils import *
from .models import *
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, self_using=False):
obj = {
"id": user.id,
"name": user.name,
"surname": user.surname,
"phone": user.phone,
"email": user.email,
"about": user.about,
"city": {"code": user.city, "name": CITIES_CHOICES[user.city]} if user.city is not None else None,
"register_datetime": int(time.mktime(user.register_datetime.timetuple())),
"role": user.role,
}
if hasattr(user, 'accountavatar'):
obj["avatar"] = user.accountavatar.photo.id if user.accountavatar.photo is not None else None
obj["profile_background"] = \
user.accountavatar.profile_background.id if user.accountavatar.profile_background is not None else None
else:
obj["avatar"] = None
obj["profile_background"] = None
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)
await AccountAvatar.objects.acreate(account=user, photo=None, profile_background=None)
# удаляем код, типа завершение транзакции
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=CITIES_CHOICES),
ApiParamInt(name="photo", required=False, default=None,
description="ID медиа, которое будет использоваться в качестве фото профиля"),
ApiParamInt(name="profile_background", required=False, default=None,
description="ID медиа, которое будет использоваться в качестве банера профиля")
],
returns="Вернет основную информацию о пользователе, иначе ошибки")
async def edit(access_token, name, surname, about, executor_type, executor_inn, city, photo, profile_background):
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 photo is not None:
p = await Media.objects.filter(pk=photo).select_related('owner').afirst()
if p is None:
raise Exception(API_ERROR_NOT_FOUND, 'field "photo" not found')
if p.owner.id == user.id and (p.extension == 'jpeg' or p.extension == 'png'):
if not hasattr(user, 'accountavatar'):
user.accountavatar = await AccountAvatar.objects.acreate(account=user)
user.accountavatar.photo = p
need_save = True
else:
raise Exception(API_ERROR_NOT_FOUND, 'field "photo" not correct')
if profile_background is not None:
p = await Media.objects.filter(pk=profile_background).select_related('owner').afirst()
if p is None:
raise Exception(API_ERROR_NOT_FOUND, 'field "profile_background" not found')
if p.owner.id == user.id and p.extension == 'jpeg':
if not hasattr(user, 'accountavatar'):
user.accountavatar = await AccountAvatar.objects.acreate(account=user)
user.accountavatar.profile_background = p
need_save = True
else:
raise Exception(API_ERROR_NOT_FOUND, 'field "profile_background" not correct')
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 Exception:
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="Название заказа"),
ApiParamEnum(name="address_city", description="Город: {choices}", choices=CITIES_CHOICES),
ApiParamStr(name='address_text', max_length=70, description="Улица, дом",
required=False, default=""),
ApiParamEnum(name='type_of_apartment', choices=Order.TYPE_OF_APARTMENT_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}"),
ApiParamInt(name='number_of_rooms', required=True, value_min=-1, value_max=100,
description="Количество комнат, -1 = студия"),
ApiParamBoolean(name="is_balcony", default=True, description="Балкон"),
ApiParamBoolean(name="is_loggia", default=True, description="Лоджия"),
ApiParamStr(name='state_of_room', max_length=40, description="Состояние помещения",
required=False, default=""),
ApiParamFloat(name='square', value_max=99999.99, value_min=1.0,
description='Площадь в м²'),
ApiParamFloat(name='ceiling_height', value_max=99.99, value_min=1.0,
description='Высота потолков в м'),
ApiParamEnum(name='type_of_renovation', choices=Order.TYPE_OF_RENOVATION_CHOICES, required=False,
default=Order.CHOICE_UNDEFINED,
description="Тип ремонта: {choices}"),
ApiParamBoolean(name="is_redevelopment", required=False, default=True,
description="Требуется перепланировка"),
ApiParamBoolean(name="is_leveling_floors", required=False, default=False,
description="Требуется выравнивание полов"),
ApiParamBoolean(name="is_heated_floor", required=False, default=False,
description="Теплый пол"),
ApiParamBoolean(name="is_leveling_walls", required=False, default=False,
description="Требуется выравнивание стен"),
ApiParamStr(name='type_of_ceiling', max_length=40, description="Тип потолка",
required=False, default=""),
ApiParamBoolean(name="is_wiring_replace", required=False, default=False,
description="Требуется замена проводки"),
ApiParamBoolean(name="is_require_design", required=False, default=False,
description="Требуется дизайн проект"),
ApiParamEnum(name='purchase_of_material', choices=Order.PURCHASE_OF_MATERIAL_CHOICES,
required=False, default=Order.CHOICE_UNDEFINED,
description="Закуп материала: {choices}"),
ApiParamBoolean(name="is_with_contract", required=False, default=False,
description="Работа по договору"),
ApiParamBoolean(name="is_with_warranty", required=False, default=True,
description="С гарантией"),
ApiParamBoolean(name="is_with_trade", required=False, default=False,
description="Возможен торг"),
ApiParamBoolean(name="is_with_cleaning", required=False, default=False,
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="Дата окончания")
ApiParamFloat(name='approximate_price', value_max=9999999999.99, value_min=1.0,
description='Примерная цена'),
ApiParamStr(name='description', max_length=1000, description="Описание заказа",
required=False, default=""),
ApiParamStr(name='video_link', max_length=160, 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_write_permissions(access_token)
try:
order = await Order.objects.acreate(owner=access_token.user, **kwargs)
return api_make_response({"order_id": order.id})
except ValidationError as ve:
return _make_model_validation_errors(ve, API_ERROR_USER_MODIFY)
@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_write_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_write_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')
class ApiMedia:
@staticmethod
def __filename_to_ext(filename: str):
formats = {
".jpeg": "jpeg",
".jpg": "jpg",
".png": "png",
".pdf": "pdf"
}
for k in formats:
if filename.endswith(k):
return formats[k]
return None
@staticmethod
def __ext_to_content_type(ext: str):
formats = {
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"png": "image/png",
"pdf": "application/pdf"
}
if ext in formats:
return formats[ext]
return "application/binary"
@staticmethod
@api_method("media.upload",
doc="Загрузка медиа на сервер. Вызывать методом POST. Обязательно должен быть прикреплен файл"
" с именем поля 'file'",
params=[
ApiRequestParam(),
ApiParamAccessToken(),
], returns="<code>id</code> медиа, в противном случае ошибку")
async def upload(request, access_token):
if request.method != "POST":
return make_error_object(Exception(API_ERROR_INVALID_REQUEST, "method must be executed http POST method"))
if 'file' not in request.FILES:
return make_error_object(Exception(API_ERROR_INVALID_REQUEST,
"you must attach file with field-name 'file'"))
filename = request.FILES['file'].name
print(filename, type(filename))
# if not access_token.user.is_completed():
# return make_error_object(Exception(API_ERROR_NEED_COMPLETED_ACCOUNT))
ext = ApiMedia.__filename_to_ext(filename)
if ext is None:
return make_error_object(Exception(API_ERROR_INVALID_REQUEST, "unsupported file extension"))
try:
storage_name = Media.generate_storage_name(filename, datetime.now(), access_token.user)
await sync_to_async(s3_upload_from_buffer)(storage_name, request.FILES['file'].read())
m = await Media.objects.acreate(owner=access_token.user, original_name=filename,
extension=ext, storage_name=storage_name, size=request.FILES['file'].size)
return api_make_response({'media_id': m.id})
except Exception:
traceback.print_exc()
return make_error_object(Exception(API_ERROR_INTERNAL_ERROR, "try to upload file after 5 minutes"))
@staticmethod
@api_method("media.get",
doc="Получение медиа",
params=[
ApiRequestParam(),
ApiParamAccessToken(),
ApiParamInt(name="media_id", description="ID медиа",
value_min=0, value_max=1000000000),
], returns="<code>медиа</code>, в противном случае ошибку")
async def get(request, access_token, media_id):
if request.method != "GET":
return make_error_object(Exception(API_ERROR_INVALID_REQUEST, "method must be executed http GET method"))
m = await Media.objects.filter(Q(owner=access_token.user) |
Q(owner__accountavatar__photo=media_id) |
Q(owner__accountavatar__profile_background_id=media_id)).filter(pk=media_id).afirst()
if m is not None:
try:
# Media.get_by_id(access_token.user, m_id)
res = HttpResponse(content=s3_get(m.storage_name))
res.headers["Content-type"] = ApiMedia.__ext_to_content_type(m.extension)
return res
except Exception:
traceback.print_exc()
return make_error_object(Exception(API_ERROR_NOT_FOUND, "object in storage not found"))
@staticmethod
@api_method("media.list",
doc="Получение списка всех загруженных медиа для текущего пользователя",
params=[
ApiParamAccessToken(),
ApiParamInt(name="count", description="Количество объектов, максимум 100, по умолчанию 25",
value_min=1, value_max=100, default=25, required=False),
ApiParamInt(name="offset", description="Смещение списка относительно начала",
value_min=0, value_max=100000, default=0, required=False),
], returns="Список с объектами media.")
async def get_list(access_token, count, offset):
ms = [item async for item in
Media.objects.filter(owner=access_token.user, ).order_by('upload_datetime')[offset:offset + count]
]
res = [{
'id': m.id,
'name': m.original_name,
'upload_time': int(time.mktime(m.upload_datetime.timetuple())),
'extension': m.extension
} for m in ms]
return api_make_response(res)
class ApiPortfolio:
# portfel.get(user_id=None, portfel_id=None, count=20, offset=0, order_by={date,-date, .... })
# portfel.create(поля портфолио)
# portfel.delete(portfel_id)
@staticmethod
@api_method("portfolio.create",
doc="Создания объекта портфолио",
params=[
ApiParamAccessToken(),
ApiParamStr(name="title", min_length=5, max_length=200,
description="Название выполненного заказа, 5-200 символов"),
ApiParamInt(name="date", required=False,
description="Дата в формате UNIX time, потом будет нормальный объект даты,"
"если не передать то будет вставлена текущая дата"),
ApiParamFloat(name="price", description="Цена заказа, актуальная на момент выполнения")
], returns="<code>id</code> созданного объекта")
async def create(access_token, title, date, price):
# проверка на роль, нужна сразу
if access_token.user.role != Account.ROLE_EXECUTOR:
raise Exception(API_ERROR_NOT_ALLOWED, "you must have executor role")
try:
p = await Portfolio.objects.acreate(account=access_token.user, title=title, actual_price=price,
actual_date=(dt.fromtimestamp(date) if date is not None else None))
return api_make_response({"portfolio_id": p.id})
except Exception:
traceback.print_exc()
raise Exception(API_ERROR_INTERNAL_ERROR)
@staticmethod
@api_method("portfolio.get",
doc="Получение портфолио или ленты портфолио",
params=[
ApiParamAccessToken(),
ApiParamInt(name="owner_id", required=False, value_min=0, default=None,
description="ID пользователя, портфолио которого нужно вернуть."),
ApiParamInt(name="portfolio_id", required=False, value_min=0, default=None,
description="ID портфолио, которое нужно вернуть."),
ApiParamInt(name="count", required=False, value_min=1, value_max=100, default=20,
description="Количество объектов, по умолчанию 20"),
ApiParamInt(name="offset", required=False, value_min=0, default=0,
description="Количество объектов")
], returns="")
async def get(access_token, owner_id, portfolio_id, count, offset):
res = Portfolio.objects.order_by('actual_date')
res = res.select_related('account', 'account__accountavatar', 'account__accountavatar__photo',
'account__accountavatar__profile_background',
'account__executoraccount')
if owner_id is not None:
res = res.filter(account_id=owner_id)
if portfolio_id is not None:
res = res.filter(id=portfolio_id)
res = res[offset:offset + count]
# выполняем fetch
objects = [{
"owner": ApiAccount.make_user_json(item.account),
"id": item.id,
"title": item.title,
"actual_date": int(time.mktime(item.actual_date.timetuple())),
"actual_price": item.actual_price,
"photos": [random.randint(1, 10) for _ in range(random.randint(1, 5))]
} async for item in res]
return api_make_response(objects)
class DatabaseApi:
# TODO переместить сюда форму заказа, список городов
# def get_order_form
# def get_citys
pass
async def api_call_method(request, method_name, params: dict):
if method_name in api_methods_dict:
return await api_methods_dict[method_name]["func"](__raw_request=request, **params)
else:
return make_error_object(Exception(API_ERROR_METHOD_NOT_FOUND))
def api_get_documentation():
out = []
for m in api_methods_dict:
params = []
for p in api_methods_dict[m]["params"]:
j = p.to_json()
if j is not None:
params.append(j)
out.append({
"name": m,
"doc": api_methods_dict[m]["doc"],
"returns": api_methods_dict[m]["returns"],
"params": params
})
return out