Merge branch 'develop' into feature/transfer-guide

# Conflicts:
#	apps/establishment/models.py
#	apps/establishment/serializers/common.py
This commit is contained in:
Anatoly 2019-12-04 20:16:21 +03:00
commit 9dc8e34e3a
73 changed files with 1269 additions and 114 deletions

113
.dockerignore Normal file
View File

@ -0,0 +1,113 @@
# Created by .ignore support plugin (hsz.mobi)
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
_*/
.git/
.idea/
_files/

View File

@ -1,8 +1,9 @@
FROM python:3.7
ENV PYTHONUNBUFFERED 1
RUN apt-get update; apt-get --assume-yes install binutils libproj-dev gdal-bin gettext
RUN mkdir /code
RUN mkdir /code /requirements
ADD ./requirements /requirements
RUN pip install --no-cache-dir -r /requirements/base.txt && \
pip install --no-cache-dir -r /requirements/development.txt
WORKDIR /code
ADD . /code/
RUN pip install --no-cache-dir -r /code/requirements/base.txt && \
pip install --no-cache-dir -r /code/requirements/development.txt
ADD . /code/

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,15 +13,6 @@ class RoleSerializer(serializers.ModelSerializer):
]
class UserRoleSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserRole
fields = [
'user',
'role'
]
class BackUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
@ -49,3 +40,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

@ -13,7 +13,7 @@ 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):

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:
raise ValueError('Empty guestonline response')
return response
period_template = iter(periods_by_name.values()).__next__().copy()
period_template.pop('total_left_seats')
@ -84,8 +84,10 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
service_response = self._preprocess_guestonline_response(service.response) \
if establishment.guestonline_id is not None \
else service.response
response.update({'details': service_response} if service and service.response else {})
else service.response if service else None
response.update({'details': service_response})
if service_response is None:
response['available'] = False
return Response(data=response, status=200)

View File

@ -0,0 +1,32 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from tqdm import tqdm
from collection.models import Collection
class Command(BaseCommand):
help = """Fix existed collections."""
def handle(self, *args, **kwarg):
update_collections = []
collections = Collection.objects.values_list('id', 'collection_type', 'description')
for id, collection_type, description in tqdm(collections):
collection = Collection.objects.get(id=id)
description = collection.description
collection_updated = False
if isinstance(description, str):
if description.lower().find('pop') != -1:
collection.collection_type = Collection.POP
collection_updated = True
if not isinstance(description, dict):
collection.description = {settings.FALLBACK_LOCALE: collection.description}
collection_updated = True
if collection_updated:
update_collections.append(collection)
Collection.objects.bulk_update(update_collections, ['collection_type', 'description', ])
self.stdout.write(self.style.WARNING(f'Updated products: {len(update_collections)}'))

View File

