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) @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) @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, related_name="photo", verbose_name="Аватар") profile_background = models.ForeignKey(Media, on_delete=models.SET_NULL, null=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] 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) actual_price = models.DecimalField(max_digits=12, decimal_places=2, blank=False, verbose_name="Цена") 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.SET_NULL, null=True, 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)