Merge branch 'develop' into feature/gm-148

# Conflicts:
#	apps/main/models.py
#	apps/news/admin.py
#	apps/news/models.py
#	apps/news/serializers.py
#	apps/news/views.py
#	project/settings/base.py
#	project/settings/production.py
#	requirements/base.txt
This commit is contained in:
Anatoly 2019-10-23 12:00:58 +03:00
commit 90e53d738d
151 changed files with 7519 additions and 547 deletions

View File

@ -1,2 +1,30 @@
# gm-backend
## Build
1. ``git clone ssh://git@gl.id-east.ru:222/gm/gm-backend.git``
1. ``cd ./gm-backend``
1. ``git checkout develop``
1. ``docker-compose build``
1. First start database: ``docker-compose up db``
1. ``docker-compose up -d``
### Migrate data
1.Connect to container with django ``docker exec -it gm-backend_gm_app_1 bash``
#### In docker container(django)
1. Migrate ``python manage.py migrate``
1. Create super-user ``python manage.py createsuperuser``
Backend is available at localhost:8000 or 0.0.0.0:8000
URL for admin http://0.0.0.0:8000/admin
URL for swagger http://0.0.0.0:8000/docs/
URL for redocs http://0.0.0.0:8000/redocs/
## Start and stop backend containers
Demonize start ``docker-compose up -d``
Stop ``docker-compose down``
Stop and remove volumes ``docker-compose down -v``

View File

@ -2,10 +2,19 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import ugettext_lazy as _
from account import models
@admin.register(models.Role)
class RoleAdmin(admin.ModelAdmin):
list_display = ['role', 'country']
@admin.register(models.UserRole)
class UserRoleAdmin(admin.ModelAdmin):
list_display = ['user', 'role', 'establishment']
@admin.register(models.User)
class UserAdmin(BaseUserAdmin):
"""User model admin settings."""

View File

@ -0,0 +1,48 @@
# Generated by Django 2.2.4 on 2019-10-11 11:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('location', '0011_country_languages'),
('account', '0008_auto_20190912_1325'),
]
operations = [
migrations.CreateModel(
name='Role',
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')),
('role', models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator')], verbose_name='Role')),
('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='location.Country', verbose_name='Country')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='UserRole',
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')),
('role', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='account.Role', verbose_name='Role')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='user',
name='roles',
field=models.ManyToManyField(through='account.UserRole', to='account.Role', verbose_name='Roles'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-10 14:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0008_auto_20190912_1325'),
]
operations = [
migrations.AddField(
model_name='user',
name='unconfirmed_email',
field=models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='unconfirmed email'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-10 17:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0009_user_unconfirmed_email'),
]
operations = [
migrations.AddField(
model_name='user',
name='password_confirmed',
field=models.BooleanField(default=True, verbose_name='is new password confirmed'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-14 08:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0009_auto_20191011_1123'),
('account', '0010_user_password_confirmed'),
]
operations = [
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-14 12:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0009_auto_20191011_1123'),
('account', '0010_user_password_confirmed'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='password_confirmed',
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-15 07:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0011_merge_20191014_0839'),
('account', '0011_merge_20191014_1258'),
]
operations = [
]

View File

@ -0,0 +1,30 @@
# Generated by Django 2.2.4 on 2019-10-16 08:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('establishment', '0033_auto_20191003_0943_squashed_0034_auto_20191003_1036'),
('account', '0012_merge_20191015_0708'),
]
operations = [
migrations.AddField(
model_name='userrole',
name='establishment',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment', verbose_name='Establishment'),
),
migrations.AlterField(
model_name='role',
name='country',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Country', verbose_name='Country'),
),
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')], verbose_name='Role'),
),
]

View File

@ -13,11 +13,42 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from authorization.models import Application
from establishment.models import Establishment
from location.models import Country
from utils.models import GMTokenGenerator
from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin
from utils.tokens import GMRefreshToken
class Role(ProjectBaseMixin):
"""Base Role model."""
STANDARD_USER = 1
COMMENTS_MODERATOR = 2
COUNTRY_ADMIN = 3
CONTENT_PAGE_MANAGER = 4
ESTABLISHMENT_MANAGER = 5
REVIEWER_MANGER = 6
RESTAURANT_REVIEWER = 7
ROLE_CHOICES = (
(STANDARD_USER, 'Standard user'),
(COMMENTS_MODERATOR, 'Comments moderator'),
(COUNTRY_ADMIN, 'Country admin'),
(CONTENT_PAGE_MANAGER, 'Content page manager'),
(ESTABLISHMENT_MANAGER, 'Establishment manager'),
(REVIEWER_MANGER, 'Reviewer manager'),
(RESTAURANT_REVIEWER, 'Restaurant reviewer')
)
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)
class UserManager(BaseUserManager):
"""Extended manager for User model."""
@ -60,6 +91,7 @@ class User(AbstractUser):
blank=True, null=True, default=None)
email = models.EmailField(_('email address'), unique=True,
null=True, default=None)
unconfirmed_email = models.EmailField(_('unconfirmed email'), blank=True, null=True, default=None)
email_confirmed = models.BooleanField(_('email status'), default=False)
newsletter = models.NullBooleanField(default=True)
@ -67,6 +99,7 @@ class User(AbstractUser):
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
roles = models.ManyToManyField(Role, verbose_name=_('Roles'), through='UserRole')
objects = UserManager.from_queryset(UserQuerySet)()
class Meta:
@ -112,6 +145,9 @@ class User(AbstractUser):
def confirm_email(self):
"""Method to confirm user email address"""
if self.unconfirmed_email is not None:
self.email = self.unconfirmed_email
self.unconfirmed_email = None
self.email_confirmed = True
self.save()
@ -195,3 +231,11 @@ class User(AbstractUser):
return render_to_string(
template_name=settings.CHANGE_EMAIL_TEMPLATE,
context=context)
class UserRole(ProjectBaseMixin):
"""UserRole model."""
user = models.ForeignKey(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)

View File

@ -0,0 +1,21 @@
"""Back account serializers"""
from rest_framework import serializers
from account import models
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = models.Role
fields = [
'role',
'country'
]
class UserRoleSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserRole
fields = [
'user',
'role'
]

View File

@ -61,9 +61,12 @@ class UserSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
"""Override update method"""
old_email = instance.email
instance = super().update(instance, validated_data)
if 'email' in validated_data:
instance.email_confirmed = False
instance.email = old_email
instance.unconfirmed_email = validated_data['email']
instance.save()
# Send verification link on user email for change email address
if settings.USE_CELERY:

View File

@ -0,0 +1,81 @@
from rest_framework.test import APITestCase
from rest_framework import status
from authorization.tests.tests_authorization import get_tokens_for_user
from django.urls import reverse
from http.cookies import SimpleCookie
from location.models import Country
from account.models import Role, User, UserRole
class RoleTests(APITestCase):
def setUp(self):
self.data = get_tokens_for_user()
self.client.cookies = SimpleCookie(
{'access_token': self.data['tokens'].get('access_token'),
'refresh_token': self.data['tokens'].get('access_token')})
def test_role_get(self):
url = reverse('back:account:role-list-create')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_role_post(self):
url = reverse('back:account:role-list-create')
country = Country.objects.create(
name='{"ru-RU":"Russia"}',
code='23',
low_price=15,
high_price=150000
)
country.save()
data = {
"role": 2,
"country": country.pk
}
response = self.client.post(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
class UserRoleTests(APITestCase):
def setUp(self):
self.data = get_tokens_for_user()
self.client.cookies = SimpleCookie(
{'access_token': self.data['tokens'].get('access_token'),
'refresh_token': self.data['tokens'].get('access_token')})
self.country_ru = Country.objects.create(
name='{"ru-RU":"Russia"}',
code='23',
low_price=15,
high_price=150000
)
self.country_ru.save()
self.country_en = Country.objects.create(
name='{"en-GB":"England"}',
code='25',
low_price=15,
high_price=150000
)
self.country_en.save()
self.role = Role.objects.create(
role=2,
country=self.country_ru
)
self.role.save()
self.user_test = User.objects.create_user(username='test',
email='testemail@mail.com',
password='passwordtest')
def test_user_role_post(self):
url = reverse('back:account:user-role-list-create')
data = {
"user": self.user_test.id,
"role": self.role.id
}
response = self.client.post(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

12
apps/account/urls/back.py Normal file
View File

@ -0,0 +1,12 @@
"""Back account URLs"""
from django.urls import path
from account.views import back as views
app_name = 'account'
urlpatterns = [
path('role/', views.RoleLstView.as_view(), name='role-list-create'),
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
]

View File

@ -0,0 +1,13 @@
from rest_framework import generics
from account.serializers import back as serializers
from account import models
class RoleLstView(generics.ListCreateAPIView):
serializer_class = serializers.RoleSerializer
queryset = models.Role.objects.all()
class UserRoleLstView(generics.ListCreateAPIView):
serializer_class = serializers.UserRoleSerializer
queryset = models.Role.objects.all()

View File

@ -40,8 +40,7 @@ class PasswordResetConfirmView(JWTGenericViewMixin):
queryset = models.User.objects.active()
def get_object(self):
"""Override get_object method
"""
"""Override get_object method"""
queryset = self.filter_queryset(self.get_queryset())
uidb64 = self.kwargs.get('uidb64')

0
apps/booking/__init__.py Normal file
View File

8
apps/booking/admin.py Normal file
View File

@ -0,0 +1,8 @@
from django.contrib import admin
from booking.models import models
@admin.register(models.Booking)
class BookingModelAdmin(admin.ModelAdmin):
"""Model admin for model Comment"""

7
apps/booking/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class BookingConfig(AppConfig):
name = 'booking'
verbose_name = _('Booking')

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.4 on 2019-10-03 10:38
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
replaces = [('booking', '0001_initial'), ('booking', '0002_booking_user'), ('booking', '0003_auto_20190916_1533'), ('booking', '0004_auto_20190916_1646'), ('booking', '0005_auto_20190918_1308'), ('booking', '0006_booking_country_code'), ('booking', '0007_booking_booking_id'), ('booking', '0008_auto_20190919_2008'), ('booking', '0009_booking_user'), ('booking', '0010_auto_20190920_1206')]
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Booking',
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')),
('type', models.CharField(choices=[('L', 'Lastable'), ('G', 'GuestOnline')], max_length=2, verbose_name='Guestonline or Lastable')),
('booking_user_locale', models.CharField(default='en', max_length=10, verbose_name='booking locale')),
('restaurant_id', models.PositiveIntegerField(default=None, verbose_name='booking service establishment id')),
('pending_booking_id', models.TextField(default=None, verbose_name='external service pending booking')),
('booking_id', models.TextField(db_index=True, default=None, null=True, verbose_name='external service booking id')),
('user', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to=settings.AUTH_USER_MODEL, verbose_name='booking owner')),
],
options={
'abstract': False,
'verbose_name': 'Booking',
'verbose_name_plural': 'Booking',
},
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.4 on 2019-10-03 16:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('booking', '0001_initial_squashed_0010_auto_20190920_1206'),
]
operations = [
migrations.RemoveField(
model_name='booking',
name='restaurant_id',
),
migrations.AddField(
model_name='booking',
name='restaurant_id',
field=models.TextField(default=None, verbose_name='booking service establishment id'),
),
]

View File

View File

View File

@ -0,0 +1,67 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from utils.models import ProjectBaseMixin
from booking.models.services import LastableService, GuestonlineService
from account.models import User
class BookingManager(models.QuerySet):
def by_user(self, user: User):
return self.filter(user=user)
class Booking(ProjectBaseMixin):
LASTABLE = 'L'
GUESTONLINE = 'G'
AVAILABLE_SERVICES = (
(LASTABLE, 'Lastable'),
(GUESTONLINE, 'GuestOnline')
)
type = models.CharField(max_length=2, choices=AVAILABLE_SERVICES, verbose_name=_('Guestonline or Lastable'))
restaurant_id = models.TextField(verbose_name=_('booking service establishment id'), default=None)
booking_user_locale = models.CharField(verbose_name=_('booking locale'), default='en', max_length=10)
pending_booking_id = models.TextField(verbose_name=_('external service pending booking'), default=None)
booking_id = models.TextField(verbose_name=_('external service booking id'), default=None, null=True,
db_index=True, )
user = models.ForeignKey(
'account.User', verbose_name=_('booking owner'), null=True,
related_name='bookings',
blank=True, default=None, on_delete=models.CASCADE)
objects = BookingManager.as_manager()
@property
def accept_email_spam(self):
return False
@property
def accept_sms_spam(self):
return False
@classmethod
def get_service_by_type(cls, type):
if type == cls.GUESTONLINE:
return GuestonlineService()
elif type == cls.LASTABLE:
return LastableService()
else:
return None
@classmethod
def get_booking_id_by_type(cls, establishment, type):
if type == cls.GUESTONLINE:
return establishment.guestonline_id
elif type == cls.LASTABLE:
return establishment.lastable_id
else:
return None
def delete(self, using=None, keep_parents=False):
service = self.get_service_by_type(self.type)
if not service.cancel_booking(self.booking_id):
raise serializers.ValidationError(detail='Something went wrong! Unable to cancel.')
super().delete(using, keep_parents)
class Meta:
verbose_name = _('Booking')
verbose_name_plural = _('Booking')

View File

@ -0,0 +1,166 @@
from abc import ABC, abstractmethod
import json
import requests
from django.conf import settings
from rest_framework import status
import booking.models.models as models
from rest_framework import serializers
class AbstractBookingService(ABC):
""" Abstract class for Guestonline && Lastable booking services"""
def __init__(self, service):
self.service = None
self.response = None
if service not in [models.Booking.LASTABLE, models.Booking.GUESTONLINE]:
raise Exception('Service %s is not implemented yet' % service)
self.service = service
if service == models.Booking.GUESTONLINE:
self.token = settings.GUESTONLINE_TOKEN
self.url = settings.GUESTONLINE_SERVICE
elif service == models.Booking.LASTABLE:
self.token = settings.LASTABLE_TOKEN
self.url = settings.LASTABLE_SERVICE
@staticmethod
def get_certain_keys(d: dict, keys_to_preserve: set) -> dict:
""" Helper """
return {key: d[key] for key in d.keys() & keys_to_preserve}
@abstractmethod
def check_whether_booking_available(self, restaurant_id, date):
""" checks whether booking is available """
if date is None:
raise serializers.ValidationError(detail='date query param is required')
@abstractmethod
def cancel_booking(self, payload):
""" cancels booking and returns the result """
pass
@abstractmethod
def create_booking(self, payload):
""" returns pending booking id if created. otherwise False """
pass
@abstractmethod
def update_booking(self, payload):
""" updates pending booking with contacts """
pass
@abstractmethod
def get_common_headers(self):
pass
@abstractmethod
def get_booking_details(self, payload):
""" fetches booking details from external service """
pass
class GuestonlineService(AbstractBookingService):
def __init__(self):
super().__init__(models.Booking.GUESTONLINE)
def get_common_headers(self):
return {'X-Token': self.token, 'Content-type': 'application/json', 'Accept': 'application/json'}
def check_whether_booking_available(self, restaurant_id, date: str):
super().check_whether_booking_available(restaurant_id, date)
url = f'{self.url}v1/runtime_services'
params = {'restaurant_id': restaurant_id, 'date': date, 'expands[]': 'table_availabilities'}
r = requests.get(url, headers=self.get_common_headers(), params=params)
if not status.is_success(r.status_code):
return False
response = json.loads(r.content)['runtime_services']
keys_to_preserve = {'left_seats', 'table_availabilities', 'closed', 'start_time', 'end_time', 'last_booking'}
response = map(lambda x: self.get_certain_keys(x, keys_to_preserve), response)
self.response = response
return True
def commit_booking(self, payload):
url = f'{self.url}v1/pending_bookings/{payload}/commit'
r = requests.put(url, headers=self.get_common_headers())
self.response = json.loads(r.content)
if status.is_success(r.status_code) and self.response is None:
raise serializers.ValidationError(detail='Booking already committed.')
return status.is_success(r.status_code)
def update_booking(self, payload):
booking_id = payload.pop('pending_booking_id')
url = f'{self.url}v1/pending_bookings/{booking_id}'
payload['lastname'] = payload.pop('last_name')
payload['firstname'] = payload.pop('first_name')
payload['mobile_phone'] = payload.pop('phone')
headers = self.get_common_headers()
r = requests.put(url, headers=headers, data=json.dumps({'contact_info': payload}))
return status.is_success(r.status_code)
def create_booking(self, payload):
url = f'{self.url}v1/pending_bookings'
payload['hour'] = payload.pop('booking_time')
payload['persons'] = payload.pop('booked_persons_number')
payload['date'] = payload.pop('booking_date')
r = requests.post(url, headers=self.get_common_headers(), data=json.dumps(payload))
return json.loads(r.content)['id'] if status.is_success(r.status_code) else False
def cancel_booking(self, payload):
url = f'{self.url}v1/pending_bookings/{payload}'
r = requests.delete(url, headers=self.get_common_headers())
return status.is_success(r.status_code)
def get_booking_details(self, payload):
url = f'{self.url}v1/bookings/{payload}'
r = requests.get(url, headers=self.get_common_headers())
return json.loads(r.content)
class LastableService(AbstractBookingService):
def __init__(self):
super().__init__(models.Booking.LASTABLE)
self.proxies = {
'http': settings.LASTABLE_PROXY,
'https': settings.LASTABLE_PROXY,
}
def create_booking(self, payload):
url = f'{self.url}v1/partner/orders'
payload['places'] = payload.pop('booked_persons_number')
payload['hour'] = payload.pop('booking_time')
payload['firstName'] = payload.pop('first_name')
payload['lastName'] = payload.pop('last_name')
r = requests.post(url, headers=self.get_common_headers(), proxies=self.proxies, data=json.dumps(payload))
return json.loads(r.content)['data']['_id'] if status.is_success(r.status_code) else False
def get_common_headers(self):
return {'Authorization': f'Bearer {self.token}', 'Content-type': 'application/json',
'Accept': 'application/json'}
def check_whether_booking_available(self, restaurant_id, date):
super().check_whether_booking_available(restaurant_id, date)
url = f'{self.url}v1/restaurant/{restaurant_id}/offers'
r = requests.get(url, headers=self.get_common_headers(), proxies=self.proxies)
response = json.loads(r.content).get('data')
if not status.is_success(r.status_code) or not response:
return False
self.response = response
return True
def commit_booking(self, payload):
""" Lastable service has no pending booking to commit """
return False
def update_booking(self, payload):
""" Lastable service has no pending booking to update """
return False
def cancel_booking(self, payload):
url = f'{self.url}v1/partner/orders/{payload}'
r = requests.delete(url, headers=self.get_common_headers(), proxies=self.proxies)
return status.is_success(r.status_code)
def get_booking_details(self, payload):
url = f'{self.url}v1/partner/orders/{payload}'
r = requests.get(url, headers=self.get_common_headers(), proxies=self.proxies)
return json.loads(r.content)

View File

View File

@ -0,0 +1,64 @@
from rest_framework import serializers
from booking.models import models
class BookingSerializer(serializers.ModelSerializer):
class Meta:
model = models.Booking
fields = (
'id',
'type',
)
class CheckBookingSerializer(serializers.ModelSerializer):
available = serializers.BooleanField()
type = serializers.ChoiceField(choices=models.Booking.AVAILABLE_SERVICES, allow_null=True)
details = serializers.DictField()
class Meta:
model = models.Booking
fields = (
'available',
'type',
'details',
)
class PendingBookingSerializer(serializers.ModelSerializer):
restaurant_id = serializers.CharField()
booking_id = serializers.CharField(allow_null=True, allow_blank=True)
id = serializers.ReadOnlyField()
user = serializers.ReadOnlyField()
class Meta:
model = models.Booking
fields = (
'id',
'type',
'restaurant_id',
'booking_id',
'pending_booking_id',
'user',
)
class UpdateBookingSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = models.Booking
fields = ('booking_id', 'id')
class GetBookingSerializer(serializers.ModelSerializer):
details = serializers.SerializerMethodField()
def get_details(self, obj):
booking = self.instance
service = booking.get_service_by_type(booking.type)
return service.get_booking_details(booking.booking_id)
class Meta:
model = models.Booking
fields = '__all__'

3
apps/booking/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

14
apps/booking/urls.py Normal file
View File

@ -0,0 +1,14 @@
"""Booking app urls."""
from django.urls import path
from booking import views
app = 'booking'
urlpatterns = [
path('<int:establishment_id>/check/', views.CheckWhetherBookingAvailable.as_view(), name='booking-check'),
path('<int:establishment_id>/create/', views.CreatePendingBooking.as_view(), name='create-pending-booking'),
path('<int:pk>/', views.UpdatePendingBooking.as_view(), name='update-pending-booking'),
path('<int:pk>/cancel/', views.CancelBooking.as_view(), name='cancel-existing-booking'),
path('last/', views.LastBooking.as_view(), name='last_booking-for-authorizer-user'),
path('retrieve/<int:pk>/', views.GetBookingById.as_view(), name='retrieves-booking-by-id'),
]

118
apps/booking/views.py Normal file
View File

@ -0,0 +1,118 @@
from rest_framework import generics, permissions, status, serializers
from django.shortcuts import get_object_or_404
from establishment.models import Establishment
from booking.models.models import Booking, GuestonlineService, LastableService
from rest_framework.response import Response
from booking.serializers.web import (PendingBookingSerializer,
UpdateBookingSerializer, GetBookingSerializer, CheckBookingSerializer)
class CheckWhetherBookingAvailable(generics.GenericAPIView):
""" Checks which service to use if establishmend is managed by any """
permission_classes = (permissions.AllowAny,)
serializer_class = CheckBookingSerializer
pagination_class = None
def get(self, request, *args, **kwargs):
is_booking_available = False
establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id'])
service = None
date = request.query_params.get('date')
g_service = GuestonlineService()
l_service = LastableService()
if (not establishment.lastable_id is None) and l_service \
.check_whether_booking_available(establishment.lastable_id, date):
is_booking_available = True
service = l_service
service.service_id = establishment.lastable_id
elif (not establishment.guestonline_id is None) and g_service \
.check_whether_booking_available(establishment.guestonline_id, date):
is_booking_available = True
service = g_service
service.service_id = establishment.guestonline_id
response = {
'available': is_booking_available,
'type': service.service if service else None,
}
response.update({'details': service.response} if service and service.response else {})
return Response(data=response, status=200)
class CreatePendingBooking(generics.CreateAPIView):
""" Creates pending booking """
permission_classes = (permissions.AllowAny,)
serializer_class = PendingBookingSerializer
def post(self, request, *args, **kwargs):
data = request.data.copy()
if data.get('type') == Booking.LASTABLE and data.get("offer_id") is None:
raise serializers.ValidationError(detail='Offer_id is required field for Lastable service')
establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id'])
data['restaurant_id'] = Booking.get_booking_id_by_type(establishment, data.get('type'))
service = Booking.get_service_by_type(request.data.get('type'))
data['user'] = request.user.pk if request.user else None
service_to_keys = {
Booking.GUESTONLINE: {'restaurant_id', 'booking_time', 'booking_date', 'booked_persons_number', },
Booking.LASTABLE: {'booking_time', 'booked_persons_number', 'offer_id', 'email', 'phone',
'first_name', 'last_name', },
}
data['pending_booking_id'] = service.create_booking(
service.get_certain_keys(data.copy(), service_to_keys[data.get('type')]))
if not data['pending_booking_id']:
return Response(status=status.HTTP_403_FORBIDDEN, data='Unable to create booking')
data['booking_id'] = data['pending_booking_id'] if data.get('type') == Booking.LASTABLE else None
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(status=status.HTTP_201_CREATED, data=serializer.data)
class UpdatePendingBooking(generics.UpdateAPIView):
""" Update pending booking with contacts """
queryset = Booking.objects.all()
permission_classes = (permissions.AllowAny,)
serializer_class = UpdateBookingSerializer
def patch(self, request, *args, **kwargs):
instance = self.get_object()
data = request.data.copy()
service = Booking.get_service_by_type(instance.type)
data['pending_booking_id'] = instance.pending_booking_id
service.update_booking(service.get_certain_keys(data, {
'email', 'phone', 'last_name', 'first_name', 'country_code', 'pending_booking_id',
}))
service.commit_booking(data['pending_booking_id'])
data = {
'booking_id': service.response.get('id'),
'id': instance.pk,
}
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.update(instance, data)
return Response(status=status.HTTP_200_OK, data=serializer.data)
class CancelBooking(generics.DestroyAPIView):
""" Cancel existing booking """
queryset = Booking.objects.all()
permission_classes = (permissions.AllowAny,)
class LastBooking(generics.RetrieveAPIView):
""" Get last booking by user credentials """
permission_classes = (permissions.IsAuthenticated,)
serializer_class = GetBookingSerializer
lookup_field = None
def get_object(self):
return Booking.objects.by_user(self.request.user).latest('modified')
class GetBookingById(generics.RetrieveAPIView):
""" Returns booking by its id"""
permission_classes = (permissions.AllowAny,)
serializer_class = GetBookingSerializer
queryset = Booking.objects.all()

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-10-22 12:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collection', '0013_collection_slug'),
]
operations = [
migrations.AlterField(
model_name='collection',
name='end',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='end'),
),
migrations.AlterField(
model_name='guide',
name='end',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='end'),
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 2.2.4 on 2019-10-23 07:15
from django.db import migrations
import utils.models
def fill_title_json_from_title(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
Collection = apps.get_model('collection', 'Collection')
for collection in Collection.objects.all():
collection.name_json = {'en-GB': collection.name}
collection.save()
class Migration(migrations.Migration):
dependencies = [
('collection', '0014_auto_20191022_1242'),
]
operations = [
migrations.AddField(
model_name='collection',
name='name_json',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='name'),
),
migrations.RunPython(fill_title_json_from_title, migrations.RunPython.noop),
migrations.RemoveField(
model_name='collection',
name='name',
),
migrations.RenameField(
model_name='collection',
old_name='name_json',
new_name='name',
),
migrations.AlterField(
model_name='collection',
name='name',
field=utils.models.TJSONField(help_text='{"en-GB":"some text"}', verbose_name='name'),
),
]

View File

@ -1,13 +1,11 @@
from django.contrib.postgres.fields import JSONField
from django.contrib.contenttypes.fields import ContentType
from utils.models import TJSONField
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.utils.translation import gettext_lazy as _
from utils.models import ProjectBaseMixin, URLImageMixin
from utils.models import TJSONField
from utils.models import TranslatedFieldsMixin
from utils.querysets import RelatedObjectsCountMixin
@ -24,7 +22,8 @@ class CollectionNameMixin(models.Model):
class CollectionDateMixin(models.Model):
"""CollectionDate mixin"""
start = models.DateTimeField(_('start'))
end = models.DateTimeField(_('end'))
end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('end'))
class Meta:
"""Meta class"""
@ -44,9 +43,11 @@ class CollectionQuerySet(RelatedObjectsCountMixin):
return self.filter(is_publish=True)
class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
class Collection(ProjectBaseMixin, CollectionDateMixin,
TranslatedFieldsMixin, URLImageMixin):
"""Collection model."""
STR_FIELD_NAME = 'name'
ORDINARY = 0 # Ordinary collection
POP = 1 # POP collection
@ -55,6 +56,8 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
(POP, _('Pop')),
)
name = TJSONField(verbose_name=_('name'),
help_text='{"en-GB":"some text"}')
collection_type = models.PositiveSmallIntegerField(choices=COLLECTION_TYPES,
default=ORDINARY,
verbose_name=_('Collection type'))
@ -80,10 +83,6 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
verbose_name = _('collection')
verbose_name_plural = _('collections')
def __str__(self):
"""String method."""
return f'{self.name}'
class GuideQuerySet(models.QuerySet):
"""QuerySet for Guide."""

