gault-millau/apps/collection/models.py
2019-12-19 16:39:42 +03:00

617 lines
24 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 location.models import Country, Region, WineRegion, WineSubRegion
from review.models import Review
from translation.models import Language
from utils.models import IntermediateGalleryModelMixin, GalleryModelMixin
from utils.models import (
ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
URLImageMixin,
)
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.slug) if hasattr(instance, 'slug') else (
instance.id, None
)
raw_objects.append(raw_object)
# parse slugs
related_objects = []
object_names = set()
re_pattern = r'[\w]+'
for object_id, 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,
'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 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 region_names(self):
return self.get_value_list(json_field=self.region_json,
model=Region,
lookup_field='name')
@property
def sub_region_names(self):
return self.get_value_list(json_field=self.region_json,
model=Country,
lookup_field='name_translated')
@property
def review_vintages(self):
return self.review_vintage_json.get('vintage')
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 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')
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