gault-millau/apps/location/models.py

435 lines
15 KiB
Python

"""Location app models."""
from django.conf import settings
from django.contrib.gis.db import models
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 functools import reduce
from typing import List
from django.contrib.postgres.fields.jsonb import KeyTextTransform
from django.contrib.postgres.fields import ArrayField
from translation.models import Language
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
TranslatedFieldsMixin, get_current_locale,
IntermediateGalleryModelMixin, GalleryMixin)
class CountryQuerySet(models.QuerySet):
"""Country queryset."""
def active(self, switcher=True):
"""Filter only active users."""
return self.filter(is_active=switcher)
class Country(TranslatedFieldsMixin,
SVGImageMixin, ProjectBaseMixin):
"""Country model."""
STR_FIELD_NAME = 'name'
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)
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 country_id(self):
return self.id
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)\
.order_by('name')
def by_region_id(self, region_id):
"""Filter regions by region id."""
return self.filter(id=region_id)\
.order_by('name')
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)\
.order_by('name')
def sub_regions_by_region_id(self, region_id):
"""Filter regions by sub region id."""
return self.filter(parent_region_id=region_id).order_by('name')
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)
class City(GalleryMixin, models.Model):
"""Region model."""
name = models.CharField(_('name'), max_length=250)
name_translated = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Translated name'),
help_text='{"en-GB":"some text"}')
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)
# deprecated
# todo: remove gallery after move to image
gallery = models.ManyToManyField('gallery.Image', through='location.CityGallery', blank=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 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 CityGallery(IntermediateGalleryModelMixin):
"""Gallery for model City."""
city = models.ForeignKey(City, null=True,
related_name='city_gallery',
on_delete=models.CASCADE,
verbose_name=_('city'))
image = models.ForeignKey('gallery.Image', null=True,
related_name='city_gallery',
on_delete=models.CASCADE,
verbose_name=_('image'))
class Meta:
"""CityGallery meta class."""
verbose_name = _('city gallery')
verbose_name_plural = _('city galleries')
unique_together = (('city', 'is_main'), ('city', 'image'))
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)
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))