diff --git a/api/admin.py b/api/admin.py index a406125..3f3e6a7 100755 --- a/api/admin.py +++ b/api/admin.py @@ -52,6 +52,12 @@ class PortfolioAdmin(admin.ModelAdmin): readonly_fields = ['actual_date', 'actual_price'] +@admin.register(PortfolioPhoto) +class PortfolioAdmin(admin.ModelAdmin): + # list_display = ['id', 'account', 'title', 'actual_date', 'actual_price'] + # readonly_fields = ['actual_date', 'actual_price'] + pass + # # # @admin.register(OrderImage) diff --git a/api/api_methods.py b/api/api_methods.py index 347dd2b..3d15928 100755 --- a/api/api_methods.py +++ b/api/api_methods.py @@ -1,9 +1,6 @@ import random import time from datetime import date as dt -import traceback - -from django.http import HttpResponseNotFound from .api_media_utils import * from .api_utils import * @@ -194,9 +191,11 @@ class ApiAccount: ApiParamEnum(name="city", description="Город, в котором находится ползователь: {choices}", required=False, choices=CITIES_CHOICES), ApiParamInt(name="photo", required=False, default=None, - description="ID медиа, которое будет использоваться в качестве фото профиля"), + description="ID медиа, которое будет использоваться в качестве фото профиля, " + "-1 для сброса"), ApiParamInt(name="profile_background", required=False, default=None, - description="ID медиа, которое будет использоваться в качестве банера профиля") + description="ID медиа, которое будет использоваться в качестве банера профиля, " + "-1 для сброса") ], returns="Вернет основную информацию о пользователе, иначе ошибки") async def edit(access_token, name, surname, about, executor_type, executor_inn, city, photo, profile_background): @@ -220,9 +219,12 @@ class ApiAccount: 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 photo != -1: + 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') + else: + p = None if p.owner.id == user.id and p.extension in Media.PHOTO_EXTENSIONS: if not hasattr(user, 'accountavatar'): @@ -233,9 +235,12 @@ class ApiAccount: 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 profile_background != -1: + 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') + else: + p = None if p.owner.id == user.id and p.extension in Media.PHOTO_EXTENSIONS: if not hasattr(user, 'accountavatar'): @@ -528,9 +533,8 @@ class ApiOrder: 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="Телефон") + ApiParamPhone(max_length=60, required=False, default=None), + ApiParamEmail(required=False, default=None) ], returns="ID созданного заказа, иначе одну из ошибок") async def create(**kwargs): @@ -541,7 +545,7 @@ class ApiOrder: 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) + return _make_model_validation_errors(ve, API_ERROR_OBJECT_VALIDATION) @staticmethod @api_method("order.setPublished", @@ -689,15 +693,15 @@ class ApiMedia: @api_method("media.get", doc="Получение медиа", params=[ - ApiRequestParam(), ApiParamAccessToken(), ApiParamInt(name="media_id", description="ID медиа", value_min=0, value_max=1000000000), ], returns="медиа, в противном случае ошибку") - async def get(request, access_token, media_id): + async def get(access_token, media_id): 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() + Q(accountavatar__photo=media_id) | + Q(accountavatar__profile_background=media_id) | + Q(portfoliophoto=media_id)).filter(pk=media_id).afirst() if m is not None: try: @@ -753,8 +757,9 @@ class ApiPortfolio: ApiParamFloat(name="price", description="Цена заказа, актуальная на момент выполнения"), ApiParamFloat(name='square', value_max=99999.99, value_min=1.0, description='Площадь в м²'), + ApiParamTags(name='tags', tags=Portfolio.TAGS_NAMES, required=False, default=[]) ], returns="id созданного объекта") - async def create(access_token, title, date, price, square): + async def create(access_token, title, date, price, square, tags): # проверка на роль, нужна сразу if access_token.user.role != Account.ROLE_EXECUTOR: raise Exception(API_ERROR_NOT_ALLOWED, "you must have executor role") @@ -762,7 +767,7 @@ class ApiPortfolio: try: p = await Portfolio.objects.acreate(account=access_token.user, actual_price=price, square=square, actual_date=(dt.fromtimestamp(date) if date is not None else None), - title=title,) + title=title, attributes={"tags": tags}) return api_make_response({"portfolio_id": p.id}) except Exception: traceback.print_exc() @@ -780,9 +785,10 @@ class ApiPortfolio: 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="Количество объектов") + description="Количество объектов"), + ApiParamTags(name='tags', tags=Portfolio.TAGS_NAMES, required=False, default=None) ], returns="") - async def get(access_token, owner_id, portfolio_id, count, offset): + async def get(access_token, owner_id, portfolio_id, tags, count, offset): res = Portfolio.objects.order_by('actual_date') res = res.select_related('account', 'account__accountavatar', 'account__accountavatar__photo', 'account__accountavatar__profile_background', @@ -794,6 +800,9 @@ class ApiPortfolio: if portfolio_id is not None: res = res.filter(id=portfolio_id) + if tags is not None: + res = res.filter(attributes__tags__contains=tags) + res = res[offset:offset + count] # выполняем fetch @@ -805,7 +814,8 @@ class ApiPortfolio: "publish_date": int(time.mktime(item.publish_date.timetuple())), "actual_price": float(item.actual_price), "square": float(item.square), - "photos": [random.randint(4, 28) for _ in range(random.randint(1, 5))] + "photos": [random.randint(4, 28) for _ in range(random.randint(1, 5))], + "attributes": item.attributes } async for item in res] return api_make_response(objects) diff --git a/api/api_params.py b/api/api_params.py index 3b0f2d2..65bf855 100755 --- a/api/api_params.py +++ b/api/api_params.py @@ -288,3 +288,45 @@ class ApiParamVerifyCode(ApiParamInt): description="Код верификации (требуется если клиенту будет отправлена " "одна из ошибок верификации)", **kwargs): super().__init__(name=name, required=False, value_min=0, value_max=9999, description=description, **kwargs) + + +class ApiParamEmail(ApiParamStr): + def __init__(self, name="email", + description="Почта", **kwargs): + super().__init__(name=name, description=description, + regex="^[\\w\\-.]+@([\\w\\-]+\\.)+[\\w\\-]{2,4}$", **kwargs) + + +class ApiParamTags(ApiParamStr): + def __init__(self, tags: list[list[str, str] | tuple[str, str]], name="tags", default=None, + description="Один или несколько тегов из списка: {tags}" + " Теги перечисляются через запятую, без кавычек", **kwargs): + super().__init__(name=name, description=description, + regex="^[\\w\\_]+(,[\\w\\_]+)*$", default=None, **kwargs) + self.tags = tags + self.__tags_names = [i[0] for i in tags] + self.__default_tags = default + + def validate(self, value): + items = super(ApiParamTags, self).validate(value) + if items is not None: + items = value.split(',') + # проверка того, что параметры входят в список + for i in items: + if i not in self.__tags_names: + _make_invalid_argument_value_error(self.name, value, "unexpected", + f"expected items in {self.__tags_names}") + else: + items = self.__default_tags + + return items + + def get_doc(self): + doc = super().get_doc() + ch = "" + return doc.replace('{tags}', ch) + diff --git a/api/models.py b/api/models.py index 63b2a36..7dcd7f2 100755 --- a/api/models.py +++ b/api/models.py @@ -407,6 +407,10 @@ class Order(models.Model): return q[0] +def _portfolio_default_attrs(): + return {"tags": []} + + class Portfolio(models.Model): account = models.ForeignKey(Account, on_delete=models.CASCADE, verbose_name="Аккаунт") @@ -416,13 +420,26 @@ class Portfolio(models.Model): 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.SET_NULL, null=True, verbose_name="Аватар") + photo = models.ForeignKey(Media, on_delete=models.CASCADE, verbose_name="Фотография") is_preview = models.BooleanField(verbose_name="Это главная фотография") class Meta: diff --git a/requirements.txt b/requirements.txt index 7c5452e..2c6a684 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -psycopg2 +psycopg2-binary==2.9.6 django==4.1.7 requests==2.28.2 python-dotenv -boto3 +boto3==1.26.120