Merge remote-tracking branch 'origin/develop' into develop

# Conflicts:
#	celerybeat-schedule
This commit is contained in:
Anatoly 2019-10-28 12:34:42 +03:00
commit fe76fa64b5
215 changed files with 7718 additions and 933 deletions

4
.gitignore vendored
View File

@ -21,4 +21,6 @@ logs/
/geoip_db/
# dev
./docker-compose.override.yml
./docker-compose.override.yml
celerybeat-schedule

40
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,40 @@
image: docker:latest
stages:
- build
- test
- deploy
- clean
clean:
stage: clean
script:
- docker-compose -f compose-ci.yml stop
- docker-compose -f compose-ci.yml rm --force gm_app
when: always
buid:
stage: build
script:
- docker-compose -f compose-ci.yml build gm_app
when: always
test:
stage: test
script:
- docker-compose -f compose-ci.yml run gm_app python manage.py test -v 3 --noinput
when: always
deploy-develop:
stage: deploy
only:
- develop
script:
- fab --roles=develop deploy
environment:
name: Develop

View File

@ -12,7 +12,7 @@ class RoleAdmin(admin.ModelAdmin):
@admin.register(models.UserRole)
class UserRoleAdmin(admin.ModelAdmin):
list_display = ['user', 'role']
list_display = ['user', 'role', 'establishment']
@admin.register(models.User)

View File

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

View File

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

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,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,14 @@
# Generated by Django 2.2.4 on 2019-10-15 09:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0011_merge_20191014_1258'),
('account', '0011_merge_20191011_1336'),
]
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

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-23 09:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0009_auto_20191002_0648'),
('account', '0013_auto_20191016_0810'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-23 13:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0014_merge_20191023_0959'),
('account', '0012_merge_20191015_0912'),
]
operations = [
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-24 08:30
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('account', '0015_merge_20191023_1317'),
]
operations = [
migrations.AlterField(
model_name='role',
name='role',
field=models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator'), (3, 'Country admin'), (4, 'Content page manager'), (5, 'Establishment manager'), (6, 'Reviewer manager'), (7, 'Restaurant reviewer')], verbose_name='Role'),
),
migrations.AlterField(
model_name='userrole',
name='establishment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment', verbose_name='Establishment'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-24 08:33
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('account', '0015_merge_20191023_1317'),
]
operations = [
migrations.AlterField(
model_name='role',
name='role',
field=models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator'), (3, 'Country admin'), (4, 'Content page manager'), (5, 'Establishment manager'), (6, 'Reviewer manager'), (7, 'Restaurant reviewer')], verbose_name='Role'),
),
migrations.AlterField(
model_name='userrole',
name='establishment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment', verbose_name='Establishment'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-24 12:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0016_auto_20191024_0830'),
('account', '0016_auto_20191024_0833'),
]
operations = [
]

View File

@ -13,6 +13,7 @@ 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
@ -23,14 +24,25 @@ 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 =(
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'), on_delete=models.CASCADE)
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)
@ -77,7 +89,7 @@ class User(AbstractUser):
blank=True, null=True, default=None)
cropped_image_url = models.URLField(verbose_name=_('Cropped image URL path'),
blank=True, null=True, default=None)
email = models.EmailField(_('email address'), blank=True,
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)
@ -202,6 +214,15 @@ class User(AbstractUser):
template_name=settings.RESETTING_TOKEN_TEMPLATE,
context=context)
def notify_password_changed_template(self, country_code):
"""Get notification email template"""
context = {'contry_code': country_code}
context.update(self.base_template)
return render_to_string(
template_name=settings.NOTIFICATION_PASSWORD_TEMPLATE,
context=context,
)
def confirm_email_template(self, country_code):
"""Get confirm email template"""
context = {'token': self.confirm_email_token,
@ -224,4 +245,6 @@ class User(AbstractUser):
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)
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

@ -127,6 +127,14 @@ class ChangePasswordSerializer(serializers.ModelSerializer):
except serializers.ValidationError as e:
raise serializers.ValidationError({'detail': e.detail})
else:
if settings.USE_CELERY:
tasks.send_password_changed_email(
user_id=self.instance.id,
country_code=self.context.get('request').country_code)
else:
tasks.send_password_changed_email(
user_id=self.instance.id,
country_code=self.context.get('request').country_code)
return attrs
def update(self, instance, validated_data):

View File

@ -1,8 +1,9 @@
"""Serializers for account web"""
from django.conf import settings
from django.contrib.auth import password_validation as password_validators
from rest_framework import serializers
from account import models
from account import models, tasks
from utils import exceptions as utils_exceptions
from utils.methods import username_validator
@ -68,4 +69,12 @@ class PasswordResetConfirmSerializer(serializers.ModelSerializer):
# Update user password from instance
instance.set_password(validated_data.get('password'))
instance.save()
if settings.USE_CELERY:
tasks.send_password_changed_email(
user_id=instance.id,
country_code=self.context.get('request').country_code)
else:
tasks.send_password_changed_email(
user_id=instance.id,
country_code=self.context.get('request').country_code)
return instance

