Merge remote-tracking branch 'origin/develop' into feature/add-city-search

# Conflicts:
#	_dockerfiles/db/Dockerfile
#	project/settings/local.py
This commit is contained in:
Dmitriy Kuzmenko 2019-12-09 11:41:02 +03:00
commit dfe4ab92a6
94 changed files with 2700 additions and 323 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:latest
FROM mdillon/postgis:10
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

@ -0,0 +1,152 @@
from account.models import OldRole, Role, User, UserRole
from main.models import SiteSettings
from django.core.management.base import BaseCommand
from django.db import connections, transaction
from django.db.models import Prefetch
from establishment.management.commands.add_position import namedtuplefetchall
from tqdm import tqdm
class Command(BaseCommand):
help = '''Add site affilations from old db to new db.
Run after migrate account models!!!'''
def map_role_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select distinct
case when role = 'news_editor' then 'CONTENT_PAGE_MANAGER'
when role in ('reviewer', 'reviwer', 'reviewer_manager') then 'REVIEWER_MANGER'
when role = 'admin' then 'SUPERUSER'
when role ='community_manager' then 'COUNTRY_ADMIN'
when role = 'site_admin' then 'COUNTRY_ADMIN'
when role = 'wine_reviewer' then 'WINERY_REVIEWER'
when role in ('salesman', 'sales_man') then 'SALES_MAN'
when role = 'seller' then 'SELLER'
else role
end as new_role,
case when role = 'GUEST' then null else role end as role
from
(
SELECT
DISTINCT
COALESCE(role, 'GUEST') as role
FROM site_affiliations AS sa
) t
''')
return namedtuplefetchall(cursor)
def add_old_roles(self):
objects = []
OldRole.objects.all().delete()
for s in tqdm(self.map_role_sql(), desc='Add permissions old'):
objects.append(
OldRole(new_role=s.new_role, old_role=s.role)
)
OldRole.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Migrated old roles.'))
def site_role_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select site_id,
role
from
(
SELECT
DISTINCT
site_id,
COALESCE(role, 'GUEST') as role
FROM site_affiliations AS sa
) t
where t.role not in ('admin', 'GUEST')
''')
return namedtuplefetchall(cursor)
def add_site_role(self):
objects = []
for s in tqdm(self.site_role_sql(), desc='Add site role'):
old_role = OldRole.objects.get(old_role=s.role)
role_choice = getattr(Role, old_role.new_role)
sites = SiteSettings.objects.filter(old_id=s.site_id)
for site in sites:
role = Role.objects.filter(site=site, role=role_choice)
if not role.exists():
objects.append(
Role(site=site, role=role_choice)
)
Role.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Added site roles.'))
def update_site_role(self):
roles = Role.objects.filter(country__isnull=True).select_related('site')\
.filter(site__id__isnull=False).select_for_update()
with transaction.atomic():
for role in tqdm(roles, desc='Update role country'):
role.country = role.site.country
role.save()
self.stdout.write(self.style.WARNING(f'Updated site roles.'))
def user_role_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select t.*
from
(
SELECT
site_id,
account_id,
COALESCE(role, 'GUEST') as role
FROM site_affiliations AS sa
) t
join accounts a on a.id = t.account_id
where t.role not in ('admin', 'GUEST')
''')
return namedtuplefetchall(cursor)
def add_role_user(self):
for s in tqdm(self.user_role_sql(), desc='Add role to user'):
sites = SiteSettings.objects.filter(old_id=s.site_id)
old_role = OldRole.objects.get(old_role=s.role)
role_choice = getattr(Role, old_role.new_role)
roles = Role.objects.filter(site__in=[site for site in sites], role=role_choice)
users = User.objects.filter(old_id=s.account_id)
for user in users:
for role in roles:
user_role = UserRole.objects.get_or_create(user=user,
role=role)
self.stdout.write(self.style.WARNING(f'Added users roles.'))
def superuser_role_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select t.*
from
(
SELECT
site_id,
account_id,
COALESCE(role, 'GUEST') as role
FROM site_affiliations AS sa
) t
join accounts a on a.id = t.account_id
where t.role in ('admin')
''')
return namedtuplefetchall(cursor)
def add_superuser(self):
for s in tqdm(self.superuser_role_sql(), desc='Add superuser'):
users = User.objects.filter(old_id=s.account_id).select_for_update()
with transaction.atomic():
for user in users:
user.is_superuser = True
user.save()
self.stdout.write(self.style.WARNING(f'Added superuser.'))
def handle(self, *args, **kwargs):
self.add_old_roles()
self.add_site_role()
self.update_site_role()
self.add_role_user()
self.add_superuser()

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.7 on 2019-12-03 10:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0020_role_site'),
]
operations = [
migrations.CreateModel(
name='OldRole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('new_role', models.CharField(max_length=512, verbose_name='New role')),
('old_role', models.CharField(max_length=512, verbose_name='Old role')),
],
options={
'unique_together': {('new_role', 'old_role')},
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-03 11:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0021_oldrole'),
]
operations = [
migrations.AlterField(
model_name='oldrole',
name='old_role',
field=models.CharField(max_length=512, null=True, verbose_name='Old role'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.7 on 2019-12-04 09:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0022_auto_20191203_1149'),
]
operations = [
migrations.AlterField(
model_name='role',
name='role',
field=models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator'), (3, 'Country admin'), (4, 'Content page manager'), (5, 'Establishment manager'), (6, 'Reviewer manager'), (7, 'Restaurant reviewer'), (8, 'Sales man'), (9, 'Winery reviewer'), (10, 'Seller')], verbose_name='Role'),
),
migrations.AlterUniqueTogether(
name='userrole',
unique_together={('user', 'role')},
),
]

View File

@ -32,6 +32,9 @@ class Role(ProjectBaseMixin):
ESTABLISHMENT_MANAGER = 5
REVIEWER_MANGER = 6
RESTAURANT_REVIEWER = 7
SALES_MAN = 8
WINERY_REVIEWER = 9
SELLER = 10
ROLE_CHOICES = (
(STANDARD_USER, 'Standard user'),
@ -40,7 +43,10 @@ class Role(ProjectBaseMixin):
(CONTENT_PAGE_MANAGER, 'Content page manager'),
(ESTABLISHMENT_MANAGER, 'Establishment manager'),
(REVIEWER_MANGER, 'Reviewer manager'),
(RESTAURANT_REVIEWER, 'Restaurant reviewer')
(RESTAURANT_REVIEWER, 'Restaurant reviewer'),
(SALES_MAN, 'Sales man'),
(WINERY_REVIEWER, 'Winery reviewer'),
(SELLER, 'Seller')
)
role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES,
null=False, blank=False)
@ -287,7 +293,19 @@ class User(AbstractUser):
class UserRole(ProjectBaseMixin):
"""UserRole model."""
user = models.ForeignKey(User, verbose_name=_('User'), on_delete=models.CASCADE)
user = models.ForeignKey('account.User',
verbose_name=_('User'),
on_delete=models.CASCADE)
role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.SET_NULL, null=True)
establishment = models.ForeignKey(Establishment, verbose_name=_('Establishment'),
on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
unique_together = ['user', 'role']
class OldRole(models.Model):
new_role = models.CharField(verbose_name=_('New role'), max_length=512)
old_role = models.CharField(verbose_name=_('Old role'), max_length=512, null=True)
class Meta:
unique_together = ('new_role', 'old_role')

View File

@ -13,19 +13,27 @@ class RoleSerializer(serializers.ModelSerializer):
]
class UserRoleSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserRole
fields = [
'user',
'role'
]
class BackUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
fields = (
'id',
'last_login',
'is_superuser',
'username',
'last_name',
'first_name',
'is_active',
'date_joined',
'image_url',
'cropped_image_url',
'email',
'email_confirmed',
'unconfirmed_email',
'email_confirmed',
'newsletter',
'roles',
)
extra_kwargs = {
'password': {'write_only': True}
}
@ -49,3 +57,13 @@ class BackDetailUserSerializer(BackUserSerializer):
user.set_password(validated_data['password'])
user.save()
return user
class UserRoleSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserRole
fields = [
'role',
'user',
'establishment'
]

View File

@ -92,7 +92,12 @@ class UserBaseSerializer(serializers.ModelSerializer):
model = models.User
fields = (
'id',
'username',
'fullname',
'first_name',
'last_name',
'email',
'cropped_image_url',
'image_url',
)

View File

@ -1,5 +1,6 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions
from rest_framework.filters import OrderingFilter
from account import models
from account.models import User
@ -13,15 +14,15 @@ class RoleLstView(generics.ListCreateAPIView):
class UserRoleLstView(generics.ListCreateAPIView):
serializer_class = serializers.UserRoleSerializer
queryset = models.Role.objects.all()
queryset = models.UserRole.objects.all()
class UserLstView(generics.ListCreateAPIView):
"""User list create view."""
queryset = User.objects.all()
queryset = User.objects.prefetch_related('roles')
serializer_class = serializers.BackUserSerializer
permission_classes = (permissions.IsAdminUser,)
filter_backends = (DjangoFilterBackend,)
filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_fields = (
'email_confirmed',
'is_staff',
@ -29,6 +30,14 @@ class UserLstView(generics.ListCreateAPIView):
'is_superuser',
'roles',
)
ordering_fields = (
'email_confirmed',
'is_staff',
'is_active',
'is_superuser',
'roles',
'last_login'
)
class UserRUDView(generics.RetrieveUpdateDestroyAPIView):

View File

View File

@ -0,0 +1,8 @@
from booking.urls import common as common_views
app = 'booking'
urlpatterns_api = []
urlpatterns = urlpatterns_api + \
common_views.urlpatterns

8
apps/booking/urls/web.py Normal file
View File

@ -0,0 +1,8 @@
from booking.urls import common as common_views
app = 'booking'
urlpatterns_api = []
urlpatterns = urlpatterns_api + \
common_views.urlpatterns

View File

@ -34,7 +34,7 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
periods = response['periods']
periods_by_name = {period['period']: period for period in periods if 'period' in period}
if not periods_by_name:
return None
return response
period_template = iter(periods_by_name.values()).__next__().copy()
period_template.pop('total_left_seats')

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 _
@ -84,27 +87,101 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
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_object_names = []
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_names.append(instance.slug if hasattr(instance, 'slug') else None)
# parse slugs
object_names = []
re_pattern = r'[\w]+'
for raw_name in raw_object_names:
result = re.findall(re_pattern, raw_name)
if result: object_names.append(' '.join(result).capitalize())
return set(object_names)
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 +193,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

@ -19,6 +19,8 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
collection_type_display = serializers.CharField(
source='get_collection_type_display', read_only=True)
country = CountrySimpleSerializer(read_only=True)
count_related_objects = serializers.IntegerField(read_only=True)
related_object_names = serializers.ListField(read_only=True)
class Meta:
model = models.Collection
@ -36,6 +38,8 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
'slug',
'start',
'end',
'count_related_objects',
'related_object_names',
]

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

