Merge branch 'develop' into fix/check_tags

This commit is contained in:
Dmitriy Kuzmenko 2019-12-09 12:06:37 +03:00
commit f6cb62c49c
175 changed files with 5097 additions and 670 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/

7
.gitignore vendored
View File

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

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

@ -1,3 +1,3 @@
FROM mdillon/postgis:9.5
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,20 @@
# Generated by Django 2.2.7 on 2019-11-22 08:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0037_sitesettings_old_id'),
('account', '0019_auto_20191108_0827'),
]
operations = [
migrations.AddField(
model_name='role',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='Site settings'),
),
]

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,16 +43,17 @@ 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)
country = models.ForeignKey(Country, verbose_name=_('Country'),
null=True, blank=True, on_delete=models.SET_NULL)
# is_list = models.BooleanField(verbose_name=_('list'), default=True, null=False)
# is_create = models.BooleanField(verbose_name=_('create'), default=False, null=False)
# is_update = models.BooleanField(verbose_name=_('update'), default=False, null=False)
# is_delete = models.BooleanField(verbose_name=_('delete'), default=False, null=False)
site = models.ForeignKey(SiteSettings, verbose_name=_('Site settings'),
null=True, blank=True, on_delete=models.SET_NULL)
class UserManager(BaseUserManager):
@ -289,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:
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

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

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2019-11-25 08:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0037_sitesettings_old_id'),
('comment', '0006_comment_is_publish'),
]
operations = [
migrations.AddField(
model_name='comment',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='site'),
),
]

View File

@ -35,7 +35,8 @@ class Comment(ProjectBaseMixin):
user = models.ForeignKey('account.User', related_name='comments', on_delete=models.CASCADE, verbose_name=_('User'))
old_id = models.IntegerField(null=True, blank=True, default=None)
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
site = models.ForeignKey('main.SiteSettings', blank=True, null=True,
on_delete=models.SET_NULL, verbose_name=_('site'))
content_type = models.ForeignKey(generic.ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')

View File

@ -8,15 +8,20 @@ from account.models import Role, User, UserRole
from authorization.tests.tests_authorization import get_tokens_for_user
from comment.models import Comment
from utils.tests.tests_permissions import BasePermissionTests
from main.models import SiteSettings
class CommentModeratorPermissionTests(BasePermissionTests):
def setUp(self):
super().setUp()
self.site_ru, created = SiteSettings.objects.get_or_create(
subdomain='ru'
)
self.role = Role.objects.create(
role=2,
country=self.country_ru
site=self.site_ru
)
self.role.save()
@ -33,11 +38,12 @@ class CommentModeratorPermissionTests(BasePermissionTests):
self.content_type = ContentType.objects.get(app_label='location', model='country')
self.user_test = get_tokens_for_user()
self.comment = Comment.objects.create(text='Test comment', mark=1,
user=self.user_test["user"],
object_id=self.country_ru.pk,
content_type_id=self.content_type.id,
country=self.country_ru
site=self.site_ru
)
self.comment.save()
self.url = reverse('back:comment:comment-crud', kwargs={"id": self.comment.id})
@ -50,7 +56,7 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"user": self.user_test["user"].id,
"object_id": self.country_ru.pk,
"content_type": self.content_type.id,
"country_id": self.country_ru.id
"site_id": self.site_ru.id
}
response = self.client.post(self.url, format='json', data=comment)
@ -61,7 +67,7 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"user": self.moderator.id,
"object_id": self.country_ru.id,
"content_type": self.content_type.id,
"country_id": self.country_ru.id
"site_id": self.site_ru.id
}
tokens = User.create_jwt_tokens(self.moderator)
@ -83,8 +89,9 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"text": "test text moderator",
"mark": 1,
"user": self.moderator.id,
"object_id": self.comment.country_id,
"content_type": self.content_type.id
"object_id": self.country_ru.id,
"content_type": self.content_type.id,
'site_id': self.site_ru.id
}
response = self.client.put(self.url, data=data, format='json')

View File

