Merge branch 'develop' of ssh://gl.id-east.ru:222/gm/gm-backend into develop

This commit is contained in:
Dmitriy Kuzmenko 2019-12-05 15:23:24 +03:00
commit c62b4016f1
46 changed files with 1868 additions and 131 deletions

4
.gitignore vendored
View File

@ -22,7 +22,9 @@ logs/
# dev
./docker-compose.override.yml
celerybeat-schedule
local_files
celerybeat.pid
/gm_viktor.dump
/docker-compose.dump.yml
/gm_production_20191029.sql

View File

@ -1,3 +1,3 @@
FROM mdillon/postgis:9.5
FROM mdillon/postgis:latest
RUN localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8
ENV LANG ru_RU.utf8

View File

@ -1,4 +1,6 @@
from django.contrib.gis import admin
from mptt.admin import DraggableMPTTAdmin, TreeRelatedFieldListFilter
from utils.admin import BaseModelAdminMixin
from collection import models
@ -11,3 +13,22 @@ class CollectionAdmin(admin.ModelAdmin):
@admin.register(models.Guide)
class GuideAdmin(admin.ModelAdmin):
"""Guide admin."""
@admin.register(models.GuideElementType)
class GuideElementType(admin.ModelAdmin):
"""Guide element admin."""
@admin.register(models.GuideElement)
class GuideElementAdmin(DraggableMPTTAdmin, BaseModelAdminMixin, admin.ModelAdmin):
"""Guide element admin."""
raw_id_fields = [
'guide_element_type', 'establishment', 'review',
'wine_region', 'product', 'city',
'wine_color_section', 'section', 'guide',
'parent',
]
# list_filter = (
# ('parent', TreeRelatedFieldListFilter),
# )

View File

@ -0,0 +1,120 @@
import re
from pprint import pprint
from django.core.management.base import BaseCommand
from tqdm import tqdm
from establishment.models import Establishment
from location.models import City
from location.models import WineRegion
from product.models import Product
from review.models import Review
from tag.models import Tag
from transfer.models import GuideElements
def decorator(f):
def decorate(self):
print(f'{"-"*20}start {f.__name__}{"-"*20}')
f(self)
print(f'{"-"*20}end {f.__name__}{"-"*20}\n')
return decorate
class Command(BaseCommand):
help = """Check guide dependencies."""
@decorator
def count_of_guide_relative_dependencies(self):
for field in GuideElements._meta.fields:
if field.name not in ['id', 'lft', 'rgt', 'depth',
'children_count', 'parent', 'order_number']:
filters = {f'{field.name}__isnull': False, }
qs = GuideElements.objects.filter(**filters).values_list(field.name, flat=True)
print(f"COUNT OF {field.name}'s: {len(set(qs))}")
@decorator
def check_regions(self):
wine_region_old_ids = set(GuideElements.objects.filter(wine_region_id__isnull=False)
.values_list('wine_region_id', flat=True))
not_existed_wine_regions = []
for old_id in tqdm(wine_region_old_ids):
if not WineRegion.objects.filter(old_id=old_id).exists():
not_existed_wine_regions.append(old_id)
print(f'NOT EXISTED WINE REGIONS: {len(not_existed_wine_regions)}')
pprint(f'{not_existed_wine_regions}')
@decorator
def check_establishments(self):
establishment_old_ids = set(GuideElements.objects.filter(establishment_id__isnull=False)
.values_list('establishment_id', flat=True))
not_existed_establishments = []
for old_id in tqdm(establishment_old_ids):
if not Establishment.objects.filter(old_id=old_id).exists():
not_existed_establishments.append(old_id)
print(f'NOT EXISTED ESTABLISHMENTS: {len(not_existed_establishments)}')
pprint(f'{not_existed_establishments}')
@decorator
def check_reviews(self):
review_old_ids = set(GuideElements.objects.filter(review_id__isnull=False)
.values_list('review_id', flat=True))
not_existed_reviews = []
for old_id in tqdm(review_old_ids):
if not Review.objects.filter(old_id=old_id).exists():
not_existed_reviews.append(old_id)
print(f'NOT EXISTED REVIEWS: {len(not_existed_reviews)}')
pprint(f'{not_existed_reviews}')
@decorator
def check_wines(self):
wine_old_ids = set(GuideElements.objects.filter(wine_id__isnull=False)
.values_list('wine_id', flat=True))
not_existed_wines = []
for old_id in tqdm(wine_old_ids):
if not Product.objects.filter(old_id=old_id).exists():
not_existed_wines.append(old_id)
print(f'NOT EXISTED WINES: {len(not_existed_wines)}')
pprint(f'{not_existed_wines}')
@decorator
def check_wine_color(self):
raw_wine_color_nodes = set(GuideElements.objects.exclude(color__iexact='')
.filter(color__isnull=False)
.values_list('color', flat=True))
raw_wine_colors = [i[:-11] for i in raw_wine_color_nodes]
raw_wine_color_index_names = []
re_exp = '[A-Z][^A-Z]*'
for raw_wine_color in tqdm(raw_wine_colors):
result = re.findall(re_exp, rf'{raw_wine_color}')
if result and len(result) >= 2:
wine_color = '-'.join(result)
else:
wine_color = result[0]
raw_wine_color_index_names.append(wine_color.lower())
not_existed_wine_colors = []
for index_name in raw_wine_color_index_names:
if not Tag.objects.filter(value=index_name).exists():
not_existed_wine_colors.append(index_name)
print(f'NOT EXISTED WINE COLOR: {len(not_existed_wine_colors)}')
pprint(f'{not_existed_wine_colors}')
@decorator
def check_cities(self):
city_old_ids = set(GuideElements.objects.filter(city_id__isnull=False)
.values_list('city_id', flat=True))
not_existed_cities = []
for old_id in tqdm(city_old_ids):
if not City.objects.filter(old_id=old_id).exists():
not_existed_cities.append(old_id)
print(f'NOT EXISTED CITIES: {len(not_existed_cities)}')
pprint(f'{not_existed_cities}')
def handle(self, *args, **kwargs):
self.count_of_guide_relative_dependencies()
self.check_regions()
self.check_establishments()
self.check_reviews()
self.check_wines()
self.check_wine_color()
self.check_cities()

View File

@ -0,0 +1,78 @@
# Generated by Django 2.2.7 on 2019-11-27 10:47
import django.contrib.postgres.fields.jsonb
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('main', '0039_sitefeature_old_id'),
('collection', '0017_collection_old_id'),
]
operations = [
migrations.CreateModel(
name='GuideType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.SlugField(max_length=255, unique=True, verbose_name='code')),
],
options={
'verbose_name': 'guide type',
'verbose_name_plural': 'guide types',
},
),
migrations.RemoveField(
model_name='guide',
name='advertorials',
),
migrations.RemoveField(
model_name='guide',
name='collection',
),
migrations.RemoveField(
model_name='guide',
name='parent',
),
migrations.AddField(
model_name='guide',
name='old_id',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='guide',
name='site',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='site settings'),
),
migrations.AddField(
model_name='guide',
name='slug',
field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='slug'),
),
migrations.AddField(
model_name='guide',
name='state',
field=models.PositiveSmallIntegerField(choices=[(0, 'built'), (1, 'waiting'), (2, 'removing'), (3, 'building')], default=1, verbose_name='state'),
),
migrations.AddField(
model_name='guide',
name='vintage',
field=models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(1900), django.core.validators.MaxValueValidator(2100)], verbose_name='guide vintage year'),
),
migrations.AddField(
model_name='guide',
name='guide_type',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='collection.GuideType', verbose_name='type'),
),
migrations.AlterField(
model_name='guide',
name='start',
field=models.DateTimeField(null=True, verbose_name='start'),
),
]

View File