View File

@ -2,18 +2,19 @@ from rest_framework import serializers
from collection import models
from location import models as location_models
from utils.serializers import TranslatedField
class CollectionBaseSerializer(serializers.ModelSerializer):
"""Collection base serializer"""
# RESPONSE
description_translated = serializers.CharField(read_only=True, allow_null=True)
name_translated = TranslatedField()
description_translated = TranslatedField()
class Meta:
model = models.Collection
fields = [
'id',
'name',
'name_translated',
'description_translated',
'image_url',
'slug',
@ -35,8 +36,7 @@ class CollectionSerializer(CollectionBaseSerializer):
queryset=location_models.Country.objects.all(),
write_only=True)
class Meta:
model = models.Collection
class Meta(CollectionBaseSerializer.Meta):
fields = CollectionBaseSerializer.Meta.fields + [
'start',
'end',

View File

@ -40,12 +40,13 @@ class CollectionDetailTests(BaseTestCase):
def setUp(self):
super().setUp()
country = Country.objects.first()
if not country:
country = Country.objects.create(
name=json.dumps({"en-GB": "Test country"}),
code="en"
)
# country = Country.objects.first()
# if not country:
country = Country.objects.create(
name=json.dumps({"en-GB": "Test country"}),
code="en"
)
country.save()
self.collection = Collection.objects.create(
name='Test collection',
@ -56,6 +57,8 @@ class CollectionDetailTests(BaseTestCase):
slug='test-collection-slug',
)
self.collection.save()
def test_collection_detail_Read(self):
response = self.client.get(f'/api/web/collections/{self.collection.slug}/establishments/?country_code=en',
format='json')
@ -66,7 +69,7 @@ class CollectionGuideTests(CollectionDetailTests):
def test_guide_list_Read(self):
response = self.client.get('/api/web/collections/guides/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
class CollectionGuideDetailTests(CollectionDetailTests):
@ -78,6 +81,7 @@ class CollectionGuideDetailTests(CollectionDetailTests):
start=datetime.now(pytz.utc),
end=datetime.now(pytz.utc)
)
self.guide.save()
def test_guide_detail_Read(self):
response = self.client.get(f'/api/web/collections/guides/{self.guide.id}/', format='json')

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.4 on 2019-10-10 11:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('translation', '0003_auto_20190901_1032'),
('comment', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='comment',
name='language',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='translation.Language', verbose_name='Locale'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-15 07:04
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('location', '0012_data_migrate'),
('comment', '0002_comment_language'),
]
operations = [
migrations.RemoveField(
model_name='comment',
name='language',
),
migrations.AddField(
model_name='comment',
name='country',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Country', verbose_name='Country'),
),
]

View File

@ -6,6 +6,8 @@ from django.utils.translation import gettext_lazy as _
from account.models import User
from utils.models import ProjectBaseMixin
from utils.querysets import ContentTypeQuerySetMixin
from translation.models import Language
from location.models import Country
class CommentQuerySet(ContentTypeQuerySetMixin):
@ -41,6 +43,8 @@ class Comment(ProjectBaseMixin):
content_object = generic.GenericForeignKey('content_type', 'object_id')
objects = CommentQuerySet.as_manager()
country = models.ForeignKey(Country, verbose_name=_('Country'),
on_delete=models.SET_NULL, null=True)
class Meta:
"""Meta class"""

View File

@ -0,0 +1,9 @@
"""Comment app common serializers."""
from comment import models
from rest_framework import serializers
class CommentBaseSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comment
fields = ('id', 'text', 'mark', 'user')

View File

@ -1 +1,107 @@
# Create your tests here.
from utils.tests.tests_permissions import BasePermissionTests
from rest_framework import status
from authorization.tests.tests_authorization import get_tokens_for_user
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
from http.cookies import SimpleCookie
from account.models import Role, User, UserRole
from comment.models import Comment
class CommentModeratorPermissionTests(BasePermissionTests):
def setUp(self):
super().setUp()
self.role = Role.objects.create(
role=2,
country=self.country_ru
)
self.role.save()
self.moderator = User.objects.create_user(username='moderator',
email='moderator@mail.com',
password='passwordmoderator')
self.userRole = UserRole.objects.create(
user=self.moderator,
role=self.role
)
self.userRole.save()
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=content_type.id,
country=self.country_ru
)
self.comment.save()
self.url = reverse('back:comment:comment-crud', kwargs={"id": self.comment.id})
def test_put_moderator(self):
tokens = User.create_jwt_tokens(self.moderator)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('access_token')})
data = {
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": self.moderator.id
}
response = self.client.put(self.url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_get(self):
response = self.client.get(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_put_other_user(self):
other_user = User.objects.create_user(username='test',
email='test@mail.com',
password='passwordtest')
tokens = User.create_jwt_tokens(other_user)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('access_token')})
data = {
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": other_user.id
}
response = self.client.put(self.url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_put_super_user(self):
super_user = User.objects.create_user(username='super',
email='super@mail.com',
password='passwordtestsuper',
is_superuser=True)
tokens = User.create_jwt_tokens(super_user)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('access_token')})
data = {
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": super_user.id
}
response = self.client.put(self.url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

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

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

View File

@ -0,0 +1,19 @@
from rest_framework import generics, permissions
from comment.serializers import back as serializers
from comment import models
from utils.permissions import IsCommentModerator, IsCountryAdmin
class CommentLstView(generics.ListCreateAPIView):
"""Comment list create view."""
serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly,]
class CommentRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view."""
serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all()
permission_classes = [IsCountryAdmin|IsCommentModerator]
lookup_field = 'id'

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from comment.models import Comment
from establishment import models
from main.models import Award, MetaDataContent
from main.models import Award
from review import models as review_models
@ -24,11 +24,6 @@ class AwardInline(GenericTabularInline):
extra = 0
class MetaDataContentInline(GenericTabularInline):
model = MetaDataContent
extra = 0
class ContactPhoneInline(admin.TabularInline):
"""Contact phone inline admin."""
model = models.ContactPhone
@ -56,8 +51,7 @@ class EstablishmentAdmin(admin.ModelAdmin):
"""Establishment admin."""
list_display = ['id', '__str__', 'image_tag', ]
inlines = [
AwardInline, MetaDataContentInline,
ContactPhoneInline, ContactEmailInline,
AwardInline, ContactPhoneInline, ContactEmailInline,
ReviewInline, CommentInline]
@ -84,4 +78,4 @@ class MenuAdmin(admin.ModelAdmin):
"""Get user's short name."""
return obj.category_translated
category_translated.short_description = _('category')
category_translated.short_description = _('category')

