1548 lines
60 KiB
Python
1548 lines
60 KiB
Python
"""Establishment models."""
|
||
from datetime import datetime
|
||
from functools import reduce
|
||
from operator import or_
|
||
from typing import List
|
||
|
||
import elasticsearch_dsl
|
||
from django.conf import settings
|
||
from django.contrib.contenttypes import fields as generic
|
||
from django.contrib.gis.db.models.functions import Distance
|
||
from django.contrib.gis.geos import Point
|
||
from django.contrib.gis.measure import Distance as DistanceMeasure
|
||
from django.contrib.postgres.fields import ArrayField
|
||
from django.contrib.postgres.indexes import GinIndex
|
||
from django.contrib.postgres.search import TrigramSimilarity
|
||
from django.core.exceptions import ValidationError
|
||
from django.core.validators import FileExtensionValidator, MaxValueValidator, MinValueValidator
|
||
from django.db import models
|
||
from django.db.models import Case, ExpressionWrapper, F, Prefetch, Q, Subquery, When
|
||
from django.shortcuts import get_object_or_404
|
||
from django.utils import timezone
|
||
from django.utils.translation import gettext_lazy as _
|
||
from phonenumber_field.modelfields import PhoneNumberField
|
||
from timezone_field import TimeZoneField
|
||
|
||
from location.models import Address
|
||
from main.models import Award, Currency
|
||
from review.models import Review
|
||
from tag.models import Tag
|
||
from timetable.models import Timetable
|
||
from utils.methods import transform_into_readable_str
|
||
from utils.models import (
|
||
BaseAttributes, FavoritesMixin, FileMixin, GalleryMixin, HasTagsMixin, IntermediateGalleryModelMixin,
|
||
ProjectBaseMixin, TJSONField, TranslatedFieldsMixin, TypeDefaultImageMixin, URLImageMixin, default_menu_bool_array
|
||
)
|
||
|
||
|
||
# todo: establishment type&subtypes check
|
||
class EstablishmentType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||
"""Establishment type model."""
|
||
|
||
STR_FIELD_NAME = 'name'
|
||
|
||
# EXAMPLE OF INDEX NAME CHOICES
|
||
RESTAURANT = 'restaurant'
|
||
ARTISAN = 'artisan'
|
||
PRODUCER = 'producer'
|
||
|
||
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
|
||
help_text='{"en-GB":"some text"}')
|
||
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
||
verbose_name=_('Index name'))
|
||
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
|
||
tag_categories = models.ManyToManyField('tag.TagCategory',
|
||
blank=True,
|
||
related_name='establishment_types',
|
||
verbose_name=_('Tag categories'))
|
||
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||
related_name='establishment_types',
|
||
blank=True, null=True, default=None,
|
||
verbose_name='default image')
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
verbose_name = _('Establishment type')
|
||
verbose_name_plural = _('Establishment types')
|
||
|
||
@property
|
||
def label(self):
|
||
return transform_into_readable_str(self.index_name)
|
||
|
||
|
||
class EstablishmentSubTypeManager(models.Manager):
|
||
"""Extended manager for establishment subtype."""
|
||
|
||
def make(self, name, establishment_type):
|
||
obj = self.model(name=name, establishment_type=establishment_type)
|
||
obj.full_clean()
|
||
obj.save()
|
||
return obj
|
||
|
||
|
||
class EstablishmentSubType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin):
|
||
"""Establishment type model."""
|
||
|
||
# EXAMPLE OF INDEX NAME CHOICES
|
||
WINERY = 'winery'
|
||
|
||
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
|
||
help_text='{"en-GB":"some text"}')
|
||
index_name = models.CharField(max_length=50, unique=True, db_index=True,
|
||
verbose_name=_('Index name'))
|
||
establishment_type = models.ForeignKey(EstablishmentType,
|
||
on_delete=models.CASCADE,
|
||
verbose_name=_('Type'))
|
||
tag_categories = models.ManyToManyField('tag.TagCategory',
|
||
blank=True,
|
||
related_name='establishment_subtypes',
|
||
verbose_name=_('Tag categories'))
|
||
default_image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||
related_name='establishment_sub_types',
|
||
blank=True, null=True, default=None,
|
||
verbose_name='default image')
|
||
|
||
objects = EstablishmentSubTypeManager()
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
verbose_name = _('Establishment subtype')
|
||
verbose_name_plural = _('Establishment subtypes')
|
||
|
||
def __str__(self):
|
||
"""Overridden str dunder."""
|
||
return self.index_name
|
||
|
||
def clean_fields(self, exclude=None):
|
||
if not self.establishment_type.use_subtypes:
|
||
raise ValidationError(_('Establishment type is not use subtypes.'))
|
||
|
||
|
||
class EstablishmentQuerySet(models.QuerySet):
|
||
"""Extended queryset for Establishment model."""
|
||
|
||
def with_base_related(self):
|
||
"""Return qs with related objects."""
|
||
return self.select_related('address', 'establishment_type'). \
|
||
prefetch_related('tags', 'tags__translation').with_main_image()
|
||
|
||
def with_schedule(self):
|
||
"""Return qs with related schedule."""
|
||
return self.prefetch_related('schedule')
|
||
|
||
def with_currency_related(self):
|
||
"""Return qs with related """
|
||
return self.prefetch_related('currency')
|
||
|
||
def with_extended_address_related(self):
|
||
"""Return qs with deeply related address models."""
|
||
return self.select_related('address__city', 'address__city__region',
|
||
'address__city__region__country',
|
||
'address__city__country')
|
||
|
||
def with_extended_related(self):
|
||
return self.with_extended_address_related().select_related('establishment_type'). \
|
||
prefetch_related('establishment_subtypes', 'awards', 'schedule',
|
||
'phones', 'gallery', 'menu_set', 'menu_set__plates',
|
||
'menu_set__plates__currency', 'currency'). \
|
||
prefetch_actual_employees()
|
||
|
||
def with_type_related(self):
|
||
return self.prefetch_related('establishment_subtypes')
|
||
|
||
def with_es_related(self):
|
||
"""Return qs with related for ES indexing objects."""
|
||
return self.select_related('address', 'establishment_type', 'address__city',
|
||
'address__city__country'). \
|
||
prefetch_related('tags', 'schedule')
|
||
|
||
def search(self, value, locale=None):
|
||
"""Search text in JSON fields."""
|
||
if locale is not None:
|
||
filters = [
|
||
{f'name__icontains': value},
|
||
{f'description__{locale}__icontains': value}
|
||
]
|
||
return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters]))
|
||
else:
|
||
return self.none()
|
||
|
||
def es_search(self, value, locale=None):
|
||
"""Search text via ElasticSearch."""
|
||
from search_indexes.documents import EstablishmentDocument
|
||
search = EstablishmentDocument.search().filter(
|
||
elasticsearch_dsl.Q('match', name=value) |
|
||
elasticsearch_dsl.Q('match', **{f'description.{locale}': value})
|
||
).execute()
|
||
ids = [result.meta.id for result in search]
|
||
return self.filter(id__in=ids)
|
||
|
||
def by_country(self, country):
|
||
"""Return establishments by country code"""
|
||
return self.filter(address__city__country=country)
|
||
|
||
def by_country_code(self, code: str):
|
||
"""Return establishments by country code"""
|
||
return self.filter(address__city__country__code=code)
|
||
|
||
def published(self):
|
||
"""
|
||
Return QuerySet with published establishments.
|
||
"""
|
||
return self.filter(status=Establishment.PUBLISHED)
|
||
|
||
def has_published_reviews(self):
|
||
"""
|
||
Return QuerySet establishments with published reviews.
|
||
"""
|
||
return self.filter(reviews__status=Review.READY, )
|
||
|
||
def annotate_distance(self, point: Point = None):
|
||
"""
|
||
Return QuerySet with annotated field - distance
|
||
Description:
|
||
|
||
"""
|
||
return self.annotate(distance=Distance(
|
||
'address__coordinates',
|
||
point,
|
||
srid=settings.GEO_DEFAULT_SRID
|
||
))
|
||
|
||
def annotate_intermediate_public_mark(self):
|
||
"""
|
||
Return QuerySet with annotated field - intermediate_public_mark.
|
||
Description:
|
||
If establishments in collection POP and its mark is null, then
|
||
intermediate_mark is set to 10;
|
||
"""
|
||
from collection.models import Collection
|
||
|
||
return self.annotate(intermediate_public_mark=Case(
|
||
When(
|
||
collections__collection_type=Collection.POP,
|
||
public_mark__isnull=True,
|
||
then=settings.DEFAULT_ESTABLISHMENT_PUBLIC_MARK
|
||
),
|
||
default='public_mark',
|
||
output_field=models.FloatField()))
|
||
|
||
def annotate_mark_similarity(self, mark):
|
||
"""
|
||
Return a QuerySet with annotated field - mark_similarity
|
||
Description:
|
||
Similarity mark determined by comparison with compared establishment mark
|
||
"""
|
||
return self.annotate(mark_similarity=ExpressionWrapper(
|
||
mark - F('intermediate_public_mark'),
|
||
output_field=models.FloatField(default=0)
|
||
))
|
||
|
||
def has_location(self):
|
||
"""Return objects with geo location."""
|
||
return self.filter(address__coordinates__isnull=False)
|
||
|
||
def similar_base(self, establishment):
|
||
"""
|
||
Return filtered QuerySet by base filters.
|
||
Filters including:
|
||
1 Filter by type (and subtype) establishment.
|
||
2 With annotated distance.
|
||
3 By country
|
||
"""
|
||
filters = {
|
||
'establishment_type': establishment.establishment_type,
|
||
'address__city__country': establishment.address.city.country
|
||
}
|
||
qs = self.exclude(id=establishment.id).filter(**filters)
|
||
if establishment.establishment_subtypes.exists():
|
||
filters.update(
|
||
{'establishment_subtypes__in': establishment.establishment_subtypes.all()})
|
||
return qs
|
||
|
||
def similar_base_subquery(self, establishment, filters: dict) -> Subquery:
|
||
"""
|
||
Return filtered Subquery object by filters.
|
||
Filters including:
|
||
1 Filter by transmitted filters.
|
||
2 With ordering by distance.
|
||
"""
|
||
qs = self.similar_base(establishment) \
|
||
.filter(**filters)
|
||
if establishment.address and establishment.address.coordinates:
|
||
return Subquery(
|
||
qs.annotate_distance(point=establishment.location)
|
||
.order_by('distance')
|
||
.distinct()
|
||
.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS]
|
||
)
|
||
return Subquery(
|
||
qs.values_list('id', flat=True)[:settings.LIMITING_QUERY_OBJECTS]
|
||
)
|
||
|
||
def similar_restaurants(self, restaurant):
|
||
"""
|
||
Return QuerySet with objects that similar to Restaurant.
|
||
:param restaurant: Establishment instance.
|
||
"""
|
||
|
||
ids_by_subquery = self.similar_base_subquery(
|
||
establishment=restaurant,
|
||
filters={
|
||
'reviews__status': Review.READY,
|
||
'public_mark__gte': 10,
|
||
'establishment_gallery__is_main': True,
|
||
}
|
||
)
|
||
return self.filter(id__in=ids_by_subquery.queryset) \
|
||
.annotate_intermediate_public_mark() \
|
||
.annotate_mark_similarity(mark=restaurant.public_mark) \
|
||
.order_by('mark_similarity') \
|
||
.distinct('mark_similarity', 'id')
|
||
|
||
def annotate_same_subtype(self, establishment):
|
||
"""Annotate flag same subtype."""
|
||
return self.annotate(same_subtype=Case(
|
||
models.When(
|
||
establishment_subtypes__in=establishment.establishment_subtypes.all(),
|
||
then=True
|
||
),
|
||
default=False,
|
||
output_field=models.BooleanField(default=False)
|
||
))
|
||
|
||
def similar_artisans_producers(self, establishment):
|
||
"""
|
||
Return QuerySet with objects that similar to Artisan/Producer(s).
|
||
:param establishment: Establishment instance
|
||
"""
|
||
base_qs = self.similar_base(establishment).annotate_same_subtype(establishment)
|
||
similarity_rules = {
|
||
'ordering': [F('same_subtype').desc(), ],
|
||
'distinctions': ['same_subtype', ]
|
||
}
|
||
if establishment.address and establishment.address.coordinates:
|
||
base_qs = base_qs.annotate_distance(point=establishment.location)
|
||
similarity_rules['ordering'].append(F('distance').asc())
|
||
similarity_rules['distinctions'].append('distance')
|
||
|
||
return base_qs.has_published_reviews() \
|
||
.order_by(*similarity_rules['ordering']) \
|
||
.distinct(*similarity_rules['distinctions'], 'id')
|
||
|
||
def by_wine_region(self, wine_region):
|
||
"""
|
||
Return filtered QuerySet by wine region in wine origin.
|
||
:param wine_region: wine region.
|
||
"""
|
||
return self.filter(wine_origin__wine_region=wine_region).distinct()
|
||
|
||
def by_wine_sub_region(self, wine_sub_region):
|
||
"""
|
||
Return filtered QuerySet by wine region in wine origin.
|
||
:param wine_sub_region: wine sub region.
|
||
"""
|
||
return self.filter(wine_origin__wine_sub_region=wine_sub_region).distinct()
|
||
|
||
def similar_wineries(self, winery):
|
||
"""
|
||
Return QuerySet with objects that similar to Winery.
|
||
:param winery: Establishment instance
|
||
"""
|
||
base_qs = self.similar_base(winery)
|
||
similarity_rules = {
|
||
'ordering': [F('wine_origins__wine_region').asc(),
|
||
F('wine_origins__wine_sub_region').asc()],
|
||
'distinctions': ['wine_origins__wine_region',
|
||
'wine_origins__wine_sub_region']
|
||
}
|
||
if winery.address and winery.address.coordinates:
|
||
base_qs = base_qs.annotate_distance(point=winery.location)
|
||
similarity_rules['ordering'].append(F('distance').asc())
|
||
similarity_rules['distinctions'].append('distance')
|
||
|
||
return base_qs.order_by(*similarity_rules['ordering']) \
|
||
.distinct(*similarity_rules['distinctions'], 'id')
|
||
|
||
def similar_distilleries(self, distillery):
|
||
"""
|
||
Return QuerySet with objects that similar to Distillery.
|
||
:param distillery: Establishment instance
|
||
"""
|
||
base_qs = self.similar_base(distillery).annotate_same_subtype(distillery).filter(
|
||
same_subtype=True
|
||
)
|
||
similarity_rules = {
|
||
'ordering': [],
|
||
'distinctions': []
|
||
}
|
||
if distillery.address and distillery.address.coordinates:
|
||
base_qs = base_qs.annotate_distance(point=distillery.location)
|
||
similarity_rules['ordering'].append(F('distance').asc())
|
||
similarity_rules['distinctions'].append('distance')
|
||
|
||
return base_qs.published() \
|
||
.has_published_reviews() \
|
||
.order_by(*similarity_rules['ordering']) \
|
||
.distinct(*similarity_rules['distinctions'], 'id')
|
||
|
||
def similar_food_producers(self, food_producer):
|
||
"""
|
||
Return QuerySet with objects that similar to Food Producer.
|
||
:param food_producer: Establishment instance
|
||
"""
|
||
base_qs = self.similar_base(food_producer).annotate_same_subtype(food_producer).filter(
|
||
same_subtype=True
|
||
)
|
||
similarity_rules = {
|
||
'ordering': [],
|
||
'distinctions': []
|
||
}
|
||
if food_producer.address and food_producer.address.coordinates:
|
||
base_qs = base_qs.annotate_distance(point=food_producer.location)
|
||
similarity_rules['ordering'].append(F('distance').asc())
|
||
similarity_rules['distinctions'].append('distance')
|
||
|
||
return base_qs.order_by(*similarity_rules['ordering']) \
|
||
.distinct(*similarity_rules['distinctions'], 'id')
|
||
|
||
def last_reviewed(self, point: Point):
|
||
"""
|
||
Return QuerySet with last reviewed establishments.
|
||
:param point: location Point object, needs to ordering
|
||
"""
|
||
subquery_filter_by_distance = Subquery(
|
||
self.filter(image_url__isnull=False, public_mark__gte=10)
|
||
.has_published_reviews()
|
||
.annotate_distance(point=point)
|
||
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
|
||
.values('id')
|
||
)
|
||
return self.filter(id__in=subquery_filter_by_distance) \
|
||
.order_by('-reviews__published_at')
|
||
|
||
def prefetch_comments(self):
|
||
"""Prefetch last comment."""
|
||
from comment.models import Comment
|
||
return self.prefetch_related(
|
||
models.Prefetch('comments',
|
||
queryset=Comment.objects.exclude(is_publish=False).order_by('-created'),
|
||
to_attr='comments_prefetched')
|
||
)
|
||
|
||
def prefetch_actual_employees(self):
|
||
"""Prefetch actual employees."""
|
||
return self.prefetch_related(
|
||
models.Prefetch('establishmentemployee_set',
|
||
queryset=EstablishmentEmployee.objects.actual().select_related(
|
||
'position'),
|
||
to_attr='actual_establishment_employees'))
|
||
|
||
def annotate_in_favorites(self, user):
|
||
"""Annotate flag in_favorites"""
|
||
favorite_establishment_ids = []
|
||
if user.is_authenticated:
|
||
favorite_establishment_ids = user.favorite_establishment_ids
|
||
return self.annotate(in_favorites=Case(
|
||
When(
|
||
id__in=favorite_establishment_ids,
|
||
then=True),
|
||
default=False,
|
||
output_field=models.BooleanField(default=False)))
|
||
|
||
def by_distance_from_point(self, center, radius, unit='m'):
|
||
"""
|
||
Returns nearest establishments
|
||
|
||
:param center: point from which to find nearby establishments
|
||
:param radius: the maximum distance within the radius of which to look for establishments
|
||
:return: all establishments within the specified radius of specified point
|
||
:param unit: length unit e.g. m, km. Default is 'm'.
|
||
"""
|
||
kwargs = {unit: radius}
|
||
return self.filter(address__coordinates__distance_lte=(center, DistanceMeasure(**kwargs)))
|
||
|
||
def artisans(self):
|
||
"""Return artisans."""
|
||
return self.filter(establishment_type__index_name__icontains=EstablishmentType.ARTISAN)
|
||
|
||
def producers(self):
|
||
"""Return producers."""
|
||
return self.filter(establishment_type__index_name__icontains=EstablishmentType.PRODUCER)
|
||
|
||
def restaurants(self):
|
||
"""Return restaurants."""
|
||
return self.filter(establishment_type__index_name__icontains=EstablishmentType.RESTAURANT)
|
||
|
||
def wineries(self):
|
||
"""Return wineries."""
|
||
return self.producers().filter(
|
||
establishment_subtypes__index_name__icontains=EstablishmentSubType.WINERY)
|
||
|
||
def by_type(self, value):
|
||
"""Return QuerySet with type by value."""
|
||
return self.filter(establishment_type__index_name__icontains=value)
|
||
|
||
def by_subtype(self, value):
|
||
"""Return QuerySet with subtype by value."""
|
||
return self.filter(establishment_subtypes__index_name__icontains=value)
|
||
|
||
def by_public_mark_range(self, min_value, max_value):
|
||
"""Filter by public mark range."""
|
||
return self.filter(public_mark__gte=min_value, public_mark__lte=max_value)
|
||
|
||
def exclude_public_mark_ranges(self, ranges):
|
||
"""Exclude public mark ranges."""
|
||
return self.exclude(reduce(or_, [Q(public_mark__gte=r[0],
|
||
public_mark__lte=r[1])
|
||
for r in ranges]))
|
||
|
||
def exclude_countries(self, countries):
|
||
"""Exclude countries."""
|
||
return self.exclude(address__city__country__in=countries)
|
||
|
||
def with_certain_tag_category_related(self, index_name, attr_name):
|
||
"""Includes extra tags."""
|
||
return self.prefetch_related(
|
||
Prefetch('tags', queryset=Tag.objects.filter(category__index_name=index_name),
|
||
to_attr=attr_name)
|
||
)
|
||
|
||
def with_main_image(self):
|
||
return self.prefetch_related(
|
||
models.Prefetch('establishment_gallery',
|
||
queryset=EstablishmentGallery.objects.main_image(),
|
||
to_attr='main_image')
|
||
)
|
||
|
||
def available_establishments(self, user, country_code: str):
|
||
"""Return QuerySet with establishments that user has an access."""
|
||
from account.models import UserRole
|
||
|
||
if not user.is_superuser:
|
||
filters = {'address__city__country__code': country_code}
|
||
if user.is_establishment_administrator and not user.is_establishment_manager:
|
||
filters.update({
|
||
'id__in': models.Subquery(
|
||
UserRole.objects.filter(user=user, role__site__country__code=country_code)
|
||
.distinct('user', 'establishment')
|
||
.values_list('establishment', flat=True)
|
||
)
|
||
})
|
||
return self.filter(**filters)
|
||
return self
|
||
|
||
def with_contacts(self):
|
||
return self.prefetch_related('emails', 'phones')
|
||
|
||
|
||
class Establishment(GalleryMixin, ProjectBaseMixin, URLImageMixin,
|
||
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
|
||
"""Establishment model."""
|
||
|
||
ABANDONED = 0
|
||
CLOSED = 1
|
||
PUBLISHED = 2
|
||
UNPICKED = 3
|
||
WAITING = 4
|
||
|
||
STATUS_CHOICES = (
|
||
(ABANDONED, _('Abandoned')),
|
||
(CLOSED, _('Closed')),
|
||
(PUBLISHED, _('Published')),
|
||
(UNPICKED, _('Unpicked')),
|
||
(WAITING, _('Waiting')),
|
||
)
|
||
|
||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||
name = models.CharField(_('name'), max_length=255, default='')
|
||
transliterated_name = models.CharField(default='', max_length=255,
|
||
verbose_name=_('Transliterated name'))
|
||
index_name = models.CharField(_('Index name'), max_length=255, default='')
|
||
description = TJSONField(blank=True, null=True, default=None,
|
||
verbose_name=_('description'),
|
||
help_text='{"en-GB":"some text"}')
|
||
public_mark = models.PositiveIntegerField(blank=True, null=True,
|
||
default=None,
|
||
verbose_name=_('public mark'), )
|
||
# todo: set default 0
|
||
toque_number = models.PositiveIntegerField(blank=True, null=True,
|
||
default=None,
|
||
verbose_name=_('toque number'), )
|
||
establishment_type = models.ForeignKey(EstablishmentType,
|
||
related_name='establishment',
|
||
on_delete=models.PROTECT,
|
||
verbose_name=_('type'))
|
||
establishment_subtypes = models.ManyToManyField(EstablishmentSubType,
|
||
blank=True,
|
||
related_name='subtype_establishment',
|
||
verbose_name=_('subtype'))
|
||
address = models.ForeignKey(Address, null=True,
|
||
on_delete=models.PROTECT,
|
||
verbose_name=_('address'))
|
||
price_level = models.PositiveIntegerField(blank=True, null=True,
|
||
default=None,
|
||
verbose_name=_('price level'))
|
||
website = models.URLField(blank=True, null=True, default=None, max_length=255,
|
||
verbose_name=_('Web site URL'))
|
||
facebook = models.URLField(blank=True, null=True, default=None, max_length=255,
|
||
verbose_name=_('Facebook URL'))
|
||
twitter = models.URLField(blank=True, null=True, default=None, max_length=255,
|
||
verbose_name=_('Twitter URL'))
|
||
instagram = models.URLField(blank=True, null=True, default=None, max_length=255,
|
||
verbose_name=_('Instagram URL'))
|
||
lafourchette = models.URLField(blank=True, null=True, default=None, max_length=255,
|
||
verbose_name=_('Lafourchette URL'))
|
||
guestonline_id = models.PositiveIntegerField(blank=True, verbose_name=_('guestonline id'),
|
||
null=True, default=None, )
|
||
lastable_id = models.TextField(blank=True, verbose_name=_('lastable id'), unique=True,
|
||
null=True, default=None, )
|
||
booking = models.URLField(blank=True, null=True, default=None, max_length=255,
|
||
verbose_name=_('Booking URL'))
|
||
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status')) # deprecated
|
||
status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=WAITING,
|
||
verbose_name=_('Status'))
|
||
schedule = models.ManyToManyField(to='timetable.Timetable', blank=True,
|
||
verbose_name=_('Establishment schedule'),
|
||
related_name='schedule')
|
||
transportation = models.TextField(blank=True, null=True, default=None,
|
||
verbose_name=_('Transportation'))
|
||
collections = models.ManyToManyField(to='collection.Collection',
|
||
related_name='establishments',
|
||
blank=True, default=None,
|
||
verbose_name=_('Collections'))
|
||
gallery = models.ManyToManyField('gallery.Image', through='EstablishmentGallery')
|
||
preview_image_url = models.URLField(verbose_name=_('Preview image URL path'), max_length=255,
|
||
blank=True, null=True, default=None)
|
||
slug = models.SlugField(unique=True, max_length=255, null=True,
|
||
verbose_name=_('Establishment slug'))
|
||
tz = TimeZoneField(default=settings.TIME_ZONE)
|
||
|
||
awards = generic.GenericRelation(to='main.Award', related_query_name='establishment')
|
||
tags = models.ManyToManyField('tag.Tag', related_name='establishments',
|
||
verbose_name=_('Tag'))
|
||
reviews = generic.GenericRelation(to='review.Review')
|
||
comments = generic.GenericRelation(to='comment.Comment')
|
||
carousels = generic.GenericRelation(to='main.Carousel')
|
||
favorites = generic.GenericRelation(to='favorites.Favorites')
|
||
currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
|
||
on_delete=models.PROTECT,
|
||
verbose_name=_('currency'))
|
||
purchased_products = models.ManyToManyField('product.Product', blank=True,
|
||
through='product.PurchasedProduct',
|
||
related_name='establishments',
|
||
verbose_name=_('purchased plaques'),
|
||
help_text=_('Attribute from legacy db.\n'
|
||
'Must be deleted after the '
|
||
'implementation of the market.'))
|
||
|
||
objects = EstablishmentQuerySet.as_manager()
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
verbose_name = _('Establishment')
|
||
verbose_name_plural = _('Establishments')
|
||
|
||
def __str__(self):
|
||
return f'id:{self.id}-{self.name}'
|
||
|
||
def delete(self, using=None, keep_parents=False):
|
||
"""Overridden delete method"""
|
||
# TODO: If this does not contradict the plan,
|
||
# it is better to change it.
|
||
# Just add CASCADE to Company and Note in establishment fk field.
|
||
# Delete all related companies
|
||
self.companies.all().delete()
|
||
# Delete all related notes
|
||
self.notes.all().delete()
|
||
return super().delete(using, keep_parents)
|
||
|
||
@property
|
||
def visible_tags(self):
|
||
return super().visible_tags \
|
||
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
|
||
'business_tag', 'business_tags_de']) \
|
||
.exclude(value__in=['rss', 'rss_selection'])
|
||
# todo: recalculate toque_number
|
||
|
||
@property
|
||
def visible_tags_detail(self):
|
||
"""Removes some tags from detail Establishment representation"""
|
||
return self.visible_tags.exclude(category__index_name__in=['tag'])
|
||
|
||
def recalculate_public_mark(self):
|
||
fresh_review = self.reviews.published().order_by('-modified').first()
|
||
if fresh_review:
|
||
self.public_mark = fresh_review.mark
|
||
else:
|
||
self.public_mark = None
|
||
self.save()
|
||
|
||
def recalculate_toque_number(self):
|
||
toque_number = 0
|
||
if self.address and self.public_mark:
|
||
toque_number = RatingStrategy.objects. \
|
||
get_toque_number(country=self.address.city.country,
|
||
public_mark=self.public_mark)
|
||
self.toque_number = toque_number
|
||
self.save()
|
||
|
||
def recalculate_price_level(self, low_price=None, high_price=None):
|
||
if low_price is None or high_price is None:
|
||
low_price, high_price = self.get_price_level()
|
||
# todo: calculate price level
|
||
self.price_level = 3
|
||
|
||
def get_price_level(self):
|
||
country = self.address.city.country
|
||
return country.low_price, country.high_price
|
||
|
||
def set_establishment_type(self, establishment_type):
|
||
self.establishment_type = establishment_type
|
||
self.establishment_subtypes.exclude(
|
||
establishement_type=establishment_type).delete()
|
||
|
||
def add_establishment_subtype(self, establishment_subtype):
|
||
if establishment_subtype.establishment_type != self.establishment_type:
|
||
raise ValidationError('Establishment type of subtype does not match')
|
||
self.establishment_subtypes.add(establishment_subtype)
|
||
|
||
@property
|
||
def vintage_year(self):
|
||
last_review = self.reviews.by_status(Review.READY).last()
|
||
if last_review:
|
||
return last_review.vintage
|
||
|
||
@property
|
||
def best_price_menu(self):
|
||
return 150
|
||
|
||
@property
|
||
def best_price_carte(self):
|
||
return 200
|
||
|
||
@property
|
||
def range_price_menu(self):
|
||
plates = self.menu_set.filter(
|
||
models.Q(category={'en-GB': 'formulas'})
|
||
).aggregate(
|
||
max=models.Max('plates__price', output_field=models.FloatField()),
|
||
min=models.Min('plates__price', output_field=models.FloatField()))
|
||
return plates
|
||
|
||
@property
|
||
def range_price_carte(self):
|
||
plates = self.menu_set.filter(
|
||
models.Q(category={'en-GB': 'starter'}) |
|
||
models.Q(category={'en-GB': 'main_course'}) |
|
||
models.Q(category={'en-GB': 'dessert'})
|
||
).aggregate(
|
||
max=models.Max('plates__price', output_field=models.FloatField()),
|
||
min=models.Min('plates__price', output_field=models.FloatField()),
|
||
)
|
||
return plates
|
||
|
||
@property
|
||
def works_noon(self):
|
||
""" Used for indexing working by day """
|
||
return [ret.weekday for ret in self.schedule.all() if ret.works_at_noon]
|
||
|
||
@property
|
||
def works_at_weekday(self):
|
||
""" Used for indexing by working whole day criteria """
|
||
return [ret.weekday for ret in self.schedule.all()]
|
||
|
||
@property
|
||
def works_evening(self):
|
||
""" Used for indexing working by day """
|
||
return [ret.weekday for ret in self.schedule.all() if ret.works_at_afternoon]
|
||
|
||
@property
|
||
def works_now(self):
|
||
""" Is establishment working now """
|
||
now_at_est_tz = datetime.now(tz=self.tz)
|
||
current_week = now_at_est_tz.weekday()
|
||
schedule_for_today = self.schedule.filter(weekday=current_week).first()
|
||
if schedule_for_today is None or schedule_for_today.opening_time is None or schedule_for_today.ending_time is \
|
||
None:
|
||
return False
|
||
time_at_est_tz = now_at_est_tz.time()
|
||
return schedule_for_today.ending_time > time_at_est_tz > schedule_for_today.opening_time
|
||
|
||
@property
|
||
def timezone_as_str(self):
|
||
""" Returns tz in str format"""
|
||
return self.tz.localize(datetime.now()).strftime('%z')
|
||
|
||
@property
|
||
def tags_indexing(self):
|
||
return [{
|
||
'id': tag.metadata.id,
|
||
'label': tag.metadata.label
|
||
} for tag in self.tags.all()]
|
||
|
||
@property
|
||
def last_published_review(self):
|
||
"""Return last published review"""
|
||
return self.reviews.published() \
|
||
.order_by('-published_at').first()
|
||
|
||
@property
|
||
def location(self):
|
||
"""
|
||
Return Point object of establishment location
|
||
"""
|
||
return self.address.coordinates
|
||
|
||
@property
|
||
def the_most_recent_award(self):
|
||
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)) \
|
||
.latest(field_name='vintage_year')
|
||
|
||
@property
|
||
def country_id(self):
|
||
"""
|
||
Return Country id of establishment location
|
||
"""
|
||
return self.address.country_id if hasattr(self.address, 'country_id') else None
|
||
|
||
@property
|
||
def wines(self):
|
||
"""Return list products with type wine"""
|
||
return self.products.wines()
|
||
|
||
@property
|
||
def _main_image(self):
|
||
"""Please consider using prefetched query_set instead due to API performance issues"""
|
||
qs = self.establishment_gallery.main_image()
|
||
image_model = qs.first()
|
||
if image_model is not None:
|
||
return image_model.image
|
||
|
||
@property
|
||
def restaurant_category_indexing(self):
|
||
return self.tags.filter(category__index_name='category')
|
||
|
||
@property
|
||
def restaurant_cuisine_indexing(self):
|
||
return self.tags.filter(category__index_name='cuisine')
|
||
|
||
@property
|
||
def artisan_category_indexing(self):
|
||
return self.tags.filter(category__index_name='shop_category')
|
||
|
||
@property
|
||
def distillery_type_indexing(self):
|
||
return self.tags.filter(category__index_name='distillery_type')
|
||
|
||
@property
|
||
def last_comment(self):
|
||
if hasattr(self, 'comments_prefetched') and len(self.comments_prefetched):
|
||
return self.comments_prefetched[0]
|
||
|
||
@property
|
||
def wine_origins_unique(self):
|
||
return self.wine_origins.distinct('wine_region')
|
||
|
||
@property
|
||
def contact_phones(self):
|
||
if self.phones:
|
||
return [phone.phone.as_e164 for phone in self.phones.all()]
|
||
|
||
@property
|
||
def contact_emails(self):
|
||
if self.phones:
|
||
return [email.email for email in self.emails.all()]
|
||
|
||
@property
|
||
def establishment_subtype_labels(self):
|
||
if self.establishment_subtypes:
|
||
return [transform_into_readable_str(label)
|
||
for label in self.establishment_subtypes.all().values_list('index_name', flat=True)]
|
||
|
||
@property
|
||
def schedule_display(self):
|
||
if self.schedule:
|
||
timetable = {}
|
||
for weekday, closed_at, opening_at in self.schedule.all().values_list('weekday', 'closed_at', 'opening_at'):
|
||
weekday = dict(Timetable.WEEKDAYS_CHOICES).get(weekday)
|
||
working_hours = 'Closed'
|
||
if closed_at and opening_at:
|
||
working_hours = f'{str(opening_at)[:-3]} - {str(closed_at)[:-3]}'
|
||
timetable.update({weekday: working_hours})
|
||
return timetable
|
||
|
||
@property
|
||
def price_level_display(self):
|
||
if self.get_price_level() and self.currency:
|
||
min_value, max_value = self.get_price_level()
|
||
currency = self.currency.sign
|
||
return f'From {min_value}{currency} to {max_value}{currency}'
|
||
|
||
@property
|
||
def last_published_review_data(self):
|
||
if self.last_published_review:
|
||
return self.last_published_review.text
|
||
|
||
@property
|
||
def public_mark_display(self):
|
||
if self.public_mark and self.public_mark > 1:
|
||
return f'{self.public_mark}/20'
|
||
|
||
@property
|
||
def metadata(self):
|
||
if self.establishment_type:
|
||
metadata = []
|
||
tag_categories = (
|
||
self.establishment_type.tag_categories.exclude(index_name__in=[
|
||
'business_tag', 'purchased_item', 'accepted_payments_de',
|
||
'accepted_payments_hr', 'drinks', 'bottles_per_year',
|
||
'serial_number', 'surface', 'cooperative', 'tag',
|
||
'outside_sits', 'private_room']).values_list('index_name', flat=True)
|
||
)
|
||
for category in tag_categories:
|
||
tags = self.tags.filter(category__index_name=category).values_list('value', flat=True)
|
||
|
||
if tags.exists():
|
||
category_tags = {category: []}
|
||
for tag in tags:
|
||
category_tags[category].append(tag)
|
||
metadata.append(category_tags)
|
||
return metadata
|
||
|
||
|
||
class EstablishmentNoteQuerySet(models.QuerySet):
|
||
"""QuerySet for model EstablishmentNote."""
|
||
|
||
|
||
class EstablishmentNote(ProjectBaseMixin):
|
||
"""Note model for Establishment entity."""
|
||
old_id = models.PositiveIntegerField(null=True, blank=True)
|
||
text = models.TextField(verbose_name=_('text'))
|
||
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
|
||
related_name='notes',
|
||
verbose_name=_('establishment'))
|
||
user = models.ForeignKey('account.User', on_delete=models.PROTECT,
|
||
null=True,
|
||
related_name='establishment_notes',
|
||
verbose_name=_('author'))
|
||
|
||
objects = EstablishmentNoteQuerySet.as_manager()
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
verbose_name_plural = _('establishment notes')
|
||
verbose_name = _('establishment note')
|
||
|
||
|
||
class EstablishmentGallery(IntermediateGalleryModelMixin):
|
||
establishment = models.ForeignKey(Establishment, null=True,
|
||
related_name='establishment_gallery',
|
||
on_delete=models.CASCADE,
|
||
verbose_name=_('establishment'))
|
||
image = models.ForeignKey('gallery.Image', null=True,
|
||
related_name='establishment_gallery',
|
||
on_delete=models.CASCADE,
|
||
verbose_name=_('image'))
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
verbose_name = _('establishment gallery')
|
||
verbose_name_plural = _('establishment galleries')
|
||
unique_together = (('establishment', 'is_main'), ('establishment', 'image'))
|
||
|
||
|
||
class Position(BaseAttributes, TranslatedFieldsMixin):
|
||
"""Position model."""
|
||
|
||
STR_FIELD_NAME = 'name'
|
||
|
||
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
|
||
help_text='{"en":"some text"}')
|
||
|
||
priority = models.IntegerField(unique=True, null=True, default=None)
|
||
|
||
index_name = models.CharField(max_length=255, db_index=True, unique=True,
|
||
null=True, verbose_name=_('Index name'))
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
verbose_name = _('Position')
|
||
verbose_name_plural = _('Positions')
|
||
|
||
|
||
class EstablishmentEmployeeQuerySet(models.QuerySet):
|
||
"""Extended queryset for EstablishmentEmployee model."""
|
||
|
||
def actual(self):
|
||
"""Actual objects.."""
|
||
now = timezone.now()
|
||
return self.filter(models.Q(from_date__lte=now),
|
||
(models.Q(to_date__gte=now) |
|
||
models.Q(to_date__isnull=True)))
|
||
|
||
|
||
class EstablishmentEmployee(BaseAttributes):
|
||
"""EstablishmentEmployee model."""
|
||
|
||
IDLE = 'I'
|
||
ACCEPTED = 'A'
|
||
DECLINED = 'D'
|
||
|
||
STATUS_CHOICES = (
|
||
(IDLE, 'Idle'),
|
||
(ACCEPTED, 'Accepted'),
|
||
(DECLINED, 'Declined'),
|
||
)
|
||
|
||
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
|
||
verbose_name=_('Establishment'))
|
||
employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT,
|
||
verbose_name=_('Employee'))
|
||
from_date = models.DateTimeField(default=timezone.now, verbose_name=_('From date'),
|
||
null=True, blank=True)
|
||
to_date = models.DateTimeField(blank=True, null=True, default=None,
|
||
verbose_name=_('To date'))
|
||
position = models.ForeignKey(Position, on_delete=models.PROTECT,
|
||
verbose_name=_('Position'))
|
||
|
||
status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=IDLE)
|
||
|
||
# old_id = affiliations_id
|
||
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
|
||
|
||
objects = EstablishmentEmployeeQuerySet.as_manager()
|
||
|
||
|
||
class EmployeeQuerySet(models.QuerySet):
|
||
|
||
def _generic_search(self, value, filter_fields_names: List[str]):
|
||
"""Generic method for searching value in specified fields"""
|
||
filters = [
|
||
{f'{field}__icontains': value}
|
||
for field in filter_fields_names
|
||
]
|
||
return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters]))
|
||
|
||
def trigram_search(self, search_value: str):
|
||
"""Search with mistakes by name or last name."""
|
||
return self.annotate(
|
||
search_exact_match=models.Case(
|
||
models.When(Q(name__iexact=search_value) | Q(last_name__iexact=search_value),
|
||
then=100),
|
||
default=0,
|
||
output_field=models.FloatField()
|
||
),
|
||
search_contains_match=models.Case(
|
||
models.When(Q(name__icontains=search_value) | Q(last_name__icontains=search_value),
|
||
then=50),
|
||
default=0,
|
||
output_field=models.FloatField()
|
||
),
|
||
search_name_similarity=models.Case(
|
||
models.When(
|
||
Q(name__isnull=False),
|
||
then=TrigramSimilarity('name', search_value.lower())
|
||
),
|
||
default=0,
|
||
output_field=models.FloatField()
|
||
),
|
||
search_last_name_similarity=models.Case(
|
||
models.When(
|
||
Q(last_name__isnull=False),
|
||
then=TrigramSimilarity('last_name', search_value.lower())
|
||
),
|
||
default=0,
|
||
output_field=models.FloatField()
|
||
),
|
||
relevance=(F('search_name_similarity') + F('search_exact_match')
|
||
+ F('search_contains_match') + F('search_last_name_similarity'))
|
||
).filter(relevance__gte=0.1).order_by('-relevance')
|
||
|
||
def search_by_name_or_last_name(self, value):
|
||
"""Search by name or last_name."""
|
||
return self._generic_search(value, ['name', 'last_name'])
|
||
|
||
def search_by_actual_employee(self):
|
||
"""Search by actual employee."""
|
||
return self.filter(
|
||
Q(establishmentemployee__from_date__lte=datetime.now()),
|
||
Q(establishmentemployee__to_date__gte=datetime.now()) |
|
||
Q(establishmentemployee__to_date__isnull=True)
|
||
)
|
||
|
||
def search_by_position_id(self, value_list):
|
||
"""Search by position_id."""
|
||
return self.search_by_actual_employee().filter(
|
||
Q(establishmentemployee__position_id__in=value_list),
|
||
)
|
||
|
||
def search_by_public_mark(self, value_list):
|
||
"""Search by establishment public_mark."""
|
||
return self.search_by_actual_employee().filter(
|
||
Q(establishmentemployee__establishment__public_mark__in=value_list),
|
||
)
|
||
|
||
def search_by_toque_number(self, value_list):
|
||
"""Search by establishment toque_number."""
|
||
return self.search_by_actual_employee().filter(
|
||
Q(establishmentemployee__establishment__toque_number__in=value_list),
|
||
)
|
||
|
||
def search_by_username_or_name(self, value):
|
||
"""Search by username or name."""
|
||
return self.search_by_actual_employee().filter(
|
||
Q(user__username__icontains=value) |
|
||
Q(user__first_name__icontains=value) |
|
||
Q(user__last_name__icontains=value)
|
||
)
|
||
|
||
def actual_establishment(self):
|
||
e = EstablishmentEmployee.objects.actual().filter(employee=self)
|
||
return self.prefetch_related(models.Prefetch('establishmentemployee_set',
|
||
queryset=EstablishmentEmployee.objects.actual()
|
||
)).all().distinct()
|
||
|
||
def with_extended_related(self):
|
||
return self.prefetch_related('establishments')
|
||
|
||
def with_back_office_related(self):
|
||
return self.prefetch_related(
|
||
Prefetch('establishmentemployee_set',
|
||
queryset=EstablishmentEmployee.objects.actual()
|
||
.prefetch_related('establishment', 'position').order_by('-from_date'),
|
||
to_attr='prefetched_establishment_employee'),
|
||
'awards'
|
||
)
|
||
|
||
|
||
class Employee(BaseAttributes):
|
||
"""Employee model."""
|
||
|
||
user = models.OneToOneField('account.User', on_delete=models.PROTECT,
|
||
null=True, blank=True, default=None,
|
||
verbose_name=_('User'))
|
||
name = models.CharField(max_length=255, verbose_name=_('Name'))
|
||
last_name = models.CharField(max_length=255, verbose_name=_('Last Name'), null=True,
|
||
default=None)
|
||
|
||
# SEX CHOICES
|
||
MALE = 0
|
||
FEMALE = 1
|
||
|
||
SEX_CHOICES = (
|
||
(MALE, _('Male')),
|
||
(FEMALE, _('Female'))
|
||
)
|
||
sex = models.PositiveSmallIntegerField(choices=SEX_CHOICES, verbose_name=_('Sex'), null=True,
|
||
default=None)
|
||
birth_date = models.DateTimeField(editable=True, verbose_name=_('Birth date'), null=True,
|
||
default=None)
|
||
email = models.EmailField(blank=True, null=True, default=None, verbose_name=_('Email'))
|
||
phone = PhoneNumberField(null=True, default=None)
|
||
toque_number = models.PositiveSmallIntegerField(verbose_name=_('Toque number'), null=True,
|
||
default=None)
|
||
|
||
establishments = models.ManyToManyField(Establishment, related_name='employees',
|
||
through=EstablishmentEmployee, )
|
||
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
|
||
tags = models.ManyToManyField('tag.Tag', related_name='employees',
|
||
verbose_name=_('Tags'))
|
||
# old_id = profile_id
|
||
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
|
||
available_for_events = models.BooleanField(_('Available for events'), default=False)
|
||
photo = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
|
||
blank=True, null=True, default=None,
|
||
related_name='employee_photo',
|
||
verbose_name=_('image instance of model Image'))
|
||
|
||
objects = EmployeeQuerySet.as_manager()
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
verbose_name = _('Employee')
|
||
verbose_name_plural = _('Employees')
|
||
indexes = [
|
||
GinIndex(fields=('name',)),
|
||
GinIndex(fields=('last_name',))
|
||
]
|
||
|
||
@property
|
||
def image_object(self):
|
||
"""Return image object."""
|
||
return self.photo.image if self.photo else None
|
||
|
||
@property
|
||
def crop_image(self):
|
||
if hasattr(self, 'photo') and hasattr(self, '_meta'):
|
||
if self.photo:
|
||
image_property = {
|
||
'id': self.photo.id,
|
||
'title': self.photo.title,
|
||
'original_url': self.photo.image.url,
|
||
'orientation_display': self.photo.get_orientation_display(),
|
||
'auto_crop_images': {},
|
||
}
|
||
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
|
||
if p.startswith(self._meta.model_name.lower())]
|
||
for crop in crop_parameters:
|
||
image_property['auto_crop_images'].update(
|
||
{crop: self.photo.get_image_url(crop)}
|
||
)
|
||
return image_property
|
||
|
||
def remove_award(self, award_id: int):
|
||
from main.models import Award
|
||
award = get_object_or_404(Award, pk=award_id)
|
||
self.awards.remove(award)
|
||
|
||
|
||
class EstablishmentScheduleQuerySet(models.QuerySet):
|
||
"""QuerySet for model EstablishmentSchedule"""
|
||
|
||
|
||
class ContactPhone(models.Model):
|
||
"""Contact phone model."""
|
||
establishment = models.ForeignKey(
|
||
Establishment, related_name='phones', on_delete=models.CASCADE)
|
||
phone = PhoneNumberField()
|
||
|
||
class Meta:
|
||
verbose_name = _('contact phone')
|
||
verbose_name_plural = _('contact phones')
|
||
|
||
def __str__(self):
|
||
return f'{self.phone.as_e164}'
|
||
|
||
@property
|
||
def country_calling_code(self):
|
||
"""Return phone code from PhonеNumberField."""
|
||
return f'+{self.phone.country_code}' if self.phone and hasattr(self, 'phone') else None
|
||
|
||
@property
|
||
def national_calling_number(self):
|
||
"""Return phone national number from from PhonеNumberField."""
|
||
return self.phone.national_number if self.phone and hasattr(self, 'phone') else None
|
||
|
||
|
||
class ContactEmail(models.Model):
|
||
"""Contact email model."""
|
||
establishment = models.ForeignKey(
|
||
Establishment, related_name='emails', on_delete=models.CASCADE)
|
||
email = models.EmailField()
|
||
|
||
class Meta:
|
||
verbose_name = _('contact email')
|
||
verbose_name_plural = _('contact emails')
|
||
|
||
def __str__(self):
|
||
return f'{self.email}'
|
||
|
||
|
||
class Plate(TranslatedFieldsMixin, models.Model):
|
||
"""Plate model."""
|
||
STR_FIELD_NAME = 'name'
|
||
|
||
name = TJSONField(
|
||
blank=True, null=True, default=None, verbose_name=_('name'),
|
||
help_text='{"en-GB":"some text"}')
|
||
description = TJSONField(
|
||
blank=True, null=True, default=None, verbose_name=_('description'),
|
||
help_text='{"en-GB":"some text"}')
|
||
price = models.DecimalField(
|
||
_('price'), max_digits=14, decimal_places=2)
|
||
is_signature_plate = models.BooleanField(_('is signature plate'), default=False)
|
||
currency = models.ForeignKey(
|
||
'main.Currency', verbose_name=_('currency'), on_delete=models.CASCADE,
|
||
blank=True, null=True, default=None)
|
||
currency_code = models.CharField(
|
||
_('currency code'), max_length=250, blank=True, null=True, default=None)
|
||
|
||
menu = models.ForeignKey(
|
||
'establishment.Menu',
|
||
verbose_name=_('menu'),
|
||
related_name='plates',
|
||
on_delete=models.CASCADE,
|
||
)
|
||
|
||
@property
|
||
def establishment_id(self):
|
||
return self.menu.establishment.id
|
||
|
||
class Meta:
|
||
verbose_name = _('plate')
|
||
verbose_name_plural = _('plates')
|
||
|
||
|
||
class MenuQuerySet(models.QuerySet):
|
||
|
||
def with_base_related(self):
|
||
return self.prefetch_related('establishment', 'uploads')
|
||
|
||
def with_schedule_plates_establishment(self):
|
||
return self.select_related(
|
||
'establishment',
|
||
).prefetch_related(
|
||
'schedule',
|
||
'plates',
|
||
)
|
||
|
||
def with_gallery(self):
|
||
return self.prefetch_related(
|
||
'menu_gallery'
|
||
)
|
||
|
||
def dishes(self):
|
||
return self.filter(
|
||
Q(category__icontains='starter') |
|
||
Q(category__icontains='dessert') |
|
||
Q(category__icontains='main_course')
|
||
)
|
||
|
||
def search_by_category(self, value):
|
||
"""Search by category."""
|
||
return self.filter(category__icontains=value)
|
||
|
||
|
||
class Menu(GalleryMixin, TranslatedFieldsMixin, BaseAttributes):
|
||
"""Menu model."""
|
||
|
||
STR_FIELD_NAME = 'category'
|
||
|
||
category = TJSONField(
|
||
blank=True, null=True, default=None, verbose_name=_('category'),
|
||
help_text='{"en-GB":"some text"}')
|
||
establishment = models.ForeignKey(
|
||
'establishment.Establishment', verbose_name=_('establishment'),
|
||
on_delete=models.CASCADE)
|
||
is_drinks_included = models.BooleanField(_('is drinks included'), default=False)
|
||
schedule = models.ManyToManyField(
|
||
to='timetable.Timetable',
|
||
blank=True,
|
||
verbose_name=_('Establishment schedule'),
|
||
related_name='menus',
|
||
)
|
||
lunch = ArrayField(models.BooleanField(
|
||
default=False,
|
||
), size=7, default=default_menu_bool_array)
|
||
diner = ArrayField(models.BooleanField(
|
||
default=False,
|
||
), size=7, default=default_menu_bool_array)
|
||
last_update = models.DateField(auto_now=True, verbose_name=_('Date updated'))
|
||
price = models.DecimalField(_('price'), max_digits=14, decimal_places=2, default=0)
|
||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||
|
||
uploads = models.ManyToManyField(
|
||
to='MenuFiles',
|
||
blank=True,
|
||
verbose_name=_('Menu files'),
|
||
)
|
||
|
||
objects = MenuQuerySet.as_manager()
|
||
|
||
class Meta:
|
||
verbose_name = _('menu')
|
||
verbose_name_plural = _('menu')
|
||
ordering = ('-created',)
|
||
|
||
|
||
class MenuFiles(FileMixin, BaseAttributes):
|
||
"""Menu files"""
|
||
|
||
name = models.CharField(_('name'), max_length=255, default='')
|
||
type = models.CharField(_('type'), max_length=65, default='')
|
||
|
||
class Meta:
|
||
verbose_name = _('menu upload')
|
||
verbose_name_plural = _('menu uploads')
|
||
|
||
|
||
class MenuGallery(IntermediateGalleryModelMixin):
|
||
menu = models.ForeignKey(
|
||
Menu,
|
||
null=True,
|
||
related_name='menu_gallery',
|
||
on_delete=models.CASCADE,
|
||
verbose_name=_('menu'),
|
||
)
|
||
image = models.ForeignKey(
|
||
'gallery.Image',
|
||
null=True,
|
||
related_name='menu_gallery',
|
||
on_delete=models.CASCADE,
|
||
verbose_name=_('image'),
|
||
)
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
verbose_name = _('menu gallery')
|
||
verbose_name_plural = _('menu galleries')
|
||
unique_together = (('menu', 'is_main'), ('menu', 'image'))
|
||
|
||
|
||
class MenuUploads(BaseAttributes):
|
||
"""Menu files"""
|
||
|
||
menu = models.ForeignKey(
|
||
Menu,
|
||
verbose_name=_('menu'),
|
||
on_delete=models.CASCADE,
|
||
related_name='menu_uploads',
|
||
)
|
||
title = models.CharField(_('title'), max_length=255, default='')
|
||
file = models.FileField(
|
||
_('File'),
|
||
validators=[FileExtensionValidator(allowed_extensions=('doc', 'docx', 'pdf')), ],
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = _('menu upload')
|
||
verbose_name_plural = _('menu uploads')
|
||
|
||
|
||
class SocialChoice(models.Model):
|
||
title = models.CharField(_('title'), max_length=255, unique=True)
|
||
|
||
class Meta:
|
||
verbose_name = _('social choice')
|
||
verbose_name_plural = _('social choices')
|
||
|
||
def __str__(self):
|
||
return self.title
|
||
|
||
|
||
class SocialNetwork(models.Model):
|
||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||
establishment = models.ForeignKey(
|
||
'Establishment',
|
||
verbose_name=_('establishment'),
|
||
related_name='socials',
|
||
on_delete=models.CASCADE,
|
||
)
|
||
network = models.ForeignKey(
|
||
SocialChoice,
|
||
verbose_name=_('social network'),
|
||
related_name='social_links',
|
||
on_delete=models.CASCADE,
|
||
)
|
||
url = models.URLField(_('URL'), max_length=255)
|
||
|
||
class Meta:
|
||
verbose_name = _('social network')
|
||
verbose_name_plural = _('social networks')
|
||
|
||
def __str__(self):
|
||
return f'{self.network.title}: {self.url}'
|
||
|
||
|
||
class RatingStrategyManager(models.Manager):
|
||
"""Extended manager for RatingStrategy."""
|
||
|
||
def get_toque_number(self, country, public_mark):
|
||
"""Get toque number for country and public_mark."""
|
||
qs = self.model.objects.by_country(country)
|
||
if not qs.exists():
|
||
qs = self.model.objects.by_country(None)
|
||
obj = qs.for_public_mark(public_mark).first()
|
||
if obj:
|
||
return obj.toque_number
|
||
return 0
|
||
|
||
|
||
class RatingStrategyQuerySet(models.QuerySet):
|
||
"""Extended queryset for RatingStrategy."""
|
||
|
||
def by_country(self, country):
|
||
"""Filter by country."""
|
||
return self.filter(country=country)
|
||
|
||
def with_country(self, switcher=True):
|
||
"""With country."""
|
||
return self.exclude(country__isnull=switcher)
|
||
|
||
def for_public_mark(self, public_mark):
|
||
"""Filter for value."""
|
||
return self.filter(public_mark_min_value__lte=public_mark,
|
||
public_mark_max_value__gte=public_mark)
|
||
|
||
|
||
class RatingStrategy(ProjectBaseMixin):
|
||
"""Rating Strategy model."""
|
||
|
||
TOQUE_NUMBER_CHOICES = (
|
||
(1, _('One')),
|
||
(2, _('Two')),
|
||
(3, _('Three')),
|
||
(4, _('Four')),
|
||
(5, _('Five')),
|
||
)
|
||
|
||
country = models.ForeignKey('location.Country', null=True, blank=True,
|
||
default=None, on_delete=models.CASCADE,
|
||
verbose_name=_('Country'))
|
||
toque_number = models.IntegerField(choices=TOQUE_NUMBER_CHOICES)
|
||
public_mark_min_value = models.IntegerField()
|
||
public_mark_max_value = models.IntegerField()
|
||
|
||
objects = RatingStrategyManager.from_queryset(RatingStrategyQuerySet)()
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
|
||
verbose_name = _('Rating strategy')
|
||
verbose_name_plural = _('Rating strategy')
|
||
unique_together = ('country', 'toque_number')
|
||
|
||
def __str__(self):
|
||
return f'{self.country.code if self.country else "Other country"}. ' \
|
||
f'"{self.toque_number}": {self.public_mark_min_value}-' \
|
||
f'{self.public_mark_max_value}'
|
||
|
||
|
||
class CompanyQuerySet(models.QuerySet):
|
||
"""QuerySet for model Company."""
|
||
|
||
|
||
class Company(ProjectBaseMixin):
|
||
"""Establishment company model."""
|
||
|
||
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
|
||
related_name='companies',
|
||
verbose_name=_('establishment'))
|
||
name = models.CharField(max_length=255, verbose_name=_('name'))
|
||
phones = ArrayField(PhoneNumberField(max_length=128),
|
||
blank=True, null=True, default=None,
|
||
verbose_name=_('contact phones'))
|
||
faxes = ArrayField(PhoneNumberField(max_length=128),
|
||
blank=True, null=True, default=None,
|
||
verbose_name=_('fax numbers'))
|
||
legal_entity = models.CharField(max_length=255,
|
||
blank=True, null=True, default=None,
|
||
verbose_name=_('legal entity'))
|
||
registry_number = models.CharField(max_length=255,
|
||
blank=True, null=True, default=None,
|
||
verbose_name=_('registry number'))
|
||
vat_number = models.CharField(max_length=30,
|
||
blank=True, null=True, default=None,
|
||
verbose_name=_('VAT identification number'))
|
||
sic_code = models.IntegerField(validators=[MinValueValidator(1),
|
||
MaxValueValidator(9999)],
|
||
blank=True, null=True, default=True,
|
||
verbose_name=_('sic code'))
|
||
address = models.ForeignKey(Address, on_delete=models.PROTECT,
|
||
blank=True, null=True, default=None,
|
||
related_name='companies',
|
||
verbose_name=_('address'))
|
||
|
||
objects = CompanyQuerySet.as_manager()
|
||
|
||
class Meta:
|
||
"""Meta class."""
|
||
verbose_name = _('company')
|
||
verbose_name_plural = _('companies')
|