@ -3,6 +3,7 @@ from establishment.models import Establishment
from location.models import Country, Language
from transfer.models import Collections
from collection.models import Collection
from django.conf import settings
from news.models import News
@ -93,9 +94,11 @@ class Command(BaseCommand):
country = Country.objects.filter(code=obj['country_code']).first()
if country:
objects.append(
Collection(name={"en-GB": obj['title']}, collection_type=Collection.ORDINARY,
Collection(name={settings.FALLBACK_LOCALE: obj['title']},
collection_type=Collection.POP if obj['description'].lower().find('pop') != -1
else Collection.ORDINARY,
country=country,
description=obj['description'],
description={settings.FALLBACK_LOCALE: obj['description']},
slug=obj['slug'], old_id=obj['collection_id'],
start=obj['start'],
image_url='https://s3.eu-central-1.amazonaws.com/gm-test.com/media/'+obj['attachment_suffix_url']

View File

@ -0,0 +1,49 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from establishment.models import Establishment, EstablishmentSubType, EstablishmentType
from transfer.models import Metadata
class Command(BaseCommand):
help = 'Add subtype for establishment artisan'
def handle(self, *args, **options):
artisans = Establishment.objects.artisans().filter(
old_id__isnull=False,
).prefetch_related('tags')
old_tags = Metadata.objects.filter(
establishment__in=list(artisans.values_list('old_id', flat=True)),
key='shop_category',
)
tags = []
for tag in tqdm(old_tags):
tags.append(tag.value)
subtypes = set(tags)
es_type, _ = EstablishmentType.objects.get_or_create(
index_name='artisan',
defaults={
'index_name': 'artisan',
'name': {'en-GB': 'artisan'},
}
)
for artisan in tqdm(artisans):
artisan_tags = artisan.tags.all()
for t in artisan_tags:
if t.value in subtypes:
tag = 'coffee_shop' if t.value == 'coffe_shop' else t.value
subtype, _ = EstablishmentSubType.objects.get_or_create(
index_name=tag,
defaults={
'index_name': tag,
'name': {'en-GB': ' '.join(tag.split('_')).capitalize()},
'establishment_type': es_type,
}
)
artisan.establishment_subtypes.add(subtype)
artisan.save()
self.stdout.write(self.style.WARNING(f'Artisans subtype updated.'))

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

@ -14,7 +14,7 @@ from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q
from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q, Prefetch
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
@ -24,6 +24,7 @@ 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
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
@ -126,7 +127,6 @@ class EstablishmentQuerySet(models.QuerySet):
'menu_set__plate_set__currency', 'currency'). \
prefetch_actual_employees()
def with_type_related(self):
return self.prefetch_related('establishment_subtypes')
@ -252,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(
@ -323,6 +332,13 @@ class EstablishmentQuerySet(models.QuerySet):
"""Exclude countries."""
return self.exclude(address__city__country__in=countries)
def with_certain_tag_category_related(self, index_name, attr_name):
"""Includes extra tags."""
return self.prefetch_related(
Prefetch('tags', queryset=Tag.objects.filter(category__index_name=index_name),
to_attr=attr_name)
)
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
@ -441,7 +457,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
def visible_tags(self):
return super().visible_tags \
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de']) \
'business_tag', 'business_tags_de', 'tag', ]) \
\
# todo: recalculate toque_number
@ -543,6 +559,11 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
time_at_est_tz = now_at_est_tz.time()
return schedule_for_today.ending_time > time_at_est_tz > schedule_for_today.opening_time
@property
def timezone_as_str(self):
""" Returns tz in str format"""
return self.tz.localize(datetime.now()).strftime('%z')
@property
def tags_indexing(self):
return [{'id': tag.metadata.id,
@ -591,6 +612,23 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
if qs.exists():
return qs.first().image
@property
def restaurant_category_indexing(self):
return self.tags.filter(category__index_name='category')
@property
def restaurant_cuisine_indexing(self):
return self.tags.filter(category__index_name='cuisine')
@property
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')

View File

@ -290,7 +290,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
preview_image = serializers.URLField(source='preview_image_url',
allow_null=True,
read_only=True)
tz = serializers.CharField(read_only=True, source='timezone_as_str')
new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True)
class Meta:
@ -315,6 +315,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'image',
'preview_image',
'new_image',
'tz',
'wine_regions',
]
@ -324,12 +325,18 @@ class EstablishmentListRetrieveSerializer(EstablishmentBaseSerializer):
address = AddressDetailSerializer()
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
restaurant_category = TagBaseSerializer(read_only=True, many=True, allow_null=True)
restaurant_cuisine = TagBaseSerializer(read_only=True, many=True, allow_null=True)
artisan_category = TagBaseSerializer(read_only=True, many=True, allow_null=True)
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
fields = EstablishmentBaseSerializer.Meta.fields + [
'schedule',
'restaurant_category',
'restaurant_cuisine',
'artisan_category',
]
@ -401,10 +408,31 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
]
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',
]
class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
address = AddressDetailSerializer(read_only=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer()
class Meta(EstablishmentBaseSerializer.Meta):
fields = EstablishmentBaseSerializer.Meta.fields + [
'schedule',
'establishment_type',
]
class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer):

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,10 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
def get_queryset(self):
return super().get_queryset().with_schedule() \
.with_extended_address_related().with_currency_related()
.with_extended_address_related().with_currency_related() \
.with_certain_tag_category_related('category', 'restaurant_category') \
.with_certain_tag_category_related('cuisine', 'restaurant_cuisine') \
.with_certain_tag_category_related('shop_category', 'artisan_category')
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
@ -48,6 +51,13 @@ class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView
return super().get_queryset().with_extended_related()
class EstablishmentMobileRetrieveView(EstablishmentRetrieveView):
serializer_class = serializers.MobileEstablishmentDetailSerializer
def get_queryset(self):
return super().get_queryset().prefetch_comments()
class EstablishmentRecentReviewListView(EstablishmentListView):
"""List view for last reviewed establishments."""

View File

@ -29,7 +29,7 @@ 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()
class FavoritesProductListView(generics.ListAPIView):

View File

@ -1,4 +1,5 @@
"""Main app serializers."""
from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
from location.serializers import CountrySerializer
@ -16,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')
@ -41,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."""
@ -71,7 +73,7 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
"""Meta class."""
model = models.SiteSettings
fields = (
fields = [
'country_code',
'time_format',
'subdomain',
@ -85,16 +87,29 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
'published_features',
'currency',
'country_name',
)
]
class SiteSerializer(serializers.ModelSerializer):
class SiteSettingsBackOfficeSerializer(SiteSettingsSerializer):
"""Site settings serializer for back office."""
class Meta(SiteSettingsSerializer.Meta):
"""Meta class."""
fields = SiteSettingsSerializer.Meta.fields + [
'id',
]
class SiteSerializer(SiteSettingsSerializer):
country = CountrySerializer()
class Meta:
"""Meta class."""
model = models.SiteSettings
fields = ('subdomain', 'site_url', 'country')
fields = SiteSettingsSerializer.Meta.fields + [
'id',
'country'
]
class SiteShortSerializer(serializers.ModelSerializer):
@ -107,19 +122,6 @@ class SiteShortSerializer(serializers.ModelSerializer):
]
# class SiteFeatureSerializer(serializers.ModelSerializer):
# """Site feature serializer."""
#
# class Meta:
# """Meta class."""
#
# model = models.SiteFeature
# fields = (
# 'id',
# 'published',
# 'site_settings',
# 'feature',
# )
class AwardBaseSerializer(serializers.ModelSerializer):
@ -216,3 +218,11 @@ class PageTypeBaseSerializer(serializers.ModelSerializer):
'id',
'name',
]
class ContentTypeBackSerializer(serializers.ModelSerializer):
"""Serializer fro model ContentType."""
class Meta:
model = ContentType
fields = '__all__'

View File

@ -9,7 +9,7 @@ from location.models import Country
from main.models import Award, AwardType
class AwardTestCase(APITestCase):
class BaseTestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
@ -25,6 +25,12 @@ class AwardTestCase(APITestCase):
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')})
class AwardTestCase(BaseTestCase):
def setUp(self):
super().setUp()
self.country_ru = Country.objects.create(
name={'en-GB': 'Russian'},
code='RU',
@ -71,3 +77,13 @@ class AwardTestCase(APITestCase):
response = self.client.delete(f'/api/back/main/awards/{self.award.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class ContentTypeTestCase(BaseTestCase):
def setUp(self):
super().setUp()
def test_content_type_list(self):
response = self.client.get('/api/back/main/content_type/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -1,11 +1,23 @@
"""Back main URLs"""
from django.urls import path
from main.views import back as views
from main import views
app_name = 'main'
urlpatterns = [
path('awards/', views.AwardLstView.as_view(), name='awards-list-create'),
path('awards/<int:id>/', views.AwardRUDView.as_view(), name='awards-rud'),
path('content_type/', views.ContentTypeView.as_view(), name='content_type-list'),
path('sites/', views.SiteListBackOfficeView.as_view(), name='site-list-create'),
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('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

@ -1,6 +1,6 @@
"""Main app urls."""
from django.urls import path
from main.views.common import *
from main.views import *
app = 'main'
@ -8,5 +8,5 @@ common_urlpatterns = [
path('awards/', AwardView.as_view(), name='awards_list'),
path('awards/<int:pk>/', AwardRetrieveView.as_view(), name='awards_retrieve'),
path('carousel/', CarouselListView.as_view(), name='carousel-list'),
path('determine-location/', DetermineLocation.as_view(), name='determine-location')
path('determine-location/', DetermineLocation.as_view(), name='determine-location'),
]

View File

@ -1,11 +1,12 @@
from main.urls.common import common_urlpatterns
from django.urls import path
from main.urls.common import common_urlpatterns
from main.views.web import DetermineSiteView, SiteListView, SiteSettingsView
urlpatterns = [
path('determine-site/', DetermineSiteView.as_view(), name='determine-site'),
path('sites/', SiteListView.as_view(), name='site-list'),
path('site-settings/<subdomain>/', SiteSettingsView.as_view(), name='site-settings'), ]
path('site-settings/<subdomain>/', SiteSettingsView.as_view(), name='site-settings'),
]
urlpatterns.extend(common_urlpatterns)

View File

@ -0,0 +1,4 @@
from .common import *
from .mobile import *
from .web import *
from .back import *

View File

@ -1,8 +1,11 @@
from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions
from main import serializers
from main.filters import AwardFilter
from main.models import Award
from main.views import SiteSettingsView, SiteListView
class AwardLstView(generics.ListCreateAPIView):
@ -19,3 +22,48 @@ class AwardRUDView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.BackAwardSerializer
permission_classes = (permissions.IsAdminUser,)
lookup_field = 'id'
class ContentTypeView(generics.ListAPIView):
"""ContentType list view"""
queryset = ContentType.objects.all()
serializer_class = serializers.ContentTypeBackSerializer
permission_classes = (permissions.IsAdminUser,)
filter_backends = (DjangoFilterBackend, )
ordering_fields = '__all__'
lookup_field = 'id'
filterset_fields = (
'id',
'model',
'app_label',
)
class FeatureBackView(generics.ListCreateAPIView):
"""Feature list or create View."""
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.SiteSerializer
class SiteListBackOfficeView(SiteListView):
"""Site settings View."""
serializer_class = serializers.SiteSerializer

View File

@ -19,16 +19,16 @@ class DetermineSiteView(generics.GenericAPIView):
return Response(data={'url': url})
class SiteSettingsView(generics.RetrieveAPIView):
class SiteSettingsView(generics.RetrieveUpdateDestroyAPIView):
"""Site settings View."""
lookup_field = 'subdomain'
permission_classes = (permissions.AllowAny,)
queryset = models.SiteSettings.objects.all()
serializer_class = serializers.SiteSettingsSerializer
serializer_class = serializers.SiteSettingsBackOfficeSerializer
class SiteListView(generics.ListAPIView):
class SiteListView(generics.ListCreateAPIView):
"""Site settings View."""
pagination_class = None

View File

@ -0,0 +1,29 @@
from django.core.management.base import BaseCommand
from django.db.models import F
from tqdm import tqdm
from account.models import User
from news.models import News
from transfer.models import PageTexts
class Command(BaseCommand):
help = 'Add author of News'
def handle(self, *args, **kwargs):
count = 0
news_list = News.objects.filter(created_by__isnull=True)
for news in tqdm(news_list, desc="Find author for exist news"):
old_news = PageTexts.objects.filter(id=news.old_id).annotate(
account_id=F('page__account_id'),
).first()
if old_news:
user = User.objects.filter(old_id=old_news.account_id).first()
if user:
news.created_by = user
news.modified_by = user
news.save()
count += 1
self.stdout.write(self.style.WARNING(f'Update {count} objects.'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-29 13:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0036_news_site'),
]
operations = [
migrations.AlterField(
model_name='news',
name='start',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Start'),
),
]

View File

@ -174,7 +174,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'),
help_text='{"en-GB":"some text"}')
start = models.DateTimeField(verbose_name=_('Start'))
start = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('Start'))
end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('End'))
slug = models.SlugField(unique=True, max_length=255,
@ -221,6 +222,10 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
def is_publish(self):
return self.state in self.PUBLISHED_STATES
@property
def is_international(self):
return self.INTERNATIONAL_TAG_VALUE in map(lambda tag: tag.value, self.tags.all())
@property
def web_url(self):
return reverse('web:news:rud', kwargs={'slug': self.slug})

View File

@ -201,6 +201,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
'site_id',
'template',
'template_display',
'is_international',
)

View File

@ -38,6 +38,7 @@ def transfer_news():
image=F('page__attachment_suffix_url'),
template=F('page__template'),
tags=GroupConcat('page__tags__id'),
account_id=F('page__account_id'),
)
serialized_data = NewsSerializer(data=list(queryset.values()), many=True)

View File

@ -21,7 +21,7 @@ class NewsMixinView:
qs = models.News.objects.published() \
.with_base_related() \
.annotate_in_favorites(self.request.user) \
.order_by('-is_highlighted', '-created')
.order_by('-is_highlighted', '-start')
country_code = self.request.country_code
if country_code:

View File

View File

@ -0,0 +1,23 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from review.models import Review
from transfer.models import Reviews
class Command(BaseCommand):
help = '''Add review priority from old db to new db.'''
def handle(self, *args, **kwargs):
reviews = Review.objects.all().values_list('old_id', flat=True)
queryset = Reviews.objects.exclude(product_id__isnull=False).filter(
id__in=list(reviews),
).values_list('id', 'priority')
for old_id, priority in tqdm(queryset, desc='Add priority to reviews'):
review = Review.objects.filter(old_id=old_id).first()
if review:
review.priority = priority
review.save()
self.stdout.write(self.style.WARNING(f'Priority added to review objects.'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-28 13:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('review', '0018_auto_20191117_1117'),
]
operations = [
migrations.AddField(
model_name='review',
name='priority',
field=models.PositiveSmallIntegerField(blank=True, default=None, null=True, verbose_name='Priority'),
),
]

View File

@ -39,7 +39,6 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
(TO_REVIEW, _('To review')),
(READY, _('Ready')),
)
reviewer = models.ForeignKey(
'account.User',
related_name='reviews',
@ -83,6 +82,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
)
vintage = models.IntegerField(_('Year of review'), validators=[MinValueValidator(1900), MaxValueValidator(2100)])
mark = models.FloatField(verbose_name=_('mark'), blank=True, null=True, default=None)
priority = models.PositiveSmallIntegerField(_('Priority'), blank=True, null=True, default=None)
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
objects = ReviewQuerySet.as_manager()

View File

@ -1,3 +1,54 @@
"""Review app back serializers."""
from review import models
from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
from account.models import User
from review.models import Review
class _ReviewerSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
'id',
'username',
'first_name',
'last_name',
'email',
)
class _ContentTypeSerializer(serializers.ModelSerializer):
class Meta:
model = ContentType
fields = (
'id',
'app_label',
'model',
)
class ReviewBackSerializer(serializers.ModelSerializer):
reviewer_data = _ReviewerSerializer(read_only=True, source='reviewer')
content_type_data = _ContentTypeSerializer(read_only=True, source='content_type')
status_display = serializers.CharField(read_only=True, source='get_status_display')
class Meta:
model = Review
fields = (
'id',
'reviewer',
'reviewer_data',
'text',
'status',
'status_display',
'mark',
'priority',
# 'child',
'published_at',
'vintage',
# 'country',
'content_type',
'content_type_data',
'object_id',
)

View File

@ -10,6 +10,7 @@ class ReviewBaseSerializer(serializers.ModelSerializer):
'id',
'reviewer',
'text',
'priority',
'status',
'child',
'published_at',
@ -33,6 +34,7 @@ class ReviewShortSerializer(ReviewBaseSerializer):
class InquiriesBaseSerializer(serializers.ModelSerializer):
"""Serializer for model Inquiries."""
class Meta:
model = Inquiries
fields = (
@ -56,6 +58,7 @@ class InquiriesBaseSerializer(serializers.ModelSerializer):
class GridItemsBaseSerializer(serializers.ModelSerializer):
"""Serializer for model GridItems."""
class Meta:
model = GridItems
fields = (

View File

@ -4,19 +4,34 @@ from review import filters
from review import models
from review import serializers
from utils.permissions import IsReviewerManager, IsRestaurantReviewer
from review.serializers.back import ReviewBackSerializer
class ReviewLstView(generics.ListCreateAPIView):
"""Comment list create view."""
serializer_class = serializers.ReviewBaseSerializer
"""Review list create view.
status values:
TO_INVESTIGATE = 0
TO_REVIEW = 1
READY = 2
"""
serializer_class = ReviewBackSerializer
queryset = models.Review.objects.all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly, ]
filterset_class = filters.ReviewFilter
class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view."""
serializer_class = serializers.ReviewBaseSerializer
"""Review RUD view.
status values:
TO_INVESTIGATE = 0
TO_REVIEW = 1
READY = 2
"""
serializer_class = ReviewBackSerializer
queryset = models.Review.objects.all()
permission_classes = [permissions.IsAdminUser | IsReviewerManager | IsRestaurantReviewer]
lookup_field = 'id'

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

@ -49,6 +49,30 @@ class EstablishmentDocument(Document):
'value': fields.KeywordField(),
},
multi=True)
restaurant_category = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True, attr='restaurant_category_indexing')
restaurant_cuisine = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True, attr='restaurant_cuisine_indexing')
artisan_category = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True, attr='artisan_category_indexing')
visible_tags = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
@ -92,6 +116,7 @@ class EstablishmentDocument(Document):
'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(
@ -124,6 +149,7 @@ class EstablishmentDocument(Document):
},
)
favorites_for_users = fields.ListField(field=fields.IntegerField())
tz = fields.KeywordField(attr='timezone_as_str')
class Django:

View File

@ -42,13 +42,12 @@ class NewsDocument(Document):
},
multi=True)
favorites_for_users = fields.ListField(field=fields.IntegerField())
start = fields.DateField(attr='start')
class Django:
model = models.News
fields = (
'id',
'start',
'end',
'slug',
'state',

View File

@ -149,6 +149,7 @@ class ProductDocument(Document):
name_ru = fields.TextField(attr='display_name', analyzer='russian')
name_fr = fields.TextField(attr='display_name', analyzer='french')
favorites_for_users = fields.ListField(field=fields.IntegerField())
created = fields.DateField(attr='created') # publishing date (?)
class Django:
model = models.Product

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

@ -1,7 +1,164 @@
"""Search indexes filters."""
from elasticsearch_dsl.query import Q
from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend
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):
"""Automatically adds centering and sorting within bounding box."""
@staticmethod
def calculate_center(first, second):
if second[1] < 0 < first[1]:
reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1])
diff = (reverse_first + reverse_second) / 2
if reverse_first < reverse_second:
result_part = -180 + (180 + second[1] - diff)
else:
result_part = 180 - (180 - first[1] - diff)
elif second[1] < 0 > first[1] or second[1] > 0 < first[1]:
reverse_first, reverse_second = 180 - abs(first[1]), 180 - abs(second[1])
result_part = ((reverse_first + reverse_second) / 2) * (-1 + (second[1] < 0) * 2)
else:
result_part = (first[1] + second[1]) / 2
return (first[0] + second[0]) / 2, result_part
def filter_queryset(self, request, queryset, view):
ret = super().filter_queryset(request, queryset, view)
bb = request.query_params.get('location__geo_bounding_box')
if bb:
center = self.calculate_center(*map(lambda point: list(map(float, point.split(','))), bb.split('__')))
request.GET._mutable = True
request.query_params.update({
'ordering': f'location__{center[0]}__{center[1]}__km'
})
request.GET._mutable = False
return ret
class CustomFacetedSearchFilterBackend(FacetedSearchFilterBackend):
def __init__(self):
self.facets_computed = {}
def aggregate(self, request, queryset, view):
"""Aggregate.
:param request:
:param queryset:
:param view:
:return:
"""
def make_filter(cur_facet):
def _filter(x):
return cur_facet['facet']._params['field'] != next(iter(x._params))
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):
agg = __facet['facet'].get_aggregation()
agg_filter = Q('match_all')
if __facet['global']:
queryset.aggs.bucket(
'_filter_' + __field,
'global'
).bucket(__field, agg)
else:
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]})
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):