@ -0,0 +1,41 @@
# Generated by Django 2.2.7 on 2019-12-02 10:11
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('collection', '0018_auto_20191127_1047'),
]
operations = [
migrations.CreateModel(
name='GuideFilter',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('establishment_type_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='establishment types')),
('country_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='countries')),
('region_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='regions')),
('sub_region_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='sub regions')),
('wine_region_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='wine regions')),
('with_mark', models.BooleanField(default=True, help_text='exclude empty marks?', verbose_name='with mark')),
('locale_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='locales')),
('max_mark', models.FloatField(help_text='mark under', null=True, verbose_name='max mark')),
('min_mark', models.FloatField(help_text='mark over', null=True, verbose_name='min mark')),
('review_vintage_json', django.contrib.postgres.fields.jsonb.JSONField(verbose_name='review vintage years')),
('review_state_json', django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True, verbose_name='review states')),
('old_id', models.IntegerField(blank=True, null=True)),
('guide', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='collection.Guide', verbose_name='guide')),
],
options={
'verbose_name': 'guide filter',
'verbose_name_plural': 'guide filters',
},
),
]

View File

@ -0,0 +1,56 @@
# Generated by Django 2.2.7 on 2019-12-02 14:05
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('collection', '0019_advertorial_guidefilter'),
]
operations = [
migrations.CreateModel(
name='GuideElementSectionCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=255, verbose_name='category name')),
],
options={
'verbose_name': 'guide element section category',
'verbose_name_plural': 'guide element section categories',
},
),
migrations.CreateModel(
name='GuideWineColorSection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=255, verbose_name='section name')),
],
options={
'verbose_name': 'guide wine color section',
'verbose_name_plural': 'guide wine color sections',
},
),
migrations.CreateModel(
name='GuideElementSection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', models.CharField(max_length=255, verbose_name='section name')),
('old_id', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='collection.GuideElementSectionCategory', verbose_name='category')),
],
options={
'verbose_name': 'guide element section',
'verbose_name_plural': 'guide element sections',
},
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.7 on 2019-12-02 14:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collection', '0020_guideelementsection_guideelementsectioncategory_guidewinecolorsection'),
]
operations = [
migrations.CreateModel(
name='GuideElementType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50, verbose_name='name')),
],
options={
'verbose_name': 'guide element type',
'verbose_name_plural': 'guide element types',
},
),
]

View File

@ -0,0 +1,48 @@
# Generated by Django 2.2.7 on 2019-12-02 14:44
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mptt.fields
class Migration(migrations.Migration):
dependencies = [
('review', '0018_auto_20191117_1117'),
('product', '0018_purchasedproduct'),
('location', '0030_auto_20191120_1010'),
('establishment', '0067_auto_20191122_1244'),
('collection', '0021_guideelementtype'),
]
operations = [
migrations.CreateModel(
name='GuideElement',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('priority', models.IntegerField(blank=True, default=None, null=True)),
('old_id', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id')),
('lft', models.PositiveIntegerField(db_index=True, editable=False)),
('rght', models.PositiveIntegerField(db_index=True, editable=False)),
('tree_id', models.PositiveIntegerField(db_index=True, editable=False)),
('level', models.PositiveIntegerField(db_index=True, editable=False)),
('city', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.City')),
('establishment', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment')),
('guide', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='collection.Guide')),
('guide_element_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='collection.GuideElementType', verbose_name='guide element type')),
('parent', mptt.fields.TreeForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='collection.GuideElement')),
('product', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='product.Product')),
('review', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='review.Review')),
('section', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='collection.GuideElementSection')),
('wine_color_section', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='collection.GuideWineColorSection')),
('wine_region', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.WineRegion')),
],
options={
'verbose_name': 'guide element',
'verbose_name_plural': 'guide elements',
},
),
]

View File

@ -0,0 +1,31 @@
# Generated by Django 2.2.7 on 2019-12-03 13:20
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('collection', '0022_guideelement'),
]
operations = [
migrations.CreateModel(
name='Advertorial',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('number_of_pages', models.PositiveIntegerField(help_text='the total number of reserved pages', verbose_name='number of pages')),
('right_pages', models.PositiveIntegerField(help_text='the number of right pages (which are part of total number).', verbose_name='number of right pages')),
('old_id', models.IntegerField(blank=True, null=True)),
('guide_element', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='advertorial', to='collection.GuideElement', verbose_name='guide element')),
],
options={
'verbose_name': 'advertorial',
'verbose_name_plural': 'advertorials',
},
),
]

View File

@ -1,5 +1,8 @@
import re
from mptt.models import MPTTModel, TreeForeignKey
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 _
@ -85,26 +88,64 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
verbose_name_plural = _('collections')
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 by_collection_id(self, collection_id):
"""Filter by collection id"""
return self.filter(collection=collection_id)
class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
"""Guide model."""
parent = models.ForeignKey(
'self', verbose_name=_('parent'), on_delete=models.CASCADE,
null=True, blank=True, default=None
BUILT = 0
WAITING = 1
REMOVING = 2
BUILDING = 3
STATE_CHOICES = (
(BUILT, 'built'),
(WAITING, 'waiting'),
(REMOVING, 'removing'),
(BUILDING, 'building'),
)
advertorials = JSONField(
_('advertorials'), null=True, blank=True,
default=None, help_text='{"key":"value"}')
collection = models.ForeignKey(Collection, on_delete=models.CASCADE,
null=True, blank=True, default=None,
verbose_name=_('collection'))
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'))
old_id = models.IntegerField(blank=True, null=True)
objects = GuideQuerySet.as_manager()
@ -116,3 +157,190 @@ class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
def __str__(self):
"""String method."""
return f'{self.name}'
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')
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 GuideElementQuerySet(models.QuerySet):
"""QuerySet for model Guide elements."""
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')
old_id = models.PositiveIntegerField(blank=True, null=True, default=None,
verbose_name=_('old id'))
objects = GuideElementQuerySet.as_manager()
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

View File

@ -56,7 +56,4 @@ class GuideSerializer(serializers.ModelSerializer):
'name',
'start',
'end',
'parent',
'advertorials',
'collection'
]

View File

