gault-millau/apps/establishment/models.py

512 lines
19 KiB
Python

"""Establishment models."""
from functools import reduce
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.measure import Distance as DistanceMeasure
from django.contrib.gis.geos import Point
from django.contrib.contenttypes import fields as generic
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from location.models import Address
from collection.models import Collection
from review.models import Review
from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField,
TranslatedFieldsMixin, BaseAttributes)
# todo: establishment type&subtypes check
class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin):
"""Establishment type model."""
STR_FIELD_NAME = 'name'
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB":"some text"}')
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
class Meta:
"""Meta class."""
verbose_name = _('Establishment type')
verbose_name_plural = _('Establishment types')
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(ProjectBaseMixin, TranslatedFieldsMixin):
"""Establishment type model."""
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB":"some text"}')
establishment_type = models.ForeignKey(EstablishmentType,
on_delete=models.CASCADE,
verbose_name=_('Type'))
objects = EstablishmentSubTypeManager()
class Meta:
"""Meta class."""
verbose_name = _('Establishment subtype')
verbose_name_plural = _('Establishment subtypes')
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 search(self, value, locale=None):
"""Search text in JSON fields."""
if locale is not None:
filters = [
{f'name__{locale}__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 by_country_code(self, code):
"""Return establishments by country code"""
return self.filter(address__city__country__code=code)
def published(self):
"""
Return QuerySet with published establishments.
"""
return self.filter(is_publish=True)
def annotate_distance(self, point: Point):
"""
Return QuerySet with annotated field - distance
Description:
"""
return self.annotate(distance=models.Value(
DistanceMeasure(Distance('address__coordinates', point, srid=4236)).m,
output_field=models.FloatField()))
def annotate_distance_mark(self):
"""
Return QuerySet with annotated field - distance_mark.
Required fields: distance.
Description:
If the radius of the establishments in QuerySet does not exceed 500 meters,
then distance_mark is set to 0.6, otherwise 0.
"""
return self.annotate(distance_mark=models.Case(
models.When(distance__lte=500,
then=0.6),
default=0,
output_field=models.FloatField()))
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;
"""
return self.annotate(intermediate_public_mark=models.Case(
models.When(
collections__collection__collection_type=Collection.POP,
public_mark__isnull=True,
then=10
),
default='public_mark',
output_field=models.FloatField()))
def annotate_additional_mark(self, public_mark: float):
"""
Return QuerySet with annotated field - additional_mark.
Required fields: intermediate_public_mark
Description:
IF
establishments public_mark + 3 > compared establishment public_mark
OR
establishments public_mark - 3 > compared establishment public_mark,
THEN
additional_mark is set to 0.4,
ELSE
set to 0.
"""
return self.annotate(additional_mark=models.Case(
models.When(
models.Q(intermediate_public_mark__lte=public_mark + 3) |
models.Q(intermediate_public_mark__lte=public_mark - 3),
then=0.4),
default=0,
output_field=models.FloatField()))
def annotate_total_mark(self):
"""
Return QuerySet with annotated field - total_mark.
Required fields: distance_mark, additional_mark.
Fields
Description:
Annotated field is obtained by formula:
(distance + additional marks) * intermediate_public_mark.
"""
return self.annotate(
total_mark=(models.F('distance_mark') + models.F('additional_mark')) *
models.F('intermediate_public_mark'))
def similar(self, establishment_pk: int):
"""
Return QuerySet with objects that similar to Establishment.
:param establishment_pk: integer
"""
establishment_qs = Establishment.objects.filter(pk=establishment_pk)
if establishment_qs.exists():
establishment = establishment_qs.first()
return self.exclude(pk=establishment_pk) \
.filter(is_publish=True,
image__isnull=False,
reviews__isnull=False,
reviews__status=Review.READY,
public_mark__gte=10) \
.annotate_distance(point=establishment.address.coordinates) \
.annotate_distance_mark() \
.annotate_intermediate_public_mark() \
.annotate_additional_mark(public_mark=establishment.public_mark) \
.annotate_total_mark()
else:
return self.none()
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_establishments = []
if user.is_authenticated:
favorite_establishments = user.favorites.by_content_type(app_label='establishment',
model='establishment')\
.values_list('object_id', flat=True)
return self.annotate(in_favorites=models.Case(
models.When(
id__in=favorite_establishments,
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'.
"""
from django.contrib.gis.measure import Distance
kwargs = {unit: radius}
return self.filter(address__coordinates__distance_lte=(center, Distance(**kwargs)))
class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin):
"""Establishment model."""
name = models.CharField(_('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'),)
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,
related_name='subtype_establishment',
verbose_name=_('subtype'))
address = models.ForeignKey(Address, blank=True, null=True, default=None,
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,
verbose_name=_('Web site URL'))
facebook = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Facebook URL'))
twitter = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Twitter URL'))
lafourchette = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Lafourchette URL'))
booking = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Booking URL'))
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
awards = generic.GenericRelation(to='main.Award')
tags = generic.GenericRelation(to='main.MetaDataContent')
reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment')
transportation = models.TextField(blank=True, null=True, default=None,
verbose_name=_('Transportation'))
collections = generic.GenericRelation(to='collection.CollectionItem')
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}'
# todo: recalculate toque_number
def recalculate_toque_number(self):
self.toque_number = 4
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
# todo: make via prefetch
@property
def subtypes(self):
return EstablishmentSubType.objects.filter(
subtype_establishment=self,
establishment_type=self.establishment_type,
establishment_type__use_subtypes=True)
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 best_price_menu(self):
return 150
@property
def best_price_carte(self):
return 200
@property
def tags_indexing(self):
return [{'id': tag.metadata.id,
'label': tag.metadata.label} for tag in self.tags.all()]
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"}')
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."""
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'))
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'))
objects = EstablishmentEmployeeQuerySet.as_manager()
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=_('Last name'))
establishments = models.ManyToManyField(Establishment, related_name='employees',
through=EstablishmentEmployee)
awards = generic.GenericRelation(to='main.Award')
tags = generic.GenericRelation(to='main.MetaDataContent')
class Meta:
"""Meta class."""
verbose_name = _('Employee')
verbose_name_plural = _('Employees')
class EstablishmentScheduleQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentSchedule"""
class EstablishmentSchedule(BaseAttributes):
"""Establishment schedule model."""
establishment = models.OneToOneField(Establishment,
related_name='schedule',
on_delete=models.CASCADE,
verbose_name=_('Establishment'))
schedule = models.ManyToManyField(to='timetable.Timetable',
verbose_name=_('Establishment schedule'))
objects = EstablishmentScheduleQuerySet.as_manager()
class Meta:
"""Meta class"""
verbose_name = _('Establishment schedule')
verbose_name_plural = _('Establishment schedules')
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}'
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 Wine(TranslatedFieldsMixin, models.Model):
# """Wine model."""
# establishment = models.ForeignKey(
# 'establishment.Establishment', verbose_name=_('establishment'),
# on_delete=models.CASCADE)
# bottles = models.IntegerField(_('bottles'))
# price_min = models.DecimalField(
# _('price min'), max_digits=14, decimal_places=2)
# price_max = models.DecimalField(
# _('price max'), max_digits=14, decimal_places=2)
# by_glass = models.BooleanField(_('by glass'))
# price_glass_min = models.DecimalField(
# _('price min'), max_digits=14, decimal_places=2)
# price_glass_max = models.DecimalField(
# _('price max'), max_digits=14, decimal_places=2)
#
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'))
currency = models.ForeignKey(
'main.Currency', verbose_name=_('currency'), on_delete=models.CASCADE)
menu = models.ForeignKey(
'establishment.Menu', verbose_name=_('menu'), on_delete=models.CASCADE)
class Meta:
verbose_name = _('plate')
verbose_name_plural = _('plates')
class Menu(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)
class Meta:
verbose_name = _('menu')
verbose_name_plural = _('menu')
class SocialNetwork(models.Model):
establishment = models.ForeignKey(
'Establishment', verbose_name=_('establishment'),
related_name='socials', on_delete=models.CASCADE)
title = models.CharField(_('title'), max_length=255)
url = models.URLField(_('URL'))
class Meta:
verbose_name = _('social network')
verbose_name_plural = _('social networks')
def __str__(self):
return self.title