gault-millau/apps/establishment/models.py
Kuroshini 92367b35d7 fix
2020-02-10 12:27:05 +03:00

1639 lines
64 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 MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Case, ExpressionWrapper, F, Prefetch, Q, Subquery, When
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from phonenumber_field.phonenumber import PhoneNumber
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, PhoneModelMixin,
AwardsModelMixin, CarouselMixin, UpdateByMixin)
# 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', 'establishment_subtypes').with_main_image()
def with_schedule(self):
"""Return qs with related schedule."""
return self.prefetch_related('schedule')
def with_reviews(self):
"""Return qs with related reviews."""
return self.prefetch_related('reviews')
def with_reviews_sorted(self):
return self.prefetch_related(
Prefetch(
'reviews',
queryset=Review.objects.published().order_by('-published_at'),
)
)
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 and not user.is_anonymous:
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')
def prefetch_plates(self):
"""Prefetches plates for card&wine backoffice section"""
return self.prefetch_related('menu_set', 'menu_set__plates', 'back_office_wine')
class Establishment(GalleryMixin,
ProjectBaseMixin,
URLImageMixin,
TranslatedFieldsMixin,
HasTagsMixin,
FavoritesMixin,
AwardsModelMixin,
CarouselMixin,
UpdateByMixin):
"""Establishment model."""
ABANDONED = 0
CLOSED = 1
PUBLISHED = 2
UNPICKED = 3
WAITING = 4
HIDDEN = 5
DELETED = 6
OUT_OF_SELECTION = 7
UNPUBLISHED = 8
STATUS_CHOICES = (
(ABANDONED, _('Abandoned')),
(CLOSED, _('Closed')),
(PUBLISHED, _('Published')),
# (UNPICKED, _('Unpicked')),
(WAITING, _('Waiting')),
# (HIDDEN, _('Hidden')),
# (DELETED, _('Deleted')),
(OUT_OF_SELECTION, _('Out of selection')),
# (UNPUBLISHED, _('Unpublished')),
)
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=None, max_length=255, null=True, blank=True,
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}'
@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', 'shop_category'])
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 last_review(self):
return self.reviews.by_status(Review.READY).last()
@property
def vintage_year(self):
last_review = self.last_review
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 food_producer_indexing(self):
return self.tags.filter(category__index_name='producer_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.CASCADE,
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', 'image')
class PositionQuerySet(models.QuerySet):
def by_establishment_type(self, value: str):
return self.filter(Q(establishment_type__index_name=value) |
Q(establishment_type__isnull=True, establishment_subtype__isnull=True))
def by_establishment_subtypes(self, value: List[str]):
return self.filter(Q(establishment_subtype__index_name__in=value) |
Q(establishment_type__isnull=True, establishment_subtype__isnull=True))
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'))
establishment_type = models.ForeignKey('EstablishmentType', null=True, related_name='available_positions',
on_delete=models.SET_NULL, default=None)
establishment_subtype = models.ForeignKey('EstablishmentSubType', null=True, related_name='available_positions',
on_delete=models.SET_NULL, default=None)
objects = PositionQuerySet.as_manager()
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'),
Prefetch('awards', queryset=Award.objects.select_related('award_type'))
)
class Employee(PhoneModelMixin, AwardsModelMixin, 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
class EstablishmentScheduleQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentSchedule"""
class ContactPhone(PhoneModelMixin, 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 Plate(TranslatedFieldsMixin, models.Model):
"""Plate model."""
name = models.TextField(blank=True, default=None, null=True, verbose_name=_('name'), help_text='Dish name')
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 by_type(self, menu_type: str):
"""Returns certain typed menus"""
return self.filter(type=menu_type)
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'
FORMULAS = 'formulas'
STARTER = 'starter'
DESSERT = 'dessert'
MAIN_COURSE = 'main_course'
MENU_CHOICES = (
(FORMULAS, _('formulas')),
(STARTER, _('starter')),
(DESSERT, _('dessert')),
(MAIN_COURSE, _('main_course')),
)
VALID_CARD_AND_WINES_CHOICES = (
STARTER, DESSERT, MAIN_COURSE
)
type = models.CharField(max_length=50, db_index=True, default=FORMULAS,
verbose_name=_('Menu type'), choices=MENU_CHOICES)
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 EstablishmentBackOfficeWine(models.Model):
"""Wine for BO Card&Wines"""
establishment = models.OneToOneField(to='Establishment', on_delete=models.SET_NULL, related_name='back_office_wine',
verbose_name=_('establishment'), null=True)
bottles = models.IntegerField(default=0, validators=[MinValueValidator(0)])
price_from = models.DecimalField(_('price from'), max_digits=14, decimal_places=2)
price_to = models.DecimalField(_('price to'), max_digits=14, decimal_places=2)
price_from_for_one = models.DecimalField(_('price for one item from'), max_digits=14, decimal_places=2,
null=True, default=None)
price_to_for_one = models.DecimalField(_('price for one item to'), max_digits=14, decimal_places=2,
null=True, default=None)
is_glass = models.BooleanField(verbose_name=_('served in glass'), default=False)
class Meta:
verbose_name = _('establishment back office wine')
verbose_name_plural = _('establishment back office wines')
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 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.CASCADE,
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')
def _phonenumber_constructor(self, field_name: str) -> list:
array = []
if field_name in [field.name for field in self._meta.fields]:
values = getattr(self, field_name, None)
if values:
for number in values:
try:
obj = PhoneNumber.from_string(
phone_number=number,
region=None if number.startswith('+')
else settings.PHONENUMBER_DEFAULT_REGION)
except Exception as e:
pass
else:
array.append(obj) if obj.is_valid() else None
return array
def phones_(self) -> list:
"""Return PhoneNumber objects."""
return self._phonenumber_constructor('phones')
def faxes_(self) -> list:
"""Return PhoneNumber objects."""
return self._phonenumber_constructor('faxes')
def _array_constructor(self, field_name: str) -> list:
array = []
for obj in self._phonenumber_constructor(field_name):
if hasattr(obj, 'country_code') and hasattr(obj, 'national_number'):
array.append({
'country_calling_code': f'+{obj.country_code}',
'national_calling_number': str(obj.national_number),
})
return array
@property
def phones_array(self) -> list:
return self._array_constructor('phones')
@property
def faxes_array(self) -> list:
return self._array_constructor('faxes')