gault-millau/apps/collection/models.py

732 lines
27 KiB
Python

import re
from django.contrib.contenttypes.fields import ContentType
from django.contrib.postgres.fields import JSONField
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
from django.conf import settings
from collection import tasks
from location.models import Country, Region, WineRegion, WineSubRegion
from review.models import Review
from translation.models import Language
from utils.models import (
ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
URLImageMixin, IntermediateGalleryModelMixin
)
from utils.querysets import RelatedObjectsCountMixin
# Mixins
class CollectionNameMixin(models.Model):
"""CollectionName mixin"""
name = models.CharField(_('name'), max_length=250)
class Meta:
"""Meta class"""
abstract = True
class CollectionDateMixin(models.Model):
"""CollectionDate mixin"""
start = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('start'))
end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('end'))
class Meta:
"""Meta class"""
abstract = True
# Models
class CollectionQuerySet(RelatedObjectsCountMixin):
"""QuerySet for model Collection"""
def by_country_code(self, code):
"""Filter collection by country code."""
return self.filter(country__code=code)
def published(self):
"""Returned only published collection"""
return self.filter(is_publish=True)
class Collection(ProjectBaseMixin, CollectionDateMixin,
TranslatedFieldsMixin, URLImageMixin):
"""Collection model."""
STR_FIELD_NAME = 'name'
ORDINARY = 0 # Ordinary collection
POP = 1 # POP collection
COLLECTION_TYPES = (
(ORDINARY, _('Ordinary')),
(POP, _('Pop')),
)
name = TJSONField(verbose_name=_('name'),
help_text='{"en-GB":"some text"}')
collection_type = models.PositiveSmallIntegerField(choices=COLLECTION_TYPES,
default=ORDINARY,
verbose_name=_('Collection type'))
is_publish = models.BooleanField(
default=False, verbose_name=_('Publish status'))
on_top = models.BooleanField(
default=False, verbose_name=_('Position on top'))
country = models.ForeignKey(
'location.Country', verbose_name=_('country'), on_delete=models.CASCADE)
block_size = JSONField(
_('collection block properties'), null=True, blank=True,
default=None, help_text='{"width": "250px", "height":"250px"}')
description = TJSONField(
_('description'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}')
slug = models.SlugField(max_length=50, unique=True,
verbose_name=_('Collection slug'), editable=True, null=True)
old_id = models.IntegerField(null=True, blank=True)
rank = models.IntegerField(null=True, default=None)
objects = CollectionQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('collection')
verbose_name_plural = _('collections')
@property
def _related_objects(self) -> list:
"""Return list of related objects."""
related_objects = []
# get related objects
for related_object in self._meta.related_objects:
related_objects.append(related_object)
return related_objects
@property
def count_related_objects(self) -> int:
"""Return count of related objects."""
counter = 0
# count of related objects
for related_object in [related_object.name for related_object in self._related_objects]:
counter += getattr(self, f'{related_object}').count()
return counter
@property
def related_object_names(self) -> list:
"""Return related object names."""
raw_objects = []
for related_object in [related_object.name for related_object in self._related_objects]:
instances = getattr(self, f'{related_object}')
if instances.exists():
for instance in instances.all():
raw_object = (instance.id, instance.establishment_type.index_name,
instance.slug) if \
hasattr(instance, 'slug') else (instance.id, None, None)
raw_objects.append(raw_object)
# parse slugs
related_objects = []
object_names = set()
re_pattern = r'[\w]+'
for object_id, object_type, raw_name, in raw_objects:
result = re.findall(re_pattern, raw_name)
if result:
name = ' '.join(result).capitalize()
if name not in object_names:
related_objects.append({
'id': object_id,
'establishment_type': object_type,
'name': name
})
object_names.add(name)
return related_objects
class GuideTypeQuerySet(models.QuerySet):
"""QuerySet for model GuideType."""
class GuideType(ProjectBaseMixin):
"""GuideType model."""
name = models.SlugField(max_length=255, unique=True,
verbose_name=_('code'))
objects = GuideTypeQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('guide type')
verbose_name_plural = _('guide types')
def __str__(self):
"""Overridden str dunder method."""
return self.name
class GuideQuerySet(models.QuerySet):
"""QuerySet for Guide."""
def with_base_related(self):
"""Return QuerySet with related."""
return self.select_related('guide_type', 'site')
def by_country_id(self, country_id):
"""Return QuerySet filtered by country code."""
return self.filter(country_json__id__contains=country_id)
def annotate_in_restaurant_section(self):
"""Annotate flag if GuideElement in RestaurantSectionNode."""
return self.annotate(
in_restaurant_section=models.Case(
models.When(
guideelement__guide_element_type__name='EstablishmentNode',
guideelement__parent__guide_element_type__name='RestaurantSectionNode',
then=True),
default=False,
output_field=models.BooleanField(default=False)
)
)
def annotate_in_shop_section(self):
"""Annotate flag if GuideElement in ShopSectionNode."""
return self.annotate(
in_shop_section=models.Case(
models.When(
guideelement__guide_element_type__name='EstablishmentNode',
guideelement__parent__guide_element_type__name='ShopSectionNode',
then=True),
default=False,
output_field=models.BooleanField(default=False)
)
)
def annotate_restaurant_counter(self):
"""Return QuerySet with annotated field - restaurant_counter."""
return self.annotate_in_restaurant_section().annotate(
restaurant_counter=models.Count(
'guideelement',
filter=models.Q(in_restaurant_section=True) &
models.Q(guideelement__parent_id__isnull=False),
distinct=True))
def annotate_shop_counter(self):
"""Return QuerySet with annotated field - shop_counter."""
return self.annotate_in_shop_section().annotate(
shop_counter=models.Count(
'guideelement',
filter=models.Q(in_shop_section=True) &
models.Q(guideelement__parent_id__isnull=False),
distinct=True))
def annotate_wine_counter(self):
"""Return QuerySet with annotated field - shop_counter."""
return self.annotate_in_restaurant_section().annotate(
wine_counter=models.Count(
'guideelement',
filter=models.Q(guideelement__guide_element_type__name='WineNode') &
models.Q(guideelement__parent_id__isnull=False),
distinct=True))
def annotate_present_objects_counter(self):
"""Return QuerySet with annotated field - present_objects_counter."""
return self.annotate_in_restaurant_section().annotate(
present_objects_counter=models.Count(
'guideelement',
filter=models.Q(guideelement__guide_element_type__name__in=['EstablishmentNode', 'WineNode']) &
models.Q(guideelement__parent_id__isnull=False),
distinct=True))
class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
"""Guide model."""
BUILT = 0
WAITING = 1
REMOVING = 2
BUILDING = 3
STATE_CHOICES = (
(BUILT, 'built'),
(WAITING, 'waiting'),
(REMOVING, 'removing'),
(BUILDING, 'building'),
)
start = models.DateTimeField(null=True,
verbose_name=_('start'))
vintage = models.IntegerField(validators=[MinValueValidator(1900),
MaxValueValidator(2100)],
null=True,
verbose_name=_('guide vintage year'))
slug = models.SlugField(max_length=255, unique=True, null=True,
verbose_name=_('slug'))
guide_type = models.ForeignKey('GuideType', on_delete=models.PROTECT,
null=True,
verbose_name=_('type'))
site = models.ForeignKey('main.SiteSettings', on_delete=models.SET_NULL,
null=True,
verbose_name=_('site settings'))
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('state'))
count_related_objects = models.PositiveIntegerField(
default=0,
help_text=_('* after rebuild guide, refresh count of related guide elements'),
verbose_name=_('count of related guide elements during initialization'))
old_id = models.IntegerField(blank=True, null=True)
objects = GuideQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('guide')
verbose_name_plural = _('guides')
def __str__(self):
"""String method."""
return f'{self.name}'
# todo: for test use, use annotation instead
# @property
# def restaurant_counter_prop(self):
# counter = 0
# root_node = GuideElement.objects.get_root_node(self)
# if root_node:
# descendants = GuideElement.objects.get_root_node(self).get_descendants()
# if descendants:
# restaurant_section_nodes = descendants.filter(
# guide_element_type__name='RestaurantSectionNode')
# counter = descendants.filter(guide_element_type__name='EstablishmentNode',
# parent__in=restaurant_section_nodes,
# parent_id__isnull=True).count()
# return counter
#
# @property
# def shop_counter_prop(self):
# counter = 0
# root_node = GuideElement.objects.get_root_node(self)
# if root_node:
# descendants = GuideElement.objects.get_root_node(self).get_descendants()
# if descendants:
# shop_section_nodes = descendants.filter(
# guide_element_type__name='ShopSectionNode')
# counter = descendants.filter(guide_element_type__name='EstablishmentNode',
# parent__in=shop_section_nodes,
# parent_id__isnull=True).count()
# return counter
#
# @property
# def wine_counter_prop(self):
# counter = 0
# root_node = GuideElement.objects.get_root_node(self)
# if root_node:
# descendants = GuideElement.objects.get_root_node(self).get_descendants()
# if descendants:
# counter = descendants.filter(guide_element_type__name='WineNode',
# parent_id__isnull=True).count()
# return counter
def update_count_related_objects(self, nodes: set = ('EstablishmentNode', 'WineNode')):
"""Update count of related guide element objects."""
descendants = GuideElement.objects.get_root_node(self) \
.get_descendants()
if descendants:
updated_count = descendants.filter(
guide_element_type__name__in=nodes).count()
self.count_related_objects = updated_count
self.save()
class AdvertorialQuerySet(models.QuerySet):
"""QuerySet for model Advertorial."""
class Advertorial(ProjectBaseMixin):
"""Guide advertorial model."""
number_of_pages = models.PositiveIntegerField(
verbose_name=_('number of pages'),
help_text=_('the total number of reserved pages'))
right_pages = models.PositiveIntegerField(
verbose_name=_('number of right pages'),
help_text=_('the number of right pages (which are part of total number).'))
guide_element = models.OneToOneField('GuideElement', on_delete=models.CASCADE,
related_name='advertorial',
verbose_name=_('guide element'))
old_id = models.IntegerField(blank=True, null=True)
objects = AdvertorialQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('advertorial')
verbose_name_plural = _('advertorials')
class GuideFilterQuerySet(models.QuerySet):
"""QuerySet for model GuideFilter."""
class GuideFilter(ProjectBaseMixin):
"""Guide filter model."""
establishment_type_json = JSONField(blank=True, null=True,
verbose_name='establishment types')
country_json = JSONField(blank=True, null=True,
verbose_name='countries')
region_json = JSONField(blank=True, null=True,
verbose_name='regions')
sub_region_json = JSONField(blank=True, null=True,
verbose_name='sub regions')
wine_region_json = JSONField(blank=True, null=True,
verbose_name='wine regions')
with_mark = models.BooleanField(default=True,
verbose_name=_('with mark'),
help_text=_('exclude empty marks?'))
locale_json = JSONField(blank=True, null=True,
verbose_name='locales')
max_mark = models.FloatField(verbose_name=_('max mark'),
null=True,
help_text=_('mark under'))
min_mark = models.FloatField(verbose_name=_('min mark'),
null=True,
help_text=_('mark over'))
review_vintage_json = JSONField(verbose_name='review vintage years')
review_state_json = JSONField(blank=True, null=True,
verbose_name='review states')
guide = models.OneToOneField(Guide, on_delete=models.CASCADE,
verbose_name=_('guide'))
old_id = models.IntegerField(blank=True, null=True)
objects = GuideFilterQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('guide filter')
verbose_name_plural = _('guide filters')
def get_value_list(self, json_field: dict,
model: object,
lookup_field: str,
search_field: int = 'id') -> list:
"""
Function to return an array with correct values from ids.
Algorithm:
1 Get values from json_field
2 Try to find model instances by search field and value from json field
3 If instance was found, then put value into array from instance by lookup field
"""
value_list = []
if hasattr(model, 'objects'):
for value in getattr(json_field, 'get')(search_field):
qs = model.objects.filter(**{search_field: value})
if qs.exists():
value_list.append(getattr(qs.first(), lookup_field))
return value_list
@property
def establishment_types(self):
from establishment.models import EstablishmentType
return self.get_value_list(json_field=self.establishment_type_json,
model=EstablishmentType,
lookup_field='index_name')
@property
def establishment_type_ids(self):
from establishment.models import EstablishmentType
return self.get_value_list(json_field=self.establishment_type_json,
model=EstablishmentType,
lookup_field='id')
@property
def locales(self):
return self.get_value_list(json_field=self.locale_json,
model=Language,
lookup_field='locale')
@property
def review_states(self):
states = []
for state in self.review_state_json.get('state'):
status_field = [field for field in Review._meta.fields
if field.name == 'status'][0]
status_field_id = Review._meta.fields.index(status_field)
states.append(dict(Review._meta.fields[status_field_id].choices).get(state))
return states
@property
def country_names(self):
return self.get_value_list(json_field=self.country_json,
model=Country,
lookup_field='name_translated')
@property
def country_ids(self):
return self.get_value_list(json_field=self.country_json,
model=Country,
lookup_field='id')
@property
def region_names(self):
return self.get_value_list(json_field=self.region_json,
model=Region,
lookup_field='name')
@property
def region_ids(self):
return self.get_value_list(json_field=self.region_json,
model=Region,
lookup_field='id')
@property
def sub_region_names(self):
return self.get_value_list(json_field=self.sub_region_json,
model=Region,
lookup_field='name_translated')
@property
def sub_region_ids(self):
return self.get_value_list(json_field=self.sub_region_json,
model=Region,
lookup_field='id')
@property
def wine_region_ids(self):
return self.get_value_list(json_field=self.wine_region_json,
model=WineRegion,
lookup_field='id')
@property
def review_vintages(self):
return self.review_vintage_json.get('vintage')
@property
def available_filters(self):
filters = list()
for i in self._meta.fields:
if isinstance(i, JSONField):
has_values = list(getattr(self, f'{i.name}').values())[0]
if has_values:
filters.append(i.name)
return filters
@property
def establishment_filter_set(self):
filters = {
# establishment.Establishment
'public_mark__in': [self.min_mark, self.max_mark],
# review.Reviews
'reviews__vintage__in': self.review_vintages,
}
if self.establishment_type_ids:
filters.update({
# establishment.EstablishmentType
'establishment_type_id__in': self.establishment_type_ids,
})
if self.country_ids:
filters.update({
# location.Country
'address__city__country_id__in': self.country_ids,
})
if self.region_ids:
filters.update({
# location.Region
'address__city__region__parent_id__in': self.region_ids,
})
if self.sub_region_ids:
filters.update({
# location.Region
'address__city__region__parent_id__in': self.region_ids,
'address__city__region_id__in': self.sub_region_ids,
})
if self.wine_region_ids:
filters.update({
# location.WineRegion
'wine_region_id__in': self.wine_region_ids,
})
if self.with_mark:
filters.update({
# establishment.Establishment
'public_mark__isnull': False,
})
if self.locale_json:
filters.update({
'reviews__text__has_any_keys': self.locales,
})
return filters
class GuideElementType(models.Model):
"""Model for type of guide elements."""
name = models.CharField(max_length=50,
verbose_name=_('name'))
class Meta:
"""Meta class."""
verbose_name = _('guide element type')
verbose_name_plural = _('guide element types')
def __str__(self):
"""Overridden str dunder."""
return self.name
class GuideWineColorSectionQuerySet(models.QuerySet):
"""QuerySet for model GuideWineColorSection."""
class GuideWineColorSection(ProjectBaseMixin):
"""Sections for wine colors."""
name = models.CharField(max_length=255, verbose_name=_('section name'))
objects = GuideWineColorSectionQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('guide wine color section')
verbose_name_plural = _('guide wine color sections')
class GuideElementSectionCategoryQuerySet(models.QuerySet):
"""QuerySet for model GuideElementSectionCategory."""
class GuideElementSectionCategory(ProjectBaseMixin):
"""Section category for guide element."""
name = models.CharField(max_length=255,
verbose_name=_('category name'))
objects = GuideElementSectionCategoryQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('guide element section category')
verbose_name_plural = _('guide element section categories')
class GuideElementSectionQuerySet(models.QuerySet):
"""QuerySet for model GuideElementSection."""
class GuideElementSection(ProjectBaseMixin):
"""Sections for guide element."""
name = models.CharField(max_length=255, verbose_name=_('section name'))
category = models.ForeignKey(GuideElementSectionCategory, on_delete=models.PROTECT,
verbose_name=_('category'))
old_id = models.PositiveIntegerField(blank=True, null=True, default=None,
verbose_name=_('old id'))
objects = GuideElementSectionQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('guide element section')
verbose_name_plural = _('guide element sections')
class GuideElementManager(models.Manager):
"""Manager for model GuideElement."""
def get_root_node(self, guide):
"""Return guide root element node."""
qs = self.filter(guide=guide, guide_element_type__name='Root')
if qs.exists():
return qs.first()
class GuideElementQuerySet(models.QuerySet):
"""QuerySet for model Guide elements."""
def base_related(self):
"""Return QuerySet with base related."""
return self.select_related(
'guide_element_type',
'establishment',
'review',
'wine_region',
'product',
'city',
'wine_color_section',
'section',
'guide',
'label_photo',
)
def restaurant_nodes(self):
"""Return GuideElement with type RestaurantSectionNode."""
return self.filter(guide_element_type__name='RestaurantSectionNode')
def shop_nodes(self):
"""Return GuideElement with type ShopSectionNode."""
return self.filter(guide_element_type__name='ShopSectionNode')
def wine_nodes(self):
"""Return GuideElement with type WineNode."""
return self.filter(guide_element_type__name='WineNode')
def descendants(self):
"""Return QuerySet with descendants."""
return self.exclude(guide_element_type__name='Root')
class GuideElement(ProjectBaseMixin, MPTTModel):
"""Frozen state of elements of guide instance."""
guide_element_type = models.ForeignKey('GuideElementType', on_delete=models.SET_NULL,
null=True,
verbose_name=_('guide element type'))
establishment = models.ForeignKey('establishment.Establishment', on_delete=models.SET_NULL,
null=True, blank=True, default=None)
review = models.ForeignKey('review.Review', on_delete=models.SET_NULL,
null=True, blank=True, default=None)
wine_region = models.ForeignKey('location.WineRegion', on_delete=models.SET_NULL,
null=True, blank=True, default=None)
product = models.ForeignKey('product.Product', on_delete=models.SET_NULL,
null=True, blank=True, default=None)
priority = models.IntegerField(null=True, blank=True, default=None)
city = models.ForeignKey('location.City', on_delete=models.SET_NULL,
null=True, blank=True, default=None)
wine_color_section = models.ForeignKey('GuideWineColorSection', on_delete=models.SET_NULL,
null=True, blank=True, default=None)
section = models.ForeignKey('GuideElementSection', on_delete=models.SET_NULL,
null=True, blank=True, default=None)
guide = models.ForeignKey('Guide', on_delete=models.SET_NULL,
null=True, blank=True, default=None)
parent = TreeForeignKey('self', on_delete=models.CASCADE,
null=True, blank=True,
related_name='children')
label_photo = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
null=True, blank=True, default=None,
verbose_name=_('label photo'))
old_id = models.PositiveIntegerField(blank=True, null=True, default=None,
verbose_name=_('old id'))
objects = GuideElementManager.from_queryset(GuideElementQuerySet)()
class Meta:
"""Meta class."""
verbose_name = _('guide element')
verbose_name_plural = _('guide elements')
class MPTTMeta:
order_insertion_by = ['guide_element_type']
def __str__(self):
"""Overridden dunder method."""
return self.guide_element_type.name if self.guide_element_type else self.id