View File

@ -1,47 +1,46 @@
"""Account app celery tasks."""
import inspect
import logging
from celery import shared_task
from django.utils.translation import gettext_lazy as _
from . import models
from account.models import User
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
def send_email(user_id: int, subject: str, message_prop: str, country_code: str):
try:
user = User.objects.get(id=user_id)
user.send_email(subject=_(subject),
message=getattr(user, message_prop, lambda _: '')(country_code))
except:
cur_frame = inspect.currentframe()
cal_frame = inspect.getouterframes(cur_frame, 2)
logger.error(f'METHOD_NAME: {cal_frame[1][3]}\n'
f'DETAIL: Exception occurred for user: {user_id}')
@shared_task
def send_reset_password_email(user_id, country_code):
"""Send email to user for reset password."""
try:
user = models.User.objects.get(id=user_id)
user.send_email(subject=_('Password resetting'),
message=user.reset_password_template(country_code))
except:
logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n'
f'DETAIL: Exception occurred for reset password: '
f'{user_id}')
send_email(user_id, 'Password_resetting', 'reset_password_template', country_code)
@shared_task
def confirm_new_email_address(user_id, country_code):
"""Send email to user new email."""
try:
user = models.User.objects.get(id=user_id)
user.send_email(subject=_('Validate new email address'),
message=user.confirm_email_template(country_code))
except:
logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')
send_email(user_id, 'Confirm new email address', 'confirm_email_template', country_code)
@shared_task
def change_email_address(user_id, country_code):
"""Send email to user new email."""
try:
user = models.User.objects.get(id=user_id)
user.send_email(subject=_('Validate new email address'),
message=user.change_email_template(country_code))
except:
logger.error(f'METHOD_NAME: {change_email_address.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')
send_email(user_id, 'Validate new email address', 'change_email_template', country_code)
@shared_task
def send_password_changed_email(user_id, country_code):
"""Send email which notifies user that his password had changed"""
send_email(user_id, 'Notify password changed', 'notify_password_changed_template', country_code)

View File

@ -1,10 +1,13 @@
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 django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import Role, User
from authorization.tests.tests_authorization import get_tokens_for_user
from location.models import Country
from account.models import Role, User, UserRole
class RoleTests(APITestCase):
def setUp(self):
@ -65,9 +68,11 @@ class UserRoleTests(APITestCase):
)
self.role.save()
self.user_test = User.objects.create_user(username='test',
email='testemail@mail.com',
password='passwordtest')
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')

View File

@ -1,9 +1,11 @@
from rest_framework.test import APITestCase
from rest_framework import status
from authorization.tests.tests_authorization import get_tokens_for_user
from http.cookies import SimpleCookie
from account.models import User
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User
from authorization.tests.tests_authorization import get_tokens_for_user
class AccountUserTests(APITestCase):
@ -62,7 +64,7 @@ class AccountChangePasswordTests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
class AccountChangePasswordTests(APITestCase):
class AccountConfirmEmail(APITestCase):
def setUp(self):
self.data = get_tokens_for_user()

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')

View File

@ -18,12 +18,6 @@ from utils.tokens import GMRefreshToken
# Serializers
class SignupSerializer(serializers.ModelSerializer):
"""Signup serializer serializer mixin"""
# REQUEST
username = serializers.CharField(write_only=True)
password = serializers.CharField(write_only=True)
email = serializers.EmailField(write_only=True)
newsletter = serializers.BooleanField(write_only=True)
class Meta:
model = account_models.User
fields = (
@ -32,6 +26,12 @@ class SignupSerializer(serializers.ModelSerializer):
'email',
'newsletter'
)
extra_kwargs = {
'username': {'write_only': True},
'password': {'write_only': True},
'email': {'write_only': True},
'newsletter': {'write_only': True}
}
def validate_username(self, value):
"""Custom username validation"""

View File

@ -1,10 +1,10 @@
"""Authorization app celery tasks."""
import logging
from django.utils.translation import gettext_lazy as _
from celery import shared_task
from django.utils.translation import gettext_lazy as _
from account import models as account_models
from smtplib import SMTPException
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)

View File

