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/models.py

505 lines
21 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.

from datetime import datetime
from asgiref.sync import sync_to_async
from django.core.validators import *
from django.db import models
from hashlib import sha512, sha256
from django.db.models import Q
from arka.settings import CITIES_CHOICES, CITIES_FIELD_SIZE
from .api_errors import *
import re
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.CharField(max_length=CITIES_FIELD_SIZE, choices=CITIES_CHOICES, default=None, null=True, blank=True)
register_datetime = models.DateTimeField(default=datetime.now, editable=False)
verified = models.BooleanField(default=False, verbose_name="Подтвержденный аккаунт")
@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', 'accountavatar')
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 is not None
class Media(models.Model):
owner = models.ForeignKey(Account, on_delete=models.SET_NULL, null=True,
related_name="media_owner", verbose_name="Владелец")
storage_name = models.CharField(max_length=80, verbose_name="Файл в хранилище")
original_name = models.CharField(max_length=100, verbose_name="Имя файла", default="")
extension = models.CharField(max_length=16)
size = models.IntegerField(verbose_name='Размер в байтах')
upload_datetime = models.DateTimeField(default=datetime.now, editable=False)
PHOTO_EXTENSIONS = ['jpg', 'jpeg', 'png']
@staticmethod
async def get_by_id(user_id: int, media_id: int):
m = Media.objects.filter(user_id=user_id, id=media_id).select_related('executoraccount', 'city')
return await m.afirst()
@staticmethod
async def get_media():
pass
@staticmethod
def generate_storage_name(original_name, upload_datetime, user_id):
source_str = f"{original_name} {upload_datetime} {user_id}"
return sha256(bytearray(source_str, 'utf-8')).hexdigest()
def __str__(self):
return f"{self.owner}: \"{self.original_name}\" ({self.id})"
class AccountAvatar(models.Model):
account = models.OneToOneField(Account, on_delete=models.CASCADE, verbose_name="Аккаунт")
photo = models.ForeignKey(Media, on_delete=models.SET_NULL, null=True, blank=True, default=None,
related_name="photo", verbose_name="Аватар")
profile_background = models.ForeignKey(Media, on_delete=models.SET_NULL, null=True, blank=True, default=None,
related_name="profile_background", verbose_name="Оформление профиля")
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)).owner
@staticmethod
async def get_by_token(token: str):
related = [
'user',
'user__accountavatar',
'user__accountavatar__profile_background',
'user__accountavatar__photo',
'user__executoraccount'
]
t = await AccessToken.objects.filter(access_token=token).select_related(*related).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):
owner = models.ForeignKey(Account, on_delete=models.CASCADE, related_name="owner", verbose_name="Владелец")
# основные поля: название и описание
name = models.CharField(max_length=200, verbose_name="Название объявления")
address_city = models.CharField(max_length=CITIES_FIELD_SIZE, choices=CITIES_CHOICES,
blank=False, verbose_name="Город")
address_text = models.CharField(max_length=70, blank=True, verbose_name="Улица, дом")
CHOICE_UNDEFINED = ''
# Раздел "Параметры объекта"
# тип квартиры
TYPE_OF_APARTMENT_PRIMARY = 'primary'
TYPE_OF_APARTMENT_SECONDARY = 'secondary'
TYPE_OF_APARTMENT_CHOICES = [
(TYPE_OF_APARTMENT_PRIMARY, 'Первичка'),
(TYPE_OF_APARTMENT_SECONDARY, 'Вторичка')
]
type_of_apartment = models.CharField(max_length=10, choices=TYPE_OF_APARTMENT_CHOICES, blank=True,
default=CHOICE_UNDEFINED, verbose_name="Вид объекта")
# тип дома
TYPE_OF_HOUSE_BLOCK = 'block'
TYPE_OF_HOUSE_BRICK = 'brick'
TYPE_OF_HOUSE_MONOLITH = 'monolith'
TYPE_OF_HOUSE_PANEL = 'panel'
# TODO добавить чойсов, их на самом деле больше
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_APARTMENT = "apartment"
TYPE_OF_ROOM_PRIVATE_HOUSE = "private_house"
TYPE_OF_ROOM_PENTHOUSE = "penthouse"
TYPE_OF_ROOM_COUNTRY_HOUSE = "country_house"
TYPE_OF_ROOM_APARTMENTS = "apartments"
TYPE_OF_ROOM_KITCHEN = "kitchen"
TYPE_OF_ROOM_BATHROOM = "bathroom"
TYPE_OF_ROOM_NURSERY = "nursery"
TYPE_OF_ROOM_CHOICES = [
(TYPE_OF_ROOM_APARTMENT, "квартира"),
(TYPE_OF_ROOM_PRIVATE_HOUSE, "частный дом"),
(TYPE_OF_ROOM_PENTHOUSE, "пентхаус"),
(TYPE_OF_ROOM_COUNTRY_HOUSE, "дачный дом"),
(TYPE_OF_ROOM_APARTMENTS, "апартаменты"),
(TYPE_OF_ROOM_KITCHEN, "кухня"),
(TYPE_OF_ROOM_BATHROOM, "ванная комната"),
(TYPE_OF_ROOM_NURSERY, "детская")
]
type_of_room = models.CharField(max_length=16, choices=TYPE_OF_ROOM_CHOICES, blank=True,
default=CHOICE_UNDEFINED, verbose_name="Тип помещения")
number_of_rooms = models.SmallIntegerField(verbose_name='Количество комнат, -1 = студия', validators=[
MinValueValidator(-1),
MaxValueValidator(100)
])
is_balcony = models.BooleanField(default=False, verbose_name="Балкон")
is_loggia = models.BooleanField(default=False, verbose_name="Лоджия")
state_of_room = models.CharField(max_length=40, blank=True, default=CHOICE_UNDEFINED,
verbose_name="Состояние помещения")
# площадь в квадратных метрах
square = models.DecimalField(max_digits=7, decimal_places=2, blank=False, verbose_name="Площадь в м²")
# высота потолков
ceiling_height = models.DecimalField(max_digits=4, decimal_places=2, blank=False,
verbose_name="Высота потолков в м")
# Раздел "Ремонт"
# тип ремонта
TYPE_OF_RENOVATION_OVERHAUL = 'overhaul'
TYPE_OF_RENOVATION_REDECOR = 'redecor'
TYPE_OF_RENOVATION_PARTIAL = 'partial'
TYPE_OF_RENOVATION_CHOICES = [
(CHOICE_UNDEFINED, 'Не определено'),
(TYPE_OF_RENOVATION_OVERHAUL, 'Капитальный'),
(TYPE_OF_RENOVATION_REDECOR, 'Косметический'),
(TYPE_OF_RENOVATION_PARTIAL, 'Частичный'),
]
type_of_renovation = models.CharField(max_length=10, choices=TYPE_OF_RENOVATION_CHOICES, default=CHOICE_UNDEFINED,
blank=True, verbose_name="Тип ремонта")
is_redevelopment = models.BooleanField(default=False, verbose_name="Перепланировка")
is_leveling_floors = models.BooleanField(default=False, verbose_name="Выравнивать полы")
is_heated_floor = models.BooleanField(default=False, verbose_name="Теплый пол")
is_leveling_walls = models.BooleanField(default=False, verbose_name="Выравнивать стены")
type_of_ceiling = models.CharField(max_length=40, blank=True, default=CHOICE_UNDEFINED,
verbose_name="Тип потолка")
is_wiring_replace = models.BooleanField(default=False, verbose_name="Требуется замена проводки")
is_require_design = models.BooleanField(default=False, 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="Закуп материала")
# дальше отдельные параметры
is_with_contract = models.BooleanField(default=False, verbose_name="Работа по договору")
is_with_warranty = models.BooleanField(default=True, verbose_name="С гарантией")
is_with_trade = models.BooleanField(default=False, verbose_name="Возможен торг")
is_with_garbage_removal = models.BooleanField(default=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="Дата окончания")
# примерная цена
approximate_price = models.DecimalField(max_digits=12, decimal_places=2, blank=False, verbose_name="Цена")
description = models.TextField(blank=True, verbose_name="Описание")
video_link = models.CharField(max_length=160, blank=True, default=CHOICE_UNDEFINED,
verbose_name="Ссылка на видео")
email = models.EmailField(blank=True, verbose_name="Email")
phone = models.CharField(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 _portfolio_default_attrs():
return {"tags": []}
class Portfolio(models.Model):
account = models.ForeignKey(Account, on_delete=models.CASCADE, verbose_name="Аккаунт")
title = models.CharField(max_length=200, verbose_name="Название")
actual_date = models.DateField(verbose_name="Дата выполнения", default=datetime.now)
publish_date = models.DateField(verbose_name="Дата публикации", default=datetime.now, editable=False)
actual_price = models.DecimalField(max_digits=12, decimal_places=2, blank=False, verbose_name="Цена")
square = models.DecimalField(max_digits=7, decimal_places=2, blank=False, verbose_name="Площадь в м²")
TAGS_NAMES = [
("housings", "Квартиры"),
("private_houses", "Частные дома"),
("country_houses", "Дачные дома"),
("penthouses", "Пентхаусы"),
("apartments", "Апартаменты"),
("rooms", "Комнаты"),
("kitchens", "Кухни"),
("bathrooms", "Ванные комнаты"),
("child_rooms", "Детские комнаты")
]
attributes = models.JSONField(verbose_name="Атрибуты", default=_portfolio_default_attrs)
def __str__(self):
return f"{self.id}: \"{self.title}\""
class PortfolioPhoto(models.Model):
portfolio = models.ForeignKey(Portfolio, on_delete=models.CASCADE, verbose_name="Портфолио")
photo = models.ForeignKey(Media, on_delete=models.CASCADE, verbose_name="Фотография")
is_preview = models.BooleanField(verbose_name="Это главная фотография")
class Meta:
constraints = [
models.UniqueConstraint(
fields=['portfolio', 'photo'], name='unique_portfoliophoto_photo'
)
]
# TODO добавить проверку того, чтобы нельзя было приложить медиа другого юзера
# 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)