@ -0,0 +1,36 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from establishment.models import Establishment
from transfer.models import Establishments
from transfer.serializers.establishment import EstablishmentSerializer
from timetable.models import Timetable
from django.db import transaction
class Command(BaseCommand):
help = 'Fix scheduler'
@transaction.atomic
def handle(self, *args, **kwargs):
count = 0
establishments = Establishment.objects.all()
old_est_list = Establishments.objects.prefetch_related(
'schedules_set',
)
# remove old records of Timetable
Timetable.objects.all().delete()
for est in tqdm(establishments, desc="Fix scheduler"):
old_est = old_est_list.filter(id=est.old_id).first()
if old_est and old_est.schedules_set.exists():
old_schedule = old_est.schedules_set.first()
timetable = old_schedule.timetable
if timetable:
new_schedules = EstablishmentSerializer.get_schedules(timetable)
est.schedule.add(*new_schedules)
est.save()
count += 1
self.stdout.write(self.style.WARNING(f'Update {count} objects.'))

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,9 +458,14 @@ 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
@property
def visible_tags_detail(self):
"""Removes some tags from detail Establishment representation"""
return self.visible_tags.exclude(category__index_name__in=['tag'])
def recalculate_toque_number(self):
toque_number = 0
if self.address and self.public_mark:
@ -614,6 +629,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