@ -1,7 +1,7 @@
from rest_framework.test import APITestCase
from account.models import User
from django.urls import reverse
# Create your tests here.
from rest_framework.test import APITestCase
from account.models import User
def get_tokens_for_user(
@ -28,7 +28,7 @@ class AuthorizationTests(APITestCase):
self.password = data["password"]
def LoginTests(self):
data ={
data = {
'username_or_email': self.username,
'password': self.password,
'remember': True

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

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-10-24 13:34
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('collection', '0015_auto_20191023_0715'),
]
operations = [
migrations.AlterField(
model_name='guide',
name='collection',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='collection.Collection', verbose_name='collection'),
),
]

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."""
@ -102,8 +101,9 @@ class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
advertorials = JSONField(
_('advertorials'), null=True, blank=True,
default=None, help_text='{"key":"value"}')
collection = models.ForeignKey(
Collection, verbose_name=_('collection'), on_delete=models.CASCADE)
collection = models.ForeignKey(Collection, on_delete=models.CASCADE,
null=True, blank=True, default=None,
verbose_name=_('collection'))
objects = GuideQuerySet.as_manager()

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

@ -7,6 +7,8 @@ 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):
"""QuerySets for Comment model."""
@ -41,7 +43,8 @@ class Comment(ProjectBaseMixin):
content_object = generic.GenericForeignKey('content_type', 'object_id')
objects = CommentQuerySet.as_manager()
language = models.ForeignKey(Language, verbose_name=_('Locale'), on_delete=models.SET_NULL, null=True)
country = models.ForeignKey(Country, verbose_name=_('Country'),
on_delete=models.SET_NULL, null=True)
class Meta:
"""Meta class"""

View File

@ -1,28 +0,0 @@
from rest_framework import permissions
from account.models import UserRole, Role, User
class IsCommentModerator(permissions.IsAuthenticatedOrReadOnly):
"""
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS or \
obj.user == request.user or request.user.is_superuser:
return True
# Must have role
role = Role.objects.filter(role=Role.COMMENTS_MODERATOR,
country__languages__id=obj.language_id)\
.first() # 'Comments moderator'
is_access = UserRole.objects.filter(user=request.user, role=role).exists()
if obj.user != request.user and is_access:
return True
return False

View File

@ -6,4 +6,4 @@ from rest_framework import serializers
class CommentBaseSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comment
fields = ('id', 'text', 'mark', 'user')
fields = ('id', 'text', 'mark', 'user', 'object_id', 'content_type')

View File

@ -1,32 +1,18 @@
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 django.contrib.contenttypes.models import ContentType
from http.cookies import SimpleCookie
from location.models import Country
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from rest_framework import status
from account.models import Role, User, UserRole
from authorization.tests.tests_authorization import get_tokens_for_user
from comment.models import Comment
from translation.models import Language
from utils.tests.tests_permissions import BasePermissionTests
class CommentModeratorPermissionTests(APITestCase):
class CommentModeratorPermissionTests(BasePermissionTests):
def setUp(self):
self.lang = Language.objects.create(
title='Russia',
locale='ru-RU'
)
self.lang.save()
self.country_ru = Country.objects.create(
name='{"ru-RU":"Russia"}',
code='23',
low_price=15,
high_price=150000,
)
self.country_ru.languages.add(self.lang)
self.country_ru.save()
super().setUp()
self.role = Role.objects.create(
role=2,
@ -44,21 +30,47 @@ class CommentModeratorPermissionTests(APITestCase):
)
self.userRole.save()
content_type = ContentType.objects.get(app_label='location', model='country')
self.content_type = ContentType.objects.get(app_label='location', model='country')
self.user_test = get_tokens_for_user()
self.comment = Comment.objects.create(text='Test comment', mark=1,
user=self.user_test["user"],
object_id= self.country_ru.pk,
content_type_id=content_type.id,
language=self.lang
object_id=self.country_ru.pk,
content_type_id=self.content_type.id,
country=self.country_ru
)
self.comment.save()
self.url = reverse('back:comment:comment-crud', kwargs={"id": self.comment.id})
def test_get(self):
response = self.client.get(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_post(self):
self.url = reverse('back:comment:comment-list-create')
comment = {
"text": "Test comment POST",
"user": self.user_test["user"].id,
"object_id": self.country_ru.pk,
"content_type": self.content_type.id,
"country_id": self.country_ru.id
}
response = self.client.post(self.url, format='json', data=comment)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
comment = {
"text": "Test comment POST moder",
"user": self.moderator.id,
"object_id": self.country_ru.id,
"content_type": self.content_type.id,
"country_id": self.country_ru.id
}
tokens = User.create_jwt_tokens(self.moderator)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('access_token')})
response = self.client.post(self.url, format='json', data=comment)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_put_moderator(self):
tokens = User.create_jwt_tokens(self.moderator)
@ -70,12 +82,18 @@ class CommentModeratorPermissionTests(APITestCase):
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": self.moderator.id
"user": self.moderator.id,
"object_id": self.comment.country_id,
"content_type": self.content_type.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_200_OK)
def test_put_other_user(self):
other_user = User.objects.create_user(username='test',
email='test@mail.com',
@ -113,11 +131,11 @@ class CommentModeratorPermissionTests(APITestCase):
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": super_user.id
"user": super_user.id,
"object_id": self.country_ru.id,
"content_type": self.content_type.id,
}
response = self.client.put(self.url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -1,19 +1,20 @@
from rest_framework import generics, permissions
from comment.serializers import back as serializers
from comment import models
from comment.permissions import IsCommentModerator
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,]
permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin]
class CommentRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view."""
serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all()
permission_classes = [IsCommentModerator]
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 @@
from django.core.management.base import BaseCommand
from pytz import timezone as py_tz
from timezonefinder import TimezoneFinder
from establishment.models import Establishment
class Command(BaseCommand):
help = 'Attach correct timestamps according to coordinates to existing establishments'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tf = TimezoneFinder(in_memory=True)
def handle(self, *args, **options):
for establishment in Establishment.objects.prefetch_related('address').all():
if establishment.address and establishment.address.latitude and establishment.address.longitude:
establishment.tz = py_tz(self.tf.certain_timezone_at(lng=establishment.address.longitude,
lat=establishment.address.latitude))
establishment.save()
self.stdout.write(self.style.SUCCESS(f'Attached timezone {establishment.tz} to {establishment}'))
else:
self.stdout.write(self.style.WARNING(f'Establishment {establishment} has no coordinates'
f'passing...'))

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,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 = 'Type %s' % 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

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-23 09:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0040_employee_tags'),
]
operations = [
migrations.AlterField(
model_name='establishment',
name='slug',
field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Establishment slug'),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 2.2.4 on 2019-10-21 17:33
import timezone_field.fields
from django.db import migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('establishment', '0041_auto_20191023_0920'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='tz',
field=timezone_field.fields.TimeZoneField(default=settings.TIME_ZONE),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.4 on 2019-10-24 13:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0022_auto_20191023_1113'),
('establishment', '0042_establishment_tz'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='currency',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Currency', verbose_name='currency'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-24 14:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0043_establishment_currency'),
]
operations = [
migrations.AddField(
model_name='position',
name='index_name',
field=models.CharField(db_index=True, max_length=255, null=True, unique=True, verbose_name='Index name'),
),
]

View File

@ -1,6 +1,8 @@
"""Establishment models."""
from datetime import datetime
from functools import reduce
import elasticsearch_dsl
from django.conf import settings
from django.contrib.contenttypes import fields as generic
from django.contrib.gis.db.models.functions import Distance
@ -15,10 +17,11 @@ from phonenumber_field.modelfields import PhoneNumberField
from collection.models import Collection
from location.models import Address
from main.models import Award, MetaDataContent
from main.models import Award, Currency
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes)
from timezone_field import TimeZoneField
# todo: establishment type&subtypes check
@ -27,9 +30,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."""
@ -51,11 +71,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()
@ -75,11 +108,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').\
@ -87,6 +117,14 @@ class EstablishmentQuerySet(models.QuerySet):
'phones').\
prefetch_actual_employees()
def with_type_related(self):
return self.prefetch_related('establishment_subtypes')
def with_es_related(self):
"""Return qs with related for ES indexing objects."""
return self.select_related('address', 'establishment_type', 'address__city', 'address__city__country').\
prefetch_related('tags', 'schedule')
def search(self, value, locale=None):
"""Search text in JSON fields."""
if locale is not None:
@ -98,15 +136,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(
# 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 es_search(self, value, locale=None):
"""Search text via ElasticSearch."""
from search_indexes.documents import EstablishmentDocument
search = EstablishmentDocument.search().filter(
elasticsearch_dsl.Q('match', name=value) |
elasticsearch_dsl.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"""
@ -174,7 +212,7 @@ class EstablishmentQuerySet(models.QuerySet):
.filter(image_url__isnull=False, public_mark__gte=10)
.has_published_reviews()
.annotate_distance(point=establishment.location)
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
.values('id')
)
return self.filter(id__in=subquery_filter_by_distance) \
@ -194,7 +232,7 @@ class EstablishmentQuerySet(models.QuerySet):
self.filter(image_url__isnull=False, public_mark__gte=10)
.has_published_reviews()
.annotate_distance(point=point)
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
.values('id')
)
return self.filter(id__in=subquery_filter_by_distance) \
@ -234,6 +272,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 +318,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,
@ -293,14 +357,19 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
verbose_name=_('Collections'))
preview_image_url = models.URLField(verbose_name=_('Preview image URL path'),
blank=True, null=True, default=None)
slug = models.SlugField(unique=True, max_length=50, null=True,
verbose_name=_('Establishment slug'), editable=True)
slug = models.SlugField(unique=True, max_length=255, null=True,
verbose_name=_('Establishment slug'))
tz = TimeZoneField(default=settings.TIME_ZONE)
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')
currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
on_delete=models.PROTECT,
verbose_name=_('currency'))
objects = EstablishmentQuerySet.as_manager()
@ -359,6 +428,27 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
def best_price_carte(self):
return 200
@property
def works_noon(self):
""" Used for indexing working by day """
return [ret.weekday for ret in self.schedule.all() if ret.works_at_noon]
@property
def works_evening(self):
""" Used for indexing working by day """
return [ret.weekday for ret in self.schedule.all() if ret.works_at_afternoon]
@property
def works_now(self):
""" Is establishment working now """
now_at_est_tz = datetime.now(tz=self.tz)
current_week = now_at_est_tz.weekday()
schedule_for_today = self.schedule.filter(weekday=current_week).first()
if schedule_for_today is None or schedule_for_today.closed_at is None or schedule_for_today.opening_at is None:
return False
time_at_est_tz = now_at_est_tz.time()
return schedule_for_today.closed_at > time_at_est_tz > schedule_for_today.opening_at
@property
def tags_indexing(self):
return [{'id': tag.metadata.id,
@ -379,8 +469,22 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
@property
def the_most_recent_award(self):
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)).latest(
field_name='vintage_year')
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):
@ -393,6 +497,9 @@ class Position(BaseAttributes, TranslatedFieldsMixin):
priority = models.IntegerField(unique=True, null=True, default=None)
index_name = models.CharField(max_length=255, db_index=True, unique=True,
null=True, verbose_name=_('Index name'))
class Meta:
"""Meta class."""
@ -437,7 +544,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."""
@ -477,6 +585,7 @@ class ContactEmail(models.Model):
def __str__(self):
return f'{self.email}'
#
# class Wine(TranslatedFieldsMixin, models.Model):
# """Wine model."""
@ -515,6 +624,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')
@ -550,3 +663,4 @@ class SocialNetwork(models.Model):
def __str__(self):
return self.title

View File

@ -1,13 +1,13 @@
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
from utils.serializers import TimeZoneChoiceField
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
@ -20,8 +20,8 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
phones = ContactPhonesSerializer(read_only=True, many=True, )
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)
tz = TimeZoneChoiceField()
class Meta:
model = models.Establishment
@ -42,6 +42,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
'is_publish',
'guestonline_id',
'lastable_id',
'tz',
]
@ -55,7 +56,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
@ -141,4 +142,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."""
@ -130,22 +147,23 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
position_translated = serializers.CharField(source='position.name_translated')
awards = AwardSerializer(source='employee.awards', many=True)
priority = serializers.IntegerField(source='position.priority')
position_index_name = serializers.CharField(source='position.index_name')
class Meta:
"""Meta class."""
model = models.Employee
fields = ('id', 'name', 'position_translated', 'awards', 'priority')
fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name')
class EstablishmentBaseSerializer(ProjectModelSerializer):
"""Base serializer for Establishment model."""
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)
currency = CurrencySerializer()
class Meta:
"""Meta class."""
@ -163,6 +181,7 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'in_favorites',
'address',
'tags',
'currency'
]
@ -171,8 +190,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 +325,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

@ -1,10 +1,15 @@
"""Establishment app tasks."""
import logging
from celery import shared_task
from celery.schedules import crontab
from celery.task import periodic_task
from django.core import management
from django_elasticsearch_dsl.management.commands import search_index
from establishment import models
from location.models import Country
logger = logging.getLogger(__name__)
@ -12,10 +17,15 @@ logger = logging.getLogger(__name__)
def recalculate_price_levels_by_country(country_id):
try:
country = Country.objects.get(pk=country_id)
except Country.DoesNotExist as ex:
except Country.DoesNotExist as _:
logger.error(f'ESTABLISHMENT. Country does not exist. ID {country_id}')
else:
qs = models.Establishment.objects.filter(address__city__country=country)
for establishment in qs:
establishment.recalculate_price_level(low_price=country.low_price,
high_price=country.high_price)
@periodic_task(run_every=crontab(minute=59))
def rebuild_establishment_indices():
management.call_command(search_index.Command(), action='rebuild', models=[models.Establishment.__name__],
force=True)

View File

@ -7,10 +7,12 @@ 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
from pytz import timezone as py_tz
class BaseTestCase(APITestCase):
def setUp(self):
self.username = 'sedragurda'
self.password = 'sedragurdaredips19'
@ -18,20 +20,58 @@ class BaseTestCase(APITestCase):
self.newsletter = True
self.user = User.objects.create_user(
username=self.username, email=self.email, password=self.password)
#get tokkens
tokkens = User.create_jwt_tokens(self.user)
# get tokens
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')})
self.establishment_type = EstablishmentType.objects.create(name="Test establishment type")
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):
@ -44,24 +84,25 @@ class EstablishmentBTests(BaseTestCase):
'type_id': self.establishment_type.id,
'is_publish': True,
'slug': 'test-establishment-slug',
'tz': py_tz('Europe/Moscow').zone
}
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)
@ -92,43 +133,48 @@ class EmployeeTests(BaseTestCase):
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
# Class to test childs
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 +331,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,13 @@ 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'."""
qs = models.Establishment.objects.published() \
.with_base_related() \
.annotate_in_favorites(self.request.user)
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
@ -30,13 +33,6 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
filter_class = filters.EstablishmentFilter
serializer_class = serializers.EstablishmentBaseSerializer
def get_queryset(self):
"""Overridden method 'get_queryset'."""
qs = super(EstablishmentListView, self).get_queryset()
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
"""Resource for getting a establishment."""
@ -84,7 +80,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 +172,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