@ -8,13 +8,13 @@ class CommentLstView(generics.ListCreateAPIView):
"""Comment list create view."""
serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin]
# permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin]
class CommentRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view."""
serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all()
permission_classes = [IsCountryAdmin | IsCommentModerator]
permission_classes = [IsCommentModerator]
# permission_classes = [IsCountryAdmin | IsCommentModerator]
lookup_field = 'id'

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

@ -55,3 +55,23 @@ class EstablishmentTypeTagFilter(filters.FilterSet):
fields = (
'type_id',
)
class EmployeeBackFilter(filters.FilterSet):
"""Employee filter set."""
search = filters.CharFilter(method='search_by_name_or_last_name')
class Meta:
"""Meta class."""
model = models.Employee
fields = (
'search',
)
def search_by_name_or_last_name(self, queryset, name, value):
"""Search by name or last name."""
if value not in EMPTY_VALUES:
return queryset.search_by_name_or_last_name(value)
return queryset

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

@ -0,0 +1,28 @@
# Generated by Django 2.2.7 on 2019-11-22 11:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0065_establishment_purchased_products'),
]
operations = [
migrations.AddField(
model_name='employee',
name='last_name',
field=models.CharField(default=None, max_length=255, null=True, verbose_name='Last Name'),
),
migrations.AddField(
model_name='establishmentemployee',
name='status',
field=models.CharField(choices=[('I', 'Idle'), ('A', 'Accepted'), ('D', 'Declined')], default='I', max_length=1),
),
migrations.AlterField(
model_name='employee',
name='name',
field=models.CharField(max_length=255, verbose_name='Name'),
),
]

View File

@ -0,0 +1,39 @@
# Generated by Django 2.2.7 on 2019-11-22 12:44
from django.db import migrations, models
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
('establishment', '0066_auto_20191122_1144'),
]
operations = [
migrations.AddField(
model_name='employee',
name='birth_date',
field=models.DateTimeField(default=None, null=True, verbose_name='Birth date'),
),
migrations.AddField(
model_name='employee',
name='email',
field=models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='Email'),
),
migrations.AddField(
model_name='employee',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(default=None, max_length=128, null=True),
),
migrations.AddField(
model_name='employee',
name='sex',
field=models.PositiveSmallIntegerField(choices=[(0, 'Male'), (1, 'Female')], default=None, null=True, verbose_name='Sex'),
),
migrations.AddField(
model_name='employee',
name='toque_number',
field=models.PositiveSmallIntegerField(default=None, null=True, verbose_name='Toque number'),
),
]

View File

@ -2,6 +2,7 @@
from datetime import datetime
from functools import reduce
from operator import or_
from typing import List
import elasticsearch_dsl
from django.conf import settings
@ -13,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
@ -21,11 +22,14 @@ 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
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
IntermediateGalleryModelMixin, HasTagsMixin)
IntermediateGalleryModelMixin, HasTagsMixin,
FavoritesMixin)
# todo: establishment type&subtypes check
@ -117,9 +121,10 @@ class EstablishmentQuerySet(models.QuerySet):
'address__city__country')
def with_extended_related(self):
return self.select_related('establishment_type'). \
return self.with_extended_address_related().select_related('establishment_type'). \
prefetch_related('establishment_subtypes', 'awards', 'schedule',
'phones'). \
'phones', 'gallery', 'menu_set', 'menu_set__plate_set',
'menu_set__plate_set__currency', 'currency'). \
prefetch_actual_employees()
def with_type_related(self):
@ -247,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(
@ -318,8 +332,16 @@ 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):
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
TranslatedFieldsMixin, HasTagsMixin, FavoritesMixin):
"""Establishment model."""
# todo: delete image URL fields after moving on gallery
@ -393,6 +415,7 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat
verbose_name=_('Tag'))
reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment')
carousels = generic.GenericRelation(to='main.Carousel')
favorites = generic.GenericRelation(to='favorites.Favorites')
currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
on_delete=models.PROTECT,
@ -432,11 +455,17 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat
@property
def visible_tags(self):
return super().visible_tags\
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de'])\
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'])
# todo: recalculate toque_number
def recalculate_toque_number(self):
toque_number = 0
if self.address and self.public_mark:
@ -535,6 +564,11 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat
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,
@ -583,6 +617,27 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, Translat
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')
class EstablishmentNoteQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentNote."""
@ -609,7 +664,6 @@ class EstablishmentNote(ProjectBaseMixin):
class EstablishmentGallery(IntermediateGalleryModelMixin):
establishment = models.ForeignKey(Establishment, null=True,
related_name='establishment_gallery',
on_delete=models.CASCADE,
@ -660,6 +714,16 @@ class EstablishmentEmployeeQuerySet(models.QuerySet):
class EstablishmentEmployee(BaseAttributes):
"""EstablishmentEmployee model."""
IDLE = 'I'
ACCEPTED = 'A'
DECLINED = 'D'
STATUS_CHOICES = (
(IDLE, 'Idle'),
(ACCEPTED, 'Accepted'),
(DECLINED, 'Declined'),
)
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
verbose_name=_('Establishment'))
employee = models.ForeignKey('establishment.Employee', on_delete=models.PROTECT,
@ -670,19 +734,53 @@ class EstablishmentEmployee(BaseAttributes):
verbose_name=_('To date'))
position = models.ForeignKey(Position, on_delete=models.PROTECT,
verbose_name=_('Position'))
status = models.CharField(max_length=1, choices=STATUS_CHOICES, default=IDLE)
# old_id = affiliations_id
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
objects = EstablishmentEmployeeQuerySet.as_manager()
class EmployeeQuerySet(models.QuerySet):
def _generic_search(self, value, filter_fields_names: List[str]):
"""Generic method for searching value in specified fields"""
filters = [
{f'{field}__icontains': value}
for field in filter_fields_names
]
return self.filter(reduce(lambda x, y: x | y, [models.Q(**i) for i in filters]))
def search_by_name_or_last_name(self, value):
"""Search by name or last_name."""
return self._generic_search(value, ['name', 'last_name'])
class Employee(BaseAttributes):
"""Employee model."""
user = models.OneToOneField('account.User', on_delete=models.PROTECT,
null=True, blank=True, default=None,
verbose_name=_('User'))
name = models.CharField(max_length=255, verbose_name=_('Last name'))
name = models.CharField(max_length=255, verbose_name=_('Name'))
last_name = models.CharField(max_length=255, verbose_name=_('Last Name'), null=True, default=None)
# SEX CHOICES
MALE = 0
FEMALE = 1
SEX_CHOICES = (
(MALE, _('Male')),
(FEMALE, _('Female'))
)
sex = models.PositiveSmallIntegerField(choices=SEX_CHOICES, verbose_name=_('Sex'), null=True, default=None)
birth_date = models.DateTimeField(editable=True, verbose_name=_('Birth date'), null=True, default=None)
email = models.EmailField(blank=True, null=True, default=None, verbose_name=_('Email'))
phone = PhoneNumberField(null=True, default=None)
toque_number = models.PositiveSmallIntegerField(verbose_name=_('Toque number'), null=True, default=None)
establishments = models.ManyToManyField(Establishment, related_name='employees',
through=EstablishmentEmployee, )
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
@ -691,6 +789,8 @@ class Employee(BaseAttributes):
# old_id = profile_id
old_id = models.IntegerField(verbose_name=_('Old id'), null=True, blank=True)
objects = EmployeeQuerySet.as_manager()
class Meta:
"""Meta class."""

View File

@ -2,8 +2,9 @@ from rest_framework import serializers
from establishment import models
from establishment import serializers as model_serializers
from location.serializers import AddressDetailSerializer
from location.serializers import AddressDetailSerializer, TranslatedField
from main.models import Currency
from main.serializers import AwardSerializer
from utils.decorators import with_base_attributes
from utils.serializers import TimeZoneChoiceField
from gallery.models import Image
@ -161,12 +162,54 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer):
class EmployeeBackSerializers(serializers.ModelSerializer):
"""Employee serializers."""
awards = AwardSerializer(many=True, read_only=True)
class Meta:
model = models.Employee
fields = [
'id',
'user',
'name'
'name',
'last_name',
'sex',
'birth_date',
'email',
'phone',
'toque_number',
'awards',
]
class PositionBackSerializer(serializers.ModelSerializer):
"""Position Back serializer."""
name_translated = TranslatedField()
class Meta:
model = models.Position
fields = [
'id',
'name_translated',
'priority',
'index_name',
]
class EstablishmentEmployeeBackSerializer(serializers.ModelSerializer):
"""Establishment Employee serializer."""
employee = EmployeeBackSerializers()
position = PositionBackSerializer()
class Meta:
model = models.EstablishmentEmployee
fields = [
'id',
'employee',
'from_date',
'to_date',
'position',
'status',
]
@ -189,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