View File

@ -1,6 +1,7 @@
"""Establishment app filters."""
from django.core.validators import EMPTY_VALUES
from django_filters import rest_framework as filters
from establishment import models
@ -10,6 +11,10 @@ class EstablishmentFilter(filters.FilterSet):
tag_id = filters.NumberFilter(field_name='tags__metadata__id',)
award_id = filters.NumberFilter(field_name='awards__id',)
search = filters.CharFilter(method='search_text')
type = filters.ChoiceFilter(choices=models.EstablishmentType.INDEX_NAME_TYPES,
method='by_type')
subtype = filters.ChoiceFilter(choices=models.EstablishmentSubType.INDEX_NAME_TYPES,
method='by_subtype')
class Meta:
"""Meta class."""
@ -19,6 +24,8 @@ class EstablishmentFilter(filters.FilterSet):
'tag_id',
'award_id',
'search',
'type',
'subtype',
)
def search_text(self, queryset, name, value):
@ -26,3 +33,27 @@ class EstablishmentFilter(filters.FilterSet):
if value not in EMPTY_VALUES:
return queryset.search(value, locale=self.request.locale)
return queryset
def by_type(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_type(value)
return queryset
def by_subtype(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_subtype(value)
return queryset
class EstablishmentTypeTagFilter(filters.FilterSet):
"""Establishment tag filter set."""
type_id = filters.NumberFilter(field_name='id')
class Meta:
"""Meta class."""
model = models.EstablishmentType
fields = (
'type_id',
)

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-09-16 15:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0019_establishment_is_publish'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='guestonline_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='guestonline id'),
),
migrations.AddField(
model_name='establishment',
name='lastable_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='lastable id'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-10-09 07:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('establishment', '0031_establishment_slug'),
]
operations = [
migrations.CreateModel(
name='EstablishmentTag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'verbose_name': 'establishment tag',
'verbose_name_plural': 'establishment tags',
},
),
migrations.CreateModel(
name='EstablishmentTypeTagCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('establishment_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tag_categories', to='establishment.EstablishmentType', verbose_name='establishment type')),
],
options={
'verbose_name': 'establishment type tag categories',
'verbose_name_plural': 'establishment type tag categories',
},
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-01 15:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0020_auto_20190916_1532'),
('establishment', '0031_establishment_slug'),
]
operations = [
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-03 10:40
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('establishment', '0033_auto_20191003_0943'), ('establishment', '0034_auto_20191003_1036')]
dependencies = [
('establishment', '0032_merge_20191001_1530'),
]
operations = [
migrations.RemoveField(
model_name='establishment',
name='lastable_id',
),
migrations.AddField(
model_name='establishment',
name='lastable_id',
field=models.TextField(blank=True, default=None, null=True, unique=True, verbose_name='lastable id'),
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 2.2.4 on 2019-10-09 07:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tag', '0001_initial'),
('establishment', '0032_establishmenttag_establishmenttypetagcategory'),
]
operations = [
migrations.AddField(
model_name='establishmenttypetagcategory',
name='tag_category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='est_type_tag_categories', to='tag.TagCategory', verbose_name='tag category'),
),
migrations.AddField(
model_name='establishmenttag',
name='establishment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='establishment.Establishment', verbose_name='establishment'),
),
migrations.AddField(
model_name='establishmenttag',
name='tag',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='tag.Tag', verbose_name='tag'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-09 14:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0033_auto_20191009_0715'),
('establishment', '0033_auto_20191003_0943_squashed_0034_auto_20191003_1036'),
]
operations = [
]

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.4 on 2019-10-11 10:47
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tag', '0002_auto_20191009_1408'),
('establishment', '0034_merge_20191009_1457'),
]
operations = [
migrations.CreateModel(
name='EstablishmentSubTypeTagCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('establishment_subtype', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tag_categories', to='establishment.EstablishmentSubType', verbose_name='establishment subtype')),
('tag_category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='est_subtype_tag_categories', to='tag.TagCategory', verbose_name='tag category')),
],
options={
'verbose_name': 'establishment subtype tag categories',
'verbose_name_plural': 'establishment subtype tag categories',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-11 13:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0035_establishmentsubtypetagcategory'),
]
operations = [
migrations.AlterField(
model_name='establishment',
name='establishment_subtypes',
field=models.ManyToManyField(blank=True, related_name='subtype_establishment', to='establishment.EstablishmentSubType', verbose_name='subtype'),
),
]

View File

@ -0,0 +1,54 @@
# Generated by Django 2.2.4 on 2019-10-15 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0002_auto_20191009_1408'),
('establishment', '0036_auto_20191011_1356'),
]
operations = [
migrations.RemoveField(
model_name='establishmenttag',
name='establishment',
),
migrations.RemoveField(
model_name='establishmenttag',
name='tag',
),
migrations.RemoveField(
model_name='establishmenttypetagcategory',
name='establishment_type',
),
migrations.RemoveField(
model_name='establishmenttypetagcategory',
name='tag_category',
),
migrations.AddField(
model_name='establishment',
name='tags',
field=models.ManyToManyField(related_name='establishments', to='tag.Tag', verbose_name='Tag'),
),
migrations.AddField(
model_name='establishmentsubtype',
name='tag_categories',
field=models.ManyToManyField(related_name='establishment_subtypes', to='tag.TagCategory', verbose_name='Tag'),
),
migrations.AddField(
model_name='establishmenttype',
name='tag_categories',
field=models.ManyToManyField(related_name='establishment_types', to='tag.TagCategory', verbose_name='Tag'),
),
migrations.DeleteModel(
name='EstablishmentSubTypeTagCategory',
),
migrations.DeleteModel(
name='EstablishmentTag',
),
migrations.DeleteModel(
name='EstablishmentTypeTagCategory',
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-10-16 11:33
from django.db import migrations, models
def fill_establishment_type(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
EstablishmentType = apps.get_model('establishment', 'EstablishmentType')
for n, et in enumerate(EstablishmentType.objects.all()):
et.index_name = f'Type {n}'
et.save()
class Migration(migrations.Migration):
dependencies = [
('establishment', '0037_auto_20191015_1404'),
]
operations = [
migrations.AddField(
model_name='establishmenttype',
name='index_name',
field=models.CharField(blank=True, db_index=True, max_length=50, null=True, unique=True, default=None, verbose_name='Index name'),
),
migrations.RunPython(fill_establishment_type, migrations.RunPython.noop),
migrations.AlterField(
model_name='establishmenttype',
name='index_name',
field=models.CharField(choices=[('restaurant', 'Restaurant'), ('artisan', 'Artisan'),
('producer', 'Producer')], db_index=True, max_length=50,
unique=True, verbose_name='Index name'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-10-18 13:47
from django.db import migrations, models
def fill_establishment_subtype(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
EstablishmentSubType = apps.get_model('establishment', 'EstablishmentSubType')
for n, et in enumerate(EstablishmentSubType.objects.all()):
et.index_name = f'Type {n}'
et.save()
class Migration(migrations.Migration):
dependencies = [
('establishment', '0038_establishmenttype_index_name'),
]
operations = [
migrations.AddField(
model_name='establishmentsubtype',
name='index_name',
field=models.CharField(blank=True, db_index=True, max_length=50, null=True, unique=True, default=None, verbose_name='Index name'),
),
migrations.RunPython(fill_establishment_subtype, migrations.RunPython.noop),
migrations.AlterField(
model_name='establishmentsubtype',
name='index_name',
field=models.CharField(choices=[('winery', 'Winery'), ], db_index=True, max_length=50,
unique=True, verbose_name='Index name'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-10-22 13:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0004_tag_priority'),
('establishment', '0039_establishmentsubtype_index_name'),
]
operations = [
migrations.AddField(
model_name='employee',
name='tags',
field=models.ManyToManyField(related_name='employees', to='tag.Tag', verbose_name='Tags'),
),
]

View File

@ -11,12 +11,11 @@ from django.db import models
from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from elasticsearch_dsl import Q
from phonenumber_field.modelfields import PhoneNumberField
from collection.models import Collection
from main.models import Award, MetaDataContent
from location.models import Address
from main.models import Award
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes)
@ -28,9 +27,26 @@ class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin):
STR_FIELD_NAME = 'name'
# INDEX NAME CHOICES
RESTAURANT = 'restaurant'
ARTISAN = 'artisan'
PRODUCER = 'producer'
INDEX_NAME_TYPES = (
(RESTAURANT, _('Restaurant')),
(ARTISAN, _('Artisan')),
(PRODUCER, _('Producer')),
)
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES,
unique=True, db_index=True,
verbose_name=_('Index name'))
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='establishment_types',
verbose_name=_('Tag'))
class Meta:
"""Meta class."""
@ -52,11 +68,24 @@ class EstablishmentSubTypeManager(models.Manager):
class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
"""Establishment type model."""
# INDEX NAME CHOICES
WINERY = 'winery'
INDEX_NAME_TYPES = (
(WINERY, _('Winery')),
)
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES,
unique=True, db_index=True,
verbose_name=_('Index name'))
establishment_type = models.ForeignKey(EstablishmentType,
on_delete=models.CASCADE,
verbose_name=_('Type'))
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='establishment_subtypes',
verbose_name=_('Tag'))
objects = EstablishmentSubTypeManager()
@ -76,11 +105,8 @@ class EstablishmentQuerySet(models.QuerySet):
def with_base_related(self):
"""Return qs with related objects."""
return self.select_related('address').prefetch_related(
models.Prefetch('tags',
MetaDataContent.objects.select_related(
'metadata__category'))
)
return self.select_related('address', 'establishment_type').\
prefetch_related('tags')
def with_extended_related(self):
return self.select_related('establishment_type').\
@ -88,6 +114,9 @@ class EstablishmentQuerySet(models.QuerySet):
'phones').\
prefetch_actual_employees()
def with_type_related(self):
return self.prefetch_related('establishment_subtypes')
def search(self, value, locale=None):
"""Search text in JSON fields."""
if locale is not None:
@ -99,15 +128,15 @@ class EstablishmentQuerySet(models.QuerySet):
else:
return self.none()
def es_search(self, value, locale=None):
"""Search text via ElasticSearch."""
from search_indexes.documents import EstablishmentDocument
search = EstablishmentDocument.search().filter(
Q('match', name=value) |
Q('match', **{f'description.{locale}': value})
).execute()
ids = [result.meta.id for result in search]
return self.filter(id__in=ids)
# def es_search(self, value, locale=None):
# """Search text via ElasticSearch."""
# from search_indexes.documents import EstablishmentDocument
# search = EstablishmentDocument.search().filter(
# Elastic_Q('match', name=value) |
# Elastic_Q('match', **{f'description.{locale}': value})
# ).execute()
# ids = [result.meta.id for result in search]
# return self.filter(id__in=ids)
def by_country_code(self, code):
"""Return establishments by country code"""
@ -181,7 +210,8 @@ class EstablishmentQuerySet(models.QuerySet):
return self.filter(id__in=subquery_filter_by_distance) \
.annotate_intermediate_public_mark() \
.annotate_mark_similarity(mark=establishment.public_mark) \
.order_by('mark_similarity')
.order_by('mark_similarity') \
.distinct('mark_similarity', 'id')
else:
return self.none()
@ -234,6 +264,31 @@ class EstablishmentQuerySet(models.QuerySet):
kwargs = {unit: radius}
return self.filter(address__coordinates__distance_lte=(center, DistanceMeasure(**kwargs)))
def artisans(self):
"""Return artisans."""
return self.filter(establishment_type__index_name=EstablishmentType.ARTISAN)
def producers(self):
"""Return producers."""
return self.filter(establishment_type__index_name=EstablishmentType.PRODUCER)
def restaurants(self):
"""Return restaurants."""
return self.filter(establishment_type__index_name=EstablishmentType.RESTAURANT)
def wineries(self):
"""Return wineries."""
return self.producers().filter(
establishment_subtypes__index_name=EstablishmentSubType.WINERY)
def by_type(self, value):
"""Return QuerySet with type by value."""
return self.filter(establishment_type__index_name=value)
def by_subtype(self, value):
"""Return QuerySet with subtype by value."""
return self.filter(establishment_subtypes__index_name=value)
class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
"""Establishment model."""
@ -255,6 +310,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
on_delete=models.PROTECT,
verbose_name=_('type'))
establishment_subtypes = models.ManyToManyField(EstablishmentSubType,
blank=True,
related_name='subtype_establishment',
verbose_name=_('subtype'))
address = models.ForeignKey(Address, blank=True, null=True, default=None,
@ -271,6 +327,10 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
verbose_name=_('Twitter URL'))
lafourchette = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Lafourchette URL'))
guestonline_id = models.PositiveIntegerField(blank=True, verbose_name=_('guestonline id'),
null=True, default=None,)
lastable_id = models.TextField(blank=True, verbose_name=_('lastable id'), unique=True,
null=True, default=None,)
booking = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Booking URL'))
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
@ -293,7 +353,8 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
verbose_name=_('Establishment slug'), editable=True)
awards = generic.GenericRelation(to='main.Award', related_query_name='establishment')
tags = generic.GenericRelation(to='main.MetaDataContent')
tags = models.ManyToManyField('tag.Tag', related_name='establishments',
verbose_name=_('Tag'))
reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment')
favorites = generic.GenericRelation(to='favorites.Favorites')
@ -355,11 +416,6 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
def best_price_carte(self):
return 200
@property
def tags_indexing(self):
return [{'id': tag.metadata.id,
'label': tag.metadata.label} for tag in self.tags.all()]
@property
def last_published_review(self):
"""Return last published review"""
@ -378,6 +434,20 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)).latest(
field_name='vintage_year')
@property
def country_id(self):
"""
Return Country id of establishment location
"""
return self.address.country_id
@property
def establishment_id(self):
"""
Return establishment id of establishment location
"""
return self.id
class Position(BaseAttributes, TranslatedFieldsMixin):
"""Position model."""
@ -433,7 +503,8 @@ class Employee(BaseAttributes):
establishments = models.ManyToManyField(Establishment, related_name='employees',
through=EstablishmentEmployee,)
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
tags = generic.GenericRelation(to='main.MetaDataContent')
tags = models.ManyToManyField('tag.Tag', related_name='employees',
verbose_name=_('Tags'))
class Meta:
"""Meta class."""
@ -473,6 +544,7 @@ class ContactEmail(models.Model):
def __str__(self):
return f'{self.email}'
#
# class Wine(TranslatedFieldsMixin, models.Model):
# """Wine model."""
@ -511,6 +583,10 @@ class Plate(TranslatedFieldsMixin, models.Model):
menu = models.ForeignKey(
'establishment.Menu', verbose_name=_('menu'), on_delete=models.CASCADE)
@property
def establishment_id(self):
return self.menu.establishment.id
class Meta:
verbose_name = _('plate')
verbose_name_plural = _('plates')
@ -546,3 +622,4 @@ class SocialNetwork(models.Model):
def __str__(self):
return self.title