@ -1,14 +1,16 @@
# Create your tests here.
from http.cookies import SimpleCookie
from django.contrib.contenttypes.models import ContentType
from rest_framework.test import APITestCase
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User
from establishment.models import Establishment, EstablishmentType
from favorites.models import Favorites
from establishment.models import Establishment, EstablishmentType, EstablishmentSubType
from news.models import NewsType, News
from datetime import datetime
class BaseTestCase(APITestCase):
@ -17,38 +19,54 @@ class BaseTestCase(APITestCase):
self.username = 'sedragurda'
self.password = 'sedragurdaredips19'
self.email = 'sedragurda@desoz.com'
self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password)
tokkens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
self.user = User.objects.create_user(
username=self.username,
email=self.email,
password=self.password
)
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"},
playlist=1, start="2020-12-03 12:00:00", end="2020-12-13 12:00:00",
state=News.PUBLISHED, slug='test-news')
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')})
self.test_content_type = ContentType.objects.get(app_label="news", model="news")
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"},
start=datetime.fromisoformat("2020-12-03 12:00:00"),
end=datetime.fromisoformat("2020-12-03 12:00:00"),
state=News.PUBLISHED,
slug='test-news'
)
self.test_favorites = Favorites.objects.create(user=self.user, content_type=self.test_content_type,
object_id=self.test_news.id)
self.test_content_type = ContentType.objects.get(
app_label="news", model="news")
self.test_establishment_type = EstablishmentType.objects.create(name={"en-GB": "test establishment type"},
use_subtypes=False)
self.test_favorites = Favorites.objects.create(
user=self.user, content_type=self.test_content_type,
object_id=self.test_news.id)
self.test_establishment = Establishment.objects.create(name="test establishment",
description={"en-GB": "description of test establishment"},
establishment_type=self.test_establishment_type,
is_publish=True)
# value for GenericRelation(reverse side) field must be iterable
# value for GenericRelation(reverse side) field must be assigned through "set" method of field
self.test_establishment_type = EstablishmentType.objects.create(
name={"en-GB": "test establishment type"},
use_subtypes=False)
self.test_establishment = Establishment.objects.create(
name="test establishment",
description={"en-GB": "description of test establishment"},
establishment_type=self.test_establishment_type,
is_publish=True)
self.test_establishment.favorites.set([self.test_favorites])
class FavoritesTestCase(BaseTestCase):
def test_func(self):
response = self.client.get("/api/web/favorites/establishments/")
print(response.json())
url = reverse('web:favorites:establishment-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -5,4 +5,9 @@ from gallery.models import Image
@admin.register(Image)
class ImageModelAdmin(admin.ModelAdmin):
"""Image model admin"""
"""Image model admin."""
list_display = ['id', 'title', 'orientation_display', 'image_tag', ]
def orientation_display(self, obj):
"""Get image orientation name."""
return obj.get_orientation_display() if obj.orientation else None

View File

@ -0,0 +1,41 @@
# Generated by Django 2.2.4 on 2019-09-30 07:14
from django.db import migrations, models
import django.db.models.deletion
import easy_thumbnails.fields
import utils.methods
class Migration(migrations.Migration):
dependencies = [
('gallery', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='image',
name='orientation',
field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Horizontal'), (1, 'Vertical')], default=None, null=True, verbose_name='image orientation'),
),
migrations.AddField(
model_name='image',
name='parent',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='parent_image', to='gallery.Image', verbose_name='parent image'),
),
migrations.AddField(
model_name='image',
name='source',
field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web'), (2, 'All')], default=0, verbose_name='Source'),
),
migrations.AddField(
model_name='image',
name='title',
field=models.CharField(default='', max_length=255, verbose_name='title'),
),
migrations.AlterField(
model_name='image',
name='image',
field=easy_thumbnails.fields.ThumbnailerImageField(upload_to=utils.methods.image_path, verbose_name='image file'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-03 12:28
from django.db import migrations
import sorl.thumbnail.fields
import utils.methods
class Migration(migrations.Migration):
dependencies = [
('gallery', '0002_auto_20190930_0714'),
]
operations = [
migrations.RemoveField(
model_name='image',
name='parent',
),
migrations.AlterField(
model_name='image',
name='image',
field=sorl.thumbnail.fields.ImageField(upload_to=utils.methods.image_path, verbose_name='image file'),
),
]

View File

@ -1,15 +1,34 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from easy_thumbnails.fields import ThumbnailerImageField
from sorl.thumbnail import delete
from sorl.thumbnail.fields import ImageField as SORLImageField
from utils.methods import image_path
from utils.models import ProjectBaseMixin, ImageMixin
from utils.models import ProjectBaseMixin, SORLImageMixin, PlatformMixin
class Image(ProjectBaseMixin, ImageMixin):
class ImageQuerySet(models.QuerySet):
"""QuerySet for model Image."""
class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
"""Image model."""
HORIZONTAL = 0
VERTICAL = 1
image = ThumbnailerImageField(upload_to=image_path,
verbose_name=_('Image file'))
ORIENTATIONS = (
(HORIZONTAL, _('Horizontal')),
(VERTICAL, _('Vertical')),
)
image = SORLImageField(upload_to=image_path,
verbose_name=_('image file'))
orientation = models.PositiveSmallIntegerField(choices=ORIENTATIONS,
blank=True, null=True, default=None,
verbose_name=_('image orientation'))
title = models.CharField(_('title'), max_length=255, default='')
objects = ImageQuerySet.as_manager()
class Meta:
"""Meta class."""
@ -18,4 +37,22 @@ class Image(ProjectBaseMixin, ImageMixin):
def __str__(self):
"""String representation"""
return str(self.id)
return f'{self.id}'
def delete_image(self, completely: bool = True):
"""
Deletes an instance and crops of instance from media storage.
:param completely: if set to False then removed only crop neither original image.
"""
try:
# Delete from remote storage
delete(file_=self.image.file, delete_file=completely)
except FileNotFoundError:
pass
finally:
if completely:
# Delete an instance of image
super().delete()

View File

@ -12,13 +12,20 @@ class ImageSerializer(serializers.ModelSerializer):
# RESPONSE
url = serializers.ImageField(source='image',
read_only=True)
orientation_display = serializers.CharField(source='get_orientation_display',
read_only=True)
class Meta:
"""Meta class"""
model = models.Image
fields = (
fields = [
'id',
'file',
'url'
)
'url',
'orientation',
'orientation_display',
'title',
]
extra_kwargs = {
'orientation': {'write_only': True}
}

23
apps/gallery/tasks.py Normal file
View File

@ -0,0 +1,23 @@
"""Gallery app celery tasks."""
import logging
from celery import shared_task
from . import models
logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)
@shared_task
def delete_image(image_id: int, completely: bool = True):
"""Delete an image from remote storage."""
image_qs = models.Image.objects.filter(id=image_id)
if image_qs.exists():
try:
image = image_qs.first()
image.delete_image(completely=completely)
except:
logger.error(f'TASK_NAME: delete_image\n'
f'DETAIL: Exception occurred while deleting an image '
f'and related crops from remote storage: image_id - {image_id}')

View File

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

View File

@ -1,10 +1,30 @@
from rest_framework import generics
from django.conf import settings
from django.db.transaction import on_commit
from rest_framework import generics, status
from rest_framework.response import Response
from . import models, serializers
from . import tasks, models, serializers
class ImageUploadView(generics.CreateAPIView):
"""Upload image to gallery"""
class ImageBaseView(generics.GenericAPIView):
"""Base Image view."""
model = models.Image
queryset = models.Image.objects.all()
serializer_class = serializers.ImageSerializer
class ImageListCreateView(ImageBaseView, generics.ListCreateAPIView):
"""List/Create Image view."""
class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView):
"""Destroy view for model Image"""
def delete(self, request, *args, **kwargs):
"""Override destroy view"""
instance = self.get_object()
if settings.USE_CELERY:
on_commit(lambda: tasks.delete_image.delay(image_id=instance.id))
else:
on_commit(lambda: tasks.delete_image(image_id=instance.id))
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -3,7 +3,7 @@ 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()

View File

@ -87,7 +87,6 @@ INSERT INTO codelang (code,country) VALUES
,('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
@ -326,7 +325,7 @@ commit;
INSERT INTO location_country
(code, "name", low_price, high_price, created, modified)
select
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,
@ -335,7 +334,7 @@ select
now() as modified
from
(
select
select
distinct c.country
from country_code c
) t
@ -348,6 +347,7 @@ commit;
INSERT INTO translation_language
(title, locale)
select
distinct
t.country as title,
t.code as locale
from

View File

@ -5,8 +5,9 @@ from django.db.models.signals import post_save
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
from utils.models import ProjectBaseMixin, SVGImageMixin, TranslatedFieldsMixin, TJSONField
class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):
@ -21,6 +22,10 @@ class Country(TranslatedFieldsMixin, SVGImageMixin, ProjectBaseMixin):
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."""
@ -49,6 +54,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."""
@ -64,6 +77,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')
@ -73,7 +88,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(
@ -112,6 +126,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

@ -16,4 +16,5 @@ class CountryBackSerializer(common.CountrySerializer):
'code',
'svg_image',
'name',
'country_id'
]

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'
@ -18,29 +19,55 @@ class BaseTestCase(APITestCase):
self.user = User.objects.create_user(
username=self.username, email=self.email, password=self.password)
# get tokens
tokens = User.create_jwt_tokens(self.user)
tokkens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.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 +91,13 @@ 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 +142,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 +183,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 +202,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 +216,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,62 @@ from rest_framework import generics
from location import models, serializers
from location.views import common
from utils.permissions import IsCountryAdmin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
# Address
class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView):
"""Create view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# City
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region"""
serializer_class = serializers.RegionSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""Retrieve view for model Region"""
serializer_class = serializers.RegionSerializer
permission_classes = [IsAuthenticatedOrReadOnly|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 = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CountryRUDView(common.CountryViewMixin, generics.RetrieveUpdateDestroyAPIView):
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Country."""
serializer_class = serializers.CountryBackSerializer
permission_classes = [IsAuthenticatedOrReadOnly|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

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

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-23 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0020_merge_20191023_0750'),
]
operations = [
migrations.AlterField(
model_name='feature',
name='slug',
field=models.SlugField(max_length=255, unique=True),
),
]