@ -13,9 +13,11 @@ from review.serializers import ReviewShortSerializer
from tag.serializers import TagBaseSerializer
from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import ImageBaseSerializer
from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer
from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer)
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -168,12 +170,51 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
awards = AwardSerializer(source='employee.awards', many=True)
priority = serializers.IntegerField(source='position.priority')
position_index_name = serializers.CharField(source='position.index_name')
status = serializers.CharField()
class Meta:
"""Meta class."""
model = models.Employee
fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name')
fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name', 'status')
class EstablishmentEmployeeCreateSerializer(serializers.ModelSerializer):
"""Serializer for establishment employee relation."""
class Meta:
"""Meta class."""
model = models.EstablishmentEmployee
fields = ('id',)
def _validate_entity(self, entity_id_param: str, entity_class):
entity_id = self.context.get('request').parser_context.get('kwargs').get(entity_id_param)
entity_qs = entity_class.objects.filter(id=entity_id)
if not entity_qs.exists():
raise serializers.ValidationError({'detail': _(f'{entity_class.__name__} not found.')})
return entity_qs.first()
def validate(self, attrs):
"""Override validate method"""
establishment = self._validate_entity("establishment_id", models.Establishment)
employee = self._validate_entity("employee_id", models.Employee)
position = self._validate_entity("position_id", models.Position)
attrs['establishment'] = establishment
attrs['employee'] = employee
attrs['position'] = position
return attrs
def create(self, validated_data, *args, **kwargs):
"""Override create method"""
validated_data.update({
'employee': validated_data.pop('employee'),
'establishment': validated_data.pop('establishment'),
'position': validated_data.pop("position")
})
return super().create(validated_data)
class EstablishmentShortSerializer(serializers.ModelSerializer):
@ -198,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()
@ -244,10 +309,12 @@ 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)
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:
@ -272,6 +339,8 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'image',
'preview_image',
'new_image',
'tz',
'wine_regions',
]
@ -280,12 +349,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',
]
@ -318,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)
@ -326,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."""
@ -352,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',
]
@ -359,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):
@ -396,6 +496,22 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer
return super().create(validated_data)
class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer):
"""Retrieve/Update/Destroy comment serializer."""
class Meta:
"""Meta class."""
model = comment_models.Comment
fields = [
'id',
'created',
'text',
'mark',
'nickname',
'profile_pic',
]
class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
"""Serializer to favorite object w/ model Establishment."""
@ -426,6 +542,29 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
return super().create(validated_data)
class EstablishmentCarouselCreateSerializer(CarouselCreateSerializer):
"""Serializer to carousel object w/ model News."""
def validate(self, attrs):
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.')})
if establishment.carousels.exists():
raise utils_exceptions.CarouselError()
attrs['establishment'] = establishment
return attrs
def create(self, validated_data, *args, **kwargs):
validated_data.update({
'content_object': validated_data.pop('establishment')
})
return super().create(validated_data)
class CompanyBaseSerializer(serializers.ModelSerializer):
"""Company base serializer"""
phone_list = serializers.SerializerMethodField(source='phones', read_only=True)

View File

@ -10,6 +10,8 @@ from translation.models import Language
from account.models import Role, UserRole
from location.models import Country, Address, City, Region
from pytz import timezone as py_tz
from main.models import SiteSettings
from timetable.models import Timetable
class BaseTestCase(APITestCase):
@ -102,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)
@ -278,13 +280,13 @@ class PlateTests(ChildTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
menu = Menu.objects.create(
category=json.dumps({"en-GB": "Test category"}),
category=json.dumps({"ru-RU": "Test category"}),
establishment=self.establishment
)
currency = Currency.objects.create(name="Test currency")
data = {
'name': json.dumps({"en-GB": "Test plate"}),
'name': json.dumps({"ru-RU": "Test plate"}),
'establishment': self.establishment.id,
'price': 10,
'menu': menu.id,
@ -298,7 +300,7 @@ class PlateTests(ChildTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'name': json.dumps({"en-GB": "Test new plate"})
'name': json.dumps({"ru-RU": "Test new plate"})
}
response = self.client.patch('/api/back/establishments/plates/1/', data=update_data)
@ -314,7 +316,7 @@ class MenuTests(ChildTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = {
'category': json.dumps({"en-GB": "Test category"}),
'category': json.dumps({"ru-RU": "Test category"}),
'establishment': self.establishment.id
}
@ -325,7 +327,7 @@ class MenuTests(ChildTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'category': json.dumps({"en-GB": "Test new category"})
'category': json.dumps({"ru-RU": "Test new category"})
}
response = self.client.patch('/api/back/establishments/menus/1/', data=update_data)
@ -336,24 +338,56 @@ class MenuTests(ChildTestCase):
class EstablishmentShedulerTests(ChildTestCase):
def test_shedule_CRUD(self):
def setUp(self):
super().setUp()
self.lang, created = Language.objects.get_or_create(
title='Russia',
locale='ru-RU'
)
self.country_ru, created = Country.objects.get_or_create(
name={"en-GB": "Russian"}
)
self.site_ru, created = SiteSettings.objects.get_or_create(
subdomain='ru'
)
role, created = Role.objects.get_or_create(
role=Role.ESTABLISHMENT_MANAGER,
country_id=self.country_ru.id,
site_id=self.site_ru.id
)
user_role, created = UserRole.objects.get_or_create(
user=self.user,
role=role,
establishment_id=self.establishment.id
)
user_role.save()
def test_schedule_CRUD(self):
data = {
'weekday': 1
}
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/schedule/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get(f'/api/back/establishments/{self.establishment.id}/schedule/1/')
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/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/1/', data=update_data)
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/1/')
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)
@ -441,3 +475,17 @@ class EstablishmentWebFavoriteTests(ChildTestCase):
f'/api/web/establishments/slug/{self.establishment.slug}/favorites/',
format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class EstablishmentCarouselTests(ChildTestCase):
def test_back_carousel_CR(self):
data = {
"object_id": self.establishment.id
}
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/slug/{self.establishment.slug}/carousels/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -8,23 +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>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.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('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'),
@ -38,10 +40,19 @@ urlpatterns = [
path('phones/<int:pk>/', views.PhonesRUDView.as_view(), name='phones-rud'),
path('emails/', views.EmailListCreateView.as_view(), name='emails'),
path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'),
path('<int:establishment_id>/employees/', views.EstablishmentEmployeeListView.as_view(),
name='establishment-employees'),
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
path('<int:establishment_id>/employee/<int:employee_id>/position/<int:position_id>',
views.EstablishmentEmployeeCreateView.as_view(),
name='employees-establishment-create'),
path('<int:establishment_id>/employee/<int:employee_id>',
views.EstablishmentEmployeeDeleteView.as_view(),
name='employees-establishment-delete'),
path('types/', views.EstablishmentTypeListCreateView.as_view(), name='type-list'),
path('types/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'),
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'),
path('positions/', views.EstablishmentPositionListView.as_view(), name='position-list'),
]

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(),
@ -17,5 +16,5 @@ urlpatterns = [
path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
name='rud-comment'),
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='create-destroy-favorites')
name='create-destroy-favorites'),
]

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

@ -1,7 +1,9 @@
"""Establishment app views."""
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from rest_framework import generics, permissions, status
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import filters, models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
@ -29,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]
@ -36,6 +39,7 @@ class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
lookup_field = 'slug'
serializer_class = ScheduleRUDSerializer
permission_classes = [IsEstablishmentManager]
@ -43,11 +47,11 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""
Returns the object the view is displaying.
"""
establishment_pk = self.kwargs.get('pk')
schedule_id = self.kwargs.get('schedule_id')
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)
@ -60,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]
@ -156,11 +161,23 @@ class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
class EmployeeListCreateView(generics.ListCreateAPIView):
"""Emplyoee list create view."""
permission_classes = (permissions.AllowAny, )
filter_class = filters.EmployeeBackFilter
serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all()
pagination_class = None
class EstablishmentEmployeeListView(generics.ListCreateAPIView):
"""Establishment emplyoees list view."""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.EstablishmentEmployeeBackSerializer
def get_queryset(self):
establishment_id = self.kwargs['establishment_id']
return models.EstablishmentEmployee.objects.filter(establishment__id=establishment_id)
class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Employee RUD view."""
serializer_class = serializers.EmployeeBackSerializers
@ -196,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):
@ -204,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'))
@ -217,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)
@ -238,6 +257,7 @@ class EstablishmentCompanyListCreateView(EstablishmentMixinViews,
generics.ListCreateAPIView):
"""List|Create establishment company view."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentCompanyListCreateSerializer
def get_object(self):
@ -245,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)
@ -261,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):
@ -268,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
@ -281,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):
@ -288,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)
@ -304,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):
@ -311,10 +334,43 @@ 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
self.check_object_permissions(self.request, note)
return note
class EstablishmentEmployeeCreateView(generics.CreateAPIView):
serializer_class = serializers.EstablishmentEmployeeCreateSerializer
queryset = models.EstablishmentEmployee.objects.all()
# TODO send email to all admins and add endpoint for changing status
class EstablishmentEmployeeDeleteView(generics.DestroyAPIView):
def _get_object_to_delete(self, establishment_id, employee_id):
result_qs = models.EstablishmentEmployee\
.objects\
.filter(establishment_id=establishment_id, employee_id=employee_id)
if not result_qs.exists():
raise Http404
return result_qs.first()
def delete(self, request, *args, **kwargs):
establishment_id = self.kwargs["establishment_id"]
employee_id = self.kwargs["employee_id"]
object_to_delete = self._get_object_to_delete(establishment_id, employee_id)
object_to_delete.delete()
return HttpResponse(status=status.HTTP_204_NO_CONTENT)
class EstablishmentPositionListView(generics.ListAPIView):
"""Establishment positions list view."""
pagination_class = None
permission_classes = (permissions.AllowAny, )
queryset = models.Position.objects.all()
serializer_class = serializers.PositionBackSerializer