View File

@ -1,13 +1,12 @@
from rest_framework import serializers
from establishment import models
from establishment.serializers import (
EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer,
ContactPhonesSerializer, SocialNetworkRelatedSerializers,
EstablishmentTypeSerializer)
from utils.decorators import with_base_attributes
EstablishmentTypeBaseSerializer)
from main.models import Currency
from utils.decorators import with_base_attributes
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
@ -21,7 +20,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
emails = ContactEmailsSerializer(read_only=True, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=True, many=True, )
slug = serializers.SlugField(required=True, allow_blank=False, max_length=50)
type = EstablishmentTypeSerializer(source='establishment_type', read_only=True)
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
class Meta:
model = models.Establishment
@ -39,7 +38,9 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
'image_url',
'slug',
# TODO: check in admin filters
'is_publish'
'is_publish',
'guestonline_id',
'lastable_id',
]
@ -53,7 +54,7 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer):
phones = ContactPhonesSerializer(read_only=False, many=True, )
emails = ContactEmailsSerializer(read_only=False, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=False, many=True, )
type = EstablishmentTypeSerializer(source='establishment_type')
type = EstablishmentTypeBaseSerializer(source='establishment_type')
class Meta:
model = models.Establishment
@ -139,4 +140,3 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
'user',
'name'
]

View File

@ -1,17 +1,19 @@
"""Establishment serializers."""
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from comment import models as comment_models
from comment.serializers import common as comment_serializers
from establishment import models
from favorites.models import Favorites
from location.serializers import AddressBaseSerializer
from main.models import MetaDataContent
from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer
from main.serializers import AwardSerializer, CurrencySerializer
from review import models as review_models
from tag.serializers import TagBaseSerializer
from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import TranslatedField, ProjectModelSerializer
from utils.serializers import ProjectModelSerializer
from utils.serializers import TranslatedField
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -86,30 +88,6 @@ class MenuRUDSerializers(ProjectModelSerializer):
]
class EstablishmentTypeSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentType model."""
name_translated = serializers.CharField(allow_null=True)
class Meta:
"""Meta class."""
model = models.EstablishmentType
fields = ('id', 'name_translated')
class EstablishmentSubTypeSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentSubType models."""
name_translated = serializers.CharField(allow_null=True)
class Meta:
"""Meta class."""
model = models.EstablishmentSubType
fields = ('id', 'name_translated')
class ReviewSerializer(serializers.ModelSerializer):
"""Serializer for model Review."""
text_translated = serializers.CharField(read_only=True)
@ -122,6 +100,45 @@ class ReviewSerializer(serializers.ModelSerializer):
)
class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentType model."""
name_translated = TranslatedField()
class Meta:
"""Meta class."""
model = models.EstablishmentType
fields = [
'id',
'name',
'name_translated',
'use_subtypes'
]
extra_kwargs = {
'name': {'write_only': True},
'use_subtypes': {'write_only': True},
}
class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentSubType models."""
name_translated = TranslatedField()
class Meta:
"""Meta class."""
model = models.EstablishmentSubType
fields = [
'id',
'name',
'name_translated',
'establishment_type'
]
extra_kwargs = {
'name': {'write_only': True},
'establishment_type': {'write_only': True}
}
class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
"""Serializer for actual employees."""
@ -144,8 +161,8 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
preview_image = serializers.URLField(source='preview_image_url')
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
address = AddressBaseSerializer()
tags = MetaDataContentSerializer(many=True)
in_favorites = serializers.BooleanField(allow_null=True)
tags = TagBaseSerializer(read_only=True, many=True)
class Meta:
"""Meta class."""
@ -171,8 +188,8 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
description_translated = TranslatedField()
image = serializers.URLField(source='image_url')
type = EstablishmentTypeSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeSerializer(many=True, source='establishment_subtypes')
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes')
awards = AwardSerializer(many=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
phones = ContactPhonesSerializer(read_only=True, many=True)
@ -306,17 +323,3 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer):
})
return super().create(validated_data)
class EstablishmentTagListSerializer(serializers.ModelSerializer):
"""List establishment tag serializer."""
id = serializers.IntegerField(source='metadata.id')
label_translated = serializers.CharField(
source='metadata.label_translated', read_only=True, allow_null=True)
class Meta:
"""Meta class."""
model = MetaDataContent
fields = [
'id',
'label_translated',
]

View File

