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_objects_during_init = 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 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