@ -0,0 +1,319 @@
from pprint import pprint
from tqdm import tqdm
from establishment.models import Establishment
from review.models import Review
from location.models import WineRegion, City
from product.models import Product
from transfer.models import Guides, GuideFilters, GuideSections, GuideElements, \
GuideAds
from transfer.serializers.guide import GuideSerializer, GuideFilterSerializer
from collection.models import GuideElementSection, GuideElementSectionCategory, \
GuideWineColorSection, GuideElementType, GuideElement, \
Guide, Advertorial
def transfer_guide():
"""Transfer Guide model."""
errors = []
queryset = Guides.objects.exclude(title__icontains='test')
serialized_data = GuideSerializer(
data=list(queryset.values()),
many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
for d in serialized_data.errors: errors.append(d) if d else None
pprint(f"ERRORS: {errors}")
print(f'COUNT OF SERIALIZED OBJECTS: {queryset.values().count()}')
def transfer_guide_filter():
"""Transfer GuideFilter model."""
errors = []
queryset = GuideFilters.objects.exclude(guide__title__icontains='test') \
.exclude(guide__isnull=True)
serialized_data = GuideFilterSerializer(
data=list(queryset.values()),
many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
for d in serialized_data.errors: errors.append(d) if d else None
pprint(f'ERRORS: {errors}')
print(f"COUNT: {len(errors)}")
print(f'COUNT OF SERIALIZED OBJECTS: {queryset.values().count()}')
def transfer_guide_element_section():
"""Transfer GuideSections model."""
created_count = 0
category, _ = GuideElementSectionCategory.objects.get_or_create(
name='shop_category')
queryset_values = GuideSections.objects.values_list('id', 'value_name')
for old_id, section_name in tqdm(queryset_values):
obj, created = GuideElementSection.objects.get_or_create(
name=section_name,
category=category,
old_id=old_id,
)
if created: created_count += 1
print(f'OBJECTS CREATED: {created_count}')
def transfer_guide_wine_color_section():
"""Transfer GuideElements model (only wine color sections)."""
created_count = 0
queryset_values = GuideElements.objects.raw(
"""
select distinct(color),
1 as id
from guide_elements where color is not null;
"""
)
for section_name in tqdm([i.color for i in queryset_values]):
obj, created = GuideWineColorSection.objects.get_or_create(
name=section_name
)
if created: created_count += 1
print(f'OBJECTS CREATED: {created_count}')
def transfer_guide_element_type():
"""Transfer GuideElements model (only element types)."""
created_count = 0
queryset_values = GuideElements.objects.raw(
"""
select distinct(type),
1 as id
from guide_elements;
"""
)
for element_type in tqdm([i.type for i in queryset_values]):
obj, created = GuideElementType.objects.get_or_create(
name=element_type
)
if created: created_count += 1
print(f'OBJECTS CREATED: {created_count}')
def transfer_guide_elements_bulk():
"""Transfer Guide elements via bulk_create."""
def get_guide_element_type(guide_element_type: str):
if guide_element_type:
qs = GuideElementType.objects.filter(name__iexact=guide_element_type)
if qs.exists():
return qs.first()
def get_establishment(old_id: int):
if old_id:
qs = Establishment.objects.filter(old_id=old_id)
if qs.exists():
return qs.first()
def get_review(old_id: int):
if old_id:
qs = Review.objects.filter(old_id=old_id)
if qs.exists():
return qs.first()
def get_wine_region(old_id: int):
if old_id:
qs = WineRegion.objects.filter(old_id=old_id)
if qs.exists():
return qs.first()
def get_wine(old_id: int):
if old_id:
qs = Product.objects.filter(old_id=old_id)
if qs.exists():
return qs.first()
def get_wine_color_section(color_section: str):
if color_section:
qs = GuideWineColorSection.objects.filter(name__iexact=color_section)
if qs.exists():
return qs.first()
def get_city(old_id: int):
if old_id:
qs = City.objects.filter(old_id=old_id)
if qs.exists():
return qs.first()
def get_guide_element_section(old_id: int):
if old_id:
qs = GuideElementSection.objects.filter(old_id=old_id)
if qs.exists():
return qs.first()
def get_guide(old_id):
if old_id:
qs = Guide.objects.filter(old_id=old_id)
if qs.exists():
return qs.first()
def get_parent(old_id):
if old_id:
qs = GuideElement.objects.filter(old_id=old_id)
if qs.exists():
return qs.first()
objects_to_update = []
base_queryset = GuideElements.objects.all()
for old_id, type, establishment_id, review_id, wine_region_id, \
wine_id, color, order_number, city_id, section_id, guide_id \
in tqdm(base_queryset.filter(parent_id__isnull=True)
.values_list('id', 'type', 'establishment_id',
'review_id', 'wine_region_id', 'wine_id',
'color', 'order_number', 'city_id',
'section_id', 'guide_id'),
desc='Check parent guide elements'):
if not GuideElement.objects.filter(old_id=old_id).exists():
guide = GuideElement(
old_id=old_id,
guide_element_type=get_guide_element_type(type),
establishment=get_establishment(establishment_id),
review=get_review(review_id),
wine_region=get_wine_region(wine_region_id),
product=get_wine(wine_id),
wine_color_section=get_wine_color_section(color),
priority=order_number,
city=get_city(city_id),
section=get_guide_element_section(section_id),
parent=None,
lft=1,
rght=1,
tree_id=1,
level=1,
)
# check old guide
if not guide_id:
objects_to_update.append(guide)
else:
old_guide = Guides.objects.exclude(title__icontains='test') \
.filter(id=guide_id)
if old_guide.exists():
guide.guide = get_guide(guide_id)
objects_to_update.append(guide)
# create parents
GuideElement.objects.bulk_create(objects_to_update)
pprint(f'CREATED PARENT GUIDE ELEMENTS W/ OLD_ID: {[i.old_id for i in objects_to_update]}')
print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}')
# attach child guide elements
queryset_values = base_queryset.filter(parent_id__isnull=False) \
.order_by('-parent_id') \
.values_list('id', 'type', 'establishment_id',
'review_id', 'wine_region_id', 'wine_id',
'color', 'order_number', 'city_id',
'section_id', 'guide_id', 'parent_id')
for old_id, type, establishment_id, review_id, wine_region_id, \
wine_id, color, order_number, city_id, section_id, guide_id, parent_id \
in tqdm(sorted(queryset_values, key=lambda value: value[len(value)-1]),
desc='Check child guide elements'):
if not GuideElement.objects.filter(old_id=old_id).exists():
# check old guide
if guide_id:
old_guide = Guides.objects.exclude(title__icontains='test') \
.filter(id=guide_id)
if old_guide.exists():
GuideElement.objects.create(
old_id=old_id,
guide_element_type=get_guide_element_type(type),
establishment=get_establishment(establishment_id),
review=get_review(review_id),
wine_region=get_wine_region(wine_region_id),
product=get_wine(wine_id),
wine_color_section=get_wine_color_section(color),
priority=order_number,
city=get_city(city_id),
section=get_guide_element_section(section_id),
parent=get_parent(parent_id),
lft=1,
rght=1,
tree_id=1,
level=1,
guide=get_guide(guide_id),
)
pprint(f'CREATED CHILD GUIDE ELEMENTS W/ OLD_ID: {[i.old_id for i in objects_to_update]}')
print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}')
# rebuild trees
GuideElement._tree_manager.rebuild()
def transfer_guide_element_advertorials():
"""Transfer Guide Advertorials model."""
def get_guide_element(old_id: int):
if old_id:
qs = GuideElement.objects.filter(old_id=old_id)
legacy_qs = GuideElements.objects.exclude(guide__isnull=True) \
.exclude(guide__title__icontains='test') \
.filter(id=guide_ad_node_id)
if qs.exists() and legacy_qs.exists():
return qs.first()
elif legacy_qs.exists() and not qs.exists():
raise ValueError(f'Guide element was not transfer correctly - {old_id}.')
objects_to_update = []
advertorials = GuideAds.objects.exclude(nb_pages__isnull=True) \
.exclude(nb_right_pages__isnull=True) \
.exclude(guide_ad_node_id__isnull=True) \
.values_list('id', 'nb_pages', 'nb_right_pages',
'guide_ad_node_id')
for old_id, nb_pages, nb_right_pages, guide_ad_node_id in tqdm(advertorials):
# check guide element
guide_element = get_guide_element(guide_ad_node_id)
if not Advertorial.objects.filter(old_id=old_id).exists() and guide_element:
objects_to_update.append(
Advertorial(
old_id=old_id,
number_of_pages=nb_pages,
right_pages=nb_right_pages,
guide_element=guide_element,
)
)
# create related child
Advertorial.objects.bulk_create(objects_to_update)
pprint(f'CREATED ADVERTORIALS W/ OLD_ID: {[i.old_id for i in objects_to_update]}')
print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}')
data_types = {
'guides': [
transfer_guide,
],
'guide_filters': [
transfer_guide_filter,
],
'guide_element_sections': [
transfer_guide_element_section,
],
'guide_wine_color_sections': [
transfer_guide_wine_color_section,
],
'guide_element_types': [
transfer_guide_element_type,
],
'guide_elements_bulk': [
transfer_guide_elements_bulk,
],
'guide_element_advertorials': [
transfer_guide_element_advertorials
],
'guide_complete': [
transfer_guide, # transfer guides from Guides
transfer_guide_filter, # transfer guide filters from GuideFilters
transfer_guide_element_section, # partial transfer element section from GuideSections
transfer_guide_wine_color_section, # partial transfer wine color section from GuideSections
transfer_guide_element_type, # partial transfer section types from GuideElements
transfer_guide_elements_bulk, # transfer result of GuideFilters from GuideElements
transfer_guide_element_advertorials, # transfer advertorials that linked to GuideElements
]
}

View File