@ -7,10 +7,11 @@ from main.models import Currency
from establishment.models import Establishment, EstablishmentType, Menu
# Create your tests here.
from translation.models import Language
from account.models import Role, UserRole
from location.models import Country, Address, City, Region
class BaseTestCase(APITestCase):
def setUp(self):
self.username = 'sedragurda'
self.password = 'sedragurdaredips19'
@ -27,11 +28,44 @@ class BaseTestCase(APITestCase):
self.establishment_type = EstablishmentType.objects.create(name="Test establishment type")
# Create lang object
Language.objects.create(
title='English',
locale='en-GB'
self.lang = Language.objects.get(
title='Russia',
locale='ru-RU'
)
self.country_ru = Country.objects.get(
name={"en-GB": "Russian"}
)
self.region = Region.objects.create(name='Moscow area', code='01',
country=self.country_ru)
self.region.save()
self.city = City.objects.create(name='Mosocow', code='01',
region=self.region, country=self.country_ru)
self.city.save()
self.address = Address.objects.create(city=self.city, street_name_1='Krasnaya',
number=2, postal_code='010100')
self.address.save()
self.role = Role.objects.create(role=Role.ESTABLISHMENT_MANAGER)
self.role.save()
self.establishment = Establishment.objects.create(
name="Test establishment",
establishment_type_id=self.establishment_type.id,
is_publish=True,
slug="test",
address=self.address
)
self.establishment.save()
self.user_role = UserRole.objects.create(user=self.user, role=self.role,
establishment=self.establishment)
self.user_role.save()
class EstablishmentBTests(BaseTestCase):
def test_establishment_CRUD(self):
@ -43,25 +77,25 @@ class EstablishmentBTests(BaseTestCase):
'name': 'Test establishment',
'type_id': self.establishment_type.id,
'is_publish': True,
'slug': 'test-establishment-slug',
'slug': 'test-establishment-slug'
}
response = self.client.post('/api/back/establishments/', data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
establishment = response.json()
response = self.client.get(f'/api/back/establishments/{establishment["id"]}/', format='json')
response = self.client.get(f'/api/back/establishments/{self.establishment.id}/', 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/{establishment["id"]}/', data=update_data)
response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/',
data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/establishments/{establishment["id"]}/', format='json')
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/',
format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@ -96,39 +130,45 @@ class EmployeeTests(BaseTestCase):
class ChildTestCase(BaseTestCase):
def setUp(self):
super().setUp()
self.establishment = Establishment.objects.create(
name="Test establishment",
establishment_type_id=self.establishment_type.id,
is_publish=True,
slug="test"
)
# Test childs
class EmailTests(ChildTestCase):
def test_email_CRUD(self):
def setUp(self):
super().setUp()
def test_get(self):
response = self.client.get('/api/back/establishments/emails/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_post(self):
data = {
'email': "test@test.com",
'establishment': self.establishment.id
}
response = self.client.post('/api/back/establishments/emails/', data=data)
self.id_email = response.json()['id']
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get('/api/back/establishments/emails/1/', format='json')
def test_get_by_pk(self):
self.test_post()
response = self.client.get(f'/api/back/establishments/emails/{self.id_email}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_patch(self):
self.test_post()
update_data = {
'email': 'testnew@test.com'
}
response = self.client.patch('/api/back/establishments/emails/1/', data=update_data)
response = self.client.patch(f'/api/back/establishments/emails/{self.id_email}/',
data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete('/api/back/establishments/emails/1/')
def test_email_CRUD(self):
self.test_post()
response = self.client.delete(f'/api/back/establishments/emails/{self.id_email}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@ -285,7 +325,7 @@ class EstablishmentWebTagTests(BaseTestCase):
def test_tag_Read(self):
response = self.client.get('/api/web/establishments/tags/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
class EstablishmentWebSlugTests(ChildTestCase):

View File

@ -26,4 +26,8 @@ urlpatterns = [
path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'),
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
]
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'),
]

View File

@ -7,9 +7,9 @@ app_name = 'establishment'
urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'),
path('tags/', views.EstablishmentTagListView.as_view(), name='tags'),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'),
# path('wineries/', views.WineriesListView.as_view(), name='wineries-list'),
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'),

View File

@ -4,4 +4,4 @@ from establishment.urls.common import urlpatterns as common_urlpatterns
urlpatterns = []
urlpatterns.extend(common_urlpatterns)
urlpatterns.extend(common_urlpatterns)

View File

@ -1,9 +1,10 @@
"""Establishment app views."""
from django.shortcuts import get_object_or_404
from rest_framework import generics
from establishment import models
from establishment import serializers
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
class EstablishmentMixinViews:
@ -18,23 +19,55 @@ class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAP
"""Establishment list/create view."""
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentListCreateSerializer
permission_classes = [IsCountryAdmin|IsEstablishmentManager]
class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentRUDSerializer
permission_classes = [IsCountryAdmin|IsEstablishmentManager]
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
serializer_class = ScheduleRUDSerializer
def get_object(self):
"""
Returns the object the view is displaying.
"""
establishment_pk = self.kwargs['pk']
schedule_id = self.kwargs['schedule_id']
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk)
schedule = get_object_or_404(klass=establishment.schedule,
id=schedule_id)
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
self.check_object_permissions(self.request, schedule)
return schedule
class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""Establishment schedule Create view"""
serializer_class = ScheduleCreateSerializer
class MenuListCreateView(generics.ListCreateAPIView):
"""Menu list create view."""
serializer_class = serializers.MenuSerializers
queryset = models.Menu.objects.all()
permission_classes = [IsEstablishmentManager]
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Menu RUD view."""
serializer_class = serializers.MenuRUDSerializers
queryset = models.Menu.objects.all()
permission_classes = [IsEstablishmentManager]
class SocialListCreateView(generics.ListCreateAPIView):
@ -42,12 +75,14 @@ class SocialListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.SocialNetworkSerializers
queryset = models.SocialNetwork.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
class SocialRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.SocialNetworkSerializers
queryset = models.SocialNetwork.objects.all()
permission_classes = [IsEstablishmentManager]
class PlateListCreateView(generics.ListCreateAPIView):
@ -55,12 +90,14 @@ class PlateListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.PlatesSerializers
queryset = models.Plate.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
class PlateRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.PlatesSerializers
queryset = models.Plate.objects.all()
permission_classes = [IsEstablishmentManager]
class PhonesListCreateView(generics.ListCreateAPIView):
@ -68,12 +105,14 @@ class PhonesListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.ContactPhoneBackSerializers
queryset = models.ContactPhone.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.ContactPhoneBackSerializers
queryset = models.ContactPhone.objects.all()
permission_classes = [IsEstablishmentManager]
class EmailListCreateView(generics.ListCreateAPIView):
@ -81,12 +120,14 @@ class EmailListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.ContactEmailBackSerializers
queryset = models.ContactEmail.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.ContactEmailBackSerializers
queryset = models.ContactEmail.objects.all()
permission_classes = [IsEstablishmentManager]
class EmployeeListCreateView(generics.ListCreateAPIView):
@ -100,3 +141,29 @@ class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all()
class EstablishmentTypeListCreateView(generics.ListCreateAPIView):
"""Establishment type list/create view."""
serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all()
pagination_class = None
class EstablishmentTypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment type retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all()
class EstablishmentSubtypeListCreateView(generics.ListCreateAPIView):
"""Establishment subtype list/create view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all()
pagination_class = None
class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment subtype retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all()

View File

@ -8,9 +8,8 @@ from comment import models as comment_models
from establishment import filters
from establishment import models, serializers
from main import methods
from main.models import MetaDataContent
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.pagination import EstablishmentPortionPagination
from utils.permissions import IsCountryAdmin
class EstablishmentMixinView:
@ -19,9 +18,10 @@ class EstablishmentMixinView:
permission_classes = (permissions.AllowAny,)
def get_queryset(self):
"""Overrided method 'get_queryset'."""
return models.Establishment.objects.published().with_base_related().\
annotate_in_favorites(self.request.user)
"""Overridden method 'get_queryset'."""
return models.Establishment.objects.published() \
.with_base_related() \
.annotate_in_favorites(self.request.user)
class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
@ -84,7 +84,7 @@ class EstablishmentTypeListView(generics.ListAPIView):
"""Resource for getting a list of establishment types."""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentTypeSerializer
serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all()
@ -176,42 +176,12 @@ class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIVi
return qs
class EstablishmentTagListView(generics.ListAPIView):
"""List view for establishment tags."""
serializer_class = serializers.EstablishmentTagListSerializer
permission_classes = (permissions.AllowAny,)
pagination_class = None
def get_queryset(self):
"""Override get_queryset method"""
return MetaDataContent.objects.by_content_type(app_label='establishment',
model='establishment')\
.distinct('metadata__label')
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
serializer_class = ScheduleRUDSerializer
def get_object(self):
"""
Returns the object the view is displaying.
"""
establishment_pk = self.kwargs['pk']
schedule_id = self.kwargs['schedule_id']
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk)
schedule = get_object_or_404(klass=establishment.schedule,
id=schedule_id)
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
self.check_object_permissions(self.request, schedule)
return schedule
class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""Establishment schedule Create view"""
serializer_class = ScheduleCreateSerializer
# Wineries
# todo: find out about difference between subtypes data
# class WineriesListView(EstablishmentListView):
# """Return list establishments with type Wineries"""
#
# def get_queryset(self):
# """Overridden get_queryset method."""
# qs = super(WineriesListView, self).get_queryset()
# return qs.with_type_related().wineries()

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-10-10 12:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('translation', '0003_auto_20190901_1032'),
('location', '0010_auto_20190904_0711'),
]
operations = [
migrations.AddField(
model_name='country',
name='languages',
field=models.ManyToManyField(to='translation.Language', verbose_name='Languages'),
),
]

View File

@ -0,0 +1,25 @@
from django.db import migrations, connection
import os
class Migration(migrations.Migration):
# Check migration
def load_data_from_sql(apps, schema_editor):
file_path = os.path.join(os.path.dirname(__file__), 'migrate_lang.sql')
sql_statement = open(file_path).read()
with connection.cursor() as c:
c.execute(sql_statement)
def revert_data(apps, schema_editor):
file_path = os.path.join(os.path.dirname(__file__), 'remigrate_lang.sql')
sql_statement = open(file_path).read()
with connection.cursor() as c:
c.execute(sql_statement)
dependencies = [
('location', '0011_country_languages'),
]
operations = [
migrations.RunPython(load_data_from_sql, revert_data),
]

View File

@ -0,0 +1,382 @@
SET search_path TO gm, public;
CREATE TABLE codelang (
code varchar(100) NULL,
country varchar(10000) NULL
);
INSERT INTO codelang (code,country) VALUES
('af','Afrikaans')
,('af-ZA','Afrikaans (South Africa)')
,('ar','Arabic')
,('ar-AE','Arabic (U.A.E.)')
,('ar-BH','Arabic (Bahrain)')
,('ar-DZ','Arabic (Algeria)')
,('ar-EG','Arabic (Egypt)')
,('ar-IQ','Arabic (Iraq)')
,('ar-JO','Arabic (Jordan)')
,('ar-KW','Arabic (Kuwait)')
;
INSERT INTO codelang (code,country) VALUES
('ar-LB','Arabic (Lebanon)')
,('ar-LY','Arabic (Libya)')
,('ar-MA','Arabic (Morocco)')
,('ar-OM','Arabic (Oman)')
,('ar-QA','Arabic (Qatar)')
,('ar-SA','Arabic (Saudi Arabia)')
,('ar-SY','Arabic (Syria)')
,('ar-TN','Arabic (Tunisia)')
,('ar-YE','Arabic (Yemen)')
,('az','Azeri (Latin)')
;
INSERT INTO codelang (code,country) VALUES
('az-AZ','Azeri (Latin) (Azerbaijan)')
,('az-AZ','Azeri (Cyrillic) (Azerbaijan)')
,('be','Belarusian')
,('be-BY','Belarusian (Belarus)')
,('bg','Bulgarian')
,('bg-BG','Bulgarian (Bulgaria)')
,('bs-BA','Bosnian (Bosnia and Herzegovina)')
,('ca','Catalan')
,('ca-ES','Catalan (Spain)')
,('cs','Czech')
;
INSERT INTO codelang (code,country) VALUES
('cs-CZ','Czech (Czech Republic)')
,('cy','Welsh')
,('cy-GB','Welsh (United Kingdom)')
,('da','Danish')
,('da-DK','Danish (Denmark)')
,('de','German')
,('de-AT','German (Austria)')
,('de-CH','German (Switzerland)')
,('de-DE','German (Germany)')
,('de-LI','German (Liechtenstein)')
;
INSERT INTO codelang (code,country) VALUES
('de-LU','German (Luxembourg)')
,('dv','Divehi')
,('dv-MV','Divehi (Maldives)')
,('el','Greek')
,('el-GR','Greek (Greece)')
,('en','English')
,('en-AU','English (Australia)')
,('en-BZ','English (Belize)')
,('en-CA','English (Canada)')
,('en-CB','English (Caribbean)')
;
INSERT INTO codelang (code,country) VALUES
('en-GB','English (United Kingdom)')
,('en-IE','English (Ireland)')
,('en-JM','English (Jamaica)')
,('en-NZ','English (New Zealand)')
,('en-PH','English (Republic of the Philippines)')
,('en-TT','English (Trinidad and Tobago)')
,('en-US','English (United States)')
,('en-ZA','English (South Africa)')
,('en-ZW','English (Zimbabwe)')
,('eo','Esperanto')
;
INSERT INTO codelang (code,country) VALUES
('es','Spanish')
,('es-AR','Spanish (Argentina)')
,('es-BO','Spanish (Bolivia)')
,('es-CL','Spanish (Chile)')
,('es-CO','Spanish (Colombia)')
,('es-CR','Spanish (Costa Rica)')
,('es-DO','Spanish (Dominican Republic)')
,('es-EC','Spanish (Ecuador)')
,('es-ES','Spanish (Spain)')
;
INSERT INTO codelang (code,country) VALUES
('es-GT','Spanish (Guatemala)')
,('es-HN','Spanish (Honduras)')
,('es-MX','Spanish (Mexico)')
,('es-NI','Spanish (Nicaragua)')
,('es-PA','Spanish (Panama)')
,('es-PE','Spanish (Peru)')
,('es-PR','Spanish (Puerto Rico)')
,('es-PY','Spanish (Paraguay)')
,('es-SV','Spanish (El Salvador)')
,('es-UY','Spanish (Uruguay)')
;
INSERT INTO codelang (code,country) VALUES
('es-VE','Spanish (Venezuela)')
,('et','Estonian')
,('et-EE','Estonian (Estonia)')
,('eu','Basque')
,('eu-ES','Basque (Spain)')
,('fa','Farsi')
,('fa-IR','Farsi (Iran)')
,('fi','Finnish')
,('fi-FI','Finnish (Finland)')
,('fo','Faroese')
;
INSERT INTO codelang (code,country) VALUES
('fo-FO','Faroese (Faroe Islands)')
,('fr','French')
,('fr-BE','French (Belgium)')
,('fr-CA','French (Canada)')
,('fr-CH','French (Switzerland)')
,('fr-FR','French (France)')
,('fr-LU','French (Luxembourg)')
,('fr-MC','French (Principality of Monaco)')
,('gl','Galician')
,('gl-ES','Galician (Spain)')
;
INSERT INTO codelang (code,country) VALUES
('gu','Gujarati')
,('gu-IN','Gujarati (India)')
,('he','Hebrew')
,('he-IL','Hebrew (Israel)')
,('hi','Hindi')
,('hi-IN','Hindi (India)')
,('hr','Croatian')
,('hr-BA','Croatian (Bosnia and Herzegovina)')
,('hr-HR','Croatian (Croatia)')
,('hu','Hungarian')
;
INSERT INTO codelang (code,country) VALUES
('hu-HU','Hungarian (Hungary)')
,('hy','Armenian')
,('hy-AM','Armenian (Armenia)')
,('id','Indonesian')
,('id-ID','Indonesian (Indonesia)')
,('is','Icelandic')
,('is-IS','Icelandic (Iceland)')
,('it','Italian')
,('it-CH','Italian (Switzerland)')
,('it-IT','Italian (Italy)')
;
INSERT INTO codelang (code,country) VALUES
('ja','Japanese')
,('ja-JP','Japanese (Japan)')
,('ka','Georgian')
,('ka-GE','Georgian (Georgia)')
,('kk','Kazakh')
,('kk-KZ','Kazakh (Kazakhstan)')
,('kn','Kannada')
,('kn-IN','Kannada (India)')
,('ko','Korean')
,('ko-KR','Korean (Korea)')
;
INSERT INTO codelang (code,country) VALUES
('kok','Konkani')
,('kok-IN','Konkani (India)')
,('ky','Kyrgyz')
,('ky-KG','Kyrgyz (Kyrgyzstan)')
,('lt','Lithuanian')
,('lt-LT','Lithuanian (Lithuania)')
,('lv','Latvian')
,('lv-LV','Latvian (Latvia)')
,('mi','Maori')
,('mi-NZ','Maori (New Zealand)')
;
INSERT INTO codelang (code,country) VALUES
('mk','FYRO Macedonian')
,('mk-MK','FYRO Macedonian (Former Yugoslav Republic of Macedonia)')
,('mn','Mongolian')
,('mn-MN','Mongolian (Mongolia)')
,('mr','Marathi')
,('mr-IN','Marathi (India)')
,('ms','Malay')
,('ms-BN','Malay (Brunei Darussalam)')
,('ms-MY','Malay (Malaysia)')
,('mt','Maltese')
;
INSERT INTO codelang (code,country) VALUES
('mt-MT','Maltese (Malta)')
,('nb','Norwegian (Bokm?l)')
,('nb-NO','Norwegian (Bokm?l) (Norway)')
,('nl','Dutch')
,('nl-BE','Dutch (Belgium)')
,('nl-NL','Dutch (Netherlands)')
,('nn-NO','Norwegian (Nynorsk) (Norway)')
,('ns','Northern Sotho')
,('ns-ZA','Northern Sotho (South Africa)')
,('pa','Punjabi')
;
INSERT INTO codelang (code,country) VALUES
('pa-IN','Punjabi (India)')
,('pl','Polish')
,('pl-PL','Polish (Poland)')
,('ps','Pashto')
,('ps-AR','Pashto (Afghanistan)')
,('pt','Portuguese')
,('pt-BR','Portuguese (Brazil)')
,('pt-PT','Portuguese (Portugal)')
,('qu','Quechua')
,('qu-BO','Quechua (Bolivia)')
;
INSERT INTO codelang (code,country) VALUES
('qu-EC','Quechua (Ecuador)')
,('qu-PE','Quechua (Peru)')
,('ro','Romanian')
,('ro-RO','Romanian (Romania)')
,('ru','Russian')
,('ru-RU','Russian (Russia)')
,('sa','Sanskrit')
,('sa-IN','Sanskrit (India)')
,('se','Sami (Northern)')
,('se-FI','Sami (Northern) (Finland)')
;
INSERT INTO codelang (code,country) VALUES
('se-FI','Sami (Skolt) (Finland)')
,('se-FI','Sami (Inari) (Finland)')
,('se-NO','Sami (Northern) (Norway)')
,('se-NO','Sami (Lule) (Norway)')
,('se-NO','Sami (Southern) (Norway)')
,('se-SE','Sami (Northern) (Sweden)')
,('se-SE','Sami (Lule) (Sweden)')
,('se-SE','Sami (Southern) (Sweden)')
,('sk','Slovak')
,('sk-SK','Slovak (Slovakia)')
;
INSERT INTO codelang (code,country) VALUES
('sl','Slovenian')
,('sl-SI','Slovenian (Slovenia)')
,('sq','Albanian')
,('sq-AL','Albanian (Albania)')
,('sr-BA','Serbian (Latin) (Bosnia and Herzegovina)')
,('sr-BA','Serbian (Cyrillic) (Bosnia and Herzegovina)')
,('sr-SP','Serbian (Latin) (Serbia and Montenegro)')
,('sr-SP','Serbian (Cyrillic) (Serbia and Montenegro)')
,('sv','Swedish')
,('sv-FI','Swedish (Finland)')
;
INSERT INTO codelang (code,country) VALUES
('sv-SE','Swedish (Sweden)')
,('sw','Swahili')
,('sw-KE','Swahili (Kenya)')
,('syr','Syriac')
,('syr-SY','Syriac (Syria)')
,('ta','Tamil')
,('ta-IN','Tamil (India)')
,('te','Telugu')
,('te-IN','Telugu (India)')
,('th','Thai')
;
INSERT INTO codelang (code,country) VALUES
('th-TH','Thai (Thailand)')
,('tl','Tagalog')
,('tl-PH','Tagalog (Philippines)')
,('tn','Tswana')
,('tn-ZA','Tswana (South Africa)')
,('tr','Turkish')
,('tr-TR','Turkish (Turkey)')
,('tt','Tatar')
,('tt-RU','Tatar (Russia)')
,('ts','Tsonga')
;
INSERT INTO codelang (code,country) VALUES
('uk','Ukrainian')
,('uk-UA','Ukrainian (Ukraine)')
,('ur','Urdu')
,('ur-PK','Urdu (Islamic Republic of Pakistan)')
,('uz','Uzbek (Latin)')
,('uz-UZ','Uzbek (Latin) (Uzbekistan)')
,('uz-UZ','Uzbek (Cyrillic) (Uzbekistan)')
,('vi','Vietnamese')
,('vi-VN','Vietnamese (Viet Nam)')
,('xh','Xhosa')
;
INSERT INTO codelang (code,country) VALUES
('xh-ZA','Xhosa (South Africa)')
,('zh','Chinese')
,('zh-CN','Chinese (S)')
,('zh-HK','Chinese (Hong Kong)')
,('zh-MO','Chinese (Macau)')
,('zh-SG','Chinese (Singapore)')
,('zh-TW','Chinese (T)')
,('zu','Zulu')
,('zu-ZA','Zulu (South Africa)')
;
/***************************/
-- Manual migrate
CREATE TABLE country_code (
code varchar(100) NULL,
country varchar(10000) NULL
);
insert into country_code(code, country)
select distinct
t.code,
coalesce(
case when length(t.country_name2) = 1 then null else t.country_name2 end,
case when length(t.contry_name1) = 1 then null else t.contry_name1 end,
t.country
) as country
from
(
select trim(c.code) as code,
substring(trim(c.country) from '\((.+)\)') as contry_name1,
substring(
substring(trim(c.country) from '\((.+)\)')
from '\((.*)$') as country_name2,
trim(c.country) as country
from codelang as c
) t;
commit;
--delete from location_country as lc
INSERT INTO location_country
(code, "name", low_price, high_price, created, modified)
select distinct
lpad((row_number() over (order by t.country asc))::text, 3, '0') as code,
jsonb_build_object('en-GB', t.country),
0 as low_price,
100 as high_price,
now() as created,
now() as modified
from
(
select
distinct c.country
from country_code c
) t
;
commit;
--delete from translation_language as tl;
INSERT INTO translation_language
(title, locale)
select
distinct
t.country as title,
t.code as locale
from
(
select
distinct c.country, c.code
from country_code c
) t
;
commit;
--delete from location_country_languages
INSERT INTO location_country_languages
(country_id, language_id)
select lc.id as country_id,
l.id as language_id
from location_country as lc
join (
select tl.*, '"'||tl.title||'"' as country
from translation_language as tl
) l on l.country = (lc."name"::json->'en-GB')::text
;
commit;
drop table country_code;
drop table codelang;
commit;

View File

@ -0,0 +1,391 @@
SET search_path TO gm, public;
CREATE TABLE codelang (
code varchar(100) NULL,
country varchar(10000) NULL
);
INSERT INTO codelang (code,country) VALUES
('af','Afrikaans')
,('af-ZA','Afrikaans (South Africa)')
,('ar','Arabic')
,('ar-AE','Arabic (U.A.E.)')
,('ar-BH','Arabic (Bahrain)')
,('ar-DZ','Arabic (Algeria)')
,('ar-EG','Arabic (Egypt)')
,('ar-IQ','Arabic (Iraq)')
,('ar-JO','Arabic (Jordan)')
,('ar-KW','Arabic (Kuwait)')
;
INSERT INTO codelang (code,country) VALUES
('ar-LB','Arabic (Lebanon)')
,('ar-LY','Arabic (Libya)')
,('ar-MA','Arabic (Morocco)')
,('ar-OM','Arabic (Oman)')
,('ar-QA','Arabic (Qatar)')
,('ar-SA','Arabic (Saudi Arabia)')
,('ar-SY','Arabic (Syria)')
,('ar-TN','Arabic (Tunisia)')
,('ar-YE','Arabic (Yemen)')
,('az','Azeri (Latin)')
;
INSERT INTO codelang (code,country) VALUES
('az-AZ','Azeri (Latin) (Azerbaijan)')
,('az-AZ','Azeri (Cyrillic) (Azerbaijan)')
,('be','Belarusian')
,('be-BY','Belarusian (Belarus)')
,('bg','Bulgarian')
,('bg-BG','Bulgarian (Bulgaria)')
,('bs-BA','Bosnian (Bosnia and Herzegovina)')
,('ca','Catalan')
,('ca-ES','Catalan (Spain)')
,('cs','Czech')
;
INSERT INTO codelang (code,country) VALUES
('cs-CZ','Czech (Czech Republic)')
,('cy','Welsh')
,('cy-GB','Welsh (United Kingdom)')
,('da','Danish')
,('da-DK','Danish (Denmark)')
,('de','German')
,('de-AT','German (Austria)')
,('de-CH','German (Switzerland)')
,('de-DE','German (Germany)')
,('de-LI','German (Liechtenstein)')
;
INSERT INTO codelang (code,country) VALUES
('de-LU','German (Luxembourg)')
,('dv','Divehi')
,('dv-MV','Divehi (Maldives)')
,('el','Greek')
,('el-GR','Greek (Greece)')
,('en','English')
,('en-AU','English (Australia)')
,('en-BZ','English (Belize)')
,('en-CA','English (Canada)')
,('en-CB','English (Caribbean)')
;
INSERT INTO codelang (code,country) VALUES
('en-GB','English (United Kingdom)')
,('en-IE','English (Ireland)')
,('en-JM','English (Jamaica)')
,('en-NZ','English (New Zealand)')
,('en-PH','English (Republic of the Philippines)')
,('en-TT','English (Trinidad and Tobago)')
,('en-US','English (United States)')
,('en-ZA','English (South Africa)')
,('en-ZW','English (Zimbabwe)')
,('eo','Esperanto')
;
INSERT INTO codelang (code,country) VALUES
('es','Spanish')
,('es-AR','Spanish (Argentina)')
,('es-BO','Spanish (Bolivia)')
,('es-CL','Spanish (Chile)')
,('es-CO','Spanish (Colombia)')
,('es-CR','Spanish (Costa Rica)')
,('es-DO','Spanish (Dominican Republic)')
,('es-EC','Spanish (Ecuador)')
,('es-ES','Spanish (Castilian)')
,('es-ES','Spanish (Spain)')
;
INSERT INTO codelang (code,country) VALUES
('es-GT','Spanish (Guatemala)')
,('es-HN','Spanish (Honduras)')
,('es-MX','Spanish (Mexico)')
,('es-NI','Spanish (Nicaragua)')
,('es-PA','Spanish (Panama)')
,('es-PE','Spanish (Peru)')
,('es-PR','Spanish (Puerto Rico)')
,('es-PY','Spanish (Paraguay)')
,('es-SV','Spanish (El Salvador)')
,('es-UY','Spanish (Uruguay)')
;
INSERT INTO codelang (code,country) VALUES
('es-VE','Spanish (Venezuela)')
,('et','Estonian')
,('et-EE','Estonian (Estonia)')
,('eu','Basque')
,('eu-ES','Basque (Spain)')
,('fa','Farsi')
,('fa-IR','Farsi (Iran)')
,('fi','Finnish')
,('fi-FI','Finnish (Finland)')
,('fo','Faroese')
;
INSERT INTO codelang (code,country) VALUES
('fo-FO','Faroese (Faroe Islands)')
,('fr','French')
,('fr-BE','French (Belgium)')
,('fr-CA','French (Canada)')
,('fr-CH','French (Switzerland)')
,('fr-FR','French (France)')
,('fr-LU','French (Luxembourg)')
,('fr-MC','French (Principality of Monaco)')
,('gl','Galician')
,('gl-ES','Galician (Spain)')
;
INSERT INTO codelang (code,country) VALUES
('gu','Gujarati')
,('gu-IN','Gujarati (India)')
,('he','Hebrew')
,('he-IL','Hebrew (Israel)')
,('hi','Hindi')
,('hi-IN','Hindi (India)')
,('hr','Croatian')
,('hr-BA','Croatian (Bosnia and Herzegovina)')
,('hr-HR','Croatian (Croatia)')
,('hu','Hungarian')
;
INSERT INTO codelang (code,country) VALUES
('hu-HU','Hungarian (Hungary)')
,('hy','Armenian')
,('hy-AM','Armenian (Armenia)')
,('id','Indonesian')
,('id-ID','Indonesian (Indonesia)')
,('is','Icelandic')
,('is-IS','Icelandic (Iceland)')
,('it','Italian')
,('it-CH','Italian (Switzerland)')
,('it-IT','Italian (Italy)')
;
INSERT INTO codelang (code,country) VALUES
('ja','Japanese')
,('ja-JP','Japanese (Japan)')
,('ka','Georgian')
,('ka-GE','Georgian (Georgia)')
,('kk','Kazakh')
,('kk-KZ','Kazakh (Kazakhstan)')
,('kn','Kannada')
,('kn-IN','Kannada (India)')
,('ko','Korean')
,('ko-KR','Korean (Korea)')
;
INSERT INTO codelang (code,country) VALUES
('kok','Konkani')
,('kok-IN','Konkani (India)')
,('ky','Kyrgyz')
,('ky-KG','Kyrgyz (Kyrgyzstan)')
,('lt','Lithuanian')
,('lt-LT','Lithuanian (Lithuania)')
,('lv','Latvian')
,('lv-LV','Latvian (Latvia)')
,('mi','Maori')
,('mi-NZ','Maori (New Zealand)')
;
INSERT INTO codelang (code,country) VALUES
('mk','FYRO Macedonian')
,('mk-MK','FYRO Macedonian (Former Yugoslav Republic of Macedonia)')
,('mn','Mongolian')
,('mn-MN','Mongolian (Mongolia)')
,('mr','Marathi')
,('mr-IN','Marathi (India)')
,('ms','Malay')
,('ms-BN','Malay (Brunei Darussalam)')
,('ms-MY','Malay (Malaysia)')
,('mt','Maltese')
;
INSERT INTO codelang (code,country) VALUES
('mt-MT','Maltese (Malta)')
,('nb','Norwegian (Bokm?l)')
,('nb-NO','Norwegian (Bokm?l) (Norway)')
,('nl','Dutch')
,('nl-BE','Dutch (Belgium)')
,('nl-NL','Dutch (Netherlands)')
,('nn-NO','Norwegian (Nynorsk) (Norway)')
,('ns','Northern Sotho')
,('ns-ZA','Northern Sotho (South Africa)')
,('pa','Punjabi')
;
INSERT INTO codelang (code,country) VALUES
('pa-IN','Punjabi (India)')
,('pl','Polish')
,('pl-PL','Polish (Poland)')
,('ps','Pashto')
,('ps-AR','Pashto (Afghanistan)')
,('pt','Portuguese')
,('pt-BR','Portuguese (Brazil)')
,('pt-PT','Portuguese (Portugal)')
,('qu','Quechua')
,('qu-BO','Quechua (Bolivia)')
;
INSERT INTO codelang (code,country) VALUES
('qu-EC','Quechua (Ecuador)')
,('qu-PE','Quechua (Peru)')
,('ro','Romanian')
,('ro-RO','Romanian (Romania)')
,('ru','Russian')
,('ru-RU','Russian (Russia)')
,('sa','Sanskrit')
,('sa-IN','Sanskrit (India)')
,('se','Sami (Northern)')
,('se-FI','Sami (Northern) (Finland)')
;
INSERT INTO codelang (code,country) VALUES
('se-FI','Sami (Skolt) (Finland)')
,('se-FI','Sami (Inari) (Finland)')
,('se-NO','Sami (Northern) (Norway)')
,('se-NO','Sami (Lule) (Norway)')
,('se-NO','Sami (Southern) (Norway)')
,('se-SE','Sami (Northern) (Sweden)')
,('se-SE','Sami (Lule) (Sweden)')
,('se-SE','Sami (Southern) (Sweden)')
,('sk','Slovak')
,('sk-SK','Slovak (Slovakia)')
;
INSERT INTO codelang (code,country) VALUES
('sl','Slovenian')
,('sl-SI','Slovenian (Slovenia)')
,('sq','Albanian')
,('sq-AL','Albanian (Albania)')
,('sr-BA','Serbian (Latin) (Bosnia and Herzegovina)')
,('sr-BA','Serbian (Cyrillic) (Bosnia and Herzegovina)')
,('sr-SP','Serbian (Latin) (Serbia and Montenegro)')
,('sr-SP','Serbian (Cyrillic) (Serbia and Montenegro)')
,('sv','Swedish')
,('sv-FI','Swedish (Finland)')
;
INSERT INTO codelang (code,country) VALUES
('sv-SE','Swedish (Sweden)')
,('sw','Swahili')
,('sw-KE','Swahili (Kenya)')
,('syr','Syriac')
,('syr-SY','Syriac (Syria)')
,('ta','Tamil')
,('ta-IN','Tamil (India)')
,('te','Telugu')
,('te-IN','Telugu (India)')
,('th','Thai')
;
INSERT INTO codelang (code,country) VALUES
('th-TH','Thai (Thailand)')
,('tl','Tagalog')
,('tl-PH','Tagalog (Philippines)')
,('tn','Tswana')
,('tn-ZA','Tswana (South Africa)')
,('tr','Turkish')
,('tr-TR','Turkish (Turkey)')
,('tt','Tatar')
,('tt-RU','Tatar (Russia)')
,('ts','Tsonga')
;
INSERT INTO codelang (code,country) VALUES
('uk','Ukrainian')
,('uk-UA','Ukrainian (Ukraine)')
,('ur','Urdu')
,('ur-PK','Urdu (Islamic Republic of Pakistan)')
,('uz','Uzbek (Latin)')
,('uz-UZ','Uzbek (Latin) (Uzbekistan)')
,('uz-UZ','Uzbek (Cyrillic) (Uzbekistan)')
,('vi','Vietnamese')
,('vi-VN','Vietnamese (Viet Nam)')
,('xh','Xhosa')
;
INSERT INTO codelang (code,country) VALUES
('xh-ZA','Xhosa (South Africa)')
,('zh','Chinese')
,('zh-CN','Chinese (S)')
,('zh-HK','Chinese (Hong Kong)')
,('zh-MO','Chinese (Macau)')
,('zh-SG','Chinese (Singapore)')
,('zh-TW','Chinese (T)')
,('zu','Zulu')
,('zu-ZA','Zulu (South Africa)')
;
/***************************/
-- Manual migrate
CREATE TABLE country_code (
code varchar(100) NULL,
country varchar(10000) NULL
);
insert into country_code(code, country)
select distinct
t.code,
coalesce(
case when length(t.country_name2) = 1 then null else t.country_name2 end,
case when length(t.contry_name1) = 1 then null else t.contry_name1 end,
t.country
) as country
from
(
select trim(c.code) as code,
substring(trim(c.country) from '\((.+)\)') as contry_name1,
substring(
substring(trim(c.country) from '\((.+)\)')
from '\((.*)$') as country_name2,
trim(c.country) as country
from codelang as c
) t;
commit;
delete from location_country_languages as lcl
where lcl.country_id in
(
select
lc.id
from
(
select
lpad((row_number() over (order by t.country asc))::text, 3, '0') as code,
jsonb_build_object('en-GB', t.country) as "name"
from
(
select
distinct c.country
from country_code c
) t
) d
join location_country lc on lc.code = d.code and d."name"=lc."name"
)
;
commit;
delete from location_country as lcl
where lcl.id in
(
select
lc.id
from
(
select
lpad((row_number() over (order by t.country asc))::text, 3, '0') as code,
jsonb_build_object('en-GB', t.country) as "name"
from
(
select
distinct c.country
from country_code c
) t
) d
join location_country lc on lc.code = d.code and d."name"=lc."name"
)
;
commit;
delete from translation_language tl
where tl.id in
(
SELECT tl.id
FROM
(
select
distinct c.country, c.code
from country_code c
) t
JOIN translation_language tl ON tl.locale = t.code and tl.title = t.country
);
commit;
drop table country_code;
drop table codelang;
commit;

View File

@ -6,6 +6,7 @@ from django.db.transaction import on_commit
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from utils.models import ProjectBaseMixin, SVGImageMixin, TranslatedFieldsMixin, TJSONField
from translation.models import Language
class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):
@ -18,6 +19,11 @@ class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):
code = models.CharField(max_length=255, unique=True, verbose_name=_('Code'))
low_price = models.IntegerField(default=25, verbose_name=_('Low price'))
high_price = models.IntegerField(default=50, verbose_name=_('High price'))
languages = models.ManyToManyField(Language, verbose_name=_('Languages'))
@property
def country_id(self):
return self.id
class Meta:
"""Meta class."""
@ -47,6 +53,14 @@ class Region(models.Model):
return self.name
class CityQuerySet(models.QuerySet):
"""Extended queryset for City model."""
def by_country_code(self, code):
"""Return establishments by country code"""
return self.filter(country__code=code)
class City(models.Model):
"""Region model."""
@ -62,6 +76,8 @@ class City(models.Model):
is_island = models.BooleanField(_('is island'), default=False)
objects = CityQuerySet.as_manager()
class Meta:
verbose_name_plural = _('cities')
verbose_name = _('city')
@ -71,7 +87,6 @@ class City(models.Model):
class Address(models.Model):
"""Address model."""
city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE)
street_name_1 = models.CharField(
@ -110,6 +125,10 @@ class Address(models.Model):
return {'lat': self.latitude,
'lon': self.longitude}
@property
def country_id(self):
return self.city.country_id
# todo: Make recalculate price levels
@receiver(post_save, sender=Country)

View File

@ -5,11 +5,12 @@ from account.models import User
from rest_framework import status
from http.cookies import SimpleCookie
from location.models import City, Region, Country
from location.models import City, Region, Country, Language
from django.contrib.gis.geos import Point
from account.models import Role, UserRole
class BaseTestCase(APITestCase):
def setUp(self):
self.username = 'sedragurda'
self.password = 'sedragurdaredips19'
@ -20,27 +21,57 @@ class BaseTestCase(APITestCase):
# get tokens
# self.user.is_superuser = True
# self.user.save()
tokkens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
self.lang = Language.objects.get(
title='Russia',
locale='ru-RU'
)
self.country_ru = Country.objects.get(
name={"en-GB": "Russian"}
)
self.role = Role.objects.create(role=Role.COUNTRY_ADMIN,
country=self.country_ru)
self.role.save()
self.user_role = UserRole.objects.create(user=self.user, role=self.role)
self.user_role.save()
class CountryTests(BaseTestCase):
def setUp(self):
super().setUp()
def test_country_CRUD(self):
response = self.client.get('/api/back/location/countries/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = {
'name': 'Test country',
'code': 'test'
'name': {"ru-RU": "NewCountry"},
'code': 'test1'
}
response = self.client.post('/api/back/location/countries/', data=data, format='json')
response_data = response.json()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
country = Country.objects.get(pk=response_data["id"])
role = Role.objects.create(role=Role.COUNTRY_ADMIN, country=country)
role.save()
user_role = UserRole.objects.create(user=self.user, role=role)
user_role.save()
response = self.client.get('/api/back/location/countries/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.get(f'/api/back/location/countries/{response_data["id"]}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -64,6 +95,14 @@ class RegionTests(BaseTestCase):
code="test"
)
role = Role.objects.create(role=Role.COUNTRY_ADMIN, country=self.country)
role.save()
user_role = UserRole.objects.create(user=self.user, role=role)
user_role.save()
def test_region_CRUD(self):
response = self.client.get('/api/back/location/regions/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -108,6 +147,13 @@ class CityTests(BaseTestCase):
country=self.country
)
role = Role.objects.create(role=Role.COUNTRY_ADMIN, country=self.country)
role.save()
user_role = UserRole.objects.create(user=self.user, role=role)
user_role.save()
def test_city_CRUD(self):
response = self.client.get('/api/back/location/cities/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -142,6 +188,7 @@ class AddressTests(BaseTestCase):
def setUp(self):
super().setUp()
self.country = Country.objects.create(
name=json.dumps({"en-GB": "Test country"}),
code="test"
@ -160,6 +207,13 @@ class AddressTests(BaseTestCase):
country=self.country
)
role = Role.objects.create(role=Role.COUNTRY_ADMIN, country=self.country)
role.save()
user_role = UserRole.objects.create(user=self.user, role=role)
user_role.save()
def test_address_CRUD(self):
response = self.client.get('/api/back/location/addresses/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -167,10 +221,8 @@ class AddressTests(BaseTestCase):
data = {
'city_id': self.city.id,
'number': '+79999999',
"coordinates": {
"latitude": 37.0625,
"longitude": -95.677068
},
"latitude": 37.0625,
"longitude": -95.677068,
"geo_lon": -95.677068,
"geo_lat": 37.0625
}

View File

@ -1,7 +1,6 @@
"""Location app mobile urlconf."""
from location.urls.common import urlpatterns as common_urlpatterns
urlpatterns = []
urlpatterns.extend(common_urlpatterns)
urlpatterns.extend(common_urlpatterns)

View File

@ -1,7 +1,6 @@
"""Location app web urlconf."""
from location.urls.common import urlpatterns as common_urlpatterns
urlpatterns = []
urlpatterns.extend(common_urlpatterns)
urlpatterns.extend(common_urlpatterns)

View File

@ -3,50 +3,57 @@ from rest_framework import generics
from location import models, serializers
from location.views import common
from utils.permissions import IsCountryAdmin
# Address
class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView):
"""Create view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsCountryAdmin]
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsCountryAdmin]
# City
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsCountryAdmin]
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsCountryAdmin]
# Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region"""
serializer_class = serializers.RegionSerializer
permission_classes = [IsCountryAdmin]
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""Retrieve view for model Region"""
serializer_class = serializers.RegionSerializer
permission_classes = [IsCountryAdmin]
# Country
class CountryListCreateView(common.CountryViewMixin, generics.ListCreateAPIView):
class CountryListCreateView(generics.ListCreateAPIView):
"""List/Create view for model Country."""
queryset = models.Country.objects.all()
serializer_class = serializers.CountryBackSerializer
pagination_class = None
permission_classes = [IsCountryAdmin]
class CountryRUDView(common.CountryViewMixin, generics.RetrieveUpdateDestroyAPIView):
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Country."""
serializer_class = serializers.CountryBackSerializer
permission_classes = [IsCountryAdmin]
queryset = models.Country.objects.all()

View File

@ -10,7 +10,7 @@ class CountryViewMixin(generics.GenericAPIView):
"""View Mixin for model Country"""
serializer_class = serializers.CountrySerializer
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
queryset = models.Country.objects.all()
@ -56,7 +56,7 @@ class RegionRetrieveView(RegionViewMixin, generics.RetrieveAPIView):
class RegionListView(RegionViewMixin, generics.ListAPIView):
"""List view for model Country"""
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CountrySerializer
@ -83,9 +83,15 @@ class CityRetrieveView(CityViewMixin, generics.RetrieveAPIView):
class CityListView(CityViewMixin, generics.ListAPIView):
"""List view for model City"""
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CitySerializer
def get_queryset(self):
qs = super().get_queryset()
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class CityDestroyView(CityViewMixin, generics.DestroyAPIView):
"""Destroy view for model City"""
@ -110,7 +116,5 @@ class AddressRetrieveView(AddressViewMixin, generics.RetrieveAPIView):
class AddressListView(AddressViewMixin, generics.ListAPIView):
"""List view for model Address"""
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.AddressDetailSerializer

View File

@ -25,22 +25,6 @@ class AwardAdmin(admin.ModelAdmin):
# list_display_links = ['id', '__str__']
@admin.register(models.MetaData)
class MetaDataAdmin(admin.ModelAdmin):
"""MetaData admin."""
@admin.register(models.MetaDataCategory)
class MetaDataCategoryAdmin(admin.ModelAdmin):
"""MetaData admin."""
list_display = ['id', 'country', 'content_type']
@admin.register(models.MetaDataContent)
class MetaDataContentAdmin(admin.ModelAdmin):
"""MetaDataContent admin"""
@admin.register(models.Currency)
class CurrencContentAdmin(admin.ModelAdmin):
"""CurrencContent admin"""

View File

@ -1,8 +1,10 @@
"""Main app methods."""
import logging
from typing import Tuple, Optional
from django.conf import settings
from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception
from geoip2.models import City
from main import models
@ -39,17 +41,16 @@ def determine_country_code(ip_addr):
return country_code
def determine_coordinates(ip_addr):
longitude, latitude = None, None
def determine_coordinates(ip_addr: str) -> Tuple[Optional[float], Optional[float]]:
if ip_addr:
try:
geoip = GeoIP2()
longitude, latitude = geoip.coords(ip_addr)
return geoip.coords(ip_addr)
except GeoIP2Exception as ex:
logger.info(f'GEOIP Exception: {ex}. ip: {ip_addr}')
logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}')
except Exception as ex:
logger.error(f'GEOIP Base exception: {ex}')
return longitude, latitude
logger.warning(f'GEOIP Base exception: {ex}')
return None, None
def determine_user_site_url(country_code):
@ -73,3 +74,12 @@ 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

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-10-07 14:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0016_merge_20190919_0954'),
]
operations = [
migrations.AddField(
model_name='feature',
name='route',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Page'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-07 14:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0017_feature_route'),
]
operations = [
migrations.AddField(
model_name='feature',
name='source',
field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web'), (2, 'All')], default=0, verbose_name='Source'),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 2.2.4 on 2019-10-22 13:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0018_feature_source'),
]
operations = [
migrations.RemoveField(
model_name='metadatacategory',
name='content_type',
),
migrations.RemoveField(
model_name='metadatacategory',
name='country',
),
migrations.RemoveField(
model_name='metadatacontent',
name='content_type',
),
migrations.RemoveField(
model_name='metadatacontent',
name='metadata',
),
migrations.DeleteModel(
name='MetaData',
),
migrations.DeleteModel(
name='MetaDataCategory',
),
migrations.DeleteModel(
name='MetaDataContent',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-22 14:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0018_feature_source'),
]
operations = [
migrations.AddField(
model_name='award',
name='image_url',
field=models.URLField(blank=True, default=None, null=True, verbose_name='Image URL path'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-23 07:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0019_award_image_url'),
('main', '0019_auto_20191022_1359'),
]
operations = [
]

View File

@ -1,19 +1,24 @@
"""Main app models."""
from typing import Iterable
from django.conf import settings
from django.contrib.contenttypes import fields as generic
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from advertisement.models import Advertisement
from configuration.models import TranslationSettings
from location.models import Country
from main import methods
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField,
TranslatedFieldsMixin, ImageMixin)
TranslatedFieldsMixin, ImageMixin,
PlatformMixin, URLImageMixin)
from utils.querysets import ContentTypeQuerySetMixin
from configuration.models import TranslationSettings
#
#
@ -150,7 +155,8 @@ class SiteSettings(ProjectBaseMixin):
@property
def published_sitefeatures(self):
return self.sitefeature_set.filter(published=True)
return self.sitefeature_set\
.filter(Q(published=True) and Q(feature__source__in=[PlatformMixin.WEB, PlatformMixin.ALL]))
@property
def site_url(self):
@ -159,11 +165,27 @@ class SiteSettings(ProjectBaseMixin):
domain=settings.SITE_DOMAIN_URI)
class Feature(ProjectBaseMixin):
class Page(models.Model):
"""Page model."""
page_name = models.CharField(max_length=255, unique=True)
advertisements = models.ManyToManyField(Advertisement)
class Meta:
"""Meta class."""
verbose_name = _('Page')
verbose_name_plural = _('Pages')
def __str__(self):
return f'{self.page_name}'
class Feature(ProjectBaseMixin, PlatformMixin):
"""Feature model."""
slug = models.CharField(max_length=255, unique=True)
priority = models.IntegerField(unique=True, null=True, default=None)
route = models.ForeignKey(Page, on_delete=models.PROTECT, null=True, default=None)
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
class Meta:
@ -181,6 +203,12 @@ class SiteFeatureQuerySet(models.QuerySet):
def published(self, switcher=True):
return self.filter(published=switcher)
def by_country_code(self, country_code: str):
return self.filter(site_settings__country__code=country_code)
def by_sources(self, sources: Iterable[int]):
return self.filter(feature__source__in=sources)
class SiteFeature(ProjectBaseMixin):
"""SiteFeature model."""
@ -200,7 +228,7 @@ class SiteFeature(ProjectBaseMixin):
unique_together = ('site_settings', 'feature')
class Award(TranslatedFieldsMixin, models.Model):
class Award(TranslatedFieldsMixin, URLImageMixin, models.Model):
"""Award model."""
award_type = models.ForeignKey('main.AwardType', on_delete=models.CASCADE)
title = TJSONField(
@ -230,49 +258,6 @@ class AwardType(models.Model):
return self.name
class MetaDataCategory(models.Model):
"""MetaData category model."""
country = models.ForeignKey(
'location.Country', null=True, default=None, on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
public = models.BooleanField()
class MetaData(TranslatedFieldsMixin, models.Model):
"""MetaData model."""
label = TJSONField(
_('label'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}')
category = models.ForeignKey(
MetaDataCategory, verbose_name=_('category'), on_delete=models.CASCADE)
class Meta:
verbose_name = _('metadata')
verbose_name_plural = _('metadata')
def __str__(self):
label = 'None'
lang = TranslationSettings.get_solo().default_language
if self.label and lang in self.label:
label = self.label[lang]
return f'id:{self.id}-{label}'
class MetaDataContentQuerySet(ContentTypeQuerySetMixin):
"""QuerySets for MetaDataContent model."""
class MetaDataContent(models.Model):
"""MetaDataContent model."""
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE)
objects = MetaDataContentQuerySet.as_manager()
class Currency(models.Model):
"""Currency model."""
name = models.CharField(_('name'), max_length=50)
@ -351,18 +336,3 @@ class Carousel(models.Model):
def model_name(self):
if hasattr(self.content_object, 'establishment_type'):
return self.content_object.establishment_type.name_translated
class Page(models.Model):
"""Page model."""
page_name = models.CharField(max_length=255, unique=True)
advertisements = models.ManyToManyField(Advertisement)
class Meta:
"""Meta class."""
verbose_name = _('Page')
verbose_name_plural = _('Pages')
def __str__(self):
return f'{self.page_name}'

View File

@ -1,9 +1,9 @@
"""Main app serializers."""
from rest_framework import serializers
from advertisement.serializers.web import AdvertisementSerializer
from location.serializers import CountrySerializer
from main import models
from establishment.models import Establishment
from utils.serializers import TranslatedField
@ -25,6 +25,8 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(source='feature.id')
slug = serializers.CharField(source='feature.slug')
priority = serializers.IntegerField(source='feature.priority')
route = serializers.CharField(source='feature.route.page_name')
source = serializers.IntegerField(source='feature.source')
class Meta:
"""Meta class."""
@ -32,7 +34,9 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
fields = ('main',
'id',
'slug',
'priority'
'priority',
'route',
'source'
)
@ -98,6 +102,7 @@ class AwardBaseSerializer(serializers.ModelSerializer):
'id',
'title_translated',
'vintage_year',
'image_url',
]
@ -109,19 +114,6 @@ class AwardSerializer(AwardBaseSerializer):
fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
class MetaDataContentSerializer(serializers.ModelSerializer):
"""MetaData content serializer."""
id = serializers.IntegerField(source='metadata.id', read_only=True)
label_translated = TranslatedField(source='metadata.label_translated')
class Meta:
"""Meta class."""
model = models.MetaDataContent
fields = ('id', 'label_translated')
class CurrencySerializer(serializers.ModelSerializer):
"""Currency serializer"""

View File

@ -1,14 +0,0 @@
"""Main app urls."""
from django.urls import path
from main import views
app = 'main'
urlpatterns = [
path('determine-site/', views.DetermineSiteView.as_view(), name='determine-site'),
path('sites/', views.SiteListView.as_view(), name='site-list'),
path('site-settings/<subdomain>/', views.SiteSettingsView.as_view(), name='site-settings'),
path('awards/', views.AwardView.as_view(), name='awards_list'),
path('awards/<int:pk>/', views.AwardRetrieveView.as_view(), name='awards_retrieve'),
path('carousel/', views.CarouselListView.as_view(), name='carousel-list'),
]

View File

12
apps/main/urls/common.py Normal file
View File

@ -0,0 +1,12 @@
"""Main app urls."""
from django.urls import path
from main.views.common import *
app = 'main'
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')
]

11
apps/main/urls/mobile.py Normal file
View File

@ -0,0 +1,11 @@
from main.urls.common import common_urlpatterns
from django.urls import path
from main.views.mobile import FeaturesView
urlpatterns = [
path('features/', FeaturesView.as_view(), name='features'),
]
urlpatterns.extend(common_urlpatterns)

11
apps/main/urls/web.py Normal file
View File

@ -0,0 +1,11 @@
from main.urls.common import common_urlpatterns
from django.urls import path
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'), ]
urlpatterns.extend(common_urlpatterns)

View File

View File

@ -1,39 +1,11 @@
"""Main app views."""
from django.http import Http404
from rest_framework import generics, permissions
from rest_framework.response import Response
from main import methods, models, serializers
from utils.serializers import EmptySerializer
class DetermineSiteView(generics.GenericAPIView):
"""Determine user's site."""
permission_classes = (permissions.AllowAny,)
serializer_class = EmptySerializer
def get(self, request, *args, **kwargs):
user_ip = methods.get_user_ip(request)
country_code = methods.determine_country_code(user_ip)
url = methods.determine_user_site_url(country_code)
return Response(data={'url': url})
class SiteSettingsView(generics.RetrieveAPIView):
"""Site settings View."""
lookup_field = 'subdomain'
permission_classes = (permissions.AllowAny,)
queryset = models.SiteSettings.objects.all()
serializer_class = serializers.SiteSettingsSerializer
class SiteListView(generics.ListAPIView):
"""Site settings View."""
pagination_class = None
permission_classes = (permissions.AllowAny,)
queryset = models.SiteSettings.objects.with_country()
serializer_class = serializers.SiteSerializer
#
# class FeatureViewMixin:
# """Feature view mixin."""
@ -70,13 +42,14 @@ class SiteListView(generics.ListAPIView):
# class SiteFeaturesRUDView(SiteFeaturesViewMixin,
# generics.RetrieveUpdateDestroyAPIView):
# """Site features RUD."""
from utils.serializers import EmptySerializer
class AwardView(generics.ListAPIView):
"""Awards list view."""
serializer_class = serializers.AwardSerializer
queryset = models.Award.objects.all()
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
class AwardRetrieveView(generics.RetrieveAPIView):
@ -90,5 +63,22 @@ class CarouselListView(generics.ListAPIView):
"""Return list of carousel items."""
queryset = models.Carousel.objects.all()
serializer_class = serializers.CarouselListSerializer
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
pagination_class = None
class DetermineLocation(generics.GenericAPIView):
"""Determine user's location."""
permission_classes = (permissions.AllowAny,)
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)
if longitude and latitude and city:
return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city})
else:
raise Http404