@ -232,9 +232,13 @@ class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer):
def validate(self, attrs):
"""Override validate method."""
establishment_pk = self.get_request_kwargs().get('pk')
establishment_slug = self.get_request_kwargs().get('slug')
search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug}
image_id = self.get_request_kwargs().get('image_id')
establishment_qs = models.Establishment.objects.filter(pk=establishment_pk)
establishment_qs = models.Establishment.objects.filter(**search_kwargs)
image_qs = Image.objects.filter(id=image_id)
if not establishment_qs.exists():

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):
@ -237,6 +239,30 @@ class EstablishmentShortSerializer(serializers.ModelSerializer):
]
class _EstablishmentAddressShortSerializer(serializers.ModelSerializer):
"""Short serializer for establishment."""
city = CitySerializer(source='address.city', allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer()
establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True)
currency = CurrencySerializer(read_only=True)
address = AddressBaseSerializer(read_only=True)
class Meta:
"""Meta class."""
model = models.Establishment
fields = [
'id',
'name',
'index_name',
'slug',
'city',
'establishment_type',
'establishment_subtypes',
'currency',
'address',
]
class EstablishmentProductShortSerializer(serializers.ModelSerializer):
"""SHORT Serializer for displaying info about an establishment on product page."""
establishment_type = EstablishmentTypeGeoSerializer()
@ -283,6 +309,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 +340,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'preview_image',
'new_image',
'tz',
'wine_regions',
]
@ -364,7 +393,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees',
many=True)
address = AddressDetailSerializer(read_only=True)
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags_detail')
menu = MenuSerializers(source='menu_set', many=True, read_only=True)
best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
@ -372,6 +401,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 +428,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 +449,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):
@ -492,7 +546,9 @@ class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer):
"""Serializer to carousel object w/ model News."""
def validate(self, attrs):
establishment = models.Establishment.objects.filter(pk=self.pk).first()
search_kwargs = {'pk': self.pk} if self.pk else {'slug': self.slug}
establishment = models.Establishment.objects.filter(**search_kwargs).first()
if not establishment:
raise serializers.ValidationError({'detail': _('Object not found.')})

View File