@ -35,6 +35,7 @@ class ContactPhoneInline(admin.TabularInline):
class GalleryImageInline(admin.TabularInline):
"""Gallery image inline admin."""
model = models.EstablishmentGallery
raw_id_fields = ['image', ]
extra = 0
@ -61,17 +62,20 @@ class ProductInline(admin.TabularInline):
class CompanyInline(admin.TabularInline):
model = models.Company
raw_id_fields = ['establishment', 'address']
extra = 0
class EstablishmentNote(admin.TabularInline):
model = models.EstablishmentNote
extra = 0
raw_id_fields = ['user', ]
class PurchasedProduct(admin.TabularInline):
class PurchasedProductInline(admin.TabularInline):
model = PurchasedProduct
extra = 0
raw_id_fields = ['product', ]
@admin.register(models.Establishment)
@ -80,13 +84,12 @@ class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin):
list_display = ['id', '__str__', 'image_tag', ]
search_fields = ['id', 'name', 'index_name', 'slug']
list_filter = ['public_mark', 'toque_number']
inlines = [GalleryImageInline, CompanyInline, EstablishmentNote,
PurchasedProduct]
inlines = [CompanyInline, EstablishmentNote, GalleryImageInline,
PurchasedProductInline, ]
# inlines = [
# AwardInline, ContactPhoneInline, ContactEmailInline,
# ReviewInline, CommentInline, ProductInline]
raw_id_fields = ('address',)
raw_id_fields = ('address', 'collections', 'tags', 'schedule')
@admin.register(models.Position)
@ -136,3 +139,4 @@ class SocialNetworkAdmin(BaseModelAdminMixin, admin.ModelAdmin):
class CompanyAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Admin conf for Company model."""
raw_id_fields = ['establishment', 'address', ]

View File

@ -0,0 +1,30 @@
from django.core.management.base import BaseCommand
from location.models import WineOriginAddress, EstablishmentWineOriginAddress
from product.models import Product
class Command(BaseCommand):
help = 'Add to establishment wine origin object.'
def handle(self, *args, **kwarg):
create_counter = 0
for product in Product.objects.exclude(establishment__isnull=True):
establishment = product.establishment
if product.wine_origins.exists():
for wine_origin in product.wine_origins.all():
wine_region = wine_origin.wine_region
wine_sub_region = wine_origin.wine_sub_region
if not EstablishmentWineOriginAddress.objects.filter(establishment=establishment,
wine_region=wine_region,
wine_sub_region=wine_sub_region) \
.exists():
EstablishmentWineOriginAddress.objects.create(
establishment=establishment,
wine_region=wine_origin.wine_region,
wine_sub_region=wine_origin.wine_sub_region,
)
create_counter += 1
self.stdout.write(self.style.WARNING(f'COUNT CREATED OBJECTS: {create_counter}'))

View File

@ -1,8 +1,8 @@
"""Establishment models."""
from datetime import datetime
from functools import reduce
from typing import List
from operator import or_
from typing import List
import elasticsearch_dsl
from django.conf import settings
@ -22,6 +22,7 @@ from timezone_field import TimeZoneField
from collection.models import Collection
from location.models import Address
from location.models import WineOriginAddressMixin
from main.models import Award, Currency
from tag.models import Tag
from review.models import Review
@ -251,6 +252,15 @@ class EstablishmentQuerySet(models.QuerySet):
return self.filter(id__in=subquery_filter_by_distance) \
.order_by('-reviews__published_at')
def prefetch_comments(self):
"""Prefetch last comment."""
from comment.models import Comment
return self.prefetch_related(
models.Prefetch('comments',
queryset=Comment.objects.exclude(is_publish=False).order_by('-created'),
to_attr='comments_prefetched')
)
def prefetch_actual_employees(self):
"""Prefetch actual employees."""
return self.prefetch_related(
@ -448,6 +458,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
return super().visible_tags \
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de']) \
.exclude(value__in=['rss', 'rss_selection']) \
\
# todo: recalculate toque_number
@ -614,6 +625,15 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
def artisan_category_indexing(self):
return self.tags.filter(category__index_name='shop_category')
@property
def last_comment(self):
if hasattr(self, 'comments_prefetched') and len(self.comments_prefetched):
return self.comments_prefetched[0]
@property
def wine_origins_unique(self):
return self.wine_origins.distinct('wine_region')
class EstablishmentNoteQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentNote."""

View File

@ -16,6 +16,8 @@ from utils import exceptions as utils_exceptions
from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer
from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer)
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -283,6 +285,8 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes')
image = serializers.URLField(source='image_url', read_only=True)
wine_regions = EstablishmentWineRegionBaseSerializer(many=True, source='wine_origins_unique',
read_only=True, allow_null=True)
preview_image = serializers.URLField(source='preview_image_url',
allow_null=True,
read_only=True)
@ -312,6 +316,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'preview_image',
'new_image',
'tz',
'wine_regions',
]
@ -372,6 +377,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
range_price_carte = RangePriceSerializer(read_only=True)
vintage_year = serializers.ReadOnlyField()
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
wine_origins = EstablishmentWineOriginBaseSerializer(many=True, read_only=True)
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
@ -398,6 +404,20 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
'transportation',
'vintage_year',
'gallery',
'wine_origins',
]
class MobileEstablishmentDetailSerializer(EstablishmentDetailSerializer):
"""Serializer for Establishment model for mobiles."""
last_comment = comment_serializers.CommentRUDSerializer(allow_null=True)
class Meta(EstablishmentDetailSerializer.Meta):
"""Meta class."""
fields = EstablishmentDetailSerializer.Meta.fields + [
'last_comment',
]
@ -405,6 +425,16 @@ class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
address = AddressDetailSerializer(read_only=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer()
artisan_category = TagBaseSerializer(many=True, allow_null=True)
class Meta(EstablishmentBaseSerializer.Meta):
fields = EstablishmentBaseSerializer.Meta.fields + [
'schedule',
'establishment_type',
'artisan_category',
]
class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer):

View File

@ -9,7 +9,6 @@ urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'),
path('slug/<slug:slug>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('slug/<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
path('slug/<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
path('slug/<slug:slug>/comments/create/', views.EstablishmentCommentCreateView.as_view(),

View File

@ -5,7 +5,8 @@ from establishment import views
from establishment.urls.common import urlpatterns as common_urlpatterns
urlpatterns = [
path('geo/', views.EstablishmentNearestRetrieveView.as_view(), name='nearest-establishments-list')
path('geo/', views.EstablishmentNearestRetrieveView.as_view(), name='nearest-establishments-list'),
path('slug/<slug:slug>/', views.EstablishmentMobileRetrieveView.as_view(), name='mobile-detail'),
]
urlpatterns.extend(common_urlpatterns)

View File

@ -1,7 +1,11 @@
"""Establishment app web urlconf."""
from establishment.urls.common import urlpatterns as common_urlpatterns
from django.urls import path
from establishment import views
urlpatterns = []
urlpatterns = [
path('slug/<slug:slug>/', views.EstablishmentRetrieveView.as_view(), name='web-detail'),
]
urlpatterns.extend(common_urlpatterns)

View File

@ -165,7 +165,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView):
pagination_class = None
class EstablishmentEmployeeListView(generics.ListAPIView):
class EstablishmentEmployeeListView(generics.ListCreateAPIView):
"""Establishment emplyoees list view."""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.EstablishmentEmployeeBackSerializer

View File

@ -51,6 +51,13 @@ class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView
return super().get_queryset().with_extended_related()
class EstablishmentMobileRetrieveView(EstablishmentRetrieveView):
serializer_class = serializers.MobileEstablishmentDetailSerializer
def get_queryset(self):
return super().get_queryset().prefetch_comments()
class EstablishmentRecentReviewListView(EstablishmentListView):
"""List view for last reviewed establishments."""

View File

@ -29,7 +29,8 @@ class FavoritesEstablishmentListView(generics.ListAPIView):
def get_queryset(self):
"""Override get_queryset method"""
return Establishment.objects.filter(favorites__user=self.request.user) \
.order_by('-favorites')
.order_by('-favorites').with_base_related() \
.with_certain_tag_category_related('shop_category', 'artisan_category')
class FavoritesProductListView(generics.ListAPIView):

View File

@ -45,3 +45,15 @@ class AddressAdmin(admin.OSMGeoAdmin):
def geo_lat(self, item):
if isinstance(item.coordinates, Point):
return item.coordinates.y
@admin.register(models.EstablishmentWineOriginAddress)
class EstablishmentWineOriginAddress(admin.ModelAdmin):
"""Admin model for EstablishmentWineOriginAddress."""
raw_id_fields = ['establishment', ]
@admin.register(models.WineOriginAddress)
class WineOriginAddress(admin.ModelAdmin):
"""Admin page for model WineOriginAddress."""
raw_id_fields = ['product', ]

View File

@ -0,0 +1,42 @@
# Generated by Django 2.2.7 on 2019-12-04 14:20
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('establishment', '0067_auto_20191122_1244'),
('product', '0019_auto_20191204_1420'),
('location', '0030_auto_20191120_1010'),
]
operations = [
migrations.CreateModel(
name='WineOriginAddress',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wine_origins', to='product.Product', verbose_name='product')),
('wine_region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='location.WineRegion', verbose_name='wine region')),
('wine_sub_region', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='location.WineSubRegion', verbose_name='wine sub region')),
],
options={
'verbose_name': 'wine origin address',
'verbose_name_plural': 'wine origin addresses',
},
),
migrations.CreateModel(
name='EstablishmentWineOriginAddress',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='wine_origins', to='establishment.Establishment', verbose_name='product')),
('wine_region', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='location.WineRegion', verbose_name='wine region')),
('wine_sub_region', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='location.WineSubRegion', verbose_name='wine sub region')),
],
options={
'verbose_name': 'establishment wine origin address',
'verbose_name_plural': 'establishment wine origin addresses',
},
),
]