View File

@ -135,6 +135,9 @@ class ProductEstablishmentDocumentSerializer(serializers.Serializer):
index_name = serializers.CharField()
city = AnotherCityDocumentShortSerializer()
def get_attribute(self, instance):
return instance.establishment if instance and instance.establishment else None
class AddressDocumentSerializer(serializers.Serializer):
"""Address serializer for ES Document."""
@ -165,6 +168,7 @@ class ScheduleDocumentSerializer(serializers.Serializer):
weekday = serializers.IntegerField()
weekday_display = serializers.CharField()
closed_at = serializers.CharField()
opening_at = serializers.CharField()
class InFavoritesMixin(DocumentSerializer):
@ -206,6 +210,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'preview_image_url',
'news_type',
'tags',
'start',
'slug',
)
@ -225,6 +230,9 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
establishment_subtypes = EstablishmentTypeSerializer(many=True)
address = AddressDocumentSerializer(allow_null=True)
tags = TagsDocumentSerializer(many=True, source='visible_tags')
restaurant_category = TagsDocumentSerializer(many=True, allow_null=True)
restaurant_cuisine = TagsDocumentSerializer(many=True, allow_null=True)
artisan_category = TagsDocumentSerializer(many=True, allow_null=True)
schedule = ScheduleDocumentSerializer(many=True, allow_null=True)
class Meta:
@ -243,10 +251,14 @@ class EstablishmentDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'preview_image',
'address',
'tags',
'restaurant_category',
'restaurant_cuisine',
'artisan_category',
'schedule',
'works_noon',
'works_evening',
'works_at_weekday',
'tz',
# 'works_now',
# 'collections',
# 'establishment_type',
@ -289,4 +301,5 @@ class ProductDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'grape_variety',
'establishment_detail',
'average_price',
'created',
)