@ -104,18 +104,18 @@ class EstablishmentBTests(BaseTestCase):
response = self.client.post('/api/back/establishments/', data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get(f'/api/back/establishments/{self.establishment.id}/', format='json')
response = self.client.get(f'/api/back/establishments/slug/{self.establishment.slug}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'name': 'Test new establishment'
}
response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/',
response = self.client.patch(f'/api/back/establishments/slug/{self.establishment.slug}/',
data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/',
response = self.client.delete(f'/api/back/establishments/slug/{self.establishment.slug}/',
format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@ -372,22 +372,22 @@ class EstablishmentShedulerTests(ChildTestCase):
'weekday': 1
}
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/schedule/', data=data)
response = self.client.post(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
schedule = response.data
response = self.client.get(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/')
response = self.client.get(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/{schedule["id"]}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'weekday': 2
}
response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/',
response = self.client.patch(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/{schedule["id"]}/',
data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/schedule/{schedule["id"]}/')
response = self.client.delete(f'/api/back/establishments/slug/{self.establishment.slug}/schedule/{schedule["id"]}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@ -484,8 +484,8 @@ class EstablishmentCarouselTests(ChildTestCase):
"object_id": self.establishment.id
}
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/carousels/', data=data)
response = self.client.post(f'/api/back/establishments/slug/{self.establishment.slug}/carousels/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/carousels/')
response = self.client.delete(f'/api/back/establishments/slug/{self.establishment.slug}/carousels/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -8,25 +8,25 @@ app_name = 'establishment'
urlpatterns = [
path('', views.EstablishmentListCreateView.as_view(), name='list'),
path('<int:pk>/', views.EstablishmentRUDView.as_view(), name='detail'),
path('<int:pk>/carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(),
path('slug/<slug:slug>/', views.EstablishmentRUDView.as_view(), name='detail'),
path('slug/<slug:slug>/carousels/', views.EstablishmentCarouselCreateDestroyView.as_view(),
name='create-destroy-carousels'),
path('<int:pk>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(),
path('slug/<slug:slug>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(),
name='schedule-rud'),
path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
path('slug/<slug:slug>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
name='schedule-create'),
path('<int:pk>/gallery/', views.EstablishmentGalleryListView.as_view(),
path('slug/<slug:slug>/gallery/', views.EstablishmentGalleryListView.as_view(),
name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/',
path('slug/<slug:slug>/gallery/<int:image_id>/',
views.EstablishmentGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
path('<int:pk>/companies/', views.EstablishmentCompanyListCreateView.as_view(),
path('slug/<slug:slug>/companies/', views.EstablishmentCompanyListCreateView.as_view(),
name='company-list-create'),
path('<int:pk>/companies/<int:company_pk>/', views.EstablishmentCompanyRUDView.as_view(),
path('slug/<slug:slug>/companies/<int:company_pk>/', views.EstablishmentCompanyRUDView.as_view(),
name='company-rud'),
path('<int:pk>/notes/', views.EstablishmentNoteListCreateView.as_view(),
path('slug/<slug:slug>/notes/', views.EstablishmentNoteListCreateView.as_view(),
name='note-list-create'),
path('<int:pk>/notes/<int:note_pk>/', views.EstablishmentNoteRUDView.as_view(),
path('slug/<slug:slug>/notes/<int:note_pk>/', views.EstablishmentNoteRUDView.as_view(),
name='note-rud'),
path('menus/', views.MenuListCreateView.as_view(), name='menu-list'),
path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),

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

@ -31,6 +31,7 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP
class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
lookup_field = 'slug'
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentRUDSerializer
permission_classes = [IsCountryAdmin | IsEstablishmentManager]
@ -38,6 +39,7 @@ class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
lookup_field = 'slug'
serializer_class = ScheduleRUDSerializer
permission_classes = [IsEstablishmentManager]
@ -45,11 +47,11 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""
Returns the object the view is displaying.
"""
establishment_pk = self.kwargs['pk']
establishment_slug = self.kwargs['slug']
schedule_id = self.kwargs['schedule_id']
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk)
slug=establishment_slug)
schedule = get_object_or_404(klass=establishment.schedule,
id=schedule_id)
@ -62,6 +64,7 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""Establishment schedule Create view"""
lookup_field = 'slug'
serializer_class = ScheduleCreateSerializer
queryset = Timetable.objects.all()
permission_classes = [IsEstablishmentManager]
@ -165,7 +168,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
@ -210,6 +213,7 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
CreateDestroyGalleryViewMixin):
"""Resource for a create|destroy gallery for establishment for back-office users."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentBackOfficeGallerySerializer
def get_object(self):
@ -218,7 +222,7 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
"""
establishment_qs = self.filter_queryset(self.get_queryset())
establishment = get_object_or_404(establishment_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(establishment_qs, slug=self.kwargs.get('slug'))
gallery = get_object_or_404(establishment.establishment_gallery,
image_id=self.kwargs.get('image_id'))
@ -231,12 +235,13 @@ class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
class EstablishmentGalleryListView(EstablishmentMixinViews,
generics.ListAPIView):
"""Resource for returning gallery for establishment for back-office users."""
lookup_field = 'slug'
serializer_class = serializers.ImageBaseSerializer
def get_object(self):
"""Override get_object method."""
qs = super(EstablishmentGalleryListView, self).get_queryset()
establishment = get_object_or_404(qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(qs, slug=self.kwargs.get('slug'))
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
@ -252,6 +257,7 @@ class EstablishmentCompanyListCreateView(EstablishmentMixinViews,
generics.ListCreateAPIView):
"""List|Create establishment company view."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentCompanyListCreateSerializer
def get_object(self):
@ -259,7 +265,7 @@ class EstablishmentCompanyListCreateView(EstablishmentMixinViews,
establishment_qs = models.Establishment.objects.all()
filtered_ad_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(filtered_ad_qs, slug=self.kwargs.get('slug'))
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
@ -275,6 +281,7 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews,
generics.RetrieveUpdateDestroyAPIView):
"""Create|Retrieve|Update|Destroy establishment company view."""
lookup_field = 'slug'
serializer_class = serializers.CompanyBaseSerializer
def get_object(self):
@ -282,7 +289,7 @@ class EstablishmentCompanyRUDView(EstablishmentMixinViews,
establishment_qs = models.Establishment.objects.all()
filtered_ad_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(filtered_ad_qs, slug=self.kwargs.get('slug'))
company = get_object_or_404(establishment.companies.all(), pk=self.kwargs.get('company_pk'))
# May raise a permission denied
@ -295,6 +302,7 @@ class EstablishmentNoteListCreateView(EstablishmentMixinViews,
generics.ListCreateAPIView):
"""Retrieve|Update|Destroy establishment note view."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentNoteListCreateSerializer
def get_object(self):
@ -302,7 +310,7 @@ class EstablishmentNoteListCreateView(EstablishmentMixinViews,
establishment_qs = models.Establishment.objects.all()
filtered_establishment_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(filtered_establishment_qs, slug=self.kwargs.get('slug'))
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
@ -318,6 +326,7 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews,
generics.RetrieveUpdateDestroyAPIView):
"""Create|Retrieve|Update|Destroy establishment note view."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentNoteBaseSerializer
def get_object(self):
@ -325,7 +334,7 @@ class EstablishmentNoteRUDView(EstablishmentMixinViews,
establishment_qs = models.Establishment.objects.all()
filtered_establishment_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk'))
establishment = get_object_or_404(filtered_establishment_qs, slug=self.kwargs.get('slug'))
note = get_object_or_404(establishment.notes.all(), pk=self.kwargs['note_pk'])
# May raise a permission denied

View File

@ -38,7 +38,7 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
.with_extended_address_related().with_currency_related() \
.with_certain_tag_category_related('category', 'restaurant_category') \
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
.with_ceratin_tag_category_related('shop_category', 'artisan_category')
.with_certain_tag_category_related('shop_category', 'artisan_category')
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
@ -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."""
@ -107,10 +114,7 @@ class EstablishmentCommentListView(generics.ListAPIView):
"""Override get_queryset method"""
establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug'])
return comment_models.Comment.objects.by_content_type(app_label='establishment',
model='establishment') \
.by_object_id(object_id=establishment.pk) \
.order_by('-created')
return establishment.comments.order_by('-created')
class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
@ -145,6 +149,7 @@ class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
class EstablishmentCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
"""View for create/destroy establishment from carousel."""
lookup_field = 'slug'
_model = models.Establishment
serializer_class = serializers.EstablishmentCarouselCreateSerializer

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,30 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from location.models import Address
from transfer.models import Locations
class Command(BaseCommand):
help = """Fix address, clear number field and fill street_name_1 like in old db"""
def handle(self, *args, **kwarg):
addresses = Address.objects.filter(
old_id__isnull=False
).values_list('old_id', flat=True)
old_addresses = Locations.objects.filter(
id__in=list(addresses)
).values_list('id', 'address')
update_address = []
for idx, address in tqdm(old_addresses):
new_address = Address.objects.filter(old_id=idx).first()
if new_address:
new_address.number = 0
new_address.street_name_2 = ''
new_address.street_name_1 = address
update_address.append(new_address)
Address.objects.bulk_update(update_address, ['number', 'street_name_1', 'street_name_2'])
self.stdout.write(self.style.WARNING(f'Updated addresses: {len(update_address)}'))

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

@ -207,12 +207,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,
@ -293,6 +293,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

@ -0,0 +1,37 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from establishment.models import Establishment
from main.models import Carousel
from transfer.models import HomePages
from location.models import Country
from django.db.models import F
class Command(BaseCommand):
help = '''Add establishment form HomePage to Carousel!'''
@staticmethod
def get_country(country_code):
return Country.objects.filter(code__iexact=country_code).first()
def handle(self, *args, **kwargs):
objects = []
deleted = 0
hp_list = HomePages.objects.annotate(
country=F('site__country_code_2'),
).all()
for hm in tqdm(hp_list, desc='Add home_page.establishments to carousel'):
est = Establishment.objects.filter(old_id=hm.selection_of_week).first()
if est:
if est.carousels.exists():
est.carousels.all().delete()
deleted += 1
carousel = Carousel(
content_object=est,
country=self.get_country(hm.country)
)
objects.append(carousel)
Carousel.objects.bulk_create(objects)
self.stdout.write(
self.style.WARNING(f'Created {len(objects)}/Deleted {deleted} carousel objects.'))

View File

@ -17,9 +17,24 @@ class FeatureSerializer(serializers.ModelSerializer):
fields = (
'id',
'slug',
'priority'
'priority',
'route',
'site_settings',
)
class CurrencySerializer(ProjectModelSerializer):
"""Currency serializer."""
name_translated = TranslatedField()
class Meta:
model = models.Currency
fields = [
'id',
'name_translated',
'sign'
]
class SiteFeatureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='feature.id')
@ -42,20 +57,6 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
)
class CurrencySerializer(ProjectModelSerializer):
"""Currency serializer."""
name_translated = TranslatedField()
class Meta:
model = models.Currency
fields = [
'id',
'name_translated',
'sign'
]
class SiteSettingsSerializer(serializers.ModelSerializer):
"""Site settings serializer."""
@ -99,23 +100,15 @@ class SiteSettingsBackOfficeSerializer(SiteSettingsSerializer):
]
class SiteSerializer(serializers.ModelSerializer):
class SiteSerializer(SiteSettingsSerializer):
country = CountrySerializer()
class Meta:
"""Meta class."""
model = models.SiteSettings
fields = [
'subdomain',
'site_url',
'country',
'default_site',
'pinterest_page_url',
'twitter_page_url',
'facebook_page_url',
'instagram_page_url',
'contact_email',
'currency'
fields = SiteSettingsSerializer.Meta.fields + [
'id',
'country'
]
@ -129,45 +122,6 @@ class SiteShortSerializer(serializers.ModelSerializer):
]
class SiteBackOfficeSerializer(SiteSerializer):
"""Serializer for back office."""
class Meta(SiteSerializer.Meta):
"""Meta class."""
fields = SiteSerializer.Meta.fields + [
'id',
]
class FeatureSerializer(serializers.ModelSerializer):
"""Site feature serializer."""
class Meta:
"""Meta class."""
model = models.Feature
fields = (
'id',
'slug',
'priority',
'route',
'site_settings',
)
# class SiteFeatureSerializer(serializers.ModelSerializer):
# """Site feature serializer."""
#
# class Meta:
# """Meta class."""
#
# model = models.SiteFeature
# fields = (
# 'id',
# 'published',
# 'site_settings',
# 'feature',
# )
class AwardBaseSerializer(serializers.ModelSerializer):

View File

@ -13,5 +13,11 @@ urlpatterns = [
path('site-settings/<subdomain>/', views.SiteSettingsBackOfficeView.as_view(),
name='site-settings'),
path('feature/', views.FeatureBackView.as_view(), name='feature-list-create'),
path('feature/<int:id>/', views.FeatureRUDBackView.as_view(), name='feature-rud')
path('feature/<int:id>/', views.FeatureRUDBackView.as_view(), name='feature-rud'),
path('site-feature/', views.SiteFeatureBackView.as_view(),
name='site-feature-list-create'),
path('site-feature/<int:id>/', views.SiteFeatureRUDBackView.as_view(),
name='site-feature-rud'),
]

View File

@ -44,16 +44,26 @@ class FeatureBackView(generics.ListCreateAPIView):
serializer_class = serializers.FeatureSerializer
class SiteFeatureBackView(generics.ListCreateAPIView):
"""Feature list or create View."""
serializer_class = serializers.SiteFeatureSerializer
class FeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
"""Feature RUD View."""
serializer_class = serializers.FeatureSerializer
class SiteFeatureRUDBackView(generics.RetrieveUpdateDestroyAPIView):
"""Feature RUD View."""
serializer_class = serializers.SiteFeatureSerializer
class SiteSettingsBackOfficeView(SiteSettingsView):
"""Site settings View."""
serializer_class = serializers.SiteSettingsBackOfficeSerializer
serializer_class = serializers.SiteSerializer
class SiteListBackOfficeView(SiteListView):
"""Site settings View."""
serializer_class = serializers.SiteBackOfficeSerializer
serializer_class = serializers.SiteSerializer

View File

@ -0,0 +1,17 @@
from django.core.management.base import BaseCommand
from news.models import News
import re
class Command(BaseCommand):
help = 'Removes empty img html tags from news description'
relative_img_regex = re.compile(r'\<img.+src=(?!https?:\/\/)([^\/].+?)[\"|\']>', re.I)
def handle(self, *args, **kwargs):
for news in News.objects.all():
if isinstance(news.description, dict):
news.description = {locale: self.relative_img_regex.sub('', rich_text)
for locale, rich_text in news.description.items()}
self.stdout.write(self.style.WARNING(f'Replaced {news} empty img html tags...\n'))
news.save()

View File

@ -8,7 +8,7 @@ from partner.serializers import common as serializers
# Mixins
class PartnerViewMixin(generics.GenericAPIView):
"""View mixin for Partner views"""
queryset = models.Partner.objects.all()
queryset = models.Partner.objects.distinct("name")
# Views

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

@ -9,6 +9,7 @@ class ProductFilterSet(filters.FilterSet):
"""Product filter set."""
establishment_id = filters.NumberFilter()
current_product = filters.CharFilter(method='without_current_product')
product_type = filters.CharFilter(method='by_product_type')
product_subtype = filters.CharFilter(method='by_product_subtype')
@ -21,6 +22,11 @@ class ProductFilterSet(filters.FilterSet):
'product_subtype',
]
def without_current_product(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.without_current_product(value)
return queryset
def by_product_type(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_type(value)

View File

@ -0,0 +1,25 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from product.models import Product
class Command(BaseCommand):
help = """Add public_mark to product from reviews."""
def handle(self, *args, **kwarg):
update_products = []
products = Product.objects.filter(
public_mark__isnull=True,
reviews__isnull=False).distinct()
for product in tqdm(products):
review = product.reviews.published().filter(
mark__isnull=False).order_by('-published_at').first()
if review:
product.public_mark = review.mark
update_products.append(product)
Product.objects.bulk_update(update_products, ['public_mark', ])
self.stdout.write(
self.style.WARNING(f'Updated products: {len(update_products)}'))

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)
@ -101,6 +102,11 @@ class ProductQuerySet(models.QuerySet):
def wines(self):
return self.filter(type__index_name__icontains=ProductType.WINE)
def without_current_product(self, current_product: str):
"""Exclude by current product."""
kwargs = {'pk': int(current_product)} if current_product.isdigit() else {'slug': current_product}
return self.exclude(**kwargs)
def by_product_type(self, product_type: str):
"""Filter by type."""
return self.filter(product_type__index_name__icontains=product_type)
@ -176,14 +182,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

@ -4,15 +4,15 @@ from rest_framework import serializers
from comment.models import Comment
from comment.serializers import CommentSerializer
from establishment.serializers import EstablishmentShortSerializer, EstablishmentProductSerializer, EstablishmentProductShortSerializer
from gallery.models import Image
from establishment.serializers import EstablishmentProductShortSerializer
from establishment.serializers.common import _EstablishmentAddressShortSerializer
from location.serializers import WineOriginRegionBaseSerializer, WineOriginBaseSerializer
from main.serializers import AwardSerializer
from product import models
from review.serializers import ReviewShortSerializer
from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer
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 tag.serializers import TagBaseSerializer, TagCategoryProductSerializer
class ProductTagSerializer(TagBaseSerializer):
@ -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',
]
@ -119,12 +119,12 @@ class ProductBaseSerializer(serializers.ModelSerializer):
class ProductDetailSerializer(ProductBaseSerializer):
"""Product detail serializer."""
description_translated = TranslatedField()
establishment_detail = EstablishmentShortSerializer(source='establishment', read_only=True)
establishment_detail = _EstablishmentAddressShortSerializer(source='establishment', read_only=True)
review = ReviewShortSerializer(source='last_published_review', read_only=True)
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

@ -60,10 +60,7 @@ class ProductCommentListView(generics.ListAPIView):
def get_queryset(self):
"""Override get_queryset method"""
product = get_object_or_404(Product, slug=self.kwargs['slug'])
return Comment.objects.by_content_type(app_label='product',
model='product') \
.by_object_id(object_id=product.pk) \
.order_by('-created')
return product.comments.order_by('-created')
class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView):

View File

@ -115,12 +115,8 @@ def transfer_product_reviews():
products = Product.objects.filter(
old_id__isnull=False).values_list('old_id', flat=True)
users = User.objects.filter(
old_id__isnull=False).values_list('old_id', flat=True)
queryset = Reviews.objects.filter(
product_id__in=list(products),
reviewer_id__in=list(users),
).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'product_id', 'mark', 'vintage')
serialized_data = ProductReviewSerializer(data=list(queryset.values()), many=True)

View File

@ -1,6 +1,7 @@
from search_indexes.documents.establishment import EstablishmentDocument
from search_indexes.documents.news import NewsDocument
from search_indexes.documents.product import ProductDocument
from search_indexes.documents.tag_category import TagCategoryDocument
from search_indexes.tasks import es_update
# todo: make signal to update documents on related fields
@ -8,5 +9,6 @@ __all__ = [
'EstablishmentDocument',
'NewsDocument',
'ProductDocument',
'TagCategoryDocument',
'es_update',
]

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

@ -0,0 +1,33 @@
"""Product app documents."""
from django.conf import settings
from django_elasticsearch_dsl import Document, Index, fields
from tag import models
TagCategoryIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'tag_category'))
TagCategoryIndex.settings(number_of_shards=2, number_of_replicas=2)
@TagCategoryIndex.doc_type
class TagCategoryDocument(Document):
"""TagCategory document."""
tags = fields.ListField(fields.ObjectField(
properties={
'id': fields.IntegerField(),
'value': fields.KeywordField(),
},
))
class Django:
model = models.TagCategory
fields = (
'id',
'index_name',
'public',
'value_type'
)
related_models = [models.Tag]
def get_queryset(self):
return super().get_queryset().with_base_related()

View File

@ -4,6 +4,8 @@ from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend, \
FacetedSearchFilterBackend, GeoSpatialFilteringFilterBackend
from search_indexes.utils import OBJECT_FIELD_PROPERTIES
from six import iteritems
from search_indexes.documents import TagCategoryDocument
from tag.models import TagCategory
class CustomGeoSpatialFilteringFilterBackend(GeoSpatialFilteringFilterBackend):
@ -11,20 +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)
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)
@ -52,10 +47,30 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend):
:param view:
:return:
"""
def makefilter(cur_facet):
def myfilter(x):
def make_filter(cur_facet):
def _filter(x):
return cur_facet['facet']._params['field'] != next(iter(x._params))
return myfilter
return _filter
def make_tags_filter(cur_facet, tags_to_remove_ids):
def _filter(x):
if hasattr(x, '_params') and (x._params.get('must') or x._params.get('should')):
ret = []
for t in ['must', 'should']:
terms = x._params.get(t)
if terms:
for term in terms:
if cur_facet['facet']._params['field'] != next(iter(term._params)):
return True # different fields. preserve filter
else:
ret.append(next(iter(term._params.values())) not in tags_to_remove_ids)
return all(ret)
if cur_facet['facet']._params['field'] != next(iter(x._params)):
return True # different fields. preserve filter
else:
return next(iter(x._params.values())) not in tags_to_remove_ids
return _filter
__facets = self.construct_facets(request, view)
setattr(view.paginator, 'facets_computed', {})
for __field, __facet in iteritems(__facets):
@ -67,29 +82,73 @@ class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend):
'global'
).bucket(__field, agg)
else:
qs = queryset.__copy__()
qs.query = queryset.query._clone()
filterer = makefilter(__facet)
for param_type in ['must', 'must_not', 'should']:
if qs.query._proxied._params.get(param_type):
qs.query._proxied._params[param_type] = list(
filter(
filterer, qs.query._proxied._params[param_type]
if __field != 'tag':
qs = queryset.__copy__()
qs.query = queryset.query._clone()
filterer = make_filter(__facet)
for param_type in ['must', 'must_not', 'should']:
if qs.query._proxied._params.get(param_type):
qs.query._proxied._params[param_type] = list(
filter(
filterer, qs.query._proxied._params[param_type]
)
)
)
sh = qs.query._proxied._params.get('should')
if (not sh or not len(sh)) \
and qs.query._proxied._params.get('minimum_should_match'):
qs.query._proxied._params.pop('minimum_should_match')
facet_name = '_filter_' + __field
qs.aggs.bucket(
facet_name,
'filter',
filter=agg_filter
).bucket(__field, agg)
view.paginator.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]})
sh = qs.query._proxied._params.get('should')
if (not sh or not len(sh)) \
and qs.query._proxied._params.get('minimum_should_match'):
qs.query._proxied._params.pop('minimum_should_match')
facet_name = '_filter_' + __field
qs.aggs.bucket(
facet_name,
'filter',
filter=agg_filter
).bucket(__field, agg)
view.paginator.facets_computed.update({facet_name: qs.execute().aggregations[facet_name]})
else:
tag_facets = []
preserve_ids = []
facet_name = '_filter_' + __field
all_tag_categories = TagCategoryDocument.search() \
.filter('term', public=True) \
.filter(Q('term', value_type=TagCategory.LIST) | Q('match', index_name='wine-color'))
for category in all_tag_categories:
tags_to_remove = list(map(lambda t: str(t.id), category.tags))
qs = queryset.__copy__()
qs.query = queryset.query._clone()
filterer = make_tags_filter(__facet, tags_to_remove)
for param_type in ['must', 'should']:
if qs.query._proxied._params.get(param_type):
if qs.query._proxied._params.get(param_type):
qs.query._proxied._params[param_type] = list(
filter(
filterer, qs.query._proxied._params[param_type]
)
)
sh = qs.query._proxied._params.get('should')
if (not sh or not len(sh)) \
and qs.query._proxied._params.get('minimum_should_match'):
qs.query._proxied._params.pop('minimum_should_match')
qs.aggs.bucket(
facet_name,
'filter',
filter=agg_filter
).bucket(__field, agg)
tag_facets.append(qs.execute().aggregations[facet_name])
preserve_ids.append(list(map(int, tags_to_remove)))
view.paginator.facets_computed.update({facet_name: self.merge_buckets(tag_facets, preserve_ids)})
return queryset
@staticmethod
def merge_buckets(buckets: list, preserve_ids: list):
"""Reduces all buckets preserving class"""
result_bucket = buckets[0]
result_bucket.tag.buckets = list(filter(lambda x: x['key'] in preserve_ids[0], result_bucket.tag.buckets._l_))
for bucket, ids in list(zip(buckets, preserve_ids))[1:]:
for tag in bucket.tag.buckets._l_:
if tag['key'] in ids:
result_bucket.tag.buckets.append(tag)
return result_bucket
class CustomSearchFilterBackend(SearchFilterBackend):
"""Custom SearchFilterBackend."""

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

@ -73,7 +73,10 @@ class TagsFilterSet(TagsBaseFilterSet):
def by_establishment_type(self, queryset, name, value):
if value == EstablishmentType.ARTISAN:
return models.Tag.objects.by_category_index_name('shop_category')[0:8]
qs = models.Tag.objects.by_category_index_name('shop_category')
if self.request.country_code and self.request.country_code not in settings.INTERNATIONAL_COUNTRY_CODES:
qs = qs.filter(establishments__address__city__country__code=self.request.country_code).distinct('id')
return qs.exclude(establishments__isnull=True)[0:8]
return queryset.by_establishment_type(value)
# TMP TODO remove it later

View File

@ -47,7 +47,7 @@ class Tag(TranslatedFieldsMixin, models.Model):
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
old_id_meta_product = models.PositiveIntegerField(_('old id metadata product'),
blank=True, null=True, default=None)
blank=True, null=True, default=None)
objects = TagQuerySet.as_manager()

View File

@ -1,7 +1,8 @@
"""Tag serializers."""
from rest_framework import serializers
from establishment.models import (Establishment, EstablishmentType,
EstablishmentSubType)
from rest_framework.fields import SerializerMethodField
from establishment.models import (Establishment, EstablishmentType)
from news.models import News, NewsType
from tag import models
from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound,
@ -12,6 +13,9 @@ from utils.serializers import TranslatedField
class TagBaseSerializer(serializers.ModelSerializer):
"""Serializer for model Tag."""
def get_extra_kwargs(self):
return super().get_extra_kwargs()
label_translated = TranslatedField()
index_name = serializers.CharField(source='value', read_only=True, allow_null=True)
@ -37,6 +41,7 @@ class TagBackOfficeSerializer(TagBaseSerializer):
'category'
)
class TagCategoryProductSerializer(serializers.ModelSerializer):
"""SHORT Serializer for TagCategory"""
@ -57,7 +62,7 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
"""Serializer for model TagCategory."""
label_translated = TranslatedField()
tags = TagBaseSerializer(many=True, read_only=True)
tags = SerializerMethodField()
class Meta:
"""Meta class."""
@ -70,6 +75,25 @@ class TagCategoryBaseSerializer(serializers.ModelSerializer):
'tags',
)
def get_tags(self, obj):
query_params = dict(self.context['request'].query_params)
if len(query_params) > 1:
return []
params = {}
if 'establishment_type' in query_params:
params = {
'establishments__isnull': False,
}
elif 'product_type' in query_params:
params = {
'products__isnull': False,
}
tags = obj.tags.filter(**params).distinct()
return TagBaseSerializer(instance=tags, many=True, read_only=True).data
class TagCategoryShortSerializer(serializers.ModelSerializer):
"""Serializer for model TagCategory."""
@ -174,15 +198,15 @@ class TagCategoryBindObjectSerializer(serializers.Serializer):
attrs['tag_category'] = tag_category
if obj_type == self.ESTABLISHMENT_TYPE:
establishment_type = EstablishmentType.objects.filter(pk=obj_id).\
establishment_type = EstablishmentType.objects.filter(pk=obj_id). \
first()
if not establishment_type:
raise BindingObjectNotFound()
if request.method == 'POST' and tag_category.establishment_types.\
if request.method == 'POST' and tag_category.establishment_types. \
filter(pk=establishment_type.pk).exists():
raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not tag_category.\
establishment_types.filter(pk=establishment_type.pk).\
if request.method == 'DELETE' and not tag_category. \
establishment_types.filter(pk=establishment_type.pk). \
exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = establishment_type
@ -190,10 +214,10 @@ class TagCategoryBindObjectSerializer(serializers.Serializer):
news_type = NewsType.objects.filter(pk=obj_id).first()
if not news_type:
raise BindingObjectNotFound()
if request.method == 'POST' and tag_category.news_types.\
if request.method == 'POST' and tag_category.news_types. \
filter(pk=news_type.pk).exists():
raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not tag_category.news_types.\
if request.method == 'DELETE' and not tag_category.news_types. \
filter(pk=news_type.pk).exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = news_type

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

@ -45,8 +45,14 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
.parser_context.get('view')\
.kwargs.get('pk')
establishment_slug = self.context.get('request')\
.parser_context.get('view')\
.kwargs.get('slug')
search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug}
# Check if establishment exists.
establishment_qs = Establishment.objects.filter(pk=establishment_pk)
establishment_qs = Establishment.objects.filter(**search_kwargs)
if not establishment_qs.exists():
raise serializers.ValidationError({'detail': _('Establishment not found.')})
attrs['establishment'] = establishment_qs.first()

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()
@ -992,7 +1000,7 @@ class ProductNotes(MigrateMixin):
db_table = 'product_notes'
class HomePages(models.Model):
class HomePages(MigrateMixin):
using = 'legacy'
site = models.ForeignKey(Sites, models.DO_NOTHING, blank=True, null=True)

View File

@ -125,7 +125,7 @@ class EstablishmentSerializer(serializers.ModelSerializer):
weekdays = {
'su': Timetable.SUNDAY,
'mo': Timetable.MONDAY,
'tu': Timetable.THURSDAY,
'tu': Timetable.TUESDAY,
'we': Timetable.WEDNESDAY,
'th': Timetable.THURSDAY,
'fr': Timetable.FRIDAY,

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

@ -55,7 +55,7 @@ class ProductReviewSerializer(ReviewSerializer):
product_id = serializers.IntegerField()
created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
aasm_state = serializers.CharField(allow_null=True)
reviewer_id = serializers.IntegerField()
reviewer_id = serializers.IntegerField(allow_null=True)
id = serializers.IntegerField()
def validate(self, data):
@ -82,9 +82,8 @@ class ProductReviewSerializer(ReviewSerializer):
@staticmethod
def get_reviewer(data):
user = User.objects.filter(old_id=data['reviewer_id']).first()
if not user:
raise ValueError(f"User account not found with old_id {data['reviewer_id']}")
return user
if user:
return user
@staticmethod
def get_product(data):

View File

@ -1,5 +1,6 @@
"""Custom middleware."""
from django.utils import translation
from django.utils import translation, timezone
from account.models import User
from configuration.models import TranslationSettings
from translation.models import Language
@ -12,6 +13,14 @@ def get_locale(cookie_dict):
def get_country_code(cookie_dict):
return cookie_dict.get('country_code')
def user_last_visit(get_response):
"""Updates user last visit w/ current"""
def middleware(request):
response = get_response(request)
if request.user.is_authenticated:
User.objects.filter(pk=request.user.pk).update(last_login=timezone.now())
return response
return middleware
def parse_cookies(get_response):
"""Parse cookies."""

View File

@ -118,6 +118,10 @@ class CarouselCreateSerializer(serializers.ModelSerializer):
def pk(self):
return self.request.parser_context.get('kwargs').get('pk')
@property
def slug(self):
return self.request.parser_context.get('kwargs').get('slug')
class RecursiveFieldSerializer(serializers.Serializer):
def to_representation(self, value):

View File

@ -158,7 +158,11 @@ class CarouselCreateDestroyMixinView(BaseCreateDestroyMixinView):
lookup_field = 'id'
def get_base_object(self):
return get_object_or_404(self._model, id=self.kwargs['pk'])
establishment_pk = self.kwargs.get('pk')
establishment_slug = self.kwargs.get('slug')
search_kwargs = {'id': establishment_pk} if establishment_pk else {'slug': establishment_slug}
return get_object_or_404(self._model, **search_kwargs)
def get_object(self):
"""

View File

@ -13,6 +13,9 @@ services:
MYSQL_ROOT_PASSWORD: rootPassword
volumes:
- gm-mysql_db:/var/lib/mysql
- .:/code
# PostgreSQL database

2
fabfile.py vendored
View File

@ -54,7 +54,7 @@ def collectstatic():
def deploy(branch=None):
role = env.roles[0]
if env.roledefs[role]['branch'] != 'develop':
if env.roledefs[role]['branch'] == 'develop':
fetch()
install_requirements()
migrate()

View File

@ -1,7 +1,10 @@
#!/usr/bin/env bash
./manage.py transfer -a
#./manage.py transfer -d
./manage.py transfer -d
./manage.py transfer -e
./manage.py transfer -n
./manage.py rm_empty_images # команда для удаления картинок с относительным урлом из news.description
./manage.py upd_transportation
./manage.py transfer --fill_city_gallery
./manage.py transfer -l
./manage.py transfer --product
@ -11,4 +14,5 @@
./manage.py transfer --wine_characteristics
./manage.py transfer --inquiries
./manage.py transfer --assemblage
./manage.py transfer --purchased_plaques
./manage.py transfer --purchased_plaques
./manage.py rm_empty_images

View File

@ -99,6 +99,7 @@ EXTERNAL_APPS = [
'storages',
'sorl.thumbnail',
'timezonefinder',
'mptt',
]
@ -117,6 +118,7 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'utils.middleware.parse_cookies',
'utils.middleware.user_last_visit',
]
ROOT_URLCONF = 'project.urls'
@ -413,10 +415,10 @@ SORL_THUMBNAIL_ALIASES = {
SIMPLE_JWT = {
# Increase access token lifetime b.c. front-end dev's cant send multiple
# requests to API in one HTTP request.
'ACCESS_TOKEN_LIFETIME': timedelta(days=30),
'ACCESS_TOKEN_LIFETIME_SECONDS': 21600, # 6 hours in seconds
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
'REFRESH_TOKEN_LIFETIME_SECONDS': 2592000, # 30 days in seconds
'ACCESS_TOKEN_LIFETIME': timedelta(days=182),
'ACCESS_TOKEN_LIFETIME_SECONDS': 15770000, # 6 months
'REFRESH_TOKEN_LIFETIME': timedelta(days=182),
'REFRESH_TOKEN_LIFETIME_SECONDS': 15770000, # 6 months
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
@ -452,7 +454,7 @@ NOTIFICATION_PASSWORD_TEMPLATE = 'account/password_change_email.html'
# COOKIES
COOKIES_MAX_AGE = 2628000 # 30 days
COOKIES_MAX_AGE = 15730000 # 6 months
SESSION_COOKIE_SAMESITE = None
@ -523,3 +525,6 @@ INTERNATIONAL_COUNTRY_CODES = ['www', 'main', 'next']
THUMBNAIL_ENGINE = 'utils.thumbnail_engine.GMEngine'
COOKIE_DOMAIN = None
ELASTICSEARCH_DSL = {}
ELASTICSEARCH_INDEX_NAMES = {}

View File

@ -42,6 +42,7 @@ ELASTICSEARCH_INDEX_NAMES = {
'search_indexes.documents.news': 'development_news',
'search_indexes.documents.establishment': 'development_establishment',
'search_indexes.documents.product': 'development_product',
'search_indexes.documents.tag_category': 'development_tag_category',
}
# ELASTICSEARCH_DSL_AUTOSYNC = False

View File

@ -30,18 +30,10 @@ 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({
'legacy': {
'ENGINE': 'django.db.backends.mysql',
# 'HOST': '172.22.0.1',
'HOST': 'mysql_db',
'PORT': 3306,
'NAME': 'dev',
'USER': 'dev',
'PASSWORD': 'octosecret123'},
DATABASES = {
'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': os.environ.get('DB_NAME'),
@ -50,10 +42,19 @@ DATABASES.update({
'HOST': os.environ.get('DB_HOSTNAME'),
'PORT': os.environ.get('DB_PORT'),
'OPTIONS': {
'options': '-c search_path=gm'
'options': '-c search_path=gm,public'
},
},
})
'legacy': {
'ENGINE': 'django.db.backends.mysql',
# 'HOST': '172.22.0.1',
'HOST': 'mysql_db',
'PORT': 3306,
'NAME': 'dev',
'USER': 'dev',
'PASSWORD': 'octosecret123'
},
}
# LOGGING
@ -104,6 +105,7 @@ ELASTICSEARCH_INDEX_NAMES = {
# 'search_indexes.documents.news': 'local_news',
'search_indexes.documents.establishment': 'local_establishment',
'search_indexes.documents.product': 'local_product',
'search_indexes.documents.tag_category': 'local_tag_category',
}
ELASTICSEARCH_DSL_AUTOSYNC = False
@ -111,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

@ -36,6 +36,7 @@ ELASTICSEARCH_INDEX_NAMES = {
'search_indexes.documents.news': 'development_news', # temporarily disabled
'search_indexes.documents.establishment': 'development_establishment',
'search_indexes.documents.product': 'development_product',
'search_indexes.documents.tag_category': 'development_tag_category',
}
sentry_sdk.init(

View File

@ -23,6 +23,7 @@ ELASTICSEARCH_DSL = {
ELASTICSEARCH_INDEX_NAMES = {
# 'search_indexes.documents.news': 'stage_news', #temporarily disabled
'search_indexes.documents.establishment': 'stage_establishment',
'search_indexes.documents.tag_category': 'stage_tag_category',
}
COOKIE_DOMAIN = '.id-east.ru'

View File

@ -3,6 +3,7 @@ from django.urls import path, include
app_name = 'mobile'
urlpatterns = [
path('booking/', include('booking.urls.web')),
path('establishments/', include('establishment.urls.mobile')),
path('location/', include('location.urls.mobile')),
path('main/', include('main.urls.mobile')),

View File

@ -19,7 +19,7 @@ app_name = 'web'
urlpatterns = [
path('account/', include('account.urls.web')),
path('booking/', include('booking.urls')),
path('booking/', include('booking.urls.web')),
path('re_blocks/', include('advertisement.urls.web')),
path('collections/', include('collection.urls.web')),
path('establishments/', include('establishment.urls.web')),

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