View File

@ -192,12 +192,12 @@ class WineRegionQuerySet(models.QuerySet):
def with_sub_region_related(self):
return self.prefetch_related('wine_sub_region')
def having_wines(self, value = True):
def having_wines(self, value=True):
"""Return qs with regions, which have any wine related to them"""
return self.exclude(wines__isnull=value)
return self.exclude(wineoriginaddress__product__isnull=value)
class WineRegion(models.Model, TranslatedFieldsMixin):
class WineRegion(TranslatedFieldsMixin, models.Model):
"""Wine region model."""
name = models.CharField(_('name'), max_length=255)
country = models.ForeignKey(Country, on_delete=models.PROTECT,
@ -278,6 +278,55 @@ class WineVillage(models.Model):
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):

View File

@ -191,10 +191,58 @@ class WineSubRegionBaseSerializer(serializers.ModelSerializer):
]
class WineRegionSerializer(WineRegionBaseSerializer):
"""Wine region w/ subregion serializer"""
class EstablishmentWineRegionBaseSerializer(serializers.ModelSerializer):
"""Establishment wine region origin serializer."""
wine_sub_region = WineSubRegionBaseSerializer(allow_null=True, many=True)
id = serializers.IntegerField(source='wine_region.id')
name = serializers.CharField(source='wine_region.name')
country = CountrySerializer(source='wine_region.country')
class Meta:
"""Meta class."""
model = models.EstablishmentWineOriginAddress
fields = [
'id',
'name',
'country',
]
class EstablishmentWineOriginBaseSerializer(serializers.ModelSerializer):
"""Serializer for intermediate model EstablishmentWineOrigin."""
wine_region = WineRegionBaseSerializer()
wine_sub_region = WineSubRegionBaseSerializer(allow_null=True)
class Meta:
"""Meta class."""
model = models.EstablishmentWineOriginAddress
fields = [
'wine_region',
'wine_sub_region',
]
class WineOriginRegionBaseSerializer(EstablishmentWineRegionBaseSerializer):
"""Product wine region origin serializer."""
class Meta(EstablishmentWineRegionBaseSerializer.Meta):
"""Meta class."""
model = models.WineOriginAddress
class WineOriginBaseSerializer(EstablishmentWineOriginBaseSerializer):
"""Serializer for intermediate model ProductWineOrigin."""
class Meta(EstablishmentWineOriginBaseSerializer.Meta):
"""Meta class."""
model = models.WineOriginAddress
class WineRegionSerializer(serializers.ModelSerializer):
"""Wine region w/ sub region serializer"""
wine_sub_region = WineSubRegionBaseSerializer(source='wine_region.wine_sub_region',
allow_null=True, many=True)
class Meta(WineRegionBaseSerializer.Meta):
fields = WineRegionBaseSerializer.Meta.fields + [

View File

@ -1,5 +1,6 @@
"""Product admin conf."""
from django.contrib import admin
from utils.admin import BaseModelAdminMixin
from .models import Product, ProductType, ProductSubType, ProductGallery, Unit

View File

@ -0,0 +1,44 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from location.models import WineOriginAddress
from product.models import Product
from transfer.models import Products
from transfer.serializers.product import ProductSerializer
class Command(BaseCommand):
help = 'Add to product wine origin object.'
def handle(self, *args, **kwarg):
def get_product(old_id: int):
if old_id:
qs = Product.objects.filter(old_id=old_id)
if qs.exists():
return qs.first()
objects_to_create = []
products = Products.objects.exclude(wine_region_id__isnull=True) \
.values_list('id', 'wine_sub_region_id', 'wine_region_id')
for old_id, wine_sub_region_id, wine_region_id in tqdm(products):
product = get_product(old_id)
if product:
wine_sub_region = ProductSerializer.get_wine_sub_region(wine_sub_region_id)
wine_region = ProductSerializer.get_wine_region(wine_region_id)
if wine_region:
filters = {
'product': product,
'wine_region': wine_region}
wine_origin_address = WineOriginAddress(
product=product,
wine_region=wine_region)
if wine_sub_region:
filters.update({'wine_sub_region': wine_sub_region})
wine_origin_address.wine_sub_region = wine_sub_region
if not WineOriginAddress.objects.filter(**filters).exists():
objects_to_create.append(wine_origin_address)
WineOriginAddress.objects.bulk_create(objects_to_create)
self.stdout.write(self.style.WARNING(f'COUNT CREATED OBJECTS: {len(objects_to_create)}'))

View File

@ -0,0 +1,21 @@
# Generated by Django 2.2.7 on 2019-12-04 14:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('product', '0018_purchasedproduct'),
]
operations = [
migrations.RemoveField(
model_name='product',
name='wine_region',
),
migrations.RemoveField(
model_name='product',
name='wine_sub_region',
),
]

View File

@ -2,11 +2,12 @@
from django.contrib.contenttypes import fields as generic
from django.contrib.gis.db import models as gis_models
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Case, When
from django.utils.translation import gettext_lazy as _
from django.core.validators import MaxValueValidator, MinValueValidator
from location.models import WineOriginAddressMixin
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
GalleryModelMixin, IntermediateGalleryModelMixin)
@ -89,8 +90,8 @@ class ProductQuerySet(models.QuerySet):
'establishment__address__city', 'establishment__address__city__country',
'establishment__establishment_subtypes', 'product_gallery',
'gallery', 'product_type', 'subtypes',
'classifications__classification_type', 'classifications__tags') \
.select_related('wine_region', 'wine_sub_region')
'classifications__classification_type', 'classifications__tags',
'wine_origins__wine_region', 'wine_origins__wine_sub_region', )
def common(self):
return self.filter(category=self.model.COMMON)
@ -176,14 +177,6 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
verbose_name=_('establishment'))
public_mark = models.PositiveIntegerField(blank=True, null=True, default=None,
verbose_name=_('public mark'), )
wine_region = models.ForeignKey('location.WineRegion', on_delete=models.PROTECT,
related_name='wines',
blank=True, null=True, default=None,
verbose_name=_('wine region'))
wine_sub_region = models.ForeignKey('location.WineSubRegion', on_delete=models.PROTECT,
related_name='wines',
blank=True, null=True, default=None,
verbose_name=_('wine sub region'))
classifications = models.ManyToManyField('ProductClassification',
blank=True,
verbose_name=_('classifications'))

View File

@ -62,8 +62,9 @@ class ProductBackOfficeDetailSerializer(ProductDetailSerializer):
'available',
'product_type',
'establishment',
'wine_region',
'wine_sub_region',
# todo: need fix
# 'wine_region',
# 'wine_sub_region',
'wine_village',
'state',
]

View File

@ -11,7 +11,7 @@ from review.serializers import ReviewShortSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import TranslatedField, FavoritesCreateSerializer, ImageBaseSerializer
from main.serializers import AwardSerializer
from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer
from location.serializers import WineOriginRegionBaseSerializer, WineOriginBaseSerializer
from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer
@ -90,7 +90,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
subtypes = ProductSubTypeBaseSerializer(many=True, read_only=True)
establishment_detail = EstablishmentProductShortSerializer(source='establishment', read_only=True)
tags = ProductTagSerializer(source='related_tags', many=True, read_only=True)
wine_region = WineRegionBaseSerializer(read_only=True)
wine_regions = WineOriginRegionBaseSerializer(many=True, source='wine_origins', read_only=True)
wine_colors = TagBaseSerializer(many=True, read_only=True)
preview_image_url = serializers.URLField(allow_null=True,
read_only=True)
@ -110,7 +110,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
'vintage',
'tags',
'preview_image_url',
'wine_region',
'wine_regions',
'wine_colors',
'in_favorites',
]
@ -124,7 +124,7 @@ class ProductDetailSerializer(ProductBaseSerializer):
awards = AwardSerializer(many=True, read_only=True)
classifications = ProductClassificationBaseSerializer(many=True, read_only=True)
standards = ProductStandardBaseSerializer(many=True, read_only=True)
wine_sub_region = WineSubRegionBaseSerializer(read_only=True)
wine_origins = WineOriginBaseSerializer(many=True, read_only=True)
bottles_produced = TagBaseSerializer(many=True, read_only=True)
sugar_contents = TagBaseSerializer(many=True, read_only=True)
grape_variety = TagBaseSerializer(many=True, read_only=True)
@ -141,7 +141,7 @@ class ProductDetailSerializer(ProductBaseSerializer):
'awards',
'classifications',
'standards',
'wine_sub_region',
'wine_origins',
'bottles_produced',
'sugar_contents',
'image_url',

