gault-millau/apps/location/models.py
2020-02-10 14:25:33 +03:00

457 lines
16 KiB
Python

"""Location app models."""
from functools import reduce
from json import dumps
from typing import List
from django.conf import settings
from django.contrib.gis.db import models
from django.contrib.postgres.fields import ArrayField
from django.db.models.signals import post_save
from django.db.transaction import on_commit
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from translation.models import Language
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
TranslatedFieldsMixin, get_current_locale,
IntermediateGalleryModelMixin)
class CountryQuerySet(models.QuerySet):
"""Country queryset."""
def active(self, switcher=True):
"""Filter only active users."""
return self.filter(is_active=switcher)
def by_country_code(self, code: str):
"""Filter QuerySet by country code."""
return self.filter(code__iexact=code)
def aggregate_country_codes(self):
"""Aggregate country codes."""
calling_codes = list(
self.model.objects.exclude(calling_code__isnull=True)
.exclude(code__iexact='aa')
.distinct()
.values_list('calling_code', flat=True)
)
# extend country calling code hardcoded codes
calling_codes.extend(settings.CALLING_CODES_ANTILLES_GUYANE_WEST_INDIES)
return [self.model.CALLING_NUMBER_MASK % i for i in calling_codes]
class Country(TranslatedFieldsMixin,
SVGImageMixin, ProjectBaseMixin):
"""Country model."""
STR_FIELD_NAME = 'name'
CALLING_NUMBER_MASK = '+%s'
TWELVE_HOURS_FORMAT_COUNTRIES = [
'ca', # Canada
'au', # Australia
'us', # USA
'nz', # New Zealand
]
name = TJSONField(null=True, blank=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
code = models.CharField(max_length=255, verbose_name=_('Code'))
low_price = models.IntegerField(default=25, verbose_name=_('Low price'))
high_price = models.IntegerField(default=50, verbose_name=_('High price'))
languages = models.ManyToManyField(Language, verbose_name=_('Languages'))
is_active = models.BooleanField(_('is active'), default=True)
old_id = models.IntegerField(null=True, blank=True, default=None)
calling_code = models.CharField(max_length=5,
blank=True, null=True, default=None,
verbose_name=_('calling code'),
help_text='i.e. "1-809"')
mysql_ids = ArrayField(models.IntegerField(), blank=True, null=True)
objects = CountryQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name_plural = _('Countries')
verbose_name = _('Country')
def __str__(self):
str_name = self.code
if isinstance(self.name, dict):
translated_name = self.name.get(get_current_locale())
if translated_name:
str_name = translated_name
return str_name
@property
def time_format(self):
if self.code.lower() not in self.TWELVE_HOURS_FORMAT_COUNTRIES:
return 'HH:mm'
return 'hh:mmA'
@property
def display_calling_code(self) -> list:
"""Return formatted calling code."""
array = []
if self.code and self.calling_code:
# hardcoded calling numbers for Antilles Guyane West Indies islands.
if self.code.lower() == 'aa':
array.extend([self.CALLING_NUMBER_MASK % i
for i in set(settings.CALLING_CODES_ANTILLES_GUYANE_WEST_INDIES)])
else:
array.append(self.CALLING_NUMBER_MASK % self.calling_code)
return array
@property
def default_calling_code(self) -> str:
"""Return default calling code based on phone number."""
if self.code and self.calling_code:
# hardcoded default calling number for Antilles Guyane West Indies islands.
if self.code.lower() == 'aa':
return (self.CALLING_NUMBER_MASK %
settings.DEFAULT_CALLING_CODE_ANTILLES_GUYANE_WEST_INDIES)
return self.CALLING_NUMBER_MASK % self.calling_code
class RegionQuerySet(models.QuerySet):
"""QuerySet for model Region."""
def without_parent_region(self, switcher: bool = True):
"""Filter regions by parent region."""
return self.filter(parent_region__isnull=switcher)
def by_region_id(self, region_id):
"""Filter regions by region id."""
return self.filter(id=region_id)
def by_sub_region_id(self, sub_region_id):
"""Filter sub regions by sub region id."""
return self.filter(parent_region_id=sub_region_id)
def sub_regions_by_region_id(self, region_id):
"""Filter regions by sub region id."""
return self.filter(parent_region_id=region_id)
class Region(models.Model):
"""Region model."""
name = models.CharField(_('name'), max_length=250)
code = models.CharField(_('code'), max_length=250)
parent_region = models.ForeignKey(
'self', verbose_name=_('parent region'), null=True,
blank=True, default=None, on_delete=models.CASCADE)
country = models.ForeignKey(
Country, verbose_name=_('country'), on_delete=models.CASCADE)
old_id = models.IntegerField(null=True, blank=True, default=None)
mysql_ids = ArrayField(models.IntegerField(), blank=True, null=True)
objects = RegionQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name_plural = _('regions')
verbose_name = _('region')
def __str__(self):
return self.name
class CityQuerySet(models.QuerySet):
"""Extended queryset for City model."""
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 search_by_name(self, value):
"""Search by name or last_name."""
return self._generic_search(value, ['name', 'code', 'postal_code'])
def by_country_code(self, code):
"""Return establishments by country code"""
return self.filter(country__code=code)
def with_base_related(self):
return self.prefetch_related('country', 'region', 'region__country')
class City(TranslatedFieldsMixin, ProjectBaseMixin):
"""Region model."""
name = TJSONField(default=None, null=True, help_text='{"en-GB":"some city name"}',
verbose_name=_('City name json'))
code = models.CharField(_('code'), max_length=250)
region = models.ForeignKey(Region, on_delete=models.CASCADE,
blank=True, null=True,
verbose_name=_('parent region'))
country = models.ForeignKey(
Country, verbose_name=_('country'), on_delete=models.CASCADE)
postal_code = models.CharField(
_('postal code'), max_length=10, default='', help_text=_('Ex.: 350018'))
is_island = models.BooleanField(_('is island'), default=False)
old_id = models.IntegerField(null=True, blank=True, default=None)
map1 = models.CharField(max_length=255, blank=True, null=True)
map2 = models.CharField(max_length=255, blank=True, null=True)
map_ref = models.CharField(max_length=255, blank=True, null=True)
situation = models.CharField(max_length=255, blank=True, null=True)
image = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
blank=True, null=True, default=None,
related_name='city_image',
verbose_name=_('image instance of model Image'))
mysql_id = models.IntegerField(blank=True, null=True, default=None)
objects = CityQuerySet.as_manager()
class Meta:
verbose_name_plural = _('cities')
verbose_name = _('city')
def __str__(self):
return f'{self.id}: {self.code}'
@property
def name_dumped(self):
"""Used for indexing as string"""
return dumps(self.name)
@property
def image_object(self):
"""Return image object."""
return self.image.image if self.image else None
@property
def crop_image(self):
if hasattr(self, 'image') and hasattr(self, '_meta'):
if self.image:
image_property = {
'id': self.image.id,
'title': self.image.title,
'original_url': self.image.image.url,
'orientation_display': self.image.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.image.get_image_url(crop)}
)
return image_property
class Address(models.Model):
"""Address model."""
city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE)
street_name_1 = models.CharField(
_('street name 1'), max_length=500, blank=True, default='')
street_name_2 = models.CharField(
_('street name 2'), max_length=500, blank=True, default='')
number = models.IntegerField(_('number'), blank=True, default=0)
postal_code = models.CharField(
_('postal code'), max_length=10, blank=True,
default='', help_text=_('Ex.: 350018'))
coordinates = models.PointField(
_('Coordinates'), blank=True, null=True, default=None)
district_name = models.CharField(
_('District name'), max_length=500, blank=True, default=None, null=True)
old_id = models.IntegerField(null=True, blank=True, default=None)
class Meta:
"""Meta class."""
verbose_name_plural = _('Address')
verbose_name = _('Address')
def __str__(self):
return f'{self.id}: {self.get_street_name()[:50]}'
def get_street_name(self):
return self.street_name_1 or self.street_name_2
@property
def latitude(self):
return self.coordinates.y if self.coordinates else float(0)
@property
def longitude(self):
return self.coordinates.x if self.coordinates else float(0)
@property
def location_field_indexing(self):
return {'lat': self.latitude,
'lon': self.longitude}
@property
def country_id(self):
return self.city.country_id
@property
def full_address(self):
full_address = self.get_street_name()
if self.number and int(self.number):
full_address = f'{self.number} {self.get_street_name()}'
return full_address
class WineRegionQuerySet(models.QuerySet):
"""Wine region queryset."""
def with_sub_region_related(self):
return self.prefetch_related('wine_sub_region')
def having_wines(self, value=True):
"""Return qs with regions, which have any wine related to them"""
return self.exclude(wineoriginaddress__product__isnull=value)
class WineRegion(TranslatedFieldsMixin, models.Model):
"""Wine region model."""
name = models.CharField(_('name'), max_length=255)
country = models.ForeignKey(Country, on_delete=models.PROTECT,
blank=True, null=True, default=None,
verbose_name=_('country'))
coordinates = models.PointField(
_('Coordinates'), blank=True, null=True, default=None)
old_id = models.PositiveIntegerField(_('old id'), default=None,
blank=True, null=True)
description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'),
help_text='{"en-GB":"some text"}')
tags = models.ManyToManyField('tag.Tag', blank=True,
related_name='wine_regions',
help_text='attribute from legacy db')
objects = WineRegionQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name_plural = _('wine regions')
verbose_name = _('wine region')
def __str__(self):
"""Override dunder method."""
return self.name
class WineSubRegionQuerySet(models.QuerySet):
"""Wine sub region QuerySet."""
class WineSubRegion(models.Model):
"""Wine sub region model."""
name = models.CharField(_('name'), max_length=255)
wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT,
related_name='wine_sub_region',
verbose_name=_('wine sub region'))
old_id = models.PositiveIntegerField(_('old id'), default=None,
blank=True, null=True)
objects = WineSubRegionQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name_plural = _('wine sub regions')
verbose_name = _('wine sub region')
def __str__(self):
"""Override dunder method."""
return self.name
class WineVillageQuerySet(models.QuerySet):
"""Wine village QuerySet."""
class WineVillage(models.Model):
"""
Wine village.
Description: Imported from legacy DB.
"""
name = models.CharField(_('name'), max_length=255)
wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT,
verbose_name=_('wine region'))
old_id = models.PositiveIntegerField(_('old id'), default=None,
blank=True, null=True)
objects = WineVillageQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('wine village')
verbose_name_plural = _('wine villages')
def __str__(self):
"""Override str dunder."""
return self.name
class WineOriginAddressMixin(models.Model):
"""Model for wine origin address."""
wine_region = models.ForeignKey('location.WineRegion', on_delete=models.CASCADE,
verbose_name=_('wine region'))
wine_sub_region = models.ForeignKey('location.WineSubRegion', on_delete=models.CASCADE,
blank=True, null=True, default=None,
verbose_name=_('wine sub region'))
class Meta:
"""Meta class."""
abstract = True
class EstablishmentWineOriginAddressQuerySet(models.QuerySet):
"""QuerySet for EstablishmentWineOriginAddress model."""
class EstablishmentWineOriginAddress(WineOriginAddressMixin):
"""Establishment wine origin address model."""
establishment = models.ForeignKey('establishment.Establishment', on_delete=models.CASCADE,
related_name='wine_origins',
verbose_name=_('product'))
objects = EstablishmentWineOriginAddressQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('establishment wine origin address')
verbose_name_plural = _('establishment wine origin addresses')
class WineOriginAddressQuerySet(models.QuerySet):
"""QuerySet for WineOriginAddress model."""
class WineOriginAddress(WineOriginAddressMixin):
"""Wine origin address model."""
product = models.ForeignKey('product.Product', on_delete=models.CASCADE,
related_name='wine_origins',
verbose_name=_('product'))
objects = WineOriginAddressQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('wine origin address')
verbose_name_plural = _('wine origin addresses')
# todo: Make recalculate price levels
@receiver(post_save, sender=Country)
def run_recalculate_price_levels(sender, instance, **kwargs):
from establishment.tasks import recalculate_price_levels_by_country
if settings.USE_CELERY:
on_commit(lambda: recalculate_price_levels_by_country.delay(
country_id=instance.id))
else:
on_commit(lambda: recalculate_price_levels_by_country(
country_id=instance.id))