View File

@ -6,9 +6,11 @@ from search_indexes import views
router = routers.SimpleRouter()
# router.register(r'news', views.NewsDocumentViewSet, basename='news') # temporarily disabled
router.register(r'establishments', views.EstablishmentDocumentViewSet, basename='establishment')
router.register(r'mobile/establishments', views.EstablishmentDocumentViewSet, basename='establishment-mobile')
router.register(r'mobile/establishments', views.MobileEstablishmentDocumentViewSet, basename='establishment-mobile')
router.register(r'news', views.NewsDocumentViewSet, basename='news')
router.register(r'mobile/news', views.MobileNewsDocumentViewSet, basename='news-mobile')
router.register(r'products', views.ProductDocumentViewSet, basename='product')
router.register(r'mobile/products', views.MobileProductDocumentViewSet, basename='product-mobile')
urlpatterns = router.urls

View File

@ -4,7 +4,8 @@ from django_elasticsearch_dsl_drf import constants
from django_elasticsearch_dsl_drf.filter_backends import (
FilteringFilterBackend,
GeoSpatialFilteringFilterBackend,
FacetedSearchFilterBackend,
GeoSpatialOrderingFilterBackend,
OrderingFilterBackend,
)
from elasticsearch_dsl import TermsFacet
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
@ -26,9 +27,16 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
filter_backends = [
filters.CustomSearchFilterBackend,
FilteringFilterBackend,
FacetedSearchFilterBackend,
filters.CustomFacetedSearchFilterBackend,
OrderingFilterBackend
]
ordering_fields = {
'start': {
'field': 'start',
},
}
faceted_search_fields = {
'tag': {
'field': 'tags.id',
@ -78,6 +86,14 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
}
class MobileNewsDocumentViewSet(NewsDocumentViewSet):
filter_backends = [
filters.CustomSearchFilterBackend,
FilteringFilterBackend,
]
class EstablishmentDocumentViewSet(BaseDocumentViewSet):
"""Establishment document ViewSet."""
@ -94,8 +110,9 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
FacetedSearchFilterBackend,
filters.CustomGeoSpatialFilteringFilterBackend,
filters.CustomFacetedSearchFilterBackend,
GeoSpatialOrderingFilterBackend,
]
faceted_search_fields = {
@ -125,7 +142,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
'enabled': True,
},
'tag': {
'field': 'visible_tags.id',
'field': 'tags.id',
'facet': TermsFacet,
'enabled': True,
'options': {
@ -277,6 +294,21 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
}
}
geo_spatial_ordering_fields = {
'location': {
'field': 'address.coordinates',
},
}
class MobileEstablishmentDocumentViewSet(EstablishmentDocumentViewSet):
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
]
class ProductDocumentViewSet(BaseDocumentViewSet):
"""Product document ViewSet."""
@ -289,9 +321,17 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
FacetedSearchFilterBackend,
filters.CustomFacetedSearchFilterBackend,
OrderingFilterBackend,
# GeoSpatialOrderingFilterBackend,
]
ordering_fields = {
'created': {
'field': 'created',
},
}
search_fields = {
'name': {'fuzziness': 'auto:2,5',
'boost': 8},
@ -306,7 +346,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
faceted_search_fields = {
'tag': {
'field': 'wine_colors.id',
'field': 'tags.id',
'enabled': True,
'facet': TermsFacet,
'options': {
@ -380,4 +420,12 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
constants.LOOKUP_QUERY_EXCLUDE,
],
},
}
}
class MobileProductDocumentViewSet(ProductDocumentViewSet):
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
]

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
@ -86,7 +89,7 @@ class TagsFilterSet(TagsBaseFilterSet):
if self.NEWS in value:
queryset = queryset.for_news().filter(value__in=settings.NEWS_CHOSEN_TAGS).distinct('value')
if self.ESTABLISHMENT in value:
queryset = queryset.for_establishments().filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct(
queryset = queryset.for_establishments().filter(category__value_type='list').filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct(
'value')
return queryset

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

@ -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

@ -7,10 +7,12 @@ from tag.models import Tag
from transfer.models import PageMetadata
from utils.legacy_parser import parse_legacy_news_content
from utils.slug_generator import generate_unique_slug
from account.models import User
class NewsSerializer(serializers.Serializer):
id = serializers.IntegerField()
account_id = serializers.IntegerField(allow_null=True)
tag_cat_id = serializers.IntegerField()
news_type_id = serializers.IntegerField()
news_title = serializers.CharField()
@ -39,6 +41,8 @@ class NewsSerializer(serializers.Serializer):
'state': self.get_state(validated_data),
'template': self.get_template(validated_data),
'country': self.get_country(validated_data),
'created_by': self.get_account(validated_data),
'modified_by': self.get_account(validated_data),
}
obj = News.objects.create(**payload)
@ -126,3 +130,8 @@ class NewsSerializer(serializers.Serializer):
else:
content = {data['locale']: data['title']}
return content
@staticmethod
def get_account(data):
"""Get account"""
return User.objects.filter(old_id=data['account_id']).first()