View File

@ -83,39 +83,66 @@ class EstablishmentDocument(Document):
multi=True)
products = fields.ObjectField(
properties={
'wine_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'country': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'code': fields.KeywordField(),
}),
# 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES),
}),
'wine_origins': fields.ListField(
fields.ObjectField(
properties={
'wine_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'country': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'code': fields.KeywordField(),
}),
# 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing',
properties=OBJECT_FIELD_PROPERTIES)
}),
'wine_sub_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
}),
})),
'wine_colors': fields.ObjectField(
properties={
'id': fields.IntegerField(),
'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True,
),
'wine_sub_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
}),
},
multi=True,)},
multi=True
)
wine_origins = fields.ListField(
fields.ObjectField(
properties={
'wine_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'country': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'code': fields.KeywordField(),
}),
# 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing',
properties=OBJECT_FIELD_PROPERTIES)
}),
'wine_sub_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
})})
)
schedule = fields.ListField(fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
'weekday': fields.IntegerField(attr='weekday'),
'weekday_display': fields.KeywordField(attr='get_weekday_display'),
'closed_at': fields.KeywordField(attr='closed_at_str'),
'opening_at': fields.KeywordField(attr='opening_at_str'),
}
))
address = fields.ObjectField(

View File

@ -83,22 +83,28 @@ class ProductDocument(Document):
},
multi=True,
)
wine_region = fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'country': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'code': fields.KeywordField(),
}),
# 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES),
})
wine_sub_region = fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
})
wine_origins = fields.ListField(
fields.ObjectField(
properties={
'wine_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'country': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'code': fields.KeywordField(),
}),
# 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing',
properties=OBJECT_FIELD_PROPERTIES)
}),
'wine_sub_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
})})
)
classifications = fields.ObjectField( # TODO
properties={
'classification_type': fields.ObjectField(properties={}),

View File

@ -13,24 +13,13 @@ class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend):
@staticmethod
def calculate_center(first, second):
if second[1] < 0 < first[1]:
reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1])
diff = (reverse_first + reverse_second) / 2
if reverse_first < reverse_second:
result_part = -180 + (180 + second[1] - diff)
else:
result_part = 180 - (180 - first[1] - diff)
elif second[1] < 0 > first[1] or second[1] > 0 < first[1]:
reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1])
result_part = ((reverse_first + reverse_second) / 2) * (-1 + (second[1] < 0) * 2)
if second[1] < first[1]:
res_longtitude = first[1] + (360 + abs(first[1]) - abs(second[1])) / 2
else:
result_part = (first[1] + second[1]) / 2
res_longtitude = first[1] + (second[1] - first[1]) / 2
return (first[0] + second[0]) / 2, result_part
# return (first[0] + second[0]) / 2, result_part
return (first[0] + second[0]) / 2, res_longtitude
def filter_queryset(self, request, queryset, view):
ret = super().filter_queryset(request, queryset, view)

View File

@ -69,6 +69,16 @@ class WineRegionDocumentSerializer(serializers.Serializer):
return instance.wine_region if instance and instance.wine_region else None
class WineSubRegionDocumentSerializer(serializers.Serializer):
"""Wine region ES document serializer."""
id = serializers.IntegerField()
name = serializers.CharField()
def get_attribute(self, instance):
return instance.wine_sub_region if instance and instance.wine_sub_region else None
class TagDocumentSerializer(serializers.Serializer):
"""Tag ES document serializer,"""
@ -168,6 +178,7 @@ class ScheduleDocumentSerializer(serializers.Serializer):
weekday = serializers.IntegerField()
weekday_display = serializers.CharField()
closed_at = serializers.CharField()
opening_at = serializers.CharField()
class InFavoritesMixin(DocumentSerializer):
@ -222,6 +233,13 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
return get_translated_value(obj.subtitle)
class WineOriginSerializer(serializers.Serializer):
"""Wine origin serializer."""
wine_region = WineRegionDocumentSerializer()
wine_sub_region = WineSubRegionDocumentSerializer(allow_null=True)
class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
"""Establishment document serializer."""
@ -233,6 +251,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
restaurant_cuisine = TagsDocumentSerializer(many=True, allow_null=True)
artisan_category = TagsDocumentSerializer(many=True, allow_null=True)
schedule = ScheduleDocumentSerializer(many=True, allow_null=True)
wine_origins = WineOriginSerializer(many=True)
class Meta:
"""Meta class."""
@ -258,6 +277,7 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'works_evening',
'works_at_weekday',
'tz',
'wine_origins',
# 'works_now',
# 'collections',
# 'establishment_type',
@ -270,11 +290,11 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer):
tags = TagsDocumentSerializer(many=True, source='related_tags')
subtypes = ProductSubtypeDocumentSerializer(many=True, allow_null=True)
wine_region = WineRegionDocumentSerializer(allow_null=True)
wine_colors = TagDocumentSerializer(many=True)
grape_variety = TagDocumentSerializer(many=True)
product_type = ProductTypeDocumentSerializer(allow_null=True)
establishment_detail = ProductEstablishmentDocumentSerializer(source='establishment', allow_null=True)
wine_origins = WineOriginSerializer(many=True)
class Meta:
"""Meta class."""
@ -295,10 +315,10 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'tags',
'product_type',
'subtypes',
'wine_region',
'wine_colors',
'grape_variety',
'establishment_detail',
'average_price',
'created',
'wine_origins',
)

View File

@ -158,7 +158,15 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
},
},
'wine_region_id': {
'field': 'products.wine_region.id',
'field': 'wine_origins.wine_region.id',
'facet': TermsFacet,
'enabled': True,
'options': {
'size': utils.FACET_MAX_RESPONSE,
},
},
'wine_sub_region_id': {
'field': 'wine_origins.wine_sub_region.id',
'facet': TermsFacet,
'enabled': True,
'options': {
@ -213,14 +221,14 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
],
},
'wine_region_id': {
'field': 'products.wine_region.id',
'field': 'wine_origins.wine_region.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'wine_sub_region_id': {
'field': 'products.wine_sub_region_id',
'field': 'wine_origins.wine_sub_region.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
@ -346,7 +354,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
faceted_search_fields = {
'tag': {
'field': 'wine_colors.id',
'field': 'tags.id',
'enabled': True,
'facet': TermsFacet,
'options': {
@ -354,13 +362,21 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
},
},
'wine_region_id': {
'field': 'wine_region.id',
'enabled': True,
'field': 'wine_origins.wine_region.id',
'facet': TermsFacet,
'enabled': True,
'options': {
'size': utils.FACET_MAX_RESPONSE,
},
},
'wine_sub_region_id': {
'field': 'wine_origins.wine_sub_region.id',
'facet': TermsFacet,
'enabled': True,
'options': {
'size': utils.FACET_MAX_RESPONSE,
},
}
}
translated_search_fields = (
@ -384,14 +400,14 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
],
},
'wine_region_id': {
'field': 'wine_region.id',
'field': 'wine_origins.wine_region.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'wine_sub_region_id': {
'field': 'wine_sub_region_id',
'field': 'wine_origins.wine_sub_region.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
@ -404,9 +420,9 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
constants.LOOKUP_QUERY_EXCLUDE,
]
},
'wine_from_country_code': {
'field': 'wine_region.country.code',
},
# 'wine_from_country_code': {
# 'field': 'wine_origins.wine_region.country.code',
# },
'for_establishment': {
'field': 'establishment.slug',
},

View File

@ -39,6 +39,10 @@ class Timetable(ProjectBaseMixin):
def closed_at_str(self):
return str(self.closed_at) if self.closed_at else None
@property
def opening_at_str(self):
return str(self.opening_at) if self.opening_at else None
@property
def opening_time(self):
return self.opening_at or self.lunch_start or self.dinner_start

View File

@ -41,6 +41,14 @@ class Command(BaseCommand):
'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1
'purchased_plaques', # №6 - перенос купленных тарелок
'fill_city_gallery', # №3 - перенос галереи городов
'guides',
'guide_filters',
'guide_element_sections',
'guide_wine_color_sections',
'guide_element_types',
'guide_elements_bulk',
'guide_element_advertorials',
'guide_complete',
]
def handle(self, *args, **options):

View File