16
apps/main/views/mobile.py Normal file
View File

@ -0,0 +1,16 @@
from rest_framework import generics, permissions
from main import models, serializers
from utils.models import PlatformMixin
class FeaturesView(generics.ListAPIView):
pagination_class = None
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.SiteFeatureSerializer
def get_queryset(self):
return models.SiteFeature.objects\
.prefetch_related('feature', 'feature__route') \
.by_country_code(self.request.country_code) \
.by_sources([PlatformMixin.ALL, PlatformMixin.MOBILE])

38
apps/main/views/web.py Normal file
View File

@ -0,0 +1,38 @@
from typing import Iterable
from rest_framework import generics, permissions
from utils.serializers import EmptySerializer
from rest_framework.response import Response
from main import methods, models, serializers
class DetermineSiteView(generics.GenericAPIView):
"""Determine user's site."""
permission_classes = (permissions.AllowAny,)
serializer_class = EmptySerializer
def get(self, request, *args, **kwargs):
user_ip = methods.get_user_ip(request)
country_code = methods.determine_country_code(user_ip)
url = methods.determine_user_site_url(country_code)
return Response(data={'url': url})
class SiteSettingsView(generics.RetrieveAPIView):
"""Site settings View."""
lookup_field = 'subdomain'
permission_classes = (permissions.AllowAny,)
queryset = models.SiteSettings.objects.all()
serializer_class = serializers.SiteSettingsSerializer
class SiteListView(generics.ListAPIView):
"""Site settings View."""
pagination_class = None
permission_classes = (permissions.AllowAny,)
queryset = models.SiteSettings.objects.with_country()
serializer_class = serializers.SiteSerializer

