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
2023-03-06 20:27:57 +03:00

403 lines
16 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 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)