View File

@ -6,7 +6,6 @@ from django.conf import settings
from rest_framework.pagination import CursorPagination, PageNumberPagination
from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination
class ProjectPageNumberPagination(PageNumberPagination):
"""Customized pagination class."""
@ -65,6 +64,23 @@ class ESDocumentPagination(ESPagination):
return None
return self.page.previous_page_number()
def get_facets(self, page=None):
"""Get facets.
:param page:
:return:
"""
if page is None:
page = self.page
if hasattr(self, 'facets_computed'):
ret = {}
for filter_field, bucket_data in self.facets_computed.items():
ret.update({filter_field: bucket_data.__dict__['_d_']})
return ret
elif hasattr(page, 'facets') and hasattr(page.facets, '_d_'):
return page.facets._d_
class EstablishmentPortionPagination(ProjectMobilePagination):
"""

View File

@ -5,7 +5,7 @@ services:
mysql_db:
image: mysql:5.7
ports:
- "3306:3306"
- "3316:3306"
environment:
MYSQL_DATABASE: dev
MYSQL_USER: dev

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,6 +1,6 @@
#!/usr/bin/env bash
./manage.py transfer -a
#./manage.py transfer -d
./manage.py transfer -d
./manage.py transfer -e
./manage.py transfer --fill_city_gallery
./manage.py transfer -l

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

@ -105,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
@ -112,3 +113,6 @@ 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

@ -19,7 +19,7 @@
<div class="letter__content" style="position: relative;margin: 0 16px 40px;padding: 0 0 1px;">
<div class="letter__header" style="margin: 1.875rem 0 2.875rem;text-align: center;">
<div class="letter__logo" style="display: block;width: 7.9375rem;height: 4.6875rem;margin: 0 auto 14px auto;">
<a href="#" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<a href="https://{{ country_code }}.{{ domain_uri }}/" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<img alt="" style="width:100%;" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/1.png" />
</a>
</div>

View File

@ -19,7 +19,7 @@
<div class="letter__content" style="position: relative;margin: 0 16px 40px;padding: 0 0 1px;">
<div class="letter__header" style="margin: 1.875rem 0 2.875rem;text-align: center;">
<div class="letter__logo" style="display: block;width: 7.9375rem;height: 4.6875rem;margin: 0 auto 14px auto;">
<a href="#" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<a href="https://{{ country_code }}.{{ domain_uri }}/" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<img alt="" style="width:100%;" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/1.png" />
</a>
</div>

View File

@ -19,7 +19,7 @@
<div class="letter__content" style="position: relative;margin: 0 16px 40px;padding: 0 0 1px;">
<div class="letter__header" style="margin: 1.875rem 0 2.875rem;text-align: center;">
<div class="letter__logo" style="display: block;width: 7.9375rem;height: 4.6875rem;margin: 0 auto 14px auto;">
<a href="#" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<a href="https://{{ country_code }}.{{ domain_uri }}/" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<img alt="" style="width:100%;" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/1.png" />
</a>
</div>

View File

@ -19,7 +19,7 @@
<div class="letter__content" style="position: relative;margin: 0 16px 40px;padding: 0 0 1px;">
<div class="letter__header" style="margin: 1.875rem 0 2.875rem;text-align: center;">
<div class="letter__logo" style="display: block;width: 7.9375rem;height: 4.6875rem;margin: 0 auto 14px auto;">
<a href="#" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<a href="https://{{ country_code }}.{{ domain_uri }}/" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<img alt="" style="width:100%;" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/1.png" />
</a>
</div>

View File

@ -18,7 +18,7 @@
<div class="letter__content" style="position: relative;margin: 0 16px 40px;padding: 0 0 1px;">
<div class="letter__header" style="margin: 1.875rem 0 2.875rem;text-align: center;">
<div class="letter__logo" style="display: block;width: 7.9375rem;height: 4.6875rem;margin: 0 auto 14px auto;">
<a href="#" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<a href="https://{{ country_code }}.{{ domain_uri }}/" class="letter__logo-photo" style="color: #000;font-weight: 700;text-decoration: none;padding: 0;border-bottom: 1.5px solid #ffee29;cursor: pointer;display: block;border: none;">
<img alt="" style="width:100%;" src="https://s3.eu-central-1.amazonaws.com/gm-test.com/manually_uploaded/1.png" />
</a>
</div>