View File

@ -4,6 +4,7 @@ from django.conf import settings
from news import models
from .tasks import send_email_with_news
@admin.register(models.NewsType)
class NewsTypeAdmin(admin.ModelAdmin):
"""News type admin."""

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-09 14:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0002_auto_20191009_1408'),
('news', '0020_remove_news_author'),
]
operations = [
migrations.AddField(
model_name='news',
name='tags',
field=models.ManyToManyField(related_name='news', to='tag.Tag', verbose_name='Tags'),
),
migrations.AddField(
model_name='newstype',
name='tag_categories',
field=models.ManyToManyField(related_name='news_types', to='tag.TagCategory'),
),
]

View File

@ -0,0 +1,57 @@
# Generated by Django 2.2.4 on 2019-10-21 13:06
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import utils.models
class Migration(migrations.Migration):
dependencies = [
('location', '0012_data_migrate'),
('news', '0021_auto_20191009_1408'),
]
operations = [
migrations.CreateModel(
name='NewsBanner',
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')),
('title', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='title')),
('image_url', models.URLField(blank=True, default=None, null=True, verbose_name='Image URL path')),
('content_url', models.URLField(blank=True, default=None, null=True, verbose_name='Content URL path')),
],
options={
'abstract': False,
},
bases=(models.Model, utils.models.TranslatedFieldsMixin),
),
migrations.CreateModel(
name='Agenda',
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')),
('event_datetime', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Event datetime')),
('content', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='content')),
('address', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Address', verbose_name='address')),
],
options={
'abstract': False,
},
bases=(models.Model, utils.models.TranslatedFieldsMixin),
),
migrations.AddField(
model_name='news',
name='agenda',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='news.Agenda', verbose_name='agenda'),
),
migrations.AddField(
model_name='news',
name='banner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='news.NewsBanner', verbose_name='banner'),
),
]