View File

@ -0,0 +1,57 @@
# Generated by Django 2.2.4 on 2019-10-23 11:13
from django.db import migrations, models
import django.db.models.deletion
import utils.models
def fill_currency_name(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.
Currency = apps.get_model('main', 'Currency')
for currency in Currency.objects.all():
currency.name_json = {'en-GB': currency.name}
currency.save()
class Migration(migrations.Migration):
dependencies = [
('main', '0021_auto_20191023_0924'),
]
operations = [
migrations.AddField(
model_name='currency',
name='sign',
field=models.CharField(default='?', max_length=1, verbose_name='sign'),
preserve_default=False,
),
migrations.AddField(
model_name='currency',
name='slug',
field=models.SlugField(default='?', max_length=255, unique=True),
preserve_default=False,
),
migrations.AddField(
model_name='sitesettings',
name='currency',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='main.Currency'),
),
migrations.AddField(
model_name='currency',
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_currency_name, migrations.RunPython.noop),
migrations.RemoveField(
model_name='currency',
name='name',
),
migrations.RenameField(
model_name='currency',
old_name='name_json',
new_name='name',
),
]

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
#
#
@ -101,6 +106,22 @@ from configuration.models import TranslationSettings
#
class Currency(TranslatedFieldsMixin, models.Model):
"""Currency model."""
name = TJSONField(
_('name'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}')
sign = models.CharField(_('sign'), max_length=1)
slug = models.SlugField(max_length=255, unique=True)
class Meta:
verbose_name = _('currency')
verbose_name_plural = _('currencies')
def __str__(self):
return f'{self.name}'
class SiteSettingsQuerySet(models.QuerySet):
"""Extended queryset for SiteSettings model."""
@ -131,6 +152,7 @@ class SiteSettings(ProjectBaseMixin):
verbose_name=_('Config'))
ad_config = models.TextField(blank=True, null=True, default=None,
verbose_name=_('AD config'))
currency = models.ForeignKey(Currency, on_delete=models.PROTECT, null=True, default=None)
objects = SiteSettingsQuerySet.as_manager()
@ -150,7 +172,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 +182,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)
slug = models.SlugField(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 +220,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 +245,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,61 +275,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)
class Meta:
verbose_name = _('currency')
verbose_name_plural = _('currencies')
def __str__(self):
return f'{self.name}'
class CarouselQuerySet(models.QuerySet):
"""Carousel QuerySet."""
@ -351,19 +341,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,10 +1,10 @@
"""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
from utils.serializers import ProjectModelSerializer, TranslatedField
class FeatureSerializer(serializers.ModelSerializer):
@ -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,18 +34,37 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
fields = ('main',
'id',
'slug',
'priority'
'priority',
'route',
'source'
)
class CurrencySerializer(ProjectModelSerializer):
"""Currency serializer."""
name_translated = TranslatedField()
class Meta:
model = models.Currency
fields = [
'id',
'name_translated',
'sign'
]
class SiteSettingsSerializer(serializers.ModelSerializer):
"""Site settings serializer."""
published_features = SiteFeatureSerializer(source='published_sitefeatures',
many=True, allow_null=True)
currency = CurrencySerializer()
# todo: remove this
country_code = serializers.CharField(source='subdomain', read_only=True)
country_name = serializers.CharField(source='country.name_translated', read_only=True)
class Meta:
"""Meta class."""
@ -59,6 +80,8 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
'config',
'ad_config',
'published_features',
'currency',
'country_name'
)
@ -98,6 +121,7 @@ class AwardBaseSerializer(serializers.ModelSerializer):
'id',
'title_translated',
'vintage_year',
'image_url',
]
@ -109,30 +133,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"""
class Meta:
model = models.Currency
fields = [
'id',
'name'
]
class CarouselListSerializer(serializers.ModelSerializer):
"""Serializer for retrieving list of carousel items."""
model_name = serializers.CharField()

View File

@ -1,15 +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('determine-location/', views.DetermineLocation.as_view(), name='determine-location'),
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'),
]

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

@ -1,56 +1,9 @@
"""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
from django.http import Http404
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 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
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
#
@ -89,6 +42,7 @@ class SiteListView(generics.ListAPIView):
# class SiteFeaturesRUDView(SiteFeaturesViewMixin,
# generics.RetrieveUpdateDestroyAPIView):
# """Site features RUD."""
from utils.serializers import EmptySerializer
class AwardView(generics.ListAPIView):
@ -111,3 +65,20 @@ class CarouselListView(generics.ListAPIView):
serializer_class = serializers.CarouselListSerializer
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])

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