@ -5,11 +5,19 @@
# * Make sure each ForeignKey has `on_delete` set to the desired behavior.
# * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table
# Feel free to rename the models, but don't rename db_table values or field names.
import yaml
from django.contrib.gis.db import models
from transfer.mixins import MigrateMixin
def convert_entry(loader, node):
return {e[0]: e[1] for e in loader.construct_pairs(node)}
yaml.add_constructor('!ruby/hash:ActiveSupport::HashWithIndifferentAccess', convert_entry)
# models.ForeignKey(ForeignModel, models.DO_NOTHING, blank=True, null=True)
class Sites(MigrateMixin):
@ -362,7 +370,7 @@ class GuideFilters(MigrateMixin):
states = models.CharField(max_length=255, blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
guide_id = models.IntegerField(blank=True, null=True)
guide = models.ForeignKey(Guides, models.DO_NOTHING, blank=True, null=True)
class Meta:
managed = False
@ -381,7 +389,7 @@ class GuideSections(MigrateMixin):
class Meta:
managed = False
db_table = 'guide_elements'
db_table = 'guide_sections'
class GuideElements(MigrateMixin):
@ -398,7 +406,7 @@ class GuideElements(MigrateMixin):
guide_ad = models.ForeignKey(GuideAds, models.DO_NOTHING, blank=True, null=True)
city = models.ForeignKey(Cities, models.DO_NOTHING, blank=True, null=True)
section = models.ForeignKey('GuideSections', models.DO_NOTHING, blank=True, null=True)
guide_id = models.IntegerField(blank=True, null=True)
guide = models.ForeignKey('Guides', models.DO_NOTHING, blank=True, null=True)
parent = models.ForeignKey('self', models.DO_NOTHING, blank=True, null=True)
lft = models.IntegerField()
rgt = models.IntegerField()

View File

@ -0,0 +1,356 @@
from itertools import chain
import yaml
from django.utils.text import slugify
from pycountry import countries, subdivisions
from rest_framework import serializers
from collection import models
from establishment.models import EstablishmentType
from location.models import Country, Region, WineRegion
from main.models import SiteSettings
from review.models import Review
from transfer.mixins import TransferSerializerMixin
from translation.models import Language
class GuideSerializer(TransferSerializerMixin):
id = serializers.IntegerField()
title = serializers.CharField()
vintage = serializers.IntegerField()
slug = serializers.CharField()
state = serializers.CharField()
site_id = serializers.IntegerField()
inserter_field = serializers.CharField()
class Meta:
model = models.Guide
fields = (
'id',
'title',
'vintage',
'slug',
'state',
'site_id',
'inserter_field',
)
def validate(self, attrs):
"""Overridden validate method."""
attrs['old_id'] = attrs.pop('id')
attrs['name'] = attrs.pop('title')
attrs['vintage'] = int(attrs.pop('vintage'))
attrs['state'] = self.get_state(attrs.pop('state'))
attrs['site'] = self.get_site(attrs.pop('site_id'))
attrs['guide_type'] = self.get_guide_type(attrs.pop('inserter_field'))
return attrs
def get_state(self, state: str):
if state == 'built':
return models.Guide.BUILT
elif state == 'removing':
return models.Guide.REMOVING
elif state == 'building':
return models.Guide.BUILDING
else:
return models.Guide.WAITING
def get_site(self, site_id):
qs = SiteSettings.objects.filter(old_id=site_id)
if qs.exists():
return qs.first()
def get_guide_type(self, inserter_field):
guide_type, _ = models.GuideType.objects.get_or_create(name=inserter_field)
return guide_type
class GuideFilterSerializer(TransferSerializerMixin):
id = serializers.IntegerField()
year = serializers.CharField(allow_null=True)
establishment_type = serializers.CharField(allow_null=True)
countries = serializers.CharField(allow_null=True)
regions = serializers.CharField(allow_null=True)
subregions = serializers.CharField(allow_null=True)
wine_regions = serializers.CharField(allow_null=True)
locales = serializers.CharField(allow_null=True)
states = serializers.CharField(allow_null=True)
max_mark = serializers.FloatField(allow_null=True)
min_mark = serializers.FloatField(allow_null=True)
marks_only = serializers.NullBooleanField()
guide_id = serializers.IntegerField()
class Meta:
model = models.GuideFilter
fields = (
'id',
'year',
'establishment_type',
'countries',
'regions',
'subregions',
'wine_regions',
'max_mark',
'min_mark',
'marks_only',
'locales',
'states',
'guide_id',
)
def create(self, validated_data):
qs = self.Meta.model.objects.filter(guide=validated_data.get('guide'))
if not qs.exists():
return super().create(validated_data)
@staticmethod
def parse_ruby_helper(raw_value: str):
"""Parse RubyActiveSupport records"""
def convert_entry(loader, node):
return {e[0]: e[1] for e in loader.construct_pairs(node)}
loader = yaml.Loader
loader.add_constructor('!ruby/hash:ActiveSupport::HashWithIndifferentAccess', convert_entry)
return yaml.load(raw_value, Loader=loader) if raw_value else None
@staticmethod
def parse_dictionary(dictionary: dict):
"""
Exclude root values from dictionary.
Convert {key_2: [value_1, value_2]} into
[value_1, value_2]
"""
return list(chain.from_iterable(dictionary.values()))
@staticmethod
def parse_nested_dictionary(dictionary: dict):
"""
Exclude root values from dictionary.
Convert {key_1: {key_2: [value_1, value_2]}} into
[value_1, value_2]
"""
l = []
for i in dictionary:
l.append(list(chain.from_iterable(list(dictionary[i].values()))))
return list(chain.from_iterable(l))
def get_country(self, code_alpha_3: str) -> Country:
country = countries.get(alpha_3=code_alpha_3.upper())
if country:
country_name = country.name
country_code = country.alpha_2
if country_name and country_code:
country, _ = Country.objects.get_or_create(
code__icontains=country_code,
name__contains={'en-GB': country_name},
defaults={
'code': country_code,
'name': {'en-GB': country_name}
}
)
return country
def get_region(self, region_code_alpha_3: str,
country_code_alpha_3: str,
sub_region_code_alpha_3: str = None):
country = self.get_country(country_code_alpha_3)
if country:
country_code_alpha_2 = country.code.upper()
region_qs = Region.objects.filter(code__iexact=region_code_alpha_3,
country__code__iexact=country_code_alpha_2)
if region_qs.exists():
return region_qs.first()
# If region isn't existed, check sub region for parent_code (region code)
if sub_region_code_alpha_3:
# sub region
subdivision = subdivisions.get(
code=f"{country_code_alpha_2}-{sub_region_code_alpha_3}")
if subdivision:
# try with parent code
subdivision_region = subdivisions.get(code=subdivision.__dict__.get('_fields')
.get('parent_code'))
if not subdivision_region:
# try with parent
subdivision_region = subdivisions.get(code=subdivision.__dict__.get('_fields')
.get('parent'))
if subdivision_region:
obj = Region.objects.create(
name=subdivision_region.name,
code=subdivision_region.code,
country=country)
return obj
def validate_year(self, value):
return self.parse_ruby_helper(value)
def validate_establishment_type(self, value):
return self.parse_ruby_helper(value)
def validate_countries(self, value):
return self.parse_ruby_helper(value)
def validate_regions(self, value):
return self.parse_ruby_helper(value)
def validate_subregions(self, value):
return self.parse_ruby_helper(value)
def validate_wine_regions(self, value):
return self.parse_ruby_helper(value)
def validate_wine_classifications(self, value):
return self.parse_ruby_helper(value)
def validate_wine_colors(self, value):
return self.parse_ruby_helper(value)
def validate_wine_types(self, value):
return self.parse_ruby_helper(value)
def validate_locales(self, value):
return self.parse_ruby_helper(value)
def validate_states(self, value):
return self.parse_ruby_helper(value)
def validate(self, attrs):
sub_regions = attrs.pop('subregions')
regions = attrs.pop('regions')
attrs['old_id'] = attrs.pop('id')
attrs['review_vintage_json'] = self.get_review_vintage(attrs.pop('year'))
attrs['establishment_type_json'] = self.get_establishment_type_ids(
attrs.pop('establishment_type'))
attrs['country_json'] = self.get_country_ids(attrs.pop('countries'))
attrs['region_json'] = self.get_region_ids(regions=regions,
sub_regions=sub_regions)
attrs['sub_region_json'] = self.get_sub_region_ids(sub_regions)
attrs['wine_region_json'] = self.get_wine_region_ids(attrs.pop('wine_regions'))
attrs['with_mark'] = attrs.pop('marks_only') or True
attrs['locale_json'] = self.get_locale_ids(attrs.pop('locales'))
attrs['review_state_json'] = self.get_review_state(attrs.pop('states'))
attrs['guide'] = self.get_guide(attrs.pop('guide_id'))
return attrs
def get_review_vintage(self, year):
if hasattr(year, '__iter__'):
return {'vintage': list(set(int(i) for i in set(year) if i.isdigit()))}
return {'vintage': [year, ]}
def get_establishment_type_ids(self, establishment_types):
establishment_type_ids = []
if establishment_types:
for establishment_type in establishment_types:
establishment_type_qs = EstablishmentType.objects.filter(index_name__iexact=establishment_type)
if not establishment_type_qs.exists():
obj = EstablishmentType.objects.create(
name={'en-GB': establishment_type.capitalize()},
index_name=slugify(establishment_type))
else:
obj = establishment_type_qs.first()
establishment_type_ids.append(obj.id)
return {'id': list(set(establishment_type_ids))}
def get_country_ids(self, country_codes_alpha_3):
country_ids = []
if country_codes_alpha_3:
for code_alpha_3 in country_codes_alpha_3:
# Code can be an empty string.
if code_alpha_3 and not code_alpha_3 == 'AAA':
country = self.get_country(code_alpha_3)
if not country:
raise serializers.ValidationError({'detail': f'Country with alpha code 3 -'
f'{code_alpha_3}, is not found.'})
country_ids.append(country.id)
return {'id': list(set(country_ids))}
def get_region_ids(self, regions, sub_regions):
region_ids = []
if regions:
for country_code_alpha_3 in regions:
for region_code_alpha_3 in regions[country_code_alpha_3]:
# Get region from sub region code.
if sub_regions and country_code_alpha_3 in sub_regions:
if region_code_alpha_3 in sub_regions[country_code_alpha_3]:
for sub_region_code_alpha_3 in sub_regions[country_code_alpha_3][region_code_alpha_3]:
region = self.get_region(
region_code_alpha_3=region_code_alpha_3,
country_code_alpha_3=country_code_alpha_3,
sub_region_code_alpha_3=sub_region_code_alpha_3)
if region:
region_ids.append(region.id)
return {'id': list(set(region_ids))}
def get_sub_region_ids(self, sub_regions):
sub_region_ids = []
if sub_regions:
for country_code_alpha_3 in sub_regions:
# FRA etc.
if country_code_alpha_3 in sub_regions:
for region_code_alpha_3 in sub_regions[country_code_alpha_3]:
# B, C, A etc.
if region_code_alpha_3 in sub_regions[country_code_alpha_3]:
for sub_region_code_alpha_3 in sub_regions[country_code_alpha_3][region_code_alpha_3]:
# 24, 32 etc.
# Get parent region
region = self.get_region(
region_code_alpha_3=region_code_alpha_3,
country_code_alpha_3=country_code_alpha_3,
sub_region_code_alpha_3=sub_region_code_alpha_3)
if region:
sub_region_qs = Region.objects.filter(parent_region__code=region.code)
if sub_region_qs.exists():
sub_region_ids.append(sub_region_qs.first().id)
else:
subdivision = subdivisions.get(code=region.code.upper())
if subdivision:
sub_region, _ = Region.objects.get_or_create(
name=subdivision.name,
code=subdivision.code,
parent_region=region,
country=region.country)
sub_region_ids.append(sub_region.id)
return {'id': list(set(sub_region_ids))}
def get_wine_region_ids(self, wine_regions):
wine_region_ids = []
if wine_regions:
for wine_region in wine_regions:
qs = WineRegion.objects.filter(name__iexact=wine_region)
if not qs.exists():
raise serializers.ValidationError({
'detail': f'Wine region - {wine_region}, is not found.'})
wine_region_ids.append(qs.first().id)
return {'id': list(set(wine_region_ids))}
def get_locale_ids(self, locales):
locale_ids = []
if locales:
for locale in [locale for locale in locales if locale]:
if len(locale) == 2:
qs = Language.objects.filter(locale__startswith=locale)
else:
qs = Language.objects.filter(locale=locale)
if not qs.exists():
raise serializers.ValidationError({
'detail': f'Language with locale - {locale}, is not found.'})
locale_ids.extend(qs.values_list('id', flat=True))
return {'id': list(set(locale_ids))}
def get_review_state(self, states):
review_states = []
if states:
for state in [state for state in states if state]:
if state == 'published':
review_states.append(Review.READY)
else:
review_states.append(Review.TO_INVESTIGATE)
return {'state': list(set(review_states))}
def get_guide(self, old_guide_id: int):
qs = models.Guide.objects.filter(old_id=old_guide_id)
if qs.exists():
return qs.first()

View File

@ -340,6 +340,9 @@ class ProductSerializer(TransferSerializerMixin):
def create(self, validated_data):
qs = self.Meta.model.objects.filter(old_id=validated_data.get('old_id'))
wine_region = validated_data.pop('wine_region')
wine_sub_region = validated_data.pop('wine_sub_region')
# classifications
classifications = [validated_data.pop('wine_classification', None)]
# standards
@ -361,6 +364,11 @@ class ProductSerializer(TransferSerializerMixin):
obj.standards.add(*[i for i in standards if i and i not in obj.standards.all()])
# adding tags
obj.tags.add(*[i for i in tags if i and i not in obj.tags.all()])
# checking wine origin address
wine_origin_address, _ = location_models.WineOriginAddress.objects.get_or_create(
product=obj,
wine_region=wine_region,
wine_sub_region=wine_sub_region)
return obj
def get_name(self, name, brand):
@ -390,17 +398,23 @@ class ProductSerializer(TransferSerializerMixin):
if classification_qs.exists():
return classification_qs.first()
def get_wine_region(self, wine_region):
@staticmethod
def get_wine_region(wine_region):
if wine_region:
old_id = wine_region if not isinstance(wine_region, transfer_models.WineLocations) \
else wine_region.id
wine_region_qs = location_models.WineRegion.objects.filter(
old_id=wine_region.id)
old_id=old_id)
if wine_region_qs.exists():
return wine_region_qs.first()
def get_wine_sub_region(self, wine_sub_region_id):
if wine_sub_region_id:
@staticmethod
def get_wine_sub_region(wine_sub_region):
if wine_sub_region:
old_id = wine_sub_region if not isinstance(wine_sub_region, transfer_models.WineLocations) \
else wine_sub_region.id
sub_region_qs = location_models.WineSubRegion.objects.filter(
old_id=wine_sub_region_id)
old_id=old_id)
if sub_region_qs.exists():
return sub_region_qs.first()