View File

@ -5,12 +5,11 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from comment import models as comment_models
from establishment import filters
from establishment import models, serializers
from comment.serializers import CommentRUDSerializer
from establishment import filters, models, serializers
from main import methods
from utils.pagination import EstablishmentPortionPagination
from utils.permissions import IsCountryAdmin
from comment.serializers import CommentRUDSerializer
from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
class EstablishmentMixinView:
@ -35,8 +34,11 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
serializer_class = serializers.EstablishmentListRetrieveSerializer
def get_queryset(self):
return super().get_queryset().with_schedule()\
.with_extended_address_related().with_currency_related()
return super().get_queryset().with_schedule() \
.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):
@ -49,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."""
@ -57,12 +66,11 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
def get_queryset(self):
"""Overridden method 'get_queryset'."""
qs = super().get_queryset()
user_ip = methods.get_user_ip(self.request)
query_params = self.request.query_params
if 'longitude' in query_params and 'latitude' in query_params:
longitude, latitude = query_params.get('longitude'), query_params.get('latitude')
else:
longitude, latitude = methods.determine_coordinates(user_ip)
longitude, latitude = methods.determine_coordinates(self.request)
if not longitude or not latitude:
return qs.none()
point = Point(x=float(longitude), y=float(latitude), srid=settings.GEO_DEFAULT_SRID)
@ -106,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):
@ -134,21 +139,19 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
return comment_obj
class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView):
class EstablishmentFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
"""View for create/destroy establishment from favorites."""
serializer_class = serializers.EstablishmentFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self):
"""
Returns the object the view is displaying.
"""
establishment = get_object_or_404(models.Establishment,
slug=self.kwargs['slug'])
favorites = get_object_or_404(establishment.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites
_model = models.Establishment
serializer_class = serializers.EstablishmentFavoritesCreateSerializer
class EstablishmentCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
"""View for create/destroy establishment from carousel."""
lookup_field = 'slug'
_model = models.Establishment
serializer_class = serializers.EstablishmentCarouselCreateSerializer
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):

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

@ -1,4 +1,9 @@
from django.conf import settings
from django.core.validators import MinValueValidator, MaxValueValidator
from rest_framework import serializers
from sorl.thumbnail.parsers import parse_crop
from sorl.thumbnail.parsers import ThumbnailParseError
from django.utils.translation import gettext_lazy as _
from . import models
@ -29,3 +34,86 @@ class ImageSerializer(serializers.ModelSerializer):
extra_kwargs = {
'orientation': {'write_only': True}
}
class CropImageSerializer(ImageSerializer):
"""Serializers for image crops."""
width = serializers.IntegerField(write_only=True, required=False)
height = serializers.IntegerField(write_only=True, required=False)
crop = serializers.CharField(write_only=True,
required=False,
default='center')
quality = serializers.IntegerField(write_only=True, required=False,
default=settings.THUMBNAIL_QUALITY,
validators=[
MinValueValidator(1),
MaxValueValidator(100)])
cropped_image = serializers.DictField(read_only=True, allow_null=True)
class Meta(ImageSerializer.Meta):
"""Meta class."""
fields = [
'id',
'url',
'orientation_display',
'width',
'height',
'crop',
'quality',
'cropped_image',
]
def validate(self, attrs):
"""Overridden validate method."""
file = self._image.image
crop_width = attrs.get('width')
crop_height = attrs.get('height')
crop = attrs.get('crop')
if (crop_height and crop_width) and (crop and crop != 'smart'):
xy_image = (file.width, file.width)
xy_window = (crop_width, crop_height)
try:
parse_crop(crop, xy_image, xy_window)
attrs['image'] = file
except ThumbnailParseError:
raise serializers.ValidationError({'margin': _('Unrecognized crop option: %s') % crop})
return attrs
def create(self, validated_data):
"""Overridden create method."""
width = validated_data.pop('width', None)
height = validated_data.pop('height', None)
quality = validated_data.pop('quality')
crop = validated_data.pop('crop')
image = self._image
if image and width and height:
setattr(image,
'cropped_image',
image.get_cropped_image(
geometry=f'{width}x{height}',
quality=quality,
crop=crop))
return image
@property
def view(self):
return self.context.get('view')
@property
def lookup_field(self):
lookup_field = 'pk'
if lookup_field in self.view.kwargs:
return self.view.kwargs.get(lookup_field)
@property
def _image(self):
"""Return image from url_kwargs."""
qs = models.Image.objects.filter(id=self.lookup_field)
if qs.exists():
return qs.first()
raise serializers.ValidationError({'detail': _('Image not found.')})

View File

@ -6,6 +6,7 @@ from . import views
app_name = 'gallery'
urlpatterns = [
path('', views.ImageListCreateView.as_view(), name='list-create-image'),
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'),
path('', views.ImageListCreateView.as_view(), name='list-create'),
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy'),
path('<int:pk>/crop/', views.CropImageCreateView.as_view(), name='create-crop'),
]

View File

@ -28,3 +28,8 @@ class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView):
else:
on_commit(lambda: tasks.delete_image(image_id=instance.id))
return Response(status=status.HTTP_204_NO_CONTENT)
class CropImageCreateView(ImageBaseView, generics.CreateAPIView):
"""Create crop image."""
serializer_class = serializers.CropImageSerializer

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

@ -192,12 +192,12 @@ class WineRegionQuerySet(models.QuerySet):
def with_sub_region_related(self):
return self.prefetch_related('wine_sub_region')
def having_wines(self, value = True):
def having_wines(self, value=True):
"""Return qs with regions, which have any wine related to them"""
return self.exclude(wines__isnull=value)
return self.exclude(wineoriginaddress__product__isnull=value)
class WineRegion(models.Model, TranslatedFieldsMixin):
class WineRegion(TranslatedFieldsMixin, models.Model):
"""Wine region model."""
name = models.CharField(_('name'), max_length=255)
country = models.ForeignKey(Country, on_delete=models.PROTECT,
@ -278,6 +278,55 @@ class WineVillage(models.Model):
return self.name
class WineOriginAddressMixin(models.Model):
"""Model for wine origin address."""
wine_region = models.ForeignKey('location.WineRegion', on_delete=models.CASCADE,
verbose_name=_('wine region'))
wine_sub_region = models.ForeignKey('location.WineSubRegion', on_delete=models.CASCADE,
blank=True, null=True, default=None,
verbose_name=_('wine sub region'))
class Meta:
"""Meta class."""
abstract = True
class EstablishmentWineOriginAddressQuerySet(models.QuerySet):
"""QuerySet for EstablishmentWineOriginAddress model."""
class EstablishmentWineOriginAddress(WineOriginAddressMixin):
"""Establishment wine origin address model."""
establishment = models.ForeignKey('establishment.Establishment', on_delete=models.CASCADE,
related_name='wine_origins',
verbose_name=_('product'))
objects = EstablishmentWineOriginAddressQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('establishment wine origin address')
verbose_name_plural = _('establishment wine origin addresses')
class WineOriginAddressQuerySet(models.QuerySet):
"""QuerySet for WineOriginAddress model."""
class WineOriginAddress(WineOriginAddressMixin):
"""Wine origin address model."""
product = models.ForeignKey('product.Product', on_delete=models.CASCADE,
related_name='wine_origins',
verbose_name=_('product'))
objects = WineOriginAddressQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('wine origin address')
verbose_name_plural = _('wine origin addresses')
# todo: Make recalculate price levels
@receiver(post_save, sender=Country)
def run_recalculate_price_levels(sender, instance, **kwargs):

View File

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

View File

@ -25,12 +25,12 @@ class BaseTestCase(APITestCase):
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')})
self.lang = Language.objects.get(
self.lang, created = Language.objects.get_or_create(
title='Russia',
locale='ru-RU'
)
self.country_ru = Country.objects.get(
self.country_ru, created = Country.objects.get_or_create(
name={"en-GB": "Russian"}
)
@ -72,7 +72,7 @@ class CountryTests(BaseTestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'name': json.dumps({"en-GB": "Test new country"})
'name': json.dumps({"ru-RU": "Test new country"})
}
response = self.client.patch(f'/api/back/location/countries/{response_data["id"]}/', data=update_data)

View File

@ -3,9 +3,15 @@ from django.contrib import admin
from main import models
class SiteSettingsInline(admin.TabularInline):
model = models.SiteFeature
extra = 1
@admin.register(models.SiteSettings)
class SiteSettingsAdmin(admin.ModelAdmin):
"""Site settings admin conf."""
inlines = [SiteSettingsInline,]
@admin.register(models.Feature)

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

@ -0,0 +1,94 @@
from django.core.management.base import BaseCommand
from django.db import connections
from django.utils.text import slugify
from establishment.management.commands.add_position import namedtuplefetchall
from main.models import SiteSettings, Feature, SiteFeature
from location.models import Country
from tqdm import tqdm
class Command(BaseCommand):
help = '''Add site_features for old db to new db.
Run after command add_site_settings!'''
def site_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select s.id, s.country_code_2
from sites as s
''')
return namedtuplefetchall(cursor)
def update_site_old_id(self):
for a in tqdm(self.site_sql(), desc='Update old_id site: '):
country = Country.objects.filter(code=a.country_code_2).first()
SiteSettings.objects.filter(country=country, old_id__isnull=True)\
.update(old_id=a.id)
self.stdout.write(self.style.WARNING(f'Updated old_id site.'))
def feature_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select f.id, slug
from features as f
''')
return namedtuplefetchall(cursor)
def add_feature(self):
objects = []
for a in tqdm(self.feature_sql(), desc='Add feature: '):
features = Feature.objects.filter(slug=slugify(a.slug)).update(old_id=a.id)
if features == 0:
objects.append(
Feature(slug=slugify(a.slug), old_id=a.id)
)
Feature.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Created feature objects.'))
def site_features_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select s.id as old_site_feature,
s.site_id,
case when s.state = 'published'
then True
else False
end as published,
s.feature_id,
c.country_code_2
from features as f
join site_features s on s.feature_id=f.id
join sites c on c.id = s.site_id
''')
return namedtuplefetchall(cursor)
def add_site_features(self):
objects = []
for a in tqdm(self.site_features_sql(), desc='Add site feature: '):
site = SiteSettings.objects.get(old_id=a.site_id,
subdomain=a.country_code_2)
feature = Feature.objects.get(old_id=a.feature_id)
site_features = SiteFeature.objects\
.filter(site_settings=site,
feature=feature
).update(old_id=a.old_site_feature, published=a.published)
if site_features == 0:
objects.append(
SiteFeature(site_settings=site,
feature=feature,
published=a.published,
main=False,
old_id=a.old_site_feature
)
)
SiteFeature.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Site feature add objects.'))
def handle(self, *args, **kwargs):
self.update_site_old_id()
self.add_feature()
self.add_site_features()

View File

@ -0,0 +1,56 @@
from django.core.management.base import BaseCommand
from django.db import connections
from establishment.management.commands.add_position import namedtuplefetchall
from main.models import SiteSettings
from location.models import Country
from tqdm import tqdm
class Command(BaseCommand):
help = '''Add add site settings from old db to new db.
Run after country migrate!!!'''
def site_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
distinct
id,
country_code_2 as code,
pinterest_page_url,
twitter_page_url,
facebook_page_url,
contact_email,
config,
released,
instagram_page_url,
ad_config
from sites as s
''')
return namedtuplefetchall(cursor)
def add_site_settings(self):
objects=[]
for s in tqdm(self.site_sql(), desc='Add site settings'):
country = Country.objects.filter(code=s.code).first()
sites = SiteSettings.objects.filter(subdomain=s.code)
if not sites.exists():
objects.append(
SiteSettings(
subdomain=s.code,
country=country,
pinterest_page_url=s.pinterest_page_url,
twitter_page_url=s.twitter_page_url,
facebook_page_url=s.facebook_page_url,
instagram_page_url=s.instagram_page_url,
contact_email=s.contact_email,
config=s.config,
ad_config=s.ad_config,
old_id=s.id
)
)
SiteSettings.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Add or get tag category objects.'))
def handle(self, *args, **kwargs):
self.add_site_settings()

View File

@ -28,31 +28,25 @@ def get_user_ip(request):
return ip
def determine_country_code(ip_addr):
def determine_country_code(request):
"""Determine country code."""
country_code = None
if ip_addr:
try:
geoip = GeoIP2()
country_code = geoip.country_code(ip_addr)
country_code = country_code.lower()
except GeoIP2Exception as ex:
logger.info(f'GEOIP Exception: {ex}. ip: {ip_addr}')
except Exception as ex:
logger.error(f'GEOIP Base exception: {ex}')
return country_code
META = request.META
country_code = META.get('X-GeoIP-Country-Code',
META.get('HTTP_X_GEOIP_COUNTRY_CODE'))
if isinstance(country_code, str):
return country_code.lower()
def determine_coordinates(ip_addr: str) -> Tuple[Optional[float], Optional[float]]:
if ip_addr:
try:
geoip = GeoIP2()
return geoip.coords(ip_addr)
except GeoIP2Exception as ex:
logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}')
except Exception as ex:
logger.warning(f'GEOIP Base exception: {ex}')
return None, None
def determine_coordinates(request):
META = request.META
longitude = META.get('X-GeoIP-Longitude',
META.get('HTTP_X_GEOIP_LONGITUDE'))
latitude = META.get('X-GeoIP-Latitude',
META.get('HTTP_X_GEOIP_LATITUDE'))
try:
return float(longitude), float(latitude)
except (TypeError, ValueError):
return None, None
def determine_user_site_url(country_code):
@ -76,15 +70,11 @@ def determine_user_site_url(country_code):
return site.site_url
def determine_user_city(ip_addr: str) -> Optional[City]:
try:
geoip = GeoIP2()
return geoip.city(ip_addr)
except GeoIP2Exception as ex:
logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}')
except Exception as ex:
logger.warning(f'GEOIP Base exception: {ex}')
return None
def determine_user_city(request):
META = request.META
city = META.get('X-GeoIP-City',
META.get('HTTP_X_GEOIP_CITY'))
return city
def determine_subdivision(

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-22 07:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0036_auto_20191115_0750'),
]
operations = [
migrations.AddField(
model_name='sitesettings',
name='old_id',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-26 11:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0037_sitesettings_old_id'),
]
operations = [
migrations.AddField(
model_name='feature',
name='old_id',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-26 12:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0038_feature_old_id'),
]
operations = [
migrations.AddField(
model_name='sitefeature',
name='old_id',
field=models.IntegerField(blank=True, null=True),
),
]

View File

@ -69,6 +69,8 @@ class SiteSettings(ProjectBaseMixin):
verbose_name=_('AD config'))
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, default=None)
old_id = models.IntegerField(blank=True, null=True)
objects = SiteSettingsQuerySet.as_manager()
class Meta:
@ -105,6 +107,7 @@ class Feature(ProjectBaseMixin, PlatformMixin):
priority = models.IntegerField(unique=True, null=True, default=None)
route = models.ForeignKey('PageType', on_delete=models.PROTECT, null=True, default=None)
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
old_id = models.IntegerField(null=True, blank=True)
class Meta:
"""Meta class."""
@ -136,6 +139,7 @@ class SiteFeature(ProjectBaseMixin):
published = models.BooleanField(default=False, verbose_name=_('Published'))
main = models.BooleanField(default=False, verbose_name=_('Main'))
nested = models.ManyToManyField('self', symmetrical=False)
old_id = models.IntegerField(null=True, blank=True)
objects = SiteFeatureQuerySet.as_manager()

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

@ -70,7 +70,7 @@ class CarouselListView(generics.ListAPIView):
def get_queryset(self):
country_code = self.request.country_code
if hasattr(settings, 'CAROUSEL_ITEMS') and country_code in ['www', 'main']:
if hasattr(settings, 'CAROUSEL_ITEMS') and country_code in settings.INTERNATIONAL_COUNTRY_CODES:
qs = models.Carousel.objects.filter(id__in=settings.CAROUSEL_ITEMS)
return qs
qs = models.Carousel.objects.is_parsed().active()
@ -86,9 +86,8 @@ class DetermineLocation(generics.GenericAPIView):
serializer_class = EmptySerializer
def get(self, request, *args, **kwargs):
user_ip = methods.get_user_ip(request)
longitude, latitude = methods.determine_coordinates(user_ip)
city = methods.determine_user_city(user_ip)
longitude, latitude = methods.determine_coordinates(request)
city = methods.determine_user_city(request)
if longitude and latitude and city:
return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city})
else:

View File

@ -14,22 +14,21 @@ class DetermineSiteView(generics.GenericAPIView):
serializer_class = EmptySerializer
def get(self, request, *args, **kwargs):
user_ip = methods.get_user_ip(request)
country_code = methods.determine_country_code(user_ip)
country_code = methods.determine_country_code(request)
url = methods.determine_user_site_url(country_code)
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

@ -20,6 +20,16 @@ class NewsListFilterSet(filters.FilterSet):
tag_value__in = filters.CharFilter(method='in_tags')
type = filters.CharFilter(method='by_type')
state = filters.NumberFilter()
SORT_BY_CREATED_CHOICE = "created"
SORT_BY_START_CHOICE = "start"
SORT_BY_CHOICES = (
(SORT_BY_CREATED_CHOICE, "created"),
(SORT_BY_START_CHOICE, "start"),
)
sort_by = filters.ChoiceFilter(method='sort_by_field', choices=SORT_BY_CHOICES)
class Meta:
"""Meta class"""
model = models.News
@ -29,6 +39,8 @@ class NewsListFilterSet(filters.FilterSet):
'tag_group',
'tag_value__exclude',
'tag_value__in',
'state',
'sort_by',
)
def in_tags(self, queryset, name, value):
@ -58,3 +70,6 @@ class NewsListFilterSet(filters.FilterSet):
return queryset.filter(news_type__name=value)
else:
return queryset
def sort_by_field(self, queryset, name, value):
return queryset.order_by(f'-{value}')

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

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2019-11-22 09:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0037_sitesettings_old_id'),
('news', '0035_news_views_count'),
]
operations = [
migrations.AddField(
model_name='news',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='main.SiteSettings', verbose_name='site settings'),
),
]

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

@ -8,7 +8,8 @@ from rest_framework.reverse import reverse
from rating.models import Rating, ViewCount
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin)
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
FavoritesMixin)
from utils.querysets import TranslationQuerysetMixin
from django.conf import settings
@ -126,7 +127,8 @@ class NewsQuerySet(TranslationQuerysetMixin):
)
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin):
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
FavoritesMixin):
"""News model."""
STR_FIELD_NAME = 'title'
@ -172,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,
@ -194,6 +197,7 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
views_count = models.OneToOneField('rating.ViewCount', blank=True, null=True, on_delete=models.SET_NULL)
ratings = generic.GenericRelation(Rating)
favorites = generic.GenericRelation(to='favorites.Favorites')
carousels = generic.GenericRelation(to='main.Carousel')
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
on_delete=models.SET_NULL,
verbose_name=_('agenda'))
@ -201,7 +205,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
banner = models.ForeignKey('news.NewsBanner', blank=True, null=True,
on_delete=models.SET_NULL,
verbose_name=_('banner'))
site = models.ForeignKey('main.SiteSettings', blank=True, null=True,
on_delete=models.SET_NULL, verbose_name=_('site settings'))
objects = NewsQuerySet.as_manager()
class Meta:
@ -217,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

@ -5,13 +5,14 @@ from rest_framework.fields import SerializerMethodField
from account.serializers.common import UserBaseSerializer
from gallery.models import Image
from main.models import SiteSettings
from location import models as location_models
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
from news import models
from tag.serializers import TagBaseSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import (TranslatedField, ProjectModelSerializer,
FavoritesCreateSerializer, ImageBaseSerializer)
FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer)
class AgendaSerializer(ProjectModelSerializer):
@ -65,7 +66,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
subtitle_translated = TranslatedField()
news_type = NewsTypeSerializer(read_only=True)
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags')
in_favorites = serializers.BooleanField(allow_null=True)
in_favorites = serializers.BooleanField(allow_null=True, read_only=True)
view_counter = serializers.IntegerField(read_only=True)
class Meta:
@ -80,7 +81,6 @@ class NewsBaseSerializer(ProjectModelSerializer):
'news_type',
'tags',
'slug',
'in_favorites',
'view_counter',
)
@ -162,6 +162,7 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
"""News back office base serializer."""
is_published = serializers.BooleanField(source='is_publish', read_only=True)
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
@ -169,6 +170,7 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
fields = NewsBaseSerializer.Meta.fields + (
'title',
'subtitle',
'is_published',
)
@ -182,6 +184,9 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
country_id = serializers.PrimaryKeyRelatedField(
source='country', write_only=True,
queryset=location_models.Country.objects.all())
site_id = serializers.PrimaryKeyRelatedField(
source='site', write_only=True,
queryset=SiteSettings.objects.all())
template_display = serializers.CharField(source='get_template_display',
read_only=True)
@ -193,8 +198,10 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
'description',
'news_type_id',
'country_id',
'site_id',
'template',
'template_display',
'is_international',
)
@ -267,3 +274,24 @@ class NewsFavoritesCreateSerializer(FavoritesCreateSerializer):
'content_object': validated_data.pop('news')
})
return super().create(validated_data)
class NewsCarouselCreateSerializer(CarouselCreateSerializer):
"""Serializer to carousel object w/ model News."""
def validate(self, attrs):
news = models.News.objects.filter(pk=self.pk).first()
if not news:
raise serializers.ValidationError({'detail': _('Object not found.')})
if news.carousels.exists():
raise utils_exceptions.CarouselError()
attrs['news'] = news
return attrs
def create(self, validated_data, *args, **kwargs):
validated_data.update({
'content_object': validated_data.pop('news')
})
return super().create(validated_data)

View File

@ -5,6 +5,7 @@ from rest_framework.test import APITestCase
from rest_framework import status
from datetime import datetime, timedelta
from main.models import SiteSettings
from news.models import NewsType, News
from account.models import User, Role, UserRole
from translation.models import Language
@ -30,18 +31,23 @@ class BaseTestCase(APITestCase):
'refresh_token': tokens.get('refresh_token')})
self.test_news_type = NewsType.objects.create(name="Test news type")
self.lang = Language.objects.get(
self.lang, created = Language.objects.get_or_create(
title='Russia',
locale='ru-RU'
)
self.country_ru = Country.objects.get(
self.country_ru, created = Country.objects.get_or_create(
name={"en-GB": "Russian"}
)
self.site_ru, created = SiteSettings.objects.get_or_create(
subdomain='ru'
)
role = Role.objects.create(
role=Role.CONTENT_PAGE_MANAGER,
country=self.country_ru
site_id=self.site_ru.id
)
role.save()
@ -51,16 +57,18 @@ class BaseTestCase(APITestCase):
)
user_role.save()
self.test_news = News.objects.create(
created_by=self.user, modified_by=self.user,
title={"en-GB": "Test news"},
title={"ru-RU": "Test news"},
news_type=self.test_news_type,
description={"en-GB": "Description test news"},
description={"ru-RU": "Description test news"},
start=datetime.now() + timedelta(hours=-2),
end=datetime.now() + timedelta(hours=2),
state=News.PUBLISHED,
slug='test-news-slug',
country=self.country_ru,
site=self.site_ru
)
@ -70,14 +78,15 @@ class NewsTestCase(BaseTestCase):
def test_news_post(self):
test_news = {
"title": {"en-GB": "Test news POST"},
"title": {"ru-RU": "Test news POST"},
"news_type_id": self.test_news_type.id,
"description": {"en-GB": "Description test news"},
"description": {"ru-RU": "Description test news"},
"start": datetime.now() + timedelta(hours=-2),
"end": datetime.now() + timedelta(hours=2),
"state": News.PUBLISHED,
"slug": 'test-news-slug_post',
"country_id": self.country_ru.id,
"site_id": self.site_ru.id
}
url = reverse("back:news:list-create")
@ -107,11 +116,12 @@ class NewsTestCase(BaseTestCase):
url = reverse('back:news:retrieve-update-destroy', kwargs={'pk': self.test_news.id})
data = {
'id': self.test_news.id,
'description': {"en-GB": "Description test news!"},
'description': {"ru-RU": "Description test news!"},
'slug': self.test_news.slug,
'start': self.test_news.start,
'news_type_id': self.test_news.news_type_id,
'country_id': self.country_ru.id
'country_id': self.country_ru.id,
"site_id": self.site_ru.id
}
response = self.client.put(url, data=data, format='json')
@ -128,3 +138,17 @@ class NewsTestCase(BaseTestCase):
response = self.client.delete(f'/api/web/news/slug/{self.test_news.slug}/favorites/', format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class NewsCarouselTests(BaseTestCase):
def test_back_carousel_CR(self):
data = {
"object_id": self.test_news.id
}
response = self.client.post(f'/api/back/news/{self.test_news.id}/carousels/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.delete(f'/api/back/news/{self.test_news.id}/carousels/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

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

@ -13,4 +13,5 @@ urlpatterns = [
name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
]
path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'),
]

View File

@ -5,5 +5,6 @@ common_urlpatterns = [
path('', views.NewsListView.as_view(), name='list'),
path('types/', views.NewsTypeListView.as_view(), name='type'),
path('slug/<slug:slug>/', views.NewsDetailView.as_view(), name='rud'),
path('slug/<slug:slug>/favorites/', views.NewsFavoritesCreateDestroyView.as_view(), name='create-destroy-favorites')
path('slug/<slug:slug>/favorites/', views.NewsFavoritesCreateDestroyView.as_view(),
name='create-destroy-favorites'),
]

View File

@ -6,7 +6,7 @@ from rest_framework import generics, permissions
from news import filters, models, serializers
from rating.tasks import add_rating
from utils.permissions import IsCountryAdmin, IsContentPageManager
from utils.views import CreateDestroyGalleryViewMixin
from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
from utils.serializers import ImageBaseSerializer
@ -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:
@ -84,6 +84,7 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
serializer_class = serializers.NewsBackOfficeBaseSerializer
filter_class = filters.NewsListFilterSet
create_serializers_class = serializers.NewsBackOfficeDetailSerializer
permission_classes = [IsCountryAdmin | IsContentPageManager]
def get_serializer_class(self):
@ -150,18 +151,15 @@ class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
return self.retrieve(request, *args, **kwargs)
class NewsFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView):
class NewsFavoritesCreateDestroyView(FavoritesCreateDestroyMixinView):
"""View for create/destroy news from favorites."""
_model = models.News
serializer_class = serializers.NewsFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self):
"""
Returns the object the view is displaying.
"""
news = get_object_or_404(models.News, slug=self.kwargs.get('slug'))
favorites = get_object_or_404(news.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites
class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
"""View for create/destroy news from carousel."""
_model = models.News
serializer_class = serializers.NewsCarouselCreateSerializer

View File

@ -0,0 +1,21 @@
"""Back account serializers"""
from rest_framework import serializers
from partner.models import Partner
class BackPartnerSerializer(serializers.ModelSerializer):
class Meta:
model = Partner
fields = (
'id',
'old_id',
'name',
'url',
'image',
'establishment',
'establishment_id',
'type',
'starting_date',
'expiry_date',
'price_per_month',
)

View File

@ -1,16 +1,90 @@
# Create your tests here.
from rest_framework.test import APITestCase
from http.cookies import SimpleCookie
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User, Role, UserRole
from establishment.models import EstablishmentType, Establishment
from location.models import Country, Region, City, Address
from partner.models import Partner
from translation.models import Language
class PartnerTestCase(APITestCase):
class BaseTestCase(APITestCase):
def setUp(self):
self.test_url = "www.example.com"
self.test_partner = Partner.objects.create(url=self.test_url)
self.username = 'test_user'
self.password = 'test_user_password'
self.email = 'test_user@mail.com'
self.user = User.objects.create_user(
username=self.username,
email=self.email,
password=self.password,
is_staff=True,
)
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie({
'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token'),
})
self.establishment_type = EstablishmentType.objects.create(name="Test establishment type")
self.role = Role.objects.create(role=Role.ESTABLISHMENT_MANAGER)
self.establishment = Establishment.objects.create(
name="Test establishment",
establishment_type_id=self.establishment_type.id,
is_publish=True,
slug="test",
)
self.user_role = UserRole.objects.create(
user=self.user,
role=self.role,
establishment=self.establishment,
)
self.partner = Partner.objects.create(
url='www.ya.ru',
establishment=self.establishment,
)
class PartnerWebTestCase(BaseTestCase):
def test_partner_list(self):
response = self.client.get("/api/web/partner/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
class PartnerBackTestCase(BaseTestCase):
def test_partner_list(self):
response = self.client.get('/api/back/partner/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_partner_post(self):
test_partner = {
'url': 'http://google.com',
}
response = self.client.post('/api/back/partner/', data=test_partner, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_partner_detail(self):
response = self.client.get(f'/api/back/partner/{self.partner.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_partner_detail_put(self):
data = {
'url': 'http://yandex.com',
'name': 'Yandex',
}
response = self.client.put(f'/api/back/partner/{self.partner.id}/', data=data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_partner_delete(self):
response = self.client.delete(f'/api/back/partner/{self.partner.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -1,11 +1,15 @@
from pprint import pprint
from establishment.models import Establishment
from partner.models import Partner
from transfer.models import EstablishmentBacklinks
from transfer.serializers.partner import PartnerSerializer
def transfer_partner():
"""
Transfer data to Partner model only after transfer Establishment
"""
establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
queryset = EstablishmentBacklinks.objects.filter(
establishment_id__in=list(establishments),
@ -24,6 +28,7 @@ def transfer_partner():
serialized_data = PartnerSerializer(data=list(queryset), many=True)
if serialized_data.is_valid():
Partner.objects.all().delete() # TODO: закоментить, если требуется сохранить старые записи
serialized_data.save()
else:
pprint(f"Partner serializer errors: {serialized_data.errors}")

11
apps/partner/urls/back.py Normal file
View File

@ -0,0 +1,11 @@
"""Back account URLs"""
from django.urls import path
from partner.views import back as views
app_name = 'partner'
urlpatterns = [
path('', views.PartnerLstView.as_view(), name='partner-list-create'),
path('<int:id>/', views.PartnerRUDView.as_view(), name='partner-rud'),
]

View File

@ -0,0 +1,27 @@
from django_filters.rest_framework import DjangoFilterBackend, filters
from rest_framework import generics, permissions
from partner.models import Partner
from partner.serializers import back as serializers
from utils.permissions import IsEstablishmentManager
class PartnerLstView(generics.ListCreateAPIView):
"""Partner list create view."""
queryset = Partner.objects.all()
serializer_class = serializers.BackPartnerSerializer
pagination_class = None
permission_classes = [permissions.IsAdminUser | IsEstablishmentManager]
filter_backends = (DjangoFilterBackend,)
filterset_fields = (
'establishment',
'type',
)
class PartnerRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Partner RUD view."""
queryset = Partner.objects.all()
serializer_class = serializers.BackPartnerSerializer
permission_classes = [permissions.IsAdminUser | IsEstablishmentManager]
lookup_field = 'id'

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 +0,0 @@
# Create your views here.

Some files were not shown because too many files have changed in this diff Show More