View File

@ -4,7 +4,7 @@ from django.contrib.contenttypes import fields as generic
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse
from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin
from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin, ProjectBaseMixin
from rating.models import Rating
@ -12,6 +12,8 @@ class NewsType(models.Model):
"""NewsType model."""
name = models.CharField(_('name'), max_length=250)
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='news_types')
class Meta:
"""Meta class."""
@ -36,7 +38,7 @@ class NewsQuerySet(models.QuerySet):
def with_extended_related(self):
"""Return qs with related objects."""
return self.select_related('created_by')
return self.select_related('created_by', 'agenda', 'banner')
def by_type(self, news_type):
"""Filter News by type"""
@ -59,15 +61,39 @@ class NewsQuerySet(models.QuerySet):
# todo: filter by best score
# todo: filter by country?
def should_read(self, news):
return self.model.objects.exclude(pk=news.pk).published().\
return self.model.objects.exclude(pk=news.pk).published(). \
with_base_related().by_type(news.news_type).distinct().order_by('?')
def same_theme(self, news):
return self.model.objects.exclude(pk=news.pk).published().\
with_base_related().by_type(news.news_type).\
return self.model.objects.exclude(pk=news.pk).published(). \
with_base_related().by_type(news.news_type). \
by_tags(news.tags.all()).distinct().order_by('-start')
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
"""News agenda model"""
event_datetime = models.DateTimeField(default=timezone.now, editable=False,
verbose_name=_('Event datetime'))
address = models.ForeignKey('location.Address', blank=True, null=True,
default=None, verbose_name=_('address'),
on_delete=models.SET_NULL)
content = TJSONField(blank=True, null=True, default=None,
verbose_name=_('content'),
help_text='{"en-GB":"some text"}')
class NewsBanner(ProjectBaseMixin, TranslatedFieldsMixin):
"""News banner model"""
title = TJSONField(blank=True, null=True, default=None,
verbose_name=_('title'),
help_text='{"en-GB":"some text"}')
image_url = models.URLField(verbose_name=_('Image URL path'),
blank=True, null=True, default=None)
content_url = models.URLField(verbose_name=_('Content URL path'),
blank=True, null=True, default=None)
class News(BaseAttributes, TranslatedFieldsMixin):
"""News model."""
@ -120,8 +146,10 @@ class News(BaseAttributes, TranslatedFieldsMixin):
verbose_name=_('State'))
is_highlighted = models.BooleanField(default=False,
verbose_name=_('Is highlighted'))
# TODO: metadata_keys - описание ключей для динамического построения полей метаданных
# TODO: metadata_values - Описание значений для динамических полей из MetadataKeys
image_url = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Image URL path'))
preview_image_url = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Preview image URL path'))
template = models.PositiveIntegerField(choices=TEMPLATE_CHOICES, default=NEWSPAPER)
address = models.ForeignKey('location.Address', blank=True, null=True,
default=None, verbose_name=_('address'),
@ -129,11 +157,19 @@ class News(BaseAttributes, TranslatedFieldsMixin):
country = models.ForeignKey('location.Country', blank=True, null=True,
on_delete=models.SET_NULL,
verbose_name=_('country'))
tags = generic.GenericRelation(to='main.MetaDataContent')
tags = models.ManyToManyField('tag.Tag', related_name='news',
verbose_name=_('Tags'))
gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery')
ratings = generic.GenericRelation(Rating)
agenda = models.ForeignKey('news.Agenda', blank=True, null=True,
on_delete=models.SET_NULL,
verbose_name=_('agenda'))
banner = models.ForeignKey('news.NewsBanner', blank=True, null=True,
on_delete=models.SET_NULL,
verbose_name=_('banner'))
objects = NewsQuerySet.as_manager()
class Meta:

View File

@ -5,12 +5,46 @@ from rest_framework import serializers
from account.serializers.common import UserBaseSerializer
from gallery.models import Image
from location import models as location_models
from location.serializers import CountrySimpleSerializer
from main.serializers import MetaDataContentSerializer
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
from news import models
from tag.serializers import TagBaseSerializer
from utils.serializers import TranslatedField, ProjectModelSerializer
class AgendaSerializer(ProjectModelSerializer):
event_datetime = serializers.DateTimeField()
address = AddressBaseSerializer()
content_translated = TranslatedField()
class Meta:
"""Meta class."""
model = models.Agenda
fields = (
'id',
'event_datetime',
'address',
'content_translated'
)
class NewsBannerSerializer(ProjectModelSerializer):
title_translated = TranslatedField()
image_url = serializers.URLField()
content_url = serializers.URLField()
class Meta:
"""Meta class."""
model = models.NewsBanner
fields = (
'id',
'title_translated',
'image_url',
'content_url'
)
class CropImageSerializer(serializers.Serializer):
"""Serializer for crop images for News object."""
preview_url = serializers.SerializerMethodField()
@ -100,8 +134,8 @@ class NewsBaseSerializer(ProjectModelSerializer):
# related fields
news_type = NewsTypeSerializer(read_only=True)
tags = MetaDataContentSerializer(read_only=True, many=True)
gallery = NewsImageSerializer(read_only=True, many=True)
tags = TagBaseSerializer(read_only=True, many=True)
class Meta:
"""Meta class."""
@ -149,6 +183,8 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
same_theme = NewsBaseSerializer(many=True, read_only=True)
should_read = NewsBaseSerializer(many=True, read_only=True)
agenda = AgendaSerializer()
banner = NewsBannerSerializer()
class Meta(NewsDetailSerializer.Meta):
"""Meta class."""
@ -156,6 +192,8 @@ class NewsDetailWebSerializer(NewsDetailSerializer):
fields = NewsDetailSerializer.Meta.fields + (
'same_theme',
'should_read',
'agenda',
'banner'
)
@ -189,11 +227,11 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
fields = NewsBackOfficeBaseSerializer.Meta.fields + \
NewsDetailSerializer.Meta.fields + (
'description',
'news_type_id',
'country_id',
'template',
'template_display',
'description',
'news_type_id',
'country_id',
'template',
'template_display',
)

View File

@ -1,28 +1,43 @@
from datetime import datetime
from celery import shared_task
from django.core.mail import send_mail
from notification.models import Subscriber
from news import models
from django.template.loader import render_to_string
from django.template.loader import render_to_string, get_template
from django.conf import settings
from smtplib import SMTPException
from django.core.validators import EMPTY_VALUES
from main.models import SiteSettings
@shared_task
def send_email_with_news(news_ids):
subscribers = Subscriber.objects.filter(state=Subscriber.USABLE)
sent_news = models.News.objects.filter(id__in=news_ids)
htmly = get_template(settings.NEWS_EMAIL_TEMPLATE)
year = datetime.now().year
socials = list(SiteSettings.objects.with_country())
socials = dict(zip(map(lambda s: s.country.code, socials), socials))
for s in subscribers:
socials_for_subscriber = socials.get(s.country_code)
try:
for n in sent_news:
send_mail("G&M News", render_to_string(settings.NEWS_EMAIL_TEMPLATE,
{"title": n.title.get(s.locale),
"subtitle": n.subtitle.get(s.locale),
"description": n.description.get(s.locale),
"code": s.update_code,
"domain_uri": settings.DOMAIN_URI,
"country_code": s.country_code}),
settings.EMAIL_HOST_USER, [s.send_to], fail_silently=False)
context = {"title": n.title.get(s.locale),
"subtitle": n.subtitle.get(s.locale),
"description": n.description.get(s.locale),
"code": s.update_code,
"image_url": n.image_url if n.image_url not in EMPTY_VALUES else None,
"domain_uri": settings.DOMAIN_URI,
"slug": n.slug,
"country_code": s.country_code,
"twitter_page_url": socials_for_subscriber.twitter_page_url if socials_for_subscriber else '#',
"instagram_page_url": socials_for_subscriber.instagram_page_url if socials_for_subscriber else '#',
"facebook_page_url": socials_for_subscriber.facebook_page_url if socials_for_subscriber else '#',
"send_to": s.send_to,
"year": year}
send_mail("G&M News", render_to_string(settings.NEWS_EMAIL_TEMPLATE, context),
settings.EMAIL_HOST_USER, [s.send_to], fail_silently=False,
html_message=htmly.render(context))
except SMTPException:
continue

View File

@ -1,3 +1,4 @@
from django.urls import reverse
from http.cookies import SimpleCookie
from rest_framework.test import APITestCase
@ -5,8 +6,9 @@ from rest_framework import status
from datetime import datetime, timedelta
from news.models import NewsType, News
from account.models import User
from account.models import User, Role, UserRole
from translation.models import Language
from location.models import Country
# Create your tests here.
@ -22,23 +24,51 @@ class BaseTestCase(APITestCase):
self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
self.test_news_type = NewsType.objects.create(name="Test news type")
self.test_news = News.objects.create(created_by=self.user, modified_by=self.user, title={"en-GB": "Test news"},
news_type=self.test_news_type, description={"en-GB": "Description test news"},
self.lang = Language.objects.get(
title='Russia',
locale='ru-RU'
)
self.country_ru = Country.objects.get(
name={"en-GB": "Russian"}
)
role = Role.objects.create(
role=Role.CONTENT_PAGE_MANAGER,
country=self.country_ru
)
role.save()
user_role = UserRole.objects.create(
user=self.user,
role=role
)
user_role.save()
self.test_news = News.objects.create(created_by=self.user, modified_by=self.user,
title={"en-GB": "Test news"},
news_type=self.test_news_type,
description={"en-GB": "Description test news"},
playlist=1, start=datetime.now() + timedelta(hours=-2),
end=datetime.now() + timedelta(hours=2),
state=News.PUBLISHED, slug='test-news-slug',)
state=News.PUBLISHED, slug='test-news-slug',
country=self.country_ru)
class NewsTestCase(BaseTestCase):
def setUp(self):
super().setUp()
def test_news_list(self):
def test_web_news(self):
response = self.client.get("/api/web/news/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_news_web_detail(self):
response = self.client.get(f"/api/web/news/slug/{self.test_news.slug}/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.get("/api/web/news/types/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_news_back_detail(self):
response = self.client.get(f"/api/back/news/{self.test_news.id}/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -47,6 +77,18 @@ class NewsTestCase(BaseTestCase):
response = self.client.get("/api/back/news/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_news_type_list(self):
response = self.client.get("/api/web/news/types/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_news_back_detail_put(self):
# retrieve-update-destroy
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!"},
'slug': self.test_news.slug,
'start': self.test_news.start,
'playlist': self.test_news.playlist,
'news_type_id':self.test_news.news_type_id,
'country_id': self.country_ru.id
}
response = self.client.put(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

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