View File

@ -99,6 +99,7 @@ EXTERNAL_APPS = [
'storages',
'sorl.thumbnail',
'timezonefinder',
'mptt',
]

View File

@ -30,10 +30,21 @@ MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION)
THUMBNAIL_DEBUG = True
# ADDED TRANSFER APP
# INSTALLED_APPS.append('transfer.apps.TransferConfig')
INSTALLED_APPS.append('transfer.apps.TransferConfig')
# DATABASES
DATABASES.update({
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USERNAME'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOSTNAME'),
'PORT': os.environ.get('DB_PORT'),
'OPTIONS': {
'options': '-c search_path=gm'
},
},
'legacy': {
'ENGINE': 'django.db.backends.mysql',
# 'HOST': '172.22.0.1',
@ -41,7 +52,9 @@ DATABASES.update({
'PORT': 3306,
'NAME': 'dev',
'USER': 'dev',
'PASSWORD': 'octosecret123'}})
'PASSWORD': 'octosecret123'
},
}
# LOGGING
@ -100,6 +113,3 @@ TESTING = sys.argv[1:2] == ['test']
if TESTING:
ELASTICSEARCH_INDEX_NAMES = {}
ELASTICSEARCH_DSL_AUTOSYNC = False
# INSTALLED APPS
INSTALLED_APPS.append('transfer.apps.TransferConfig')

View File

@ -57,3 +57,9 @@ redis==3.2.0
django_redis==4.10.0 # used byes indexing cache
kombu==4.6.6
celery==4.3.0
# country information
pycountry==19.8.18
# sql-tree
django-mptt==0.9.1