diff --git a/README.md b/README.md index 62dc8dc5..3cb1c6b9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # gm-backend +## Build + +1. ``git clone ssh://git@gl.id-east.ru:222/gm/gm-backend.git`` +1. ``cd ./gm-backend`` +1. ``git checkout develop`` +1. ``docker-compose build`` +1. First start database: ``docker-compose up db`` +1. ``docker-compose up -d`` +### Migrate data + +1.Connect to container with django ``docker exec -it gm-backend_gm_app_1 bash`` + +#### In docker container(django) + +1. Migrate ``python manage.py migrate`` +1. Create super-user ``python manage.py createsuperuser`` + +Backend is available at localhost:8000 or 0.0.0.0:8000 + +URL for admin http://0.0.0.0:8000/admin +URL for swagger http://0.0.0.0:8000/docs/ +URL for redocs http://0.0.0.0:8000/redocs/ + +## Start and stop backend containers + +Demonize start ``docker-compose up -d`` +Stop ``docker-compose down`` +Stop and remove volumes ``docker-compose down -v`` \ No newline at end of file diff --git a/apps/account/admin.py b/apps/account/admin.py index 3b247289..651e5a5a 100644 --- a/apps/account/admin.py +++ b/apps/account/admin.py @@ -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) diff --git a/apps/account/migrations/0011_merge_20191014_0839.py b/apps/account/migrations/0011_merge_20191014_0839.py new file mode 100644 index 00000000..653f39b7 --- /dev/null +++ b/apps/account/migrations/0011_merge_20191014_0839.py @@ -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 = [ + ] diff --git a/apps/account/migrations/0012_merge_20191015_0708.py b/apps/account/migrations/0012_merge_20191015_0708.py new file mode 100644 index 00000000..91dba02e --- /dev/null +++ b/apps/account/migrations/0012_merge_20191015_0708.py @@ -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 = [ + ] diff --git a/apps/account/migrations/0013_auto_20191016_0810.py b/apps/account/migrations/0013_auto_20191016_0810.py new file mode 100644 index 00000000..72955cee --- /dev/null +++ b/apps/account/migrations/0013_auto_20191016_0810.py @@ -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'), + ), + ] diff --git a/apps/account/models.py b/apps/account/models.py index 5052969e..a9f739bd 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -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) @@ -224,4 +236,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) \ No newline at end of file + 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) \ No newline at end of file diff --git a/apps/account/views/web.py b/apps/account/views/web.py index 897c955e..9f2ebcfd 100644 --- a/apps/account/views/web.py +++ b/apps/account/views/web.py @@ -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') diff --git a/apps/authorization/tasks.py b/apps/authorization/tasks.py index c97fbbae..cb186142 100644 --- a/apps/authorization/tasks.py +++ b/apps/authorization/tasks.py @@ -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__) diff --git a/apps/collection/migrations/0014_auto_20191022_1242.py b/apps/collection/migrations/0014_auto_20191022_1242.py new file mode 100644 index 00000000..d70c0cfa --- /dev/null +++ b/apps/collection/migrations/0014_auto_20191022_1242.py @@ -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'), + ), + ] diff --git a/apps/collection/migrations/0015_auto_20191023_0715.py b/apps/collection/migrations/0015_auto_20191023_0715.py new file mode 100644 index 00000000..53bfdc2d --- /dev/null +++ b/apps/collection/migrations/0015_auto_20191023_0715.py @@ -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'), + ), + ] diff --git a/apps/collection/models.py b/apps/collection/models.py index 25bf1ef9..0a2700bd 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -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.""" diff --git a/apps/collection/serializers/common.py b/apps/collection/serializers/common.py index 78612a55..846236d5 100644 --- a/apps/collection/serializers/common.py +++ b/apps/collection/serializers/common.py @@ -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', diff --git a/apps/collection/tests.py b/apps/collection/tests.py index 72b40c37..b2b8231b 100644 --- a/apps/collection/tests.py +++ b/apps/collection/tests.py @@ -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') diff --git a/apps/comment/migrations/0003_auto_20191015_0704.py b/apps/comment/migrations/0003_auto_20191015_0704.py new file mode 100644 index 00000000..09296253 --- /dev/null +++ b/apps/comment/migrations/0003_auto_20191015_0704.py @@ -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'), + ), + ] diff --git a/apps/comment/models.py b/apps/comment/models.py index 0193055d..55c7802e 100644 --- a/apps/comment/models.py +++ b/apps/comment/models.py @@ -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""" diff --git a/apps/comment/permissions.py b/apps/comment/permissions.py deleted file mode 100644 index 6d691c07..00000000 --- a/apps/comment/permissions.py +++ /dev/null @@ -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 - diff --git a/apps/comment/tests.py b/apps/comment/tests.py index 949ba597..9b060f4e 100644 --- a/apps/comment/tests.py +++ b/apps/comment/tests.py @@ -1,32 +1,16 @@ -from rest_framework.test import APITestCase +from utils.tests.tests_permissions import BasePermissionTests from rest_framework import status from authorization.tests.tests_authorization import get_tokens_for_user from django.urls import reverse from django.contrib.contenttypes.models import ContentType from http.cookies import SimpleCookie -from location.models import Country from account.models import Role, User, UserRole from comment.models import Comment -from translation.models import Language -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, @@ -51,14 +35,11 @@ class CommentModeratorPermissionTests(APITestCase): user=self.user_test["user"], object_id= self.country_ru.pk, content_type_id=content_type.id, - language=self.lang + 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_put_moderator(self): tokens = User.create_jwt_tokens(self.moderator) @@ -76,6 +57,10 @@ class CommentModeratorPermissionTests(APITestCase): response = self.client.put(self.url, data=data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_get(self): + response = self.client.get(self.url, format='json') + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + def test_put_other_user(self): other_user = User.objects.create_user(username='test', email='test@mail.com', @@ -120,4 +105,3 @@ class CommentModeratorPermissionTests(APITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) - diff --git a/apps/comment/views/back.py b/apps/comment/views/back.py index 77edfa97..2895fdbe 100644 --- a/apps/comment/views/back.py +++ b/apps/comment/views/back.py @@ -1,7 +1,7 @@ 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): @@ -15,5 +15,5 @@ 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' diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index f95dd5c8..50c21b90 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -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') \ No newline at end of file + category_translated.short_description = _('category') diff --git a/apps/establishment/filters.py b/apps/establishment/filters.py index 51b207dc..91670031 100644 --- a/apps/establishment/filters.py +++ b/apps/establishment/filters.py @@ -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', + ) diff --git a/apps/establishment/migrations/0032_establishmenttag_establishmenttypetagcategory.py b/apps/establishment/migrations/0032_establishmenttag_establishmenttypetagcategory.py new file mode 100644 index 00000000..ec9966d8 --- /dev/null +++ b/apps/establishment/migrations/0032_establishmenttag_establishmenttypetagcategory.py @@ -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', + }, + ), + ] diff --git a/apps/establishment/migrations/0033_auto_20191009_0715.py b/apps/establishment/migrations/0033_auto_20191009_0715.py new file mode 100644 index 00000000..5df367d6 --- /dev/null +++ b/apps/establishment/migrations/0033_auto_20191009_0715.py @@ -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'), + ), + ] diff --git a/apps/establishment/migrations/0034_merge_20191009_1457.py b/apps/establishment/migrations/0034_merge_20191009_1457.py new file mode 100644 index 00000000..945860f7 --- /dev/null +++ b/apps/establishment/migrations/0034_merge_20191009_1457.py @@ -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 = [ + ] diff --git a/apps/establishment/migrations/0035_establishmentsubtypetagcategory.py b/apps/establishment/migrations/0035_establishmentsubtypetagcategory.py new file mode 100644 index 00000000..6a85fca7 --- /dev/null +++ b/apps/establishment/migrations/0035_establishmentsubtypetagcategory.py @@ -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', + }, + ), + ] diff --git a/apps/establishment/migrations/0036_auto_20191011_1356.py b/apps/establishment/migrations/0036_auto_20191011_1356.py new file mode 100644 index 00000000..c2eb2e4e --- /dev/null +++ b/apps/establishment/migrations/0036_auto_20191011_1356.py @@ -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'), + ), + ] diff --git a/apps/establishment/migrations/0037_auto_20191015_1404.py b/apps/establishment/migrations/0037_auto_20191015_1404.py new file mode 100644 index 00000000..971970e2 --- /dev/null +++ b/apps/establishment/migrations/0037_auto_20191015_1404.py @@ -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', + ), + ] diff --git a/apps/establishment/migrations/0038_establishmenttype_index_name.py b/apps/establishment/migrations/0038_establishmenttype_index_name.py new file mode 100644 index 00000000..5f9d5879 --- /dev/null +++ b/apps/establishment/migrations/0038_establishmenttype_index_name.py @@ -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'), + ), + ] diff --git a/apps/establishment/migrations/0039_establishmentsubtype_index_name.py b/apps/establishment/migrations/0039_establishmentsubtype_index_name.py new file mode 100644 index 00000000..a29b1ae0 --- /dev/null +++ b/apps/establishment/migrations/0039_establishmentsubtype_index_name.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.4 on 2019-10-18 13:47 + +from django.db import migrations, models + + +def fill_establishment_subtype(apps, schema_editor): + # We can't import the Person model directly as it may be a newer + # version than this migration expects. We use the historical version. + EstablishmentSubType = apps.get_model('establishment', 'EstablishmentSubType') + for n, et in enumerate(EstablishmentSubType.objects.all()): + et.index_name = f'Type {n}' + et.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0038_establishmenttype_index_name'), + ] + + operations = [ + migrations.AddField( + model_name='establishmentsubtype', + name='index_name', + field=models.CharField(blank=True, db_index=True, max_length=50, null=True, unique=True, default=None, verbose_name='Index name'), + ), + migrations.RunPython(fill_establishment_subtype, migrations.RunPython.noop), + migrations.AlterField( + model_name='establishmentsubtype', + name='index_name', + field=models.CharField(choices=[('winery', 'Winery'), ], db_index=True, max_length=50, + unique=True, verbose_name='Index name'), + ), + + ] diff --git a/apps/establishment/migrations/0040_employee_tags.py b/apps/establishment/migrations/0040_employee_tags.py new file mode 100644 index 00000000..9f9405f5 --- /dev/null +++ b/apps/establishment/migrations/0040_employee_tags.py @@ -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'), + ), + ] diff --git a/apps/establishment/migrations/0041_auto_20191023_0920.py b/apps/establishment/migrations/0041_auto_20191023_0920.py new file mode 100644 index 00000000..dc5b2e02 --- /dev/null +++ b/apps/establishment/migrations/0041_auto_20191023_0920.py @@ -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'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 2fc63bd5..1406e552 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -15,7 +15,7 @@ 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 from review.models import Review from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, TranslatedFieldsMixin, BaseAttributes) @@ -27,9 +27,26 @@ class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin): STR_FIELD_NAME = 'name' + # INDEX NAME CHOICES + RESTAURANT = 'restaurant' + ARTISAN = 'artisan' + PRODUCER = 'producer' + + INDEX_NAME_TYPES = ( + (RESTAURANT, _('Restaurant')), + (ARTISAN, _('Artisan')), + (PRODUCER, _('Producer')), + ) + name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), help_text='{"en-GB":"some text"}') + index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, + unique=True, db_index=True, + verbose_name=_('Index name')) use_subtypes = models.BooleanField(_('Use subtypes'), default=True) + tag_categories = models.ManyToManyField('tag.TagCategory', + related_name='establishment_types', + verbose_name=_('Tag')) class Meta: """Meta class.""" @@ -51,11 +68,24 @@ class EstablishmentSubTypeManager(models.Manager): class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin): """Establishment type model.""" + # INDEX NAME CHOICES + WINERY = 'winery' + + INDEX_NAME_TYPES = ( + (WINERY, _('Winery')), + ) + name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), help_text='{"en-GB":"some text"}') + index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, + unique=True, db_index=True, + verbose_name=_('Index name')) establishment_type = models.ForeignKey(EstablishmentType, on_delete=models.CASCADE, verbose_name=_('Type')) + tag_categories = models.ManyToManyField('tag.TagCategory', + related_name='establishment_subtypes', + verbose_name=_('Tag')) objects = EstablishmentSubTypeManager() @@ -75,11 +105,8 @@ class EstablishmentQuerySet(models.QuerySet): def with_base_related(self): """Return qs with related objects.""" - return self.select_related('address').prefetch_related( - models.Prefetch('tags', - MetaDataContent.objects.select_related( - 'metadata__category')) - ) + return self.select_related('address', 'establishment_type').\ + prefetch_related('tags') def with_extended_related(self): return self.select_related('establishment_type').\ @@ -87,6 +114,9 @@ class EstablishmentQuerySet(models.QuerySet): 'phones').\ prefetch_actual_employees() + def with_type_related(self): + return self.prefetch_related('establishment_subtypes') + def search(self, value, locale=None): """Search text in JSON fields.""" if locale is not None: @@ -234,6 +264,31 @@ class EstablishmentQuerySet(models.QuerySet): kwargs = {unit: radius} return self.filter(address__coordinates__distance_lte=(center, DistanceMeasure(**kwargs))) + def artisans(self): + """Return artisans.""" + return self.filter(establishment_type__index_name=EstablishmentType.ARTISAN) + + def producers(self): + """Return producers.""" + return self.filter(establishment_type__index_name=EstablishmentType.PRODUCER) + + def restaurants(self): + """Return restaurants.""" + return self.filter(establishment_type__index_name=EstablishmentType.RESTAURANT) + + def wineries(self): + """Return wineries.""" + return self.producers().filter( + establishment_subtypes__index_name=EstablishmentSubType.WINERY) + + def by_type(self, value): + """Return QuerySet with type by value.""" + return self.filter(establishment_type__index_name=value) + + def by_subtype(self, value): + """Return QuerySet with subtype by value.""" + return self.filter(establishment_subtypes__index_name=value) + class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): """Establishment model.""" @@ -255,6 +310,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): on_delete=models.PROTECT, verbose_name=_('type')) establishment_subtypes = models.ManyToManyField(EstablishmentSubType, + blank=True, related_name='subtype_establishment', verbose_name=_('subtype')) address = models.ForeignKey(Address, blank=True, null=True, default=None, @@ -293,11 +349,12 @@ 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')) 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') @@ -359,11 +416,6 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): def best_price_carte(self): return 200 - @property - def tags_indexing(self): - return [{'id': tag.metadata.id, - 'label': tag.metadata.label} for tag in self.tags.all()] - @property def last_published_review(self): """Return last published review""" @@ -382,6 +434,20 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)).latest( field_name='vintage_year') + @property + def country_id(self): + """ + Return Country id of establishment location + """ + return self.address.country_id + + @property + def establishment_id(self): + """ + Return establishment id of establishment location + """ + return self.id + class Position(BaseAttributes, TranslatedFieldsMixin): """Position model.""" @@ -437,7 +503,8 @@ class Employee(BaseAttributes): establishments = models.ManyToManyField(Establishment, related_name='employees', through=EstablishmentEmployee,) awards = generic.GenericRelation(to='main.Award', related_query_name='employees') - tags = generic.GenericRelation(to='main.MetaDataContent') + tags = models.ManyToManyField('tag.Tag', related_name='employees', + verbose_name=_('Tags')) class Meta: """Meta class.""" @@ -477,6 +544,7 @@ class ContactEmail(models.Model): def __str__(self): return f'{self.email}' + # # class Wine(TranslatedFieldsMixin, models.Model): # """Wine model.""" @@ -515,6 +583,10 @@ class Plate(TranslatedFieldsMixin, models.Model): menu = models.ForeignKey( 'establishment.Menu', verbose_name=_('menu'), on_delete=models.CASCADE) + @property + def establishment_id(self): + return self.menu.establishment.id + class Meta: verbose_name = _('plate') verbose_name_plural = _('plates') @@ -550,3 +622,4 @@ class SocialNetwork(models.Model): def __str__(self): return self.title + diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 8bd09e85..59725710 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -1,13 +1,12 @@ from rest_framework import serializers + from establishment import models from establishment.serializers import ( EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer, ContactPhonesSerializer, SocialNetworkRelatedSerializers, - EstablishmentTypeSerializer) - -from utils.decorators import with_base_attributes - + EstablishmentTypeBaseSerializer) from main.models import Currency +from utils.decorators import with_base_attributes class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): @@ -21,7 +20,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): emails = ContactEmailsSerializer(read_only=True, many=True, ) socials = SocialNetworkRelatedSerializers(read_only=True, many=True, ) slug = serializers.SlugField(required=True, allow_blank=False, max_length=50) - type = EstablishmentTypeSerializer(source='establishment_type', read_only=True) + type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) class Meta: model = models.Establishment @@ -55,7 +54,7 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer): phones = ContactPhonesSerializer(read_only=False, many=True, ) emails = ContactEmailsSerializer(read_only=False, many=True, ) socials = SocialNetworkRelatedSerializers(read_only=False, many=True, ) - type = EstablishmentTypeSerializer(source='establishment_type') + type = EstablishmentTypeBaseSerializer(source='establishment_type') class Meta: model = models.Establishment @@ -141,4 +140,3 @@ class EmployeeBackSerializers(serializers.ModelSerializer): 'user', 'name' ] - diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index f09c8200..389d0d1c 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -1,17 +1,19 @@ """Establishment serializers.""" from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers + from comment import models as comment_models from comment.serializers import common as comment_serializers from establishment import models from favorites.models import Favorites from location.serializers import AddressBaseSerializer -from main.models import MetaDataContent -from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer +from main.serializers import AwardSerializer, CurrencySerializer from review import models as review_models +from tag.serializers import TagBaseSerializer from timetable.serialziers import ScheduleRUDSerializer from utils import exceptions as utils_exceptions -from utils.serializers import TranslatedField, ProjectModelSerializer +from utils.serializers import ProjectModelSerializer +from utils.serializers import TranslatedField class ContactPhonesSerializer(serializers.ModelSerializer): @@ -86,30 +88,6 @@ class MenuRUDSerializers(ProjectModelSerializer): ] -class EstablishmentTypeSerializer(serializers.ModelSerializer): - """Serializer for EstablishmentType model.""" - - name_translated = serializers.CharField(allow_null=True) - - class Meta: - """Meta class.""" - - model = models.EstablishmentType - fields = ('id', 'name_translated') - - -class EstablishmentSubTypeSerializer(serializers.ModelSerializer): - """Serializer for EstablishmentSubType models.""" - - name_translated = serializers.CharField(allow_null=True) - - class Meta: - """Meta class.""" - - model = models.EstablishmentSubType - fields = ('id', 'name_translated') - - class ReviewSerializer(serializers.ModelSerializer): """Serializer for model Review.""" text_translated = serializers.CharField(read_only=True) @@ -122,6 +100,45 @@ class ReviewSerializer(serializers.ModelSerializer): ) +class EstablishmentTypeBaseSerializer(serializers.ModelSerializer): + """Serializer for EstablishmentType model.""" + name_translated = TranslatedField() + + class Meta: + """Meta class.""" + model = models.EstablishmentType + fields = [ + 'id', + 'name', + 'name_translated', + 'use_subtypes' + ] + extra_kwargs = { + 'name': {'write_only': True}, + 'use_subtypes': {'write_only': True}, + } + + +class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer): + """Serializer for EstablishmentSubType models.""" + + name_translated = TranslatedField() + + class Meta: + """Meta class.""" + model = models.EstablishmentSubType + fields = [ + 'id', + 'name', + 'name_translated', + 'establishment_type' + ] + extra_kwargs = { + 'name': {'write_only': True}, + 'establishment_type': {'write_only': True} + } + + class EstablishmentEmployeeSerializer(serializers.ModelSerializer): """Serializer for actual employees.""" @@ -144,8 +161,8 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): preview_image = serializers.URLField(source='preview_image_url') slug = serializers.SlugField(allow_blank=False, required=True, max_length=50) address = AddressBaseSerializer() - tags = MetaDataContentSerializer(many=True) in_favorites = serializers.BooleanField(allow_null=True) + tags = TagBaseSerializer(read_only=True, many=True) class Meta: """Meta class.""" @@ -171,8 +188,8 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer): description_translated = TranslatedField() image = serializers.URLField(source='image_url') - type = EstablishmentTypeSerializer(source='establishment_type', read_only=True) - subtypes = EstablishmentSubTypeSerializer(many=True, source='establishment_subtypes') + type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) + subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') awards = AwardSerializer(many=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True) phones = ContactPhonesSerializer(read_only=True, many=True) @@ -306,17 +323,3 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): }) return super().create(validated_data) - -class EstablishmentTagListSerializer(serializers.ModelSerializer): - """List establishment tag serializer.""" - id = serializers.IntegerField(source='metadata.id') - label_translated = serializers.CharField( - source='metadata.label_translated', read_only=True, allow_null=True) - - class Meta: - """Meta class.""" - model = MetaDataContent - fields = [ - 'id', - 'label_translated', - ] diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 39e28861..a1d8fcb5 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -7,10 +7,11 @@ from main.models import Currency from establishment.models import Establishment, EstablishmentType, Menu # Create your tests here. from translation.models import Language +from account.models import Role, UserRole +from location.models import Country, Address, City, Region class BaseTestCase(APITestCase): - def setUp(self): self.username = 'sedragurda' self.password = 'sedragurdaredips19' @@ -27,11 +28,44 @@ class BaseTestCase(APITestCase): self.establishment_type = EstablishmentType.objects.create(name="Test establishment type") # Create lang object - Language.objects.create( - title='English', - locale='en-GB' + self.lang = Language.objects.get( + title='Russia', + locale='ru-RU' ) + self.country_ru = Country.objects.get( + name={"en-GB": "Russian"} + ) + + self.region = Region.objects.create(name='Moscow area', code='01', + country=self.country_ru) + self.region.save() + + self.city = City.objects.create(name='Mosocow', code='01', + region=self.region, country=self.country_ru) + self.city.save() + + self.address = Address.objects.create(city=self.city, street_name_1='Krasnaya', + number=2, postal_code='010100') + self.address.save() + + self.role = Role.objects.create(role=Role.ESTABLISHMENT_MANAGER) + self.role.save() + + self.establishment = Establishment.objects.create( + name="Test establishment", + establishment_type_id=self.establishment_type.id, + is_publish=True, + slug="test", + address=self.address + ) + + self.establishment.save() + + self.user_role = UserRole.objects.create(user=self.user, role=self.role, + establishment=self.establishment) + self.user_role.save() + class EstablishmentBTests(BaseTestCase): def test_establishment_CRUD(self): @@ -43,25 +77,25 @@ class EstablishmentBTests(BaseTestCase): 'name': 'Test establishment', 'type_id': self.establishment_type.id, 'is_publish': True, - 'slug': 'test-establishment-slug', + 'slug': 'test-establishment-slug' } response = self.client.post('/api/back/establishments/', data=data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - establishment = response.json() - - response = self.client.get(f'/api/back/establishments/{establishment["id"]}/', format='json') + response = self.client.get(f'/api/back/establishments/{self.establishment.id}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { 'name': 'Test new establishment' } - response = self.client.patch(f'/api/back/establishments/{establishment["id"]}/', data=update_data) + response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/', + data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.delete(f'/api/back/establishments/{establishment["id"]}/', format='json') + response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/', + format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -96,39 +130,45 @@ class EmployeeTests(BaseTestCase): class ChildTestCase(BaseTestCase): def setUp(self): super().setUp() - self.establishment = Establishment.objects.create( - name="Test establishment", - establishment_type_id=self.establishment_type.id, - is_publish=True, - slug="test" - ) - # Test childs class EmailTests(ChildTestCase): - def test_email_CRUD(self): + def setUp(self): + super().setUp() + + def test_get(self): response = self.client.get('/api/back/establishments/emails/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_post(self): data = { 'email': "test@test.com", 'establishment': self.establishment.id } response = self.client.post('/api/back/establishments/emails/', data=data) + self.id_email = response.json()['id'] self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.get('/api/back/establishments/emails/1/', format='json') + def test_get_by_pk(self): + self.test_post() + response = self.client.get(f'/api/back/establishments/emails/{self.id_email}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_patch(self): + self.test_post() + update_data = { 'email': 'testnew@test.com' } - response = self.client.patch('/api/back/establishments/emails/1/', data=update_data) + response = self.client.patch(f'/api/back/establishments/emails/{self.id_email}/', + data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.delete('/api/back/establishments/emails/1/') + def test_email_CRUD(self): + self.test_post() + response = self.client.delete(f'/api/back/establishments/emails/{self.id_email}/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) @@ -285,7 +325,7 @@ class EstablishmentWebTagTests(BaseTestCase): def test_tag_Read(self): response = self.client.get('/api/web/establishments/tags/', format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) class EstablishmentWebSlugTests(ChildTestCase): diff --git a/apps/establishment/urls/back.py b/apps/establishment/urls/back.py index dca5fb55..6a12e792 100644 --- a/apps/establishment/urls/back.py +++ b/apps/establishment/urls/back.py @@ -26,4 +26,8 @@ urlpatterns = [ path('emails//', views.EmailRUDView.as_view(), name='emails-rud'), path('employees/', views.EmployeeListCreateView.as_view(), name='employees'), path('employees//', views.EmployeeRUDView.as_view(), name='employees-rud'), -] \ No newline at end of file + path('types/', views.EstablishmentTypeListCreateView.as_view(), name='type-list'), + path('types//', views.EstablishmentTypeRUDView.as_view(), name='type-rud'), + path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'), + path('subtypes//', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'), +] diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 1e9225d6..8d9453c1 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -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//', views.EstablishmentRetrieveView.as_view(), name='detail'), path('slug//similar/', views.EstablishmentSimilarListView.as_view(), name='similar'), path('slug//comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), diff --git a/apps/establishment/urls/web.py b/apps/establishment/urls/web.py index b732d171..b4d1942d 100644 --- a/apps/establishment/urls/web.py +++ b/apps/establishment/urls/web.py @@ -4,4 +4,4 @@ from establishment.urls.common import urlpatterns as common_urlpatterns urlpatterns = [] -urlpatterns.extend(common_urlpatterns) \ No newline at end of file +urlpatterns.extend(common_urlpatterns) diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 5cba8255..e490d576 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -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() diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 0d2249b1..1a0d5f58 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -8,9 +8,8 @@ from comment import models as comment_models from establishment import filters from establishment import models, serializers from main import methods -from main.models import MetaDataContent -from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer from utils.pagination import EstablishmentPortionPagination +from utils.permissions import IsCountryAdmin class EstablishmentMixinView: @@ -19,9 +18,10 @@ class EstablishmentMixinView: permission_classes = (permissions.AllowAny,) def get_queryset(self): - """Overrided method 'get_queryset'.""" - return models.Establishment.objects.published().with_base_related().\ - annotate_in_favorites(self.request.user) + """Overridden method 'get_queryset'.""" + return models.Establishment.objects.published() \ + .with_base_related() \ + .annotate_in_favorites(self.request.user) class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView): @@ -86,7 +86,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() @@ -178,42 +178,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() diff --git a/apps/location/migrations/0012_data_migrate.py b/apps/location/migrations/0012_data_migrate.py index 511990db..b61c43df 100644 --- a/apps/location/migrations/0012_data_migrate.py +++ b/apps/location/migrations/0012_data_migrate.py @@ -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() diff --git a/apps/location/migrations/migrate_lang.sql b/apps/location/migrations/migrate_lang.sql index 11c93573..c3b716b1 100644 --- a/apps/location/migrations/migrate_lang.sql +++ b/apps/location/migrations/migrate_lang.sql @@ -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 diff --git a/apps/location/models.py b/apps/location/models.py index 2298c28e..c12f7ff0 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -21,6 +21,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 +53,14 @@ class Region(models.Model): return self.name +class CityQuerySet(models.QuerySet): + """Extended queryset for City model.""" + + def by_country_code(self, code): + """Return establishments by country code""" + return self.filter(country__code=code) + + class City(models.Model): """Region model.""" @@ -64,6 +76,8 @@ class City(models.Model): is_island = models.BooleanField(_('is island'), default=False) + objects = CityQuerySet.as_manager() + class Meta: verbose_name_plural = _('cities') verbose_name = _('city') @@ -73,7 +87,6 @@ class City(models.Model): class Address(models.Model): - """Address model.""" city = models.ForeignKey(City, verbose_name=_('city'), on_delete=models.CASCADE) street_name_1 = models.CharField( @@ -112,6 +125,10 @@ class Address(models.Model): return {'lat': self.latitude, 'lon': self.longitude} + @property + def country_id(self): + return self.city.country_id + # todo: Make recalculate price levels @receiver(post_save, sender=Country) diff --git a/apps/location/tests.py b/apps/location/tests.py index f68ba56b..cb574036 100644 --- a/apps/location/tests.py +++ b/apps/location/tests.py @@ -5,11 +5,12 @@ from account.models import User from rest_framework import status from http.cookies import SimpleCookie -from location.models import City, Region, Country +from location.models import City, Region, Country, Language +from django.contrib.gis.geos import Point +from account.models import Role, UserRole class BaseTestCase(APITestCase): - def setUp(self): self.username = 'sedragurda' self.password = 'sedragurdaredips19' @@ -20,27 +21,57 @@ class BaseTestCase(APITestCase): # get tokens + # self.user.is_superuser = True + # self.user.save() + tokkens = User.create_jwt_tokens(self.user) self.client.cookies = SimpleCookie( {'access_token': tokkens.get('access_token'), 'refresh_token': tokkens.get('refresh_token')}) + self.lang = Language.objects.get( + title='Russia', + locale='ru-RU' + ) + + self.country_ru = Country.objects.get( + name={"en-GB": "Russian"} + ) + + self.role = Role.objects.create(role=Role.COUNTRY_ADMIN, + country=self.country_ru) + self.role.save() + + self.user_role = UserRole.objects.create(user=self.user, role=self.role) + + self.user_role.save() + class CountryTests(BaseTestCase): + def setUp(self): + super().setUp() def test_country_CRUD(self): - response = self.client.get('/api/back/location/countries/', format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - data = { - 'name': 'Test country', - 'code': 'test' + 'name': {"ru-RU": "NewCountry"}, + 'code': 'test1' } response = self.client.post('/api/back/location/countries/', data=data, format='json') response_data = response.json() self.assertEqual(response.status_code, status.HTTP_201_CREATED) + country = Country.objects.get(pk=response_data["id"]) + role = Role.objects.create(role=Role.COUNTRY_ADMIN, country=country) + role.save() + + user_role = UserRole.objects.create(user=self.user, role=role) + + user_role.save() + + response = self.client.get('/api/back/location/countries/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + response = self.client.get(f'/api/back/location/countries/{response_data["id"]}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -64,6 +95,14 @@ class RegionTests(BaseTestCase): code="test" ) + role = Role.objects.create(role=Role.COUNTRY_ADMIN, country=self.country) + role.save() + + user_role = UserRole.objects.create(user=self.user, role=role) + + user_role.save() + + def test_region_CRUD(self): response = self.client.get('/api/back/location/regions/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -108,6 +147,13 @@ class CityTests(BaseTestCase): country=self.country ) + role = Role.objects.create(role=Role.COUNTRY_ADMIN, country=self.country) + role.save() + + user_role = UserRole.objects.create(user=self.user, role=role) + + user_role.save() + def test_city_CRUD(self): response = self.client.get('/api/back/location/cities/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -142,6 +188,7 @@ class AddressTests(BaseTestCase): def setUp(self): super().setUp() + self.country = Country.objects.create( name=json.dumps({"en-GB": "Test country"}), code="test" @@ -160,6 +207,13 @@ class AddressTests(BaseTestCase): country=self.country ) + role = Role.objects.create(role=Role.COUNTRY_ADMIN, country=self.country) + role.save() + + user_role = UserRole.objects.create(user=self.user, role=role) + + user_role.save() + def test_address_CRUD(self): response = self.client.get('/api/back/location/addresses/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -167,10 +221,8 @@ class AddressTests(BaseTestCase): data = { 'city_id': self.city.id, 'number': '+79999999', - "coordinates": { - "latitude": 37.0625, - "longitude": -95.677068 - }, + "latitude": 37.0625, + "longitude": -95.677068, "geo_lon": -95.677068, "geo_lat": 37.0625 } diff --git a/apps/location/urls/mobile.py b/apps/location/urls/mobile.py index b808d5b6..879ded79 100644 --- a/apps/location/urls/mobile.py +++ b/apps/location/urls/mobile.py @@ -1,7 +1,6 @@ """Location app mobile urlconf.""" from location.urls.common import urlpatterns as common_urlpatterns - urlpatterns = [] -urlpatterns.extend(common_urlpatterns) \ No newline at end of file +urlpatterns.extend(common_urlpatterns) diff --git a/apps/location/urls/web.py b/apps/location/urls/web.py index cac89037..e86a3992 100644 --- a/apps/location/urls/web.py +++ b/apps/location/urls/web.py @@ -1,7 +1,6 @@ """Location app web urlconf.""" from location.urls.common import urlpatterns as common_urlpatterns - urlpatterns = [] -urlpatterns.extend(common_urlpatterns) \ No newline at end of file +urlpatterns.extend(common_urlpatterns) diff --git a/apps/location/views/back.py b/apps/location/views/back.py index ce6589ed..cb8246a4 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -3,50 +3,57 @@ from rest_framework import generics from location import models, serializers from location.views import common - +from utils.permissions import IsCountryAdmin # Address class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView): """Create view for model Address.""" serializer_class = serializers.AddressDetailSerializer queryset = models.Address.objects.all() + permission_classes = [IsCountryAdmin] class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView): """RUD view for model Address.""" serializer_class = serializers.AddressDetailSerializer queryset = models.Address.objects.all() + permission_classes = [IsCountryAdmin] # City class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView): """Create view for model City.""" serializer_class = serializers.CitySerializer - + permission_classes = [IsCountryAdmin] class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): """RUD view for model City.""" serializer_class = serializers.CitySerializer + permission_classes = [IsCountryAdmin] # Region class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView): """Create view for model Region""" serializer_class = serializers.RegionSerializer - + permission_classes = [IsCountryAdmin] class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView): """Retrieve view for model Region""" serializer_class = serializers.RegionSerializer + permission_classes = [IsCountryAdmin] # Country -class CountryListCreateView(common.CountryViewMixin, generics.ListCreateAPIView): +class CountryListCreateView(generics.ListCreateAPIView): """List/Create view for model Country.""" + queryset = models.Country.objects.all() serializer_class = serializers.CountryBackSerializer pagination_class = None + permission_classes = [IsCountryAdmin] - -class CountryRUDView(common.CountryViewMixin, generics.RetrieveUpdateDestroyAPIView): +class CountryRUDView(generics.RetrieveUpdateDestroyAPIView): """RUD view for model Country.""" serializer_class = serializers.CountryBackSerializer + permission_classes = [IsCountryAdmin] + queryset = models.Country.objects.all() \ No newline at end of file diff --git a/apps/location/views/common.py b/apps/location/views/common.py index 792fce91..b4a3f1cb 100644 --- a/apps/location/views/common.py +++ b/apps/location/views/common.py @@ -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 - - diff --git a/apps/main/admin.py b/apps/main/admin.py index bdbfe46e..f14a3470 100644 --- a/apps/main/admin.py +++ b/apps/main/admin.py @@ -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""" diff --git a/apps/main/methods.py b/apps/main/methods.py index 67da3480..845a99a4 100644 --- a/apps/main/methods.py +++ b/apps/main/methods.py @@ -1,8 +1,10 @@ """Main app methods.""" import logging +from typing import Tuple, Optional from django.conf import settings from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception +from geoip2.models import City from main import models @@ -39,17 +41,16 @@ def determine_country_code(ip_addr): return country_code -def determine_coordinates(ip_addr): - longitude, latitude = None, None +def determine_coordinates(ip_addr: str) -> Tuple[Optional[float], Optional[float]]: if ip_addr: try: geoip = GeoIP2() - longitude, latitude = geoip.coords(ip_addr) + return geoip.coords(ip_addr) except GeoIP2Exception as ex: - logger.info(f'GEOIP Exception: {ex}. ip: {ip_addr}') + logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}') except Exception as ex: - logger.error(f'GEOIP Base exception: {ex}') - return longitude, latitude + logger.warning(f'GEOIP Base exception: {ex}') + return None, None def determine_user_site_url(country_code): @@ -73,3 +74,12 @@ def determine_user_site_url(country_code): return site.site_url +def determine_user_city(ip_addr: str) -> Optional[City]: + try: + geoip = GeoIP2() + return geoip.city(ip_addr) + except GeoIP2Exception as ex: + logger.warning(f'GEOIP Exception: {ex}. ip: {ip_addr}') + except Exception as ex: + logger.warning(f'GEOIP Base exception: {ex}') + return None diff --git a/apps/main/migrations/0017_feature_route.py b/apps/main/migrations/0017_feature_route.py new file mode 100644 index 00000000..0b0f46bb --- /dev/null +++ b/apps/main/migrations/0017_feature_route.py @@ -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'), + ), + ] diff --git a/apps/main/migrations/0018_feature_source.py b/apps/main/migrations/0018_feature_source.py new file mode 100644 index 00000000..cf94cad2 --- /dev/null +++ b/apps/main/migrations/0018_feature_source.py @@ -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'), + ), + ] diff --git a/apps/main/migrations/0019_auto_20191022_1359.py b/apps/main/migrations/0019_auto_20191022_1359.py new file mode 100644 index 00000000..127ac55d --- /dev/null +++ b/apps/main/migrations/0019_auto_20191022_1359.py @@ -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', + ), + ] diff --git a/apps/main/migrations/0019_award_image_url.py b/apps/main/migrations/0019_award_image_url.py new file mode 100644 index 00000000..9f5a73ed --- /dev/null +++ b/apps/main/migrations/0019_award_image_url.py @@ -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'), + ), + ] diff --git a/apps/main/migrations/0020_merge_20191023_0750.py b/apps/main/migrations/0020_merge_20191023_0750.py new file mode 100644 index 00000000..eac41bfc --- /dev/null +++ b/apps/main/migrations/0020_merge_20191023_0750.py @@ -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 = [ + ] diff --git a/apps/main/migrations/0021_auto_20191023_0924.py b/apps/main/migrations/0021_auto_20191023_0924.py new file mode 100644 index 00000000..6bd6e1d6 --- /dev/null +++ b/apps/main/migrations/0021_auto_20191023_0924.py @@ -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), + ), + ] diff --git a/apps/main/models.py b/apps/main/models.py index fa6cf7d1..e5d947fd 100644 --- a/apps/main/models.py +++ b/apps/main/models.py @@ -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 + # # @@ -109,7 +114,6 @@ class SiteSettingsQuerySet(models.QuerySet): class SiteSettings(ProjectBaseMixin): - subdomain = models.CharField(max_length=255, db_index=True, unique=True, verbose_name=_('Subdomain')) country = models.OneToOneField(Country, on_delete=models.PROTECT, @@ -150,7 +154,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 +164,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 +202,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 +227,7 @@ class SiteFeature(ProjectBaseMixin): unique_together = ('site_settings', 'feature') -class Award(TranslatedFieldsMixin, models.Model): +class Award(TranslatedFieldsMixin, URLImageMixin, models.Model): """Award model.""" award_type = models.ForeignKey('main.AwardType', on_delete=models.CASCADE) title = TJSONField( @@ -230,49 +257,6 @@ class AwardType(models.Model): return self.name -class MetaDataCategory(models.Model): - """MetaData category model.""" - - country = models.ForeignKey( - 'location.Country', null=True, default=None, on_delete=models.CASCADE) - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - public = models.BooleanField() - - -class MetaData(TranslatedFieldsMixin, models.Model): - """MetaData model.""" - label = TJSONField( - _('label'), null=True, blank=True, - default=None, help_text='{"en-GB":"some text"}') - category = models.ForeignKey( - MetaDataCategory, verbose_name=_('category'), on_delete=models.CASCADE) - - class Meta: - verbose_name = _('metadata') - verbose_name_plural = _('metadata') - - def __str__(self): - label = 'None' - lang = TranslationSettings.get_solo().default_language - if self.label and lang in self.label: - label = self.label[lang] - return f'id:{self.id}-{label}' - - -class MetaDataContentQuerySet(ContentTypeQuerySetMixin): - """QuerySets for MetaDataContent model.""" - - -class MetaDataContent(models.Model): - """MetaDataContent model.""" - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey('content_type', 'object_id') - metadata = models.ForeignKey(MetaData, on_delete=models.CASCADE) - - objects = MetaDataContentQuerySet.as_manager() - - class Currency(models.Model): """Currency model.""" name = models.CharField(_('name'), max_length=50) @@ -351,19 +335,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}' diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 03ea73e6..e2523fa9 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -1,9 +1,9 @@ """Main app serializers.""" from rest_framework import serializers + from advertisement.serializers.web import AdvertisementSerializer from location.serializers import CountrySerializer from main import models -from establishment.models import Establishment from utils.serializers import TranslatedField @@ -25,6 +25,8 @@ class SiteFeatureSerializer(serializers.ModelSerializer): id = serializers.IntegerField(source='feature.id') slug = serializers.CharField(source='feature.slug') priority = serializers.IntegerField(source='feature.priority') + route = serializers.CharField(source='feature.route.page_name') + source = serializers.IntegerField(source='feature.source') class Meta: """Meta class.""" @@ -32,7 +34,9 @@ class SiteFeatureSerializer(serializers.ModelSerializer): fields = ('main', 'id', 'slug', - 'priority' + 'priority', + 'route', + 'source' ) @@ -98,6 +102,7 @@ class AwardBaseSerializer(serializers.ModelSerializer): 'id', 'title_translated', 'vintage_year', + 'image_url', ] @@ -109,19 +114,6 @@ class AwardSerializer(AwardBaseSerializer): fields = AwardBaseSerializer.Meta.fields + ['award_type', ] -class MetaDataContentSerializer(serializers.ModelSerializer): - """MetaData content serializer.""" - - id = serializers.IntegerField(source='metadata.id', read_only=True) - label_translated = TranslatedField(source='metadata.label_translated') - - class Meta: - """Meta class.""" - - model = models.MetaDataContent - fields = ('id', 'label_translated') - - class CurrencySerializer(serializers.ModelSerializer): """Currency serializer""" diff --git a/apps/main/urls.py b/apps/main/urls.py deleted file mode 100644 index a74c0b49..00000000 --- a/apps/main/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Main app urls.""" -from django.urls import path -from main import views - -app = 'main' - -urlpatterns = [ - path('determine-site/', views.DetermineSiteView.as_view(), name='determine-site'), - path('sites/', views.SiteListView.as_view(), name='site-list'), - path('site-settings//', views.SiteSettingsView.as_view(), name='site-settings'), - path('awards/', views.AwardView.as_view(), name='awards_list'), - path('awards//', views.AwardRetrieveView.as_view(), name='awards_retrieve'), - path('carousel/', views.CarouselListView.as_view(), name='carousel-list'), -] \ No newline at end of file diff --git a/apps/products/__init__.py b/apps/main/urls/__init__.py similarity index 100% rename from apps/products/__init__.py rename to apps/main/urls/__init__.py diff --git a/apps/main/urls/common.py b/apps/main/urls/common.py new file mode 100644 index 00000000..bac16add --- /dev/null +++ b/apps/main/urls/common.py @@ -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//', AwardRetrieveView.as_view(), name='awards_retrieve'), + path('carousel/', CarouselListView.as_view(), name='carousel-list'), +path('determine-location/', DetermineLocation.as_view(), name='determine-location') +] diff --git a/apps/main/urls/mobile.py b/apps/main/urls/mobile.py new file mode 100644 index 00000000..b0383d4e --- /dev/null +++ b/apps/main/urls/mobile.py @@ -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) diff --git a/apps/main/urls/web.py b/apps/main/urls/web.py new file mode 100644 index 00000000..2126b0c0 --- /dev/null +++ b/apps/main/urls/web.py @@ -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//', SiteSettingsView.as_view(), name='site-settings'), ] + +urlpatterns.extend(common_urlpatterns) diff --git a/apps/products/migrations/__init__.py b/apps/main/views/__init__.py similarity index 100% rename from apps/products/migrations/__init__.py rename to apps/main/views/__init__.py diff --git a/apps/main/views.py b/apps/main/views/common.py similarity index 73% rename from apps/main/views.py rename to apps/main/views/common.py index d7d1fa2c..e6fd3444 100644 --- a/apps/main/views.py +++ b/apps/main/views/common.py @@ -1,39 +1,11 @@ """Main app views.""" +from django.http import Http404 from rest_framework import generics, permissions from rest_framework.response import Response + from main import methods, models, serializers -from utils.serializers import EmptySerializer -class DetermineSiteView(generics.GenericAPIView): - """Determine user's site.""" - - permission_classes = (permissions.AllowAny,) - serializer_class = EmptySerializer - - def get(self, request, *args, **kwargs): - user_ip = methods.get_user_ip(request) - country_code = methods.determine_country_code(user_ip) - url = methods.determine_user_site_url(country_code) - return Response(data={'url': url}) - - -class SiteSettingsView(generics.RetrieveAPIView): - """Site settings View.""" - - lookup_field = 'subdomain' - permission_classes = (permissions.AllowAny,) - queryset = models.SiteSettings.objects.all() - serializer_class = serializers.SiteSettingsSerializer - - -class SiteListView(generics.ListAPIView): - """Site settings View.""" - - pagination_class = None - permission_classes = (permissions.AllowAny,) - queryset = models.SiteSettings.objects.with_country() - serializer_class = serializers.SiteSerializer # # class FeatureViewMixin: # """Feature view mixin.""" @@ -70,13 +42,14 @@ class SiteListView(generics.ListAPIView): # class SiteFeaturesRUDView(SiteFeaturesViewMixin, # generics.RetrieveUpdateDestroyAPIView): # """Site features RUD.""" +from utils.serializers import EmptySerializer class AwardView(generics.ListAPIView): """Awards list view.""" serializer_class = serializers.AwardSerializer queryset = models.Award.objects.all() - permission_classes = (permissions.AllowAny, ) + permission_classes = (permissions.AllowAny,) class AwardRetrieveView(generics.RetrieveAPIView): @@ -90,5 +63,22 @@ class CarouselListView(generics.ListAPIView): """Return list of carousel items.""" queryset = models.Carousel.objects.all() serializer_class = serializers.CarouselListSerializer - permission_classes = (permissions.AllowAny, ) + permission_classes = (permissions.AllowAny,) pagination_class = None + + +class DetermineLocation(generics.GenericAPIView): + """Determine user's location.""" + + permission_classes = (permissions.AllowAny,) + serializer_class = EmptySerializer + + def get(self, request, *args, **kwargs): + user_ip = methods.get_user_ip(request) + longitude, latitude = methods.determine_coordinates(user_ip) + city = methods.determine_user_city(user_ip) + if longitude and latitude and city: + return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city}) + else: + raise Http404 + diff --git a/apps/main/views/mobile.py b/apps/main/views/mobile.py new file mode 100644 index 00000000..b992dbb8 --- /dev/null +++ b/apps/main/views/mobile.py @@ -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]) diff --git a/apps/main/views/web.py b/apps/main/views/web.py new file mode 100644 index 00000000..e1dc32ef --- /dev/null +++ b/apps/main/views/web.py @@ -0,0 +1,38 @@ +from typing import Iterable + +from rest_framework import generics, permissions + +from utils.serializers import EmptySerializer +from rest_framework.response import Response +from main import methods, models, serializers + + +class DetermineSiteView(generics.GenericAPIView): + """Determine user's site.""" + + permission_classes = (permissions.AllowAny,) + serializer_class = EmptySerializer + + def get(self, request, *args, **kwargs): + user_ip = methods.get_user_ip(request) + country_code = methods.determine_country_code(user_ip) + url = methods.determine_user_site_url(country_code) + return Response(data={'url': url}) + + +class SiteSettingsView(generics.RetrieveAPIView): + """Site settings View.""" + + lookup_field = 'subdomain' + permission_classes = (permissions.AllowAny,) + queryset = models.SiteSettings.objects.all() + serializer_class = serializers.SiteSettingsSerializer + + +class SiteListView(generics.ListAPIView): + """Site settings View.""" + + pagination_class = None + permission_classes = (permissions.AllowAny,) + queryset = models.SiteSettings.objects.with_country() + serializer_class = serializers.SiteSerializer diff --git a/apps/news/admin.py b/apps/news/admin.py index 77ea8388..5d7f79f0 100644 --- a/apps/news/admin.py +++ b/apps/news/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin - from news import models from .tasks import send_email_with_news + @admin.register(models.NewsType) class NewsTypeAdmin(admin.ModelAdmin): """News type admin.""" diff --git a/apps/news/migrations/0021_auto_20191009_1408.py b/apps/news/migrations/0021_auto_20191009_1408.py new file mode 100644 index 00000000..81a4d7fa --- /dev/null +++ b/apps/news/migrations/0021_auto_20191009_1408.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.4 on 2019-10-09 14:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0002_auto_20191009_1408'), + ('news', '0020_remove_news_author'), + ] + + operations = [ + migrations.AddField( + model_name='news', + name='tags', + field=models.ManyToManyField(related_name='news', to='tag.Tag', verbose_name='Tags'), + ), + migrations.AddField( + model_name='newstype', + name='tag_categories', + field=models.ManyToManyField(related_name='news_types', to='tag.TagCategory'), + ), + ] diff --git a/apps/news/migrations/0022_auto_20191021_1306.py b/apps/news/migrations/0022_auto_20191021_1306.py new file mode 100644 index 00000000..de8747f5 --- /dev/null +++ b/apps/news/migrations/0022_auto_20191021_1306.py @@ -0,0 +1,57 @@ +# Generated by Django 2.2.4 on 2019-10-21 13:06 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0012_data_migrate'), + ('news', '0021_auto_20191009_1408'), + ] + + operations = [ + migrations.CreateModel( + name='NewsBanner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('title', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='title')), + ('image_url', models.URLField(blank=True, default=None, null=True, verbose_name='Image URL path')), + ('content_url', models.URLField(blank=True, default=None, null=True, verbose_name='Content URL path')), + ], + options={ + 'abstract': False, + }, + bases=(models.Model, utils.models.TranslatedFieldsMixin), + ), + migrations.CreateModel( + name='Agenda', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')), + ('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('event_datetime', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Event datetime')), + ('content', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='content')), + ('address', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Address', verbose_name='address')), + ], + options={ + 'abstract': False, + }, + bases=(models.Model, utils.models.TranslatedFieldsMixin), + ), + migrations.AddField( + model_name='news', + name='agenda', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='news.Agenda', verbose_name='agenda'), + ), + migrations.AddField( + model_name='news', + name='banner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='news.NewsBanner', verbose_name='banner'), + ), + ] diff --git a/apps/news/migrations/0023_auto_20191023_0903.py b/apps/news/migrations/0023_auto_20191023_0903.py new file mode 100644 index 00000000..e1380b30 --- /dev/null +++ b/apps/news/migrations/0023_auto_20191023_0903.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-10-23 09:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0022_auto_20191021_1306'), + ] + + operations = [ + migrations.AlterField( + model_name='news', + name='slug', + field=models.SlugField(max_length=255, unique=True, verbose_name='News slug'), + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index 6e91b912..9e2a2926 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -4,7 +4,7 @@ from django.contrib.contenttypes import fields as generic from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework.reverse import reverse -from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin +from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin, ProjectBaseMixin from rating.models import Rating @@ -12,6 +12,8 @@ class NewsType(models.Model): """NewsType model.""" name = models.CharField(_('name'), max_length=250) + tag_categories = models.ManyToManyField('tag.TagCategory', + related_name='news_types') class Meta: """Meta class.""" @@ -36,7 +38,7 @@ class NewsQuerySet(models.QuerySet): def with_extended_related(self): """Return qs with related objects.""" - return self.select_related('created_by') + return self.select_related('created_by', 'agenda', 'banner') def by_type(self, news_type): """Filter News by type""" @@ -59,15 +61,39 @@ class NewsQuerySet(models.QuerySet): # todo: filter by best score # todo: filter by country? def should_read(self, news): - return self.model.objects.exclude(pk=news.pk).published().\ + return self.model.objects.exclude(pk=news.pk).published(). \ with_base_related().by_type(news.news_type).distinct().order_by('?') def same_theme(self, news): - return self.model.objects.exclude(pk=news.pk).published().\ - with_base_related().by_type(news.news_type).\ + return self.model.objects.exclude(pk=news.pk).published(). \ + with_base_related().by_type(news.news_type). \ by_tags(news.tags.all()).distinct().order_by('-start') +class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): + """News agenda model""" + + event_datetime = models.DateTimeField(default=timezone.now, editable=False, + verbose_name=_('Event datetime')) + address = models.ForeignKey('location.Address', blank=True, null=True, + default=None, verbose_name=_('address'), + on_delete=models.SET_NULL) + content = TJSONField(blank=True, null=True, default=None, + verbose_name=_('content'), + help_text='{"en-GB":"some text"}') + + +class NewsBanner(ProjectBaseMixin, TranslatedFieldsMixin): + """News banner model""" + title = TJSONField(blank=True, null=True, default=None, + verbose_name=_('title'), + help_text='{"en-GB":"some text"}') + image_url = models.URLField(verbose_name=_('Image URL path'), + blank=True, null=True, default=None) + content_url = models.URLField(verbose_name=_('Content URL path'), + blank=True, null=True, default=None) + + class News(BaseAttributes, TranslatedFieldsMixin): """News model.""" @@ -113,15 +139,13 @@ class News(BaseAttributes, TranslatedFieldsMixin): start = models.DateTimeField(verbose_name=_('Start')) end = models.DateTimeField(blank=True, null=True, default=None, verbose_name=_('End')) - slug = models.SlugField(unique=True, max_length=50, + slug = models.SlugField(unique=True, max_length=255, verbose_name=_('News slug')) playlist = models.IntegerField(_('playlist')) state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, verbose_name=_('State')) is_highlighted = models.BooleanField(default=False, verbose_name=_('Is highlighted')) - # TODO: metadata_keys - описание ключей для динамического построения полей метаданных - # TODO: metadata_values - Описание значений для динамических полей из MetadataKeys image_url = models.URLField(blank=True, null=True, default=None, verbose_name=_('Image URL path')) preview_image_url = models.URLField(blank=True, null=True, default=None, @@ -133,10 +157,18 @@ class News(BaseAttributes, TranslatedFieldsMixin): country = models.ForeignKey('location.Country', blank=True, null=True, on_delete=models.SET_NULL, verbose_name=_('country')) - tags = generic.GenericRelation(to='main.MetaDataContent') - + tags = models.ManyToManyField('tag.Tag', related_name='news', + verbose_name=_('Tags')) ratings = generic.GenericRelation(Rating) + agenda = models.ForeignKey('news.Agenda', blank=True, null=True, + on_delete=models.SET_NULL, + verbose_name=_('agenda')) + + banner = models.ForeignKey('news.NewsBanner', blank=True, null=True, + on_delete=models.SET_NULL, + verbose_name=_('banner')) + objects = NewsQuerySet.as_manager() class Meta: @@ -163,4 +195,3 @@ class News(BaseAttributes, TranslatedFieldsMixin): @property def same_theme(self): return self.__class__.objects.same_theme(self)[:3] - diff --git a/apps/news/serializers.py b/apps/news/serializers.py index c473be1d..10662880 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -2,12 +2,46 @@ from rest_framework import serializers from account.serializers.common import UserBaseSerializer from location import models as location_models -from location.serializers import CountrySimpleSerializer -from main.serializers import MetaDataContentSerializer +from location.serializers import CountrySimpleSerializer, AddressBaseSerializer from news import models +from tag.serializers import TagBaseSerializer from utils.serializers import TranslatedField, ProjectModelSerializer +class AgendaSerializer(ProjectModelSerializer): + event_datetime = serializers.DateTimeField() + address = AddressBaseSerializer() + content_translated = TranslatedField() + + class Meta: + """Meta class.""" + + model = models.Agenda + fields = ( + 'id', + 'event_datetime', + 'address', + 'content_translated' + ) + + +class NewsBannerSerializer(ProjectModelSerializer): + title_translated = TranslatedField() + image_url = serializers.URLField() + content_url = serializers.URLField() + + class Meta: + """Meta class.""" + + model = models.NewsBanner + fields = ( + 'id', + 'title_translated', + 'image_url', + 'content_url' + ) + + class NewsTypeSerializer(serializers.ModelSerializer): """News type serializer.""" @@ -22,12 +56,12 @@ class NewsBaseSerializer(ProjectModelSerializer): """Base serializer for News model.""" # read only fields - title_translated = TranslatedField() + title_translated = TranslatedField(source='title') subtitle_translated = TranslatedField() # related fields news_type = NewsTypeSerializer(read_only=True) - tags = MetaDataContentSerializer(read_only=True, many=True) + tags = TagBaseSerializer(read_only=True, many=True) class Meta: """Meta class.""" @@ -76,6 +110,8 @@ class NewsDetailWebSerializer(NewsDetailSerializer): same_theme = NewsBaseSerializer(many=True, read_only=True) should_read = NewsBaseSerializer(many=True, read_only=True) + agenda = AgendaSerializer() + banner = NewsBannerSerializer() class Meta(NewsDetailSerializer.Meta): """Meta class.""" @@ -83,6 +119,8 @@ class NewsDetailWebSerializer(NewsDetailSerializer): fields = NewsDetailSerializer.Meta.fields + ( 'same_theme', 'should_read', + 'agenda', + 'banner' ) @@ -116,10 +154,9 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, fields = NewsBackOfficeBaseSerializer.Meta.fields + \ NewsDetailSerializer.Meta.fields + ( - 'description', - 'news_type_id', - 'country_id', - 'template', - 'template_display', + 'description', + 'news_type_id', + 'country_id', + 'template', + 'template_display', ) - diff --git a/apps/news/tests.py b/apps/news/tests.py index 2e24ac45..b4e2b296 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -1,3 +1,4 @@ +from django.urls import reverse from http.cookies import SimpleCookie from rest_framework.test import APITestCase @@ -5,8 +6,9 @@ from rest_framework import status from datetime import datetime, timedelta from news.models import NewsType, News -from account.models import User - +from account.models import User, Role, UserRole +from translation.models import Language +from location.models import Country # Create your tests here. @@ -22,23 +24,51 @@ class BaseTestCase(APITestCase): self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'), 'refresh_token': tokkens.get('refresh_token')}) self.test_news_type = NewsType.objects.create(name="Test news type") - self.test_news = News.objects.create(created_by=self.user, modified_by=self.user, title={"en-GB": "Test news"}, - news_type=self.test_news_type, description={"en-GB": "Description test news"}, + + self.lang = Language.objects.get( + title='Russia', + locale='ru-RU' + ) + + self.country_ru = Country.objects.get( + name={"en-GB": "Russian"} + ) + + role = Role.objects.create( + role=Role.CONTENT_PAGE_MANAGER, + country=self.country_ru + ) + role.save() + + user_role = UserRole.objects.create( + user=self.user, + role=role + ) + user_role.save() + + self.test_news = News.objects.create(created_by=self.user, modified_by=self.user, + title={"en-GB": "Test news"}, + news_type=self.test_news_type, + description={"en-GB": "Description test news"}, playlist=1, start=datetime.now() + timedelta(hours=-2), end=datetime.now() + timedelta(hours=2), - state=News.PUBLISHED, slug='test-news-slug',) - + state=News.PUBLISHED, slug='test-news-slug', + country=self.country_ru) class NewsTestCase(BaseTestCase): + def setUp(self): + super().setUp() - def test_news_list(self): + def test_web_news(self): response = self.client.get("/api/web/news/") self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_news_web_detail(self): response = self.client.get(f"/api/web/news/slug/{self.test_news.slug}/") self.assertEqual(response.status_code, status.HTTP_200_OK) + response = self.client.get("/api/web/news/types/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_news_back_detail(self): response = self.client.get(f"/api/back/news/{self.test_news.id}/") self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -47,6 +77,18 @@ class NewsTestCase(BaseTestCase): response = self.client.get("/api/back/news/") self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_news_type_list(self): - response = self.client.get("/api/web/news/types/") - self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_news_back_detail_put(self): + # retrieve-update-destroy + url = reverse('back:news:retrieve-update-destroy', kwargs={'pk': self.test_news.id}) + data = { + 'id': self.test_news.id, + 'description': {"en-GB": "Description test news!"}, + 'slug': self.test_news.slug, + 'start': self.test_news.start, + 'playlist': self.test_news.playlist, + 'news_type_id':self.test_news.news_type_id, + 'country_id': self.country_ru.id + } + + response = self.client.put(url, data=data, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) \ No newline at end of file diff --git a/apps/news/views.py b/apps/news/views.py index 61a57251..78a3502b 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -1,7 +1,10 @@ """News app views.""" +from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions from news import filters, models, serializers from rating.tasks import add_rating +from utils.permissions import IsCountryAdmin, IsContentPageManager + class NewsMixinView: """News mixin.""" @@ -34,6 +37,7 @@ class NewsDetailView(NewsMixinView, generics.RetrieveAPIView): """Override get_queryset method.""" return super().get_queryset().with_extended_related() + class NewsTypeListView(generics.ListAPIView): """NewsType list view.""" @@ -57,6 +61,7 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView, serializer_class = serializers.NewsBackOfficeBaseSerializer create_serializers_class = serializers.NewsBackOfficeDetailSerializer + permission_classes = [IsCountryAdmin|IsContentPageManager] def get_serializer_class(self): """Override serializer class.""" @@ -74,6 +79,7 @@ class NewsBackOfficeRUDView(NewsBackOfficeMixinView, """Resource for detailed information about news for back-office users.""" serializer_class = serializers.NewsBackOfficeDetailSerializer + permission_classes = [IsCountryAdmin|IsContentPageManager] def get(self, request, pk, *args, **kwargs): add_rating(remote_addr=request.META.get('REMOTE_ADDR'), diff --git a/apps/products/serializers/__init__.py b/apps/product/__init__.py similarity index 100% rename from apps/products/serializers/__init__.py rename to apps/product/__init__.py diff --git a/apps/product/apps.py b/apps/product/apps.py new file mode 100644 index 00000000..7d1fc554 --- /dev/null +++ b/apps/product/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class ProductConfig(AppConfig): + name = 'product' + verbose_name = _('Product') diff --git a/apps/products/urls/__init__.py b/apps/product/migrations/__init__.py similarity index 100% rename from apps/products/urls/__init__.py rename to apps/product/migrations/__init__.py diff --git a/apps/product/models.py b/apps/product/models.py new file mode 100644 index 00000000..41f0c7c6 --- /dev/null +++ b/apps/product/models.py @@ -0,0 +1,110 @@ +"""Product app models.""" +from django.db import models +from django.contrib.postgres.fields import JSONField +from django.utils.translation import gettext_lazy as _ +from utils.models import (BaseAttributes, ProjectBaseMixin, + TranslatedFieldsMixin, TJSONField) + + +class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): + """ProductType model.""" + + name = TJSONField(blank=True, null=True, default=None, + verbose_name=_('Name'), help_text='{"en-GB":"some text"}') + index_name = models.CharField(max_length=50, unique=True, db_index=True, + verbose_name=_('Index name')) + use_subtypes = models.BooleanField(_('Use subtypes'), default=True) + + class Meta: + """Meta class.""" + + verbose_name = _('Product type') + verbose_name_plural = _('Product types') + + +class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin): + """ProductSubtype model.""" + + product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE, + related_name='subtypes', + verbose_name=_('Product type')) + name = TJSONField(blank=True, null=True, default=None, + verbose_name=_('Name'), help_text='{"en-GB":"some text"}') + index_name = models.CharField(max_length=50, unique=True, db_index=True, + verbose_name=_('Index name')) + + class Meta: + """Meta class.""" + + verbose_name = _('Product type') + verbose_name_plural = _('Product types') + + +class ProductManager(models.Manager): + """Extended manager for Product model.""" + + +class ProductQuerySet(models.QuerySet): + """Product queryset.""" + + def common(self): + return self.filter(category=self.model.COMMON) + + def online(self): + return self.filter(category=self.model.ONLINE) + + +class Product(TranslatedFieldsMixin, BaseAttributes): + """Product models.""" + + COMMON = 0 + ONLINE = 1 + + CATEGORY_CHOICES = ( + (COMMON, _('Common')), + (ONLINE, _('Online')), + ) + + category = models.PositiveIntegerField(choices=CATEGORY_CHOICES, + default=COMMON) + name = TJSONField(_('Name'), null=True, blank=True, default=None, + help_text='{"en-GB":"some text"}') + description = TJSONField(_('Description'), null=True, blank=True, + default=None, help_text='{"en-GB":"some text"}') + characteristics = JSONField(_('Characteristics')) + country = models.ForeignKey('location.Country', on_delete=models.PROTECT, + verbose_name=_('Country')) + available = models.BooleanField(_('Available'), default=True) + type = models.ForeignKey(ProductType, on_delete=models.PROTECT, + related_name='products', verbose_name=_('Type')) + subtypes = models.ManyToManyField(ProductSubType, related_name='products', + verbose_name=_('Subtypes')) + + objects = ProductManager.from_queryset(ProductQuerySet)() + + class Meta: + """Meta class.""" + + verbose_name = _('Product') + verbose_name_plural = _('Products') + + +class OnlineProductManager(ProductManager): + """Extended manger for OnlineProduct model.""" + + def get_queryset(self): + """Overrided get_queryset method.""" + return super().get_queryset().online() + + +class OnlineProduct(Product): + """Online product.""" + + objects = OnlineProductManager.from_queryset(ProductQuerySet)() + + class Meta: + """Meta class.""" + + proxy = True + verbose_name = _('Online product') + verbose_name_plural = _('Online products') diff --git a/apps/products/views/__init__.py b/apps/product/serializers/__init__.py similarity index 100% rename from apps/products/views/__init__.py rename to apps/product/serializers/__init__.py diff --git a/apps/products/serializers/common.py b/apps/product/serializers/common.py similarity index 100% rename from apps/products/serializers/common.py rename to apps/product/serializers/common.py diff --git a/apps/products/serializers/mobile.py b/apps/product/serializers/mobile.py similarity index 100% rename from apps/products/serializers/mobile.py rename to apps/product/serializers/mobile.py diff --git a/apps/products/serializers/web.py b/apps/product/serializers/web.py similarity index 100% rename from apps/products/serializers/web.py rename to apps/product/serializers/web.py diff --git a/apps/account/permissions.py b/apps/product/urls/__init__.py similarity index 100% rename from apps/account/permissions.py rename to apps/product/urls/__init__.py diff --git a/apps/products/urls/common.py b/apps/product/urls/back.py similarity index 100% rename from apps/products/urls/common.py rename to apps/product/urls/back.py diff --git a/apps/products/views/common.py b/apps/product/urls/common.py similarity index 100% rename from apps/products/views/common.py rename to apps/product/urls/common.py diff --git a/apps/products/urls/mobile.py b/apps/product/urls/mobile.py similarity index 100% rename from apps/products/urls/mobile.py rename to apps/product/urls/mobile.py diff --git a/apps/products/urls/web.py b/apps/product/urls/web.py similarity index 100% rename from apps/products/urls/web.py rename to apps/product/urls/web.py diff --git a/apps/products/views/mobile.py b/apps/product/views/__init__.py similarity index 100% rename from apps/products/views/mobile.py rename to apps/product/views/__init__.py diff --git a/apps/products/views/web.py b/apps/product/views/back.py similarity index 100% rename from apps/products/views/web.py rename to apps/product/views/back.py diff --git a/apps/product/views/common.py b/apps/product/views/common.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/product/views/mobile.py b/apps/product/views/mobile.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/product/views/web.py b/apps/product/views/web.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/products/admin.py b/apps/products/admin.py deleted file mode 100644 index 8c38f3f3..00000000 --- a/apps/products/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/apps/products/apps.py b/apps/products/apps.py deleted file mode 100644 index 17d75292..00000000 --- a/apps/products/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -class ProductsConfig(AppConfig): - """Products model.""" - name = 'products' - verbose_name = _('products') diff --git a/apps/products/models.py b/apps/products/models.py deleted file mode 100644 index 3c0e6ee8..00000000 --- a/apps/products/models.py +++ /dev/null @@ -1,63 +0,0 @@ -# from django.contrib.postgres.fields import JSONField -# from django.db import models -# from django.utils.translation import gettext_lazy as _ -# -# from utils.models import BaseAttributes -# -# -# class ProductManager(models.Manager): -# """Product manager.""" -# -# -# class ProductQuerySet(models.QuerySet): -# """Product queryset.""" -# -# -# class Product(BaseAttributes): -# """Product models.""" -# name = models.CharField(_('name'), max_length=255) -# country = models.ForeignKey('location.Country', on_delete=models.CASCADE) -# region = models.ForeignKey('location.Region', on_delete=models.CASCADE) -# # ASK: What is the "subregion" -# -# description = JSONField(_('description')) -# characteristics = JSONField(_('characteristics')) -# metadata_values = JSONField(_('metadata_values')) -# # common_relations_id -# # product_region_id -# code = models.CharField(_('code'), max_length=255) -# available = models.BooleanField(_('available')) -# -# # dealer_type -# # target_scope -# # target_type -# # rank -# # excluding_tax_unit_price -# # column_21 -# # currencies_id -# # vintage -# # producer_price -# # producer_description -# # annual_produced_quantity -# # production_method_description -# # unit_name -# # unit -# # unit_values -# # organic_source -# # certificates -# # establishments_id -# # restrictions -# # -# objects = ProductManager.from_queryset(ProductQuerySet)() -# -# class Meta: -# verbose_name = _('product') -# verbose_name_plural = _('products') -# -# -# class ProductType(models.Model): -# """ProductType model.""" -# -# class Meta: -# verbose_name_plural = _('product types') -# verbose_name = _('product type') diff --git a/apps/products/views/views.py b/apps/products/views/views.py deleted file mode 100644 index 60f00ef0..00000000 --- a/apps/products/views/views.py +++ /dev/null @@ -1 +0,0 @@ -# Create your views here. diff --git a/apps/review/migrations/0004_review_country.py b/apps/review/migrations/0004_review_country.py new file mode 100644 index 00000000..1d4173e0 --- /dev/null +++ b/apps/review/migrations/0004_review_country.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.4 on 2019-10-17 12:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0012_data_migrate'), + ('review', '0003_review_text'), + ] + + operations = [ + migrations.AddField( + model_name='review', + name='country', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='country', to='location.Country', verbose_name='Country'), + ), + ] diff --git a/apps/review/models.py b/apps/review/models.py index 9d3a39c4..4c7f3385 100644 --- a/apps/review/models.py +++ b/apps/review/models.py @@ -65,6 +65,9 @@ class Review(BaseAttributes, TranslatedFieldsMixin): validators=[MinValueValidator(1900), MaxValueValidator(2100)]) + country = models.ForeignKey('location.Country', on_delete=models.CASCADE, + related_name='country', verbose_name=_('Country'), + null=True) objects = ReviewQuerySet.as_manager() class Meta: diff --git a/apps/review/serializers/back.py b/apps/review/serializers/back.py new file mode 100644 index 00000000..3e816394 --- /dev/null +++ b/apps/review/serializers/back.py @@ -0,0 +1,18 @@ +"""Review app common serializers.""" +from review import models +from rest_framework import serializers + + +class ReviewBaseSerializer(serializers.ModelSerializer): + class Meta: + model = models.Review + fields = ('id', + 'reviewer', + 'text', + 'language', + 'status', + 'child', + 'published_at', + 'vintage', + 'country' + ) \ No newline at end of file diff --git a/apps/review/urls/back.py b/apps/review/urls/back.py new file mode 100644 index 00000000..84ca49f3 --- /dev/null +++ b/apps/review/urls/back.py @@ -0,0 +1,11 @@ +"""Back review URLs""" +from django.urls import path + +from review.views import back as views + +app_name = 'review' + +urlpatterns = [ + path('', views.ReviewLstView.as_view(), name='review-list-create'), + path('/', views.ReviewRUDView.as_view(), name='review-crud'), +] diff --git a/apps/review/views/back.py b/apps/review/views/back.py new file mode 100644 index 00000000..2b4288d2 --- /dev/null +++ b/apps/review/views/back.py @@ -0,0 +1,19 @@ +from rest_framework import generics, permissions +from review.serializers import back as serializers +from review import models +from utils.permissions import IsReviewerManager, IsRestaurantReviewer + + +class ReviewLstView(generics.ListCreateAPIView): + """Comment list create view.""" + serializer_class = serializers.ReviewBaseSerializer + queryset = models.Review.objects.all() + permission_classes = [permissions.IsAuthenticatedOrReadOnly,] + + +class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView): + """Comment RUD view.""" + serializer_class = serializers.ReviewBaseSerializer + queryset = models.Review.objects.all() + permission_classes = [IsReviewerManager|IsRestaurantReviewer] + lookup_field = 'id' diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 2d43154e..c30d4c58 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -21,24 +21,22 @@ class EstablishmentDocument(Document): properties={ 'id': fields.IntegerField(), 'name': fields.ObjectField(attr='name_indexing', - properties=OBJECT_FIELD_PROPERTIES) + properties=OBJECT_FIELD_PROPERTIES), }) establishment_subtypes = fields.ObjectField( properties={ 'id': fields.IntegerField(), 'name': fields.ObjectField(attr='name_indexing', - properties=OBJECT_FIELD_PROPERTIES) + properties={ + 'id': fields.IntegerField(), + }), }, multi=True) tags = fields.ObjectField( properties={ - 'id': fields.IntegerField(attr='metadata.id'), - 'label': fields.ObjectField(attr='metadata.label_indexing', + 'id': fields.IntegerField(attr='id'), + 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), - 'category': fields.ObjectField(attr='metadata.category', - properties={ - 'id': fields.IntegerField(), - }) }, multi=True) address = fields.ObjectField( diff --git a/apps/search_indexes/documents/news.py b/apps/search_indexes/documents/news.py index 6e0974d8..99071e53 100644 --- a/apps/search_indexes/documents/news.py +++ b/apps/search_indexes/documents/news.py @@ -26,11 +26,9 @@ class NewsDocument(Document): web_url = fields.KeywordField(attr='web_url') tags = fields.ObjectField( properties={ - 'id': fields.IntegerField(attr='metadata.id'), - 'label': fields.ObjectField(attr='metadata.label_indexing', + 'id': fields.IntegerField(attr='id'), + 'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES), - 'category': fields.ObjectField(attr='metadata.category', - properties={'id': fields.IntegerField()}) }, multi=True) diff --git a/apps/search_indexes/signals.py b/apps/search_indexes/signals.py index f7520b57..77660a2c 100644 --- a/apps/search_indexes/signals.py +++ b/apps/search_indexes/signals.py @@ -29,23 +29,23 @@ def update_document(sender, **kwargs): registry.update(establishment) if app_label == 'establishment': + # todo: remove after migration + from establishment import models as establishment_models if model_name == 'establishmenttype': - establishments = Establishment.objects.filter( - establishment_type=instance) - for establishment in establishments: - registry.update(establishment) + if isinstance(instance, establishment_models.EstablishmentType): + establishments = Establishment.objects.filter( + establishment_type=instance) + for establishment in establishments: + registry.update(establishment) if model_name == 'establishmentsubtype': - establishments = Establishment.objects.filter( - establishment_subtypes=instance) - for establishment in establishments: - registry.update(establishment) + if instance(instance, establishment_models.EstablishmentSubType): + establishments = Establishment.objects.filter( + establishment_subtypes=instance) + for establishment in establishments: + registry.update(establishment) - if app_label == 'main': - if model_name == 'metadata': - establishments = Establishment.objects.filter(tags__metadata=instance) - for establishment in establishments: - registry.update(establishment) - if model_name == 'metadatacontent': + if app_label == 'tag': + if model_name == 'tag': establishments = Establishment.objects.filter(tags=instance) for establishment in establishments: registry.update(establishment) @@ -70,12 +70,8 @@ def update_news(sender, **kwargs): for news in qs: registry.update(news) - if app_label == 'main': - if model_name == 'metadata': - qs = News.objects.filter(tags__metadata=instance) - for news in qs: - registry.update(news) - if model_name == 'metadatacontent': + if app_label == 'tag': + if model_name == 'tag': qs = News.objects.filter(tags=instance) for news in qs: registry.update(news) diff --git a/apps/tag/__init__.py b/apps/tag/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/tag/admin.py b/apps/tag/admin.py new file mode 100644 index 00000000..ea7f9394 --- /dev/null +++ b/apps/tag/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from .models import Tag, TagCategory + + +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): + """Admin model for model Tag.""" + + +@admin.register(TagCategory) +class TagCategoryAdmin(admin.ModelAdmin): + """Admin model for model TagCategory.""" diff --git a/apps/tag/apps.py b/apps/tag/apps.py new file mode 100644 index 00000000..a1cce249 --- /dev/null +++ b/apps/tag/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class TagConfig(AppConfig): + name = 'tag' + verbose_name = _('tag') diff --git a/apps/tag/filters.py b/apps/tag/filters.py new file mode 100644 index 00000000..8816f820 --- /dev/null +++ b/apps/tag/filters.py @@ -0,0 +1,42 @@ +"""Tag app filters.""" +from django_filters import rest_framework as filters +from establishment.models import EstablishmentType +from tag import models + + +class TagCategoryFilterSet(filters.FilterSet): + """TagCategory filterset.""" + + # Object type choices + NEWS = 'news' + ESTABLISHMENT = 'establishment' + + TYPE_CHOICES = ( + (NEWS, 'News'), + (ESTABLISHMENT, 'Establishment'), + ) + + type = filters.MultipleChoiceFilter(choices=TYPE_CHOICES, + method='filter_by_type') + + establishment_type = filters.ChoiceFilter( + choices=EstablishmentType.INDEX_NAME_TYPES, + method='by_establishment_type') + + class Meta: + """Meta class.""" + + model = models.TagCategory + fields = ('type', + 'establishment_type', ) + + def filter_by_type(self, queryset, name, value): + if self.NEWS in value: + queryset = queryset.for_news() + if self.ESTABLISHMENT in value: + queryset = queryset.for_establishments() + return queryset + + # todo: filter by establishment type + def by_establishment_type(self, queryset, name, value): + return queryset.by_establishment_type(value) diff --git a/apps/tag/migrations/0001_initial.py b/apps/tag/migrations/0001_initial.py new file mode 100644 index 00000000..543eb035 --- /dev/null +++ b/apps/tag/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 2.2.4 on 2019-10-09 07:15 + +from django.db import migrations, models +import django.db.models.deletion +import utils.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('location', '0010_auto_20190904_0711'), + ] + + operations = [ + migrations.CreateModel( + name='TagCategory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='label')), + ('public', models.BooleanField(default=False)), + ('country', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Country')), + ], + options={ + 'verbose_name': 'tag category', + 'verbose_name_plural': 'tag categories', + }, + bases=(utils.models.TranslatedFieldsMixin, models.Model), + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('label', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='label')), + ('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tags', to='tag.TagCategory', verbose_name='category')), + ], + options={ + 'verbose_name': 'tag', + 'verbose_name_plural': 'tags', + }, + bases=(utils.models.TranslatedFieldsMixin, models.Model), + ), + ] diff --git a/apps/tag/migrations/0002_auto_20191009_1408.py b/apps/tag/migrations/0002_auto_20191009_1408.py new file mode 100644 index 00000000..472d9596 --- /dev/null +++ b/apps/tag/migrations/0002_auto_20191009_1408.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.4 on 2019-10-09 14:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='tag', + options={'verbose_name': 'Tag', 'verbose_name_plural': 'Tags'}, + ), + migrations.AlterModelOptions( + name='tagcategory', + options={'verbose_name': 'Tag category', 'verbose_name_plural': 'Tag categories'}, + ), + migrations.AlterField( + model_name='tag', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tags', to='tag.TagCategory', verbose_name='Category'), + ), + ] diff --git a/apps/tag/migrations/0003_auto_20191018_0758.py b/apps/tag/migrations/0003_auto_20191018_0758.py new file mode 100644 index 00000000..3814d05a --- /dev/null +++ b/apps/tag/migrations/0003_auto_20191018_0758.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.4 on 2019-10-18 07:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0002_auto_20191009_1408'), + ] + + operations = [ + migrations.AlterField( + model_name='tag', + name='category', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='tag.TagCategory', verbose_name='Category'), + ), + ] diff --git a/apps/tag/migrations/0004_tag_priority.py b/apps/tag/migrations/0004_tag_priority.py new file mode 100644 index 00000000..3e7a6d7f --- /dev/null +++ b/apps/tag/migrations/0004_tag_priority.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-10-21 13:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0003_auto_20191018_0758'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='priority', + field=models.IntegerField(default=None, null=True, unique=True), + ), + ] diff --git a/apps/tag/migrations/__init__.py b/apps/tag/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/tag/models.py b/apps/tag/models.py new file mode 100644 index 00000000..44eacddc --- /dev/null +++ b/apps/tag/models.py @@ -0,0 +1,86 @@ +"""Tag app models.""" +from django.db import models +from django.utils.translation import gettext_lazy as _ +from configuration.models import TranslationSettings +from utils.models import TJSONField, TranslatedFieldsMixin + + +class Tag(TranslatedFieldsMixin, models.Model): + """Tag model.""" + + label = TJSONField(blank=True, null=True, default=None, + verbose_name=_('label'), + help_text='{"en-GB":"some text"}') + category = models.ForeignKey('TagCategory', on_delete=models.CASCADE, + null=True, related_name='tags', + verbose_name=_('Category')) + priority = models.IntegerField(unique=True, null=True, default=None) + + class Meta: + """Meta class.""" + + verbose_name = _('Tag') + verbose_name_plural = _('Tags') + + 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 TagCategoryQuerySet(models.QuerySet): + """Extended queryset for TagCategory model.""" + + def with_base_related(self): + """Select related objects.""" + return self.prefetch_related('tags') + + def with_extended_related(self): + """Select related objects.""" + return self.select_related('country') + + def for_news(self): + """Select tag categories for news.""" + return self.filter(news_types__isnull=True) + + def for_establishments(self): + """Select tag categories for establishments.""" + return self.filter(models.Q(establishment_types__isnull=False) | + models.Q(establishment_subtypes__isnull=False)) + + def by_establishment_type(self, index_name): + """Filter by establishment type index name.""" + return self.filter(establishment_types__index_name=index_name) + + def with_tags(self, switcher=True): + """Filter by existing tags.""" + return self.filter(tags__isnull=not switcher) + + +class TagCategory(TranslatedFieldsMixin, models.Model): + """Tag base category model.""" + + label = TJSONField(blank=True, null=True, default=None, + verbose_name=_('label'), + help_text='{"en-GB":"some text"}') + country = models.ForeignKey('location.Country', + on_delete=models.SET_NULL, null=True, + default=None) + public = models.BooleanField(default=False) + + objects = TagCategoryQuerySet.as_manager() + + class Meta: + """Meta class.""" + + verbose_name = _('Tag category') + verbose_name_plural = _('Tag categories') + + 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}' diff --git a/apps/tag/serializers.py b/apps/tag/serializers.py new file mode 100644 index 00000000..6ee55c84 --- /dev/null +++ b/apps/tag/serializers.py @@ -0,0 +1,167 @@ +"""Tag serializers.""" +from rest_framework import serializers +from establishment.models import (Establishment, EstablishmentType, + EstablishmentSubType) +from news.models import News, NewsType +from tag import models +from utils.exceptions import (ObjectAlreadyAdded, BindingObjectNotFound, + RemovedBindingObjectNotFound) +from utils.serializers import TranslatedField + + +class TagBaseSerializer(serializers.ModelSerializer): + """Serializer for model Tag.""" + + label_translated = TranslatedField() + + class Meta: + """Meta class.""" + + model = models.Tag + fields = ( + 'id', + 'label_translated', + ) + + +class TagBackOfficeSerializer(TagBaseSerializer): + """Serializer for Tag model for Back office users.""" + + class Meta(TagBaseSerializer.Meta): + """Meta class.""" + + fields = TagBaseSerializer.Meta.fields + ( + 'label', + 'category' + ) + + +class TagCategoryBaseSerializer(serializers.ModelSerializer): + """Serializer for model TagCategory.""" + + label_translated = TranslatedField() + tags = TagBaseSerializer(many=True, read_only=True) + + class Meta: + """Meta class.""" + + model = models.TagCategory + fields = ( + 'id', + 'label_translated', + 'tags' + ) + + +class TagCategoryBackOfficeDetailSerializer(TagCategoryBaseSerializer): + """Tag Category detail serializer for back-office users.""" + + country_translated = TranslatedField(source='country.name_translated') + + class Meta(TagCategoryBaseSerializer.Meta): + """Meta class.""" + + fields = TagCategoryBaseSerializer.Meta.fields + ( + 'label', + 'country', + 'country_translated', + ) + + +class TagBindObjectSerializer(serializers.Serializer): + """Serializer for binding tag category and objects""" + + ESTABLISHMENT = 'establishment' + NEWS = 'news' + + TYPE_CHOICES = ( + (ESTABLISHMENT, 'Establishment type'), + (NEWS, 'News type'), + ) + + type = serializers.ChoiceField(TYPE_CHOICES) + object_id = serializers.IntegerField() + + def validate(self, attrs): + view = self.context.get('view') + request = self.context.get('request') + + obj_type = attrs.get('type') + obj_id = attrs.get('object_id') + + tag = view.get_object() + attrs['tag'] = tag + + if obj_type == self.ESTABLISHMENT: + establishment = Establishment.objects.filter(pk=obj_id).first() + if not establishment: + raise BindingObjectNotFound() + if request.method == 'POST' and tag.establishments.filter( + pk=establishment.pk).exists(): + raise ObjectAlreadyAdded() + if request.method == 'DELETE' and not tag.establishments.filter( + pk=establishment.pk).exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = establishment + elif obj_type == self.NEWS: + news = News.objects.filter(pk=obj_id).first() + if not news: + raise BindingObjectNotFound() + if request.method == 'POST' and tag.news.filter(pk=news.pk).exists(): + raise ObjectAlreadyAdded() + if request.method == 'DELETE' and not tag.news.filter( + pk=news.pk).exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = news + return attrs + + +class TagCategoryBindObjectSerializer(serializers.Serializer): + """Serializer for binding tag category and objects""" + + ESTABLISHMENT_TYPE = 'establishment_type' + NEWS_TYPE = 'news_type' + + TYPE_CHOICES = ( + (ESTABLISHMENT_TYPE, 'Establishment type'), + (NEWS_TYPE, 'News type'), + ) + + type = serializers.ChoiceField(TYPE_CHOICES) + object_id = serializers.IntegerField() + + def validate(self, attrs): + view = self.context.get('view') + request = self.context.get('request') + + obj_type = attrs.get('type') + obj_id = attrs.get('object_id') + + tag_category = view.get_object() + attrs['tag_category'] = tag_category + + if obj_type == self.ESTABLISHMENT_TYPE: + establishment_type = EstablishmentType.objects.filter(pk=obj_id).\ + first() + if not establishment_type: + raise BindingObjectNotFound() + if request.method == 'POST' and tag_category.establishment_types.\ + filter(pk=establishment_type.pk).exists(): + raise ObjectAlreadyAdded() + if request.method == 'DELETE' and not tag_category.\ + establishment_types.filter(pk=establishment_type.pk).\ + exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = establishment_type + elif obj_type == self.NEWS_TYPE: + news_type = NewsType.objects.filter(pk=obj_id).first() + if not news_type: + raise BindingObjectNotFound() + if request.method == 'POST' and tag_category.news_types.\ + filter(pk=news_type.pk).exists(): + raise ObjectAlreadyAdded() + if request.method == 'DELETE' and not tag_category.news_types.\ + filter(pk=news_type.pk).exists(): + raise RemovedBindingObjectNotFound() + attrs['related_object'] = news_type + return attrs diff --git a/apps/products/tests.py b/apps/tag/tests.py similarity index 100% rename from apps/products/tests.py rename to apps/tag/tests.py diff --git a/apps/tag/urls/__init__.py b/apps/tag/urls/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/tag/urls/back.py b/apps/tag/urls/back.py new file mode 100644 index 00000000..03733297 --- /dev/null +++ b/apps/tag/urls/back.py @@ -0,0 +1,11 @@ +"""Urlconf for app tag.""" +from rest_framework.routers import SimpleRouter +from tag import views + +app_name = 'tag' + +router = SimpleRouter() +router.register(r'categories', views.TagCategoryBackOfficeViewSet) +router.register(r'', views.TagBackOfficeViewSet) + +urlpatterns = router.urls diff --git a/apps/tag/urls/web.py b/apps/tag/urls/web.py new file mode 100644 index 00000000..c99253eb --- /dev/null +++ b/apps/tag/urls/web.py @@ -0,0 +1,16 @@ +"""Tag app urlpatterns web users.""" +from rest_framework.routers import SimpleRouter +from tag import views + + +app_name = 'tag' + +router = SimpleRouter() +router.register(r'categories', views.TagCategoryViewSet) + +urlpatterns = [ + +] + +urlpatterns += router.urls + diff --git a/apps/tag/views.py b/apps/tag/views.py new file mode 100644 index 00000000..2a0ff0f5 --- /dev/null +++ b/apps/tag/views.py @@ -0,0 +1,111 @@ +"""Tag views.""" +from rest_framework import viewsets, mixins, status +from rest_framework.decorators import action +from rest_framework.response import Response +from tag import filters, models, serializers +from rest_framework import permissions + + +# User`s views & viewsets +class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + """ViewSet for TagCategory model.""" + + filterset_class = filters.TagCategoryFilterSet + pagination_class = None + permission_classes = (permissions.AllowAny, ) + queryset = models.TagCategory.objects.with_tags().with_base_related().\ + distinct() + serializer_class = serializers.TagCategoryBaseSerializer + + +# BackOffice user`s views & viewsets +class BindObjectMixin: + """Bind object mixin.""" + + def get_serializer_class(self): + if self.action == 'bind_object': + return self.bind_object_serializer_class + return self.serializer_class + + def perform_binding(self, serializer): + raise NotImplemented + + def perform_unbinding(self, serializer): + raise NotImplemented + + @action(methods=['post', 'delete'], detail=True, url_path='bind-object') + def bind_object(self, request, pk=None): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + if request.method == 'POST': + self.perform_binding(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED) + elif request.method == 'DELETE': + self.perform_unbinding(serializer) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class TagBackOfficeViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, + mixins.UpdateModelMixin, mixins.DestroyModelMixin, + BindObjectMixin, viewsets.GenericViewSet): + """List/create tag view.""" + + pagination_class = None + permission_classes = (permissions.IsAuthenticated, ) + queryset = models.Tag.objects.all() + serializer_class = serializers.TagBackOfficeSerializer + bind_object_serializer_class = serializers.TagBindObjectSerializer + + def perform_binding(self, serializer): + data = serializer.validated_data + tag = data.pop('tag') + obj_type = data.get('type') + related_object = data.get('related_object') + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT: + tag.establishments.add(related_object) + elif obj_type == self.bind_object_serializer_class.NEWS: + tag.news.add(related_object) + + def perform_unbinding(self, serializer): + data = serializer.validated_data + tag = data.pop('tag') + obj_type = data.get('type') + related_object = data.get('related_object') + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT: + tag.establishments.remove(related_object) + elif obj_type == self.bind_object_serializer_class.NEWS: + tag.news.remove(related_object) + + +class TagCategoryBackOfficeViewSet(mixins.CreateModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.RetrieveModelMixin, + BindObjectMixin, + TagCategoryViewSet): + """ViewSet for TagCategory model for BackOffice users.""" + + permission_classes = (permissions.IsAuthenticated, ) + queryset = TagCategoryViewSet.queryset.with_extended_related() + serializer_class = serializers.TagCategoryBackOfficeDetailSerializer + bind_object_serializer_class = serializers.TagCategoryBindObjectSerializer + + def perform_binding(self, serializer): + data = serializer.validated_data + tag_category = data.pop('tag_category') + obj_type = data.get('type') + related_object = data.get('related_object') + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT_TYPE: + tag_category.establishment_types.add(related_object) + elif obj_type == self.bind_object_serializer_class.NEWS_TYPE: + tag_category.news_types.add(related_object) + + def perform_unbinding(self, serializer): + data = serializer.validated_data + tag_category = data.pop('tag_category') + obj_type = data.get('type') + related_object = data.get('related_object') + if obj_type == self.bind_object_serializer_class.ESTABLISHMENT_TYPE: + tag_category.establishment_types.remove(related_object) + elif obj_type == self.bind_object_serializer_class.NEWS_TYPE: + tag_category.news_types.remove(related_object) diff --git a/apps/translation/migrations/0004_auto_20191018_0832.py b/apps/translation/migrations/0004_auto_20191018_0832.py new file mode 100644 index 00000000..d2d26a2b --- /dev/null +++ b/apps/translation/migrations/0004_auto_20191018_0832.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-10-18 08:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('translation', '0003_auto_20190901_1032'), + ] + + operations = [ + migrations.AlterField( + model_name='language', + name='locale', + field=models.CharField(max_length=10, unique=True, verbose_name='Locale identifier'), + ), + ] diff --git a/apps/translation/migrations/0005_auto_20191021_1201.py b/apps/translation/migrations/0005_auto_20191021_1201.py new file mode 100644 index 00000000..61cc3294 --- /dev/null +++ b/apps/translation/migrations/0005_auto_20191021_1201.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.4 on 2019-10-21 12:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('translation', '0004_auto_20191018_0832'), + ] + + operations = [ + migrations.AlterField( + model_name='language', + name='locale', + field=models.CharField(max_length=10, verbose_name='Locale identifier'), + ), + migrations.AlterUniqueTogether( + name='language', + unique_together={('title', 'locale')}, + ), + ] diff --git a/apps/translation/models.py b/apps/translation/models.py index 42530965..cb0729ea 100644 --- a/apps/translation/models.py +++ b/apps/translation/models.py @@ -32,6 +32,7 @@ class Language(models.Model): verbose_name = _('Language') verbose_name_plural = _('Languages') + unique_together = ('title', 'locale') def __str__(self): """String method""" diff --git a/apps/utils/authentication.py b/apps/utils/authentication.py index 044d6d75..e8375ffe 100644 --- a/apps/utils/authentication.py +++ b/apps/utils/authentication.py @@ -23,14 +23,24 @@ class GMJWTAuthentication(JWTAuthentication): """ def authenticate(self, request): - token = get_token_from_cookies(request) - if token is None: + try: + token = get_token_from_cookies(request) + # Return non-authorized user if token not in cookies + assert token + + raw_token = self.get_raw_token(token) + # Return non-authorized user if cant get raw token + assert raw_token + + validated_token = self.get_validated_token(raw_token) + user = self.get_user(validated_token) + + # Check record in DB + token_is_valid = user.access_tokens.valid() \ + .by_jti(jti=validated_token.payload.get('jti')) + assert token_is_valid.exists() + except: + # Return non-authorized user if token is invalid or raised an error when run checks. return None - - raw_token = self.get_raw_token(token) - if raw_token is None: - return None - - validated_token = self.get_validated_token(raw_token) - - return self.get_user(validated_token), None + else: + return user, None diff --git a/apps/utils/exceptions.py b/apps/utils/exceptions.py index 440f4ed4..37786ce7 100644 --- a/apps/utils/exceptions.py +++ b/apps/utils/exceptions.py @@ -1,5 +1,5 @@ from django.utils.translation import gettext_lazy as _ -from rest_framework import exceptions, status +from rest_framework import exceptions, serializers, status class ProjectBaseException(exceptions.APIException): @@ -142,3 +142,24 @@ class PasswordResetRequestExistedError(exceptions.APIException): """ status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Password reset request is already exists and valid.') + + +class ObjectAlreadyAdded(serializers.ValidationError): + """ + The exception must be thrown if the object has already been added to the + list. + """ + + default_detail = _('Object has already been added.') + + +class BindingObjectNotFound(serializers.ValidationError): + """The exception must be thrown if the object not found.""" + + default_detail = _('Binding object not found.') + + +class RemovedBindingObjectNotFound(serializers.ValidationError): + """The exception must be thrown if the object not found.""" + + default_detail = _('Removed binding object not found.') diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 09b24ecd..45d978a0 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -1,12 +1,15 @@ """Project custom permissions""" -from rest_framework.permissions import BasePermission +from django.contrib.contenttypes.models import ContentType + +from rest_framework import permissions from rest_framework_simplejwt.tokens import AccessToken +from account.models import UserRole, Role from authorization.models import JWTRefreshToken from utils.tokens import GMRefreshToken -class IsAuthenticatedAndTokenIsValid(BasePermission): +class IsAuthenticatedAndTokenIsValid(permissions.BasePermission): """ Check if user has a valid token and authenticated """ @@ -24,7 +27,7 @@ class IsAuthenticatedAndTokenIsValid(BasePermission): return False -class IsRefreshTokenValid(BasePermission): +class IsRefreshTokenValid(permissions.BasePermission): """ Check if user has a valid refresh token and authenticated """ @@ -38,3 +41,158 @@ class IsRefreshTokenValid(BasePermission): return refresh_token_qs.exists() else: return False + + 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 + return False + + +class IsGuest(permissions.IsAuthenticatedOrReadOnly): + """ + Object-level permission to only allow owners of an object to edit it. + """ + def has_permission(self, request, view): + return request.user.is_authenticated + + def has_object_permission(self, request, view, obj): + + rules = [ + request.user.is_superuser, + request.method in permissions.SAFE_METHODS + ] + return any(rules) + + +class IsStandardUser(IsGuest): + """ + 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 + rules = [ + super().has_object_permission(request, view, obj) + ] + + if hasattr(obj, 'user'): + rules = [ + obj.user == request.user and obj.user.email_confirmed, + super().has_object_permission(request, view, obj) + ] + + return any(rules) + + +class IsContentPageManager(IsStandardUser): + """ + 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. + + role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, + country_id=obj.country_id)\ + .first() # 'Comments moderator' + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + # and obj.user != request.user, + super().has_object_permission(request, view, obj) + ] + return any(rules) + + +class IsCountryAdmin(IsStandardUser): + """ + 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. + role = Role.objects.filter(role=Role.COUNTRY_ADMIN, + country_id=obj.country_id) \ + .first() # 'Comments moderator' + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_object_permission(request, view, obj), + ] + + return any(rules) + + +class IsCommentModerator(IsStandardUser): + """ + 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. + role = Role.objects.filter(role=Role.COMMENTS_MODERATOR, + country_id=obj.country_id)\ + .first() # 'Comments moderator' + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists() and + obj.user != request.user, + super().has_object_permission(request, view, obj) + ] + return any(rules) + + +class IsEstablishmentManager(IsStandardUser): + + def has_object_permission(self, request, view, obj): + role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER)\ + .first() # 'Comments moderator' + + rules = [ + UserRole.objects.filter(user=request.user, role=role, + establishment_id=obj.establishment_id + ).exists(), + super().has_object_permission(request, view, obj) + ] + + return any(rules) + + +class IsReviewerManager(IsStandardUser): + + def has_object_permission(self, request, view, obj): + + role = Role.objects.filter(role=Role.REVIEWER_MANGER, + country_id=obj.country_id)\ + .first() + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_object_permission(request, view, obj) + ] + + return any(rules) + + +class IsRestaurantReviewer(IsStandardUser): + + def has_object_permission(self, request, view, obj): + + content_type = ContentType.objects.get(app_lable='establishment', + model='establishment') + + role = Role.objects.filter(role=Role.RESTAURANT_REVIEWER, + country=obj.country_id).first() + + rules = [ + obj.content_type_id == content_type.id and + UserRole.objects.filter(user=request.user, role=role, + establishment_id=obj.object_id + ).exists(), + super().has_object_permission(request, view, obj) + ] + + return any(rules) diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index 2b2282d1..90efea00 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -33,8 +33,8 @@ def validate_tjson(value): code='invalid_json', params={'value': value}, ) - lang_count = Language.objects.filter(locale__in=value.keys()).count() - if lang_count != len(value.keys()): + is_lang = Language.objects.filter(locale__in=value.keys()).exists() + if not is_lang: raise exceptions.ValidationError( 'invalid_translated_keys', code='invalid_translated_keys', diff --git a/apps/utils/tests/__init__.py b/apps/utils/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/utils/tests/tests_json_field.py b/apps/utils/tests/tests_json_field.py new file mode 100644 index 00000000..c0207def --- /dev/null +++ b/apps/utils/tests/tests_json_field.py @@ -0,0 +1,37 @@ +from django.test import TestCase +from translation.models import Language +from django.core import exceptions +from utils.serializers import validate_tjson + + +class ValidJSONTest(TestCase): + + def test_valid_json(self): + lang = Language.objects.create(title='English', locale='en-GB') + lang.save() + + data = 'str' + + with self.assertRaises(exceptions.ValidationError) as err: + validate_tjson(data) + + self.assertEqual(err.exception.code, 'invalid_json') + + data = { + "string": "value" + } + + with self.assertRaises(exceptions.ValidationError) as err: + validate_tjson(data) + + self.assertEqual(err.exception.code, 'invalid_translated_keys') + + data = { + "en-GB": "English" + } + + try: + validate_tjson(data) + self.assertTrue(True) + except exceptions.ValidationError: + self.assert_(False, "Test json translated FAILED") \ No newline at end of file diff --git a/apps/utils/tests/tests_permissions.py b/apps/utils/tests/tests_permissions.py new file mode 100644 index 00000000..edc1a5d7 --- /dev/null +++ b/apps/utils/tests/tests_permissions.py @@ -0,0 +1,18 @@ +from rest_framework.test import APITestCase +from location.models import Country +from translation.models import Language + + +class BasePermissionTests(APITestCase): + def setUp(self): + self.lang = Language.objects.get( + title='Russia', + locale='ru-RU' + ) + + self.country_ru = Country.objects.get( + name={"en-GB": "Russian"} + ) + + + diff --git a/apps/utils/tests.py b/apps/utils/tests/tests_translated.py similarity index 77% rename from apps/utils/tests.py rename to apps/utils/tests/tests_translated.py index 0eaf343d..c6a990c0 100644 --- a/apps/utils/tests.py +++ b/apps/utils/tests/tests_translated.py @@ -8,11 +8,6 @@ from http.cookies import SimpleCookie from account.models import User from news.models import News, NewsType -from django.test import TestCase -from translation.models import Language -from django.core import exceptions -from .serializers import validate_tjson - from establishment.models import Establishment, EstablishmentType, Employee @@ -42,6 +37,7 @@ class TranslateFieldTests(BaseTestCase): super().setUp() self.news_type = NewsType.objects.create(name="Test news type") + self.news_type.save() self.news_item = News.objects.create( created_by=self.user, @@ -58,9 +54,11 @@ class TranslateFieldTests(BaseTestCase): slug='test', state=News.PUBLISHED, ) + self.news_item.save() def test_model_field(self): - self.assertIsNotNone(getattr(self.news_item, "title_translated", None)) + self.assertTrue(hasattr(self.news_item, "title_translated")) + def test_read_locale(self): response = self.client.get(f"/api/web/news/slug/{self.news_item.slug}/", format='json') @@ -69,7 +67,7 @@ class TranslateFieldTests(BaseTestCase): self.assertIn("title_translated", news_data) - self.assertEqual(news_data['title_translated'], "Test news item") + self.assertIn("Test news item", news_data['title_translated']) class BaseAttributeTests(BaseTestCase): @@ -125,36 +123,3 @@ class BaseAttributeTests(BaseTestCase): employee.refresh_from_db() self.assertEqual(modify_user, employee.modified_by) self.assertEqual(self.user, employee.created_by) - - -class ValidJSONTest(TestCase): - - def test_valid_json(self): - lang = Language.objects.create(title='English', locale='en-GB') - lang.save() - - data = 'str' - - with self.assertRaises(exceptions.ValidationError) as err: - validate_tjson(data) - - self.assertEqual(err.exception.code, 'invalid_json') - - data = { - "string": "value" - } - - with self.assertRaises(exceptions.ValidationError) as err: - validate_tjson(data) - - self.assertEqual(err.exception.code, 'invalid_translated_keys') - - data = { - "en-GB": "English" - } - - try: - validate_tjson(data) - self.assertTrue(True) - except exceptions.ValidationError: - self.assert_(False, "Test json translated FAILED") \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7c4e49d2..3b446101 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: - "5436:5432" volumes: - gm-db:/var/lib/postgresql/data/ + elasticsearch: image: elasticsearch:7.3.1 volumes: @@ -27,11 +28,18 @@ services: - discovery.type=single-node - xpack.security.enabled=false - # RabbitMQ - rabbitmq: - image: rabbitmq:latest + # Redis + redis: + image: redis:2.8.23 ports: - - "5672:5672" + - "6379:6379" + + # RabbitMQ + #rabbitmq: + # image: rabbitmq:latest + # ports: + # - "5672:5672" + # Celery worker: build: . @@ -47,7 +55,9 @@ services: - .:/code links: - db - - rabbitmq +# - rabbitmq + - redis + worker_beat: build: . command: ./run_celery_beat.sh @@ -62,7 +72,8 @@ services: - .:/code links: - db - - rabbitmq +# - rabbitmq + - redis # App: G&M gm_app: build: . @@ -76,7 +87,8 @@ services: - DB_PASSWORD=postgres depends_on: - db - - rabbitmq +# - rabbitmq + - redis - worker - worker_beat - elasticsearch diff --git a/project/locale/ru/LC_MESSAGES/django.po b/project/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 00000000..8092593a --- /dev/null +++ b/project/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,3091 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-10-17 13:52+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n" +"%100>=11 && n%100<=14)? 2 : 3);\n" + +#: apps/account/admin.py:30 +msgid "Personal info" +msgstr "" + +#: apps/account/admin.py:34 +msgid "Subscription" +msgstr "" + +#: apps/account/admin.py:39 +msgid "Important dates" +msgstr "" + +#: apps/account/admin.py:40 +msgid "Permissions" +msgstr "" + +#: apps/account/admin.py:59 apps/location/models.py:18 +#: venv/lib/python3.6/site-packages/fcm_django/models.py:14 +msgid "Name" +msgstr "" + +#: apps/account/apps.py:7 +msgid "Account" +msgstr "" + +#: apps/account/forms.py:15 +msgid "The two password fields didn't match." +msgstr "" + +#: apps/account/forms.py:16 +msgid "Password already in use." +msgstr "" + +#: apps/account/forms.py:19 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_form.html:50 +msgid "New password" +msgstr "" + +#: apps/account/forms.py:25 +msgid "New password confirmation" +msgstr "" + +#: apps/account/models.py:31 apps/account/models.py:227 +msgid "Role" +msgstr "" + +#: apps/account/models.py:33 apps/location/models.py:28 apps/main/models.py:117 +msgid "Country" +msgstr "" + +#: apps/account/models.py:76 apps/news/models.py:126 apps/utils/models.py:194 +msgid "Image URL path" +msgstr "" + +#: apps/account/models.py:78 +msgid "Cropped image URL path" +msgstr "" + +#: apps/account/models.py:80 +msgid "email address" +msgstr "" + +#: apps/account/models.py:82 +msgid "unconfirmed email" +msgstr "" + +#: apps/account/models.py:83 +msgid "email status" +msgstr "" + +#: apps/account/models.py:90 +msgid "Roles" +msgstr "" + +#: apps/account/models.py:95 apps/account/models.py:226 +#: apps/comment/models.py:38 apps/establishment/models.py:435 +#: apps/favorites/models.py:23 apps/notification/models.py:79 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/object_history.html:30 +msgid "User" +msgstr "" + +#: apps/account/models.py:96 +msgid "Users" +msgstr "" + +#: apps/account/serializers/common.py:121 +msgid "Old password mismatch." +msgstr "" + +#: apps/account/serializers/common.py:124 apps/utils/exceptions.py:103 +msgid "Password is already in use" +msgstr "" + +#: apps/account/tasks.py:18 +msgid "Password resetting" +msgstr "" + +#: apps/account/tasks.py:31 apps/account/tasks.py:43 +msgid "Validate new email address" +msgstr "" + +#: apps/advertisement/apps.py:7 +msgid "advertisement" +msgstr "" + +#: apps/advertisement/models.py:15 +msgid "Ad URL" +msgstr "" + +#: apps/advertisement/models.py:16 +msgid "Block width" +msgstr "" + +#: apps/advertisement/models.py:17 +msgid "Block height" +msgstr "" + +#: apps/advertisement/models.py:18 +msgid "Block level" +msgstr "" + +#: apps/advertisement/models.py:22 apps/advertisement/models.py:23 +msgid "Advertisement" +msgstr "" + +#: apps/authorization/apps.py:8 +msgid "Authorization" +msgstr "" + +#: apps/authorization/models.py:86 +msgid "Expiration datetime" +msgstr "" + +#: apps/authorization/models.py:94 +msgid "Access token" +msgstr "" + +#: apps/authorization/models.py:95 +msgid "Access tokens" +msgstr "" + +#: apps/authorization/models.py:154 +msgid "Refresh token" +msgstr "" + +#: apps/authorization/models.py:155 +msgid "Refresh tokens" +msgstr "" + +#: apps/authorization/tasks.py:18 +msgid "Email confirmation" +msgstr "" + +#: apps/authorization/views/common.py:40 +msgid "Application is not found" +msgstr "" + +#: apps/authorization/views/common.py:50 +msgid "Not found an application with this source" +msgstr "" + +#: apps/booking/apps.py:7 apps/booking/models/models.py:66 +#: apps/booking/models/models.py:67 +msgid "Booking" +msgstr "" + +#: apps/booking/models/models.py:21 +msgid "Guestonline or Lastable" +msgstr "" + +#: apps/booking/models/models.py:22 +msgid "booking service establishment id" +msgstr "" + +#: apps/booking/models/models.py:23 +msgid "booking locale" +msgstr "" + +#: apps/booking/models/models.py:24 +msgid "external service pending booking" +msgstr "" + +#: apps/booking/models/models.py:25 +msgid "external service booking id" +msgstr "" + +#: apps/booking/models/models.py:28 +msgid "booking owner" +msgstr "" + +#: apps/collection/apps.py:7 apps/collection/models.py:80 +#: apps/collection/models.py:106 +msgid "collection" +msgstr "" + +#: apps/collection/models.py:17 apps/establishment/models.py:241 +#: apps/establishment/models.py:504 apps/location/models.py:34 +#: apps/location/models.py:55 apps/main/models.py:227 apps/main/models.py:278 +#: apps/news/models.py:14 +msgid "name" +msgstr "" + +#: apps/collection/models.py:26 +msgid "start" +msgstr "" + +#: apps/collection/models.py:27 +msgid "end" +msgstr "" + +#: apps/collection/models.py:54 +msgid "Ordinary" +msgstr "" + +#: apps/collection/models.py:55 +msgid "Pop" +msgstr "" + +#: apps/collection/models.py:60 +msgid "Collection type" +msgstr "" + +#: apps/collection/models.py:62 apps/establishment/models.py:280 +msgid "Publish status" +msgstr "" + +#: apps/collection/models.py:64 +msgid "Position on top" +msgstr "" + +#: apps/collection/models.py:66 apps/location/models.py:40 +#: apps/location/models.py:60 apps/main/models.py:226 apps/news/models.py:135 +msgid "country" +msgstr "" + +#: apps/collection/models.py:68 +msgid "collection block properties" +msgstr "" + +#: apps/collection/models.py:71 apps/establishment/models.py:245 +#: apps/establishment/models.py:507 apps/news/models.py:111 +msgid "description" +msgstr "" + +#: apps/collection/models.py:74 +msgid "Collection slug" +msgstr "" + +#: apps/collection/models.py:81 +msgid "collections" +msgstr "" + +#: apps/collection/models.py:99 +msgid "parent" +msgstr "" + +#: apps/collection/models.py:103 +msgid "advertorials" +msgstr "" + +#: apps/collection/models.py:112 +msgid "guide" +msgstr "" + +#: apps/collection/models.py:113 +msgid "guides" +msgstr "" + +#: apps/comment/apps.py:7 +msgid "comment" +msgstr "" + +#: apps/comment/apps.py:8 +msgid "comments" +msgstr "" + +#: apps/comment/models.py:32 +msgid "Comment text" +msgstr "" + +#: apps/comment/models.py:34 +msgid "Mark" +msgstr "" + +#: apps/comment/models.py:44 +msgid "Locale" +msgstr "" + +#: apps/comment/models.py:48 +msgid "Comment" +msgstr "" + +#: apps/comment/models.py:49 +msgid "Comments" +msgstr "" + +#: apps/configuration/apps.py:7 +msgid "configuration" +msgstr "" + +#: apps/configuration/models.py:9 +msgid "default language" +msgstr "" + +#: apps/establishment/admin.py:87 apps/establishment/models.py:529 +#: apps/main/models.py:248 +msgid "category" +msgstr "" + +#: apps/establishment/apps.py:8 apps/establishment/models.py:310 +#: apps/establishment/models.py:418 +msgid "Establishment" +msgstr "" + +#: apps/establishment/models.py:30 apps/establishment/models.py:54 +#: apps/establishment/models.py:391 apps/recipe/models.py:52 +msgid "Description" +msgstr "" + +#: apps/establishment/models.py:32 +msgid "Use subtypes" +msgstr "" + +#: apps/establishment/models.py:37 +msgid "Establishment type" +msgstr "" + +#: apps/establishment/models.py:38 +msgid "Establishment types" +msgstr "" + +#: apps/establishment/models.py:58 +msgid "Type" +msgstr "" + +#: apps/establishment/models.py:65 +msgid "Establishment subtype" +msgstr "" + +#: apps/establishment/models.py:66 +msgid "Establishment subtypes" +msgstr "" + +#: apps/establishment/models.py:70 +msgid "Establishment type is not use subtypes." +msgstr "" + +#: apps/establishment/models.py:242 +msgid "Transliterated name" +msgstr "" + +#: apps/establishment/models.py:249 +msgid "public mark" +msgstr "" + +#: apps/establishment/models.py:252 +msgid "toque number" +msgstr "" + +#: apps/establishment/models.py:256 +msgid "type" +msgstr "" + +#: apps/establishment/models.py:259 +msgid "subtype" +msgstr "" + +#: apps/establishment/models.py:262 apps/news/models.py:131 +msgid "address" +msgstr "" + +#: apps/establishment/models.py:265 +msgid "price level" +msgstr "" + +#: apps/establishment/models.py:267 +msgid "Web site URL" +msgstr "" + +#: apps/establishment/models.py:269 +msgid "Facebook URL" +msgstr "" + +#: apps/establishment/models.py:271 +msgid "Twitter URL" +msgstr "" + +#: apps/establishment/models.py:273 +msgid "Lafourchette URL" +msgstr "" + +#: apps/establishment/models.py:274 +msgid "guestonline id" +msgstr "" + +#: apps/establishment/models.py:276 +msgid "lastable id" +msgstr "" + +#: apps/establishment/models.py:279 +msgid "Booking URL" +msgstr "" + +#: apps/establishment/models.py:282 +msgid "Establishment schedule" +msgstr "" + +#: apps/establishment/models.py:289 +msgid "Transportation" +msgstr "" + +#: apps/establishment/models.py:293 +msgid "Collections" +msgstr "" + +#: apps/establishment/models.py:294 apps/news/models.py:128 +msgid "Preview image URL path" +msgstr "" + +#: apps/establishment/models.py:297 +msgid "Establishment slug" +msgstr "" + +#: apps/establishment/models.py:311 +msgid "Establishments" +msgstr "" + +#: apps/establishment/models.py:399 apps/establishment/models.py:425 +msgid "Position" +msgstr "" + +#: apps/establishment/models.py:400 +msgid "Positions" +msgstr "" + +#: apps/establishment/models.py:420 apps/establishment/models.py:445 +msgid "Employee" +msgstr "" + +#: apps/establishment/models.py:421 +msgid "From date" +msgstr "" + +#: apps/establishment/models.py:423 +msgid "To date" +msgstr "" + +#: apps/establishment/models.py:436 +msgid "Last name" +msgstr "" + +#: apps/establishment/models.py:446 +msgid "Employees" +msgstr "" + +#: apps/establishment/models.py:460 +msgid "contact phone" +msgstr "" + +#: apps/establishment/models.py:461 +msgid "contact phones" +msgstr "" + +#: apps/establishment/models.py:474 +msgid "contact email" +msgstr "" + +#: apps/establishment/models.py:475 +msgid "contact emails" +msgstr "" + +#: apps/establishment/models.py:510 +msgid "price" +msgstr "" + +#: apps/establishment/models.py:511 +msgid "is signature plate" +msgstr "" + +#: apps/establishment/models.py:513 apps/main/models.py:281 +msgid "currency" +msgstr "" + +#: apps/establishment/models.py:516 apps/establishment/models.py:536 +#: apps/establishment/models.py:537 +msgid "menu" +msgstr "" + +#: apps/establishment/models.py:519 +msgid "plate" +msgstr "" + +#: apps/establishment/models.py:520 +msgid "plates" +msgstr "" + +#: apps/establishment/models.py:532 apps/establishment/models.py:542 +msgid "establishment" +msgstr "" + +#: apps/establishment/models.py:544 apps/main/models.py:207 +#: apps/news/models.py:105 +msgid "title" +msgstr "" + +#: apps/establishment/models.py:545 +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2234 +msgid "URL" +msgstr "" + +#: apps/establishment/models.py:548 +msgid "social network" +msgstr "" + +#: apps/establishment/models.py:549 +msgid "social networks" +msgstr "" + +#: apps/establishment/serializers/common.py:237 +#: apps/timetable/serialziers.py:47 +msgid "Establishment not found." +msgstr "" + +#: apps/establishment/serializers/common.py:288 +msgid "Object not found." +msgstr "" + +#: apps/favorites/apps.py:7 apps/favorites/models.py:32 +#: apps/favorites/models.py:33 +msgid "Favorites" +msgstr "" + +#: apps/gallery/apps.py:7 +msgid "gallery" +msgstr "" + +#: apps/gallery/models.py:12 +msgid "Image file" +msgstr "" + +#: apps/gallery/models.py:16 apps/utils/models.py:147 apps/utils/models.py:176 +#: apps/utils/models.py:207 +#: venv/lib/python3.6/site-packages/django/db/models/fields/files.py:360 +msgid "Image" +msgstr "" + +#: apps/gallery/models.py:17 +msgid "Images" +msgstr "" + +#: apps/location/admin.py:28 apps/main/apps.py:8 apps/main/models.py:191 +msgid "Main" +msgstr "" + +#: apps/location/admin.py:31 +msgid "Location" +msgstr "" + +#: apps/location/admin.py:34 +msgid "Address detail" +msgstr "" + +#: apps/location/apps.py:7 +msgid "location" +msgstr "" + +#: apps/location/models.py:19 +msgid "Code" +msgstr "" + +#: apps/location/models.py:20 +msgid "Low price" +msgstr "" + +#: apps/location/models.py:21 +msgid "High price" +msgstr "" + +#: apps/location/models.py:22 apps/translation/models.py:34 +msgid "Languages" +msgstr "" + +#: apps/location/models.py:27 +msgid "Countries" +msgstr "" + +#: apps/location/models.py:35 apps/location/models.py:56 +msgid "code" +msgstr "" + +#: apps/location/models.py:37 apps/location/models.py:58 +msgid "parent region" +msgstr "" + +#: apps/location/models.py:45 +msgid "regions" +msgstr "" + +#: apps/location/models.py:46 +msgid "region" +msgstr "" + +#: apps/location/models.py:63 apps/location/models.py:85 +msgid "postal code" +msgstr "" + +#: apps/location/models.py:63 apps/location/models.py:86 +msgid "Ex.: 350018" +msgstr "" + +#: apps/location/models.py:65 +msgid "is island" +msgstr "" + +#: apps/location/models.py:68 +msgid "cities" +msgstr "" + +#: apps/location/models.py:69 apps/location/models.py:78 +msgid "city" +msgstr "" + +#: apps/location/models.py:80 +msgid "street name 1" +msgstr "" + +#: apps/location/models.py:82 +msgid "street name 2" +msgstr "" + +#: apps/location/models.py:83 +msgid "number" +msgstr "" + +#: apps/location/models.py:88 +msgid "Coordinates" +msgstr "" + +#: apps/location/models.py:93 apps/location/models.py:94 +msgid "Address" +msgstr "" + +#: apps/location/serializers/common.py:120 +#: apps/location/serializers/common.py:125 +msgid "Invalid value" +msgstr "" + +#: apps/main/models.py:114 +msgid "Subdomain" +msgstr "" + +#: apps/main/models.py:119 +msgid "Default site" +msgstr "" + +#: apps/main/models.py:121 +msgid "Pinterest page URL" +msgstr "" + +#: apps/main/models.py:123 +msgid "Twitter page URL" +msgstr "" + +#: apps/main/models.py:125 +msgid "Facebook page URL" +msgstr "" + +#: apps/main/models.py:127 +msgid "Instagram page URL" +msgstr "" + +#: apps/main/models.py:129 +msgid "Contact email" +msgstr "" + +#: apps/main/models.py:131 +msgid "Config" +msgstr "" + +#: apps/main/models.py:133 +msgid "AD config" +msgstr "" + +#: apps/main/models.py:140 +msgid "Site setting" +msgstr "" + +#: apps/main/models.py:141 +msgid "Site settings" +msgstr "" + +#: apps/main/models.py:171 +msgid "Feature" +msgstr "" + +#: apps/main/models.py:172 +msgid "Features" +msgstr "" + +#: apps/main/models.py:190 apps/news/models.py:98 apps/recipe/models.py:42 +msgid "Published" +msgstr "" + +#: apps/main/models.py:198 +msgid "Site feature" +msgstr "" + +#: apps/main/models.py:199 +msgid "Site features" +msgstr "" + +#: apps/main/models.py:209 +msgid "vintage year" +msgstr "" + +#: apps/main/models.py:245 +msgid "label" +msgstr "" + +#: apps/main/models.py:251 apps/main/models.py:252 +msgid "metadata" +msgstr "" + +#: apps/main/models.py:282 +msgid "currencies" +msgstr "" + +#: apps/main/models.py:302 apps/main/models.py:303 +msgid "Carousel" +msgstr "" + +#: apps/main/models.py:365 apps/translation/models.py:49 +msgid "Page" +msgstr "" + +#: apps/main/models.py:366 +msgid "Pages" +msgstr "" + +#: apps/news/apps.py:7 apps/news/models.py:145 apps/news/models.py:146 +msgid "news" +msgstr "" + +#: apps/news/models.py:19 +msgid "news types" +msgstr "" + +#: apps/news/models.py:20 apps/news/models.py:103 +msgid "news type" +msgstr "" + +#: apps/news/models.py:96 apps/recipe/models.py:40 +msgid "Waiting" +msgstr "" + +#: apps/news/models.py:97 apps/recipe/models.py:41 +msgid "Hidden" +msgstr "" + +#: apps/news/models.py:99 apps/recipe/models.py:43 +msgid "Published exclusive" +msgstr "" + +#: apps/news/models.py:108 +msgid "subtitle" +msgstr "" + +#: apps/news/models.py:113 +msgid "Start" +msgstr "" + +#: apps/news/models.py:115 +msgid "End" +msgstr "" + +#: apps/news/models.py:117 +msgid "News slug" +msgstr "" + +#: apps/news/models.py:118 +msgid "playlist" +msgstr "" + +#: apps/news/models.py:120 apps/notification/models.py:89 +#: apps/recipe/models.py:55 +msgid "State" +msgstr "" + +#: apps/news/models.py:122 +msgid "Is highlighted" +msgstr "" + +#: apps/notification/apps.py:7 +msgid "notification" +msgstr "" + +#: apps/notification/models.py:73 +msgid "Unusable" +msgstr "" + +#: apps/notification/models.py:74 +msgid "Usable" +msgstr "" + +#: apps/notification/models.py:81 +msgid "Email" +msgstr "" + +#: apps/notification/models.py:83 +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1891 +msgid "IP address" +msgstr "" + +#: apps/notification/models.py:85 +msgid "Country code" +msgstr "" + +#: apps/notification/models.py:87 apps/translation/models.py:26 +msgid "Locale identifier" +msgstr "" + +#: apps/notification/models.py:91 +msgid "Token" +msgstr "" + +#: apps/notification/models.py:98 +msgid "Subscriber" +msgstr "" + +#: apps/notification/models.py:99 +msgid "Subscribers" +msgstr "" + +#: apps/notification/serializers/common.py:29 +msgid "Does not match user email" +msgstr "" + +#: apps/notification/serializers/common.py:32 +#: venv/lib/python3.6/site-packages/django/forms/fields.py:53 +msgid "This field is required." +msgstr "" + +#: apps/partner/apps.py:7 apps/partner/models.py:12 +msgid "partner" +msgstr "" + +#: apps/partner/models.py:9 +msgid "Partner URL" +msgstr "" + +#: apps/partner/models.py:13 +msgid "partners" +msgstr "" + +#: apps/products/apps.py:8 +msgid "products" +msgstr "" + +#: apps/rating/models.py:11 +msgid "ip" +msgstr "" + +#: apps/recipe/apps.py:8 +msgid "RecipeConfig" +msgstr "" + +#: apps/recipe/models.py:48 +msgid "Title" +msgstr "" + +#: apps/recipe/models.py:50 +msgid "Subtitle" +msgstr "" + +#: apps/recipe/models.py:57 +msgid "Author" +msgstr "" + +#: apps/recipe/models.py:58 apps/recipe/models.py:60 +msgid "Published at" +msgstr "" + +#: apps/recipe/models.py:61 apps/recipe/models.py:63 +msgid "Published scheduled at" +msgstr "" + +#: apps/recipe/models.py:70 +msgid "Recipe" +msgstr "" + +#: apps/recipe/models.py:71 +msgid "Recipes" +msgstr "" + +#: apps/review/apps.py:7 +msgid "reviews" +msgstr "" + +#: apps/review/models.py:37 +msgid "To investigate" +msgstr "" + +#: apps/review/models.py:38 +msgid "To review" +msgstr "" + +#: apps/review/models.py:39 +msgid "Ready" +msgstr "" + +#: apps/review/models.py:45 +msgid "Reviewer" +msgstr "" + +#: apps/review/models.py:47 +msgid "text" +msgstr "" + +#: apps/review/models.py:55 +msgid "Review language" +msgstr "" + +#: apps/review/models.py:60 +msgid "Child review" +msgstr "" + +#: apps/review/models.py:61 +msgid "Publish datetime" +msgstr "" + +#: apps/review/models.py:63 +msgid "Review published datetime" +msgstr "" + +#: apps/review/models.py:64 +msgid "Year of review" +msgstr "" + +#: apps/review/models.py:72 +msgid "Review" +msgstr "" + +#: apps/review/models.py:73 +msgid "Reviews" +msgstr "" + +#: apps/search_indexes/apps.py:7 +msgid "Search indexes" +msgstr "" + +#: apps/timetable/apps.py:7 +msgid "timetable" +msgstr "" + +#: apps/timetable/models.py:18 +#: venv/lib/python3.6/site-packages/django/utils/dates.py:6 +msgid "Monday" +msgstr "" + +#: apps/timetable/models.py:19 +#: venv/lib/python3.6/site-packages/django/utils/dates.py:6 +msgid "Tuesday" +msgstr "" + +#: apps/timetable/models.py:20 +#: venv/lib/python3.6/site-packages/django/utils/dates.py:6 +msgid "Wednesday" +msgstr "" + +#: apps/timetable/models.py:21 +#: venv/lib/python3.6/site-packages/django/utils/dates.py:6 +msgid "Thursday" +msgstr "" + +#: apps/timetable/models.py:22 +#: venv/lib/python3.6/site-packages/django/utils/dates.py:6 +msgid "Friday" +msgstr "" + +#: apps/timetable/models.py:23 +#: venv/lib/python3.6/site-packages/django/utils/dates.py:7 +msgid "Saturday" +msgstr "" + +#: apps/timetable/models.py:24 +#: venv/lib/python3.6/site-packages/django/utils/dates.py:7 +msgid "Sunday" +msgstr "" + +#: apps/timetable/models.py:26 +msgid "Week day" +msgstr "" + +#: apps/timetable/models.py:28 +msgid "Lunch start time" +msgstr "" + +#: apps/timetable/models.py:29 +msgid "Lunch end time" +msgstr "" + +#: apps/timetable/models.py:30 +msgid "Dinner start time" +msgstr "" + +#: apps/timetable/models.py:31 +msgid "Dinner end time" +msgstr "" + +#: apps/timetable/models.py:32 +msgid "Opening time" +msgstr "" + +#: apps/timetable/models.py:33 +msgid "Closed time" +msgstr "" + +#: apps/timetable/models.py:37 +msgid "Timetable" +msgstr "" + +#: apps/timetable/models.py:38 +msgid "Timetables" +msgstr "" + +#: apps/translation/apps.py:7 +msgid "Translation" +msgstr "" + +#: apps/translation/models.py:24 +msgid "Language title" +msgstr "" + +#: apps/translation/models.py:33 +msgid "Language" +msgstr "" + +#: apps/translation/models.py:51 +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2075 +msgid "Text" +msgstr "" + +#: apps/translation/models.py:59 apps/translation/models.py:60 +msgid "Site interface dictionary" +msgstr "" + +#: apps/utils/exceptions.py:8 +msgid "Bad request" +msgstr "" + +#: apps/utils/exceptions.py:27 +msgid "Service is temporarily unavailable" +msgstr "" + +#: apps/utils/exceptions.py:32 +msgid "User not found" +msgstr "" + +#: apps/utils/exceptions.py:38 +#, python-format +msgid "Unable to send message to mailbox %s" +msgstr "" + +#: apps/utils/exceptions.py:53 +#, python-format +msgid "Locale not found in database (%s)" +msgstr "" + +#: apps/utils/exceptions.py:68 +msgid "Wrong username" +msgstr "" + +#: apps/utils/exceptions.py:76 +msgid "Not valid token" +msgstr "" + +#: apps/utils/exceptions.py:83 +msgid "Not valid access token" +msgstr "" + +#: apps/utils/exceptions.py:90 +msgid "Not valid refresh token" +msgstr "" + +#: apps/utils/exceptions.py:95 +msgid "OAuth2 Error" +msgstr "" + +#: apps/utils/exceptions.py:111 +msgid "Email address is already confirmed" +msgstr "" + +#: apps/utils/exceptions.py:119 +msgid "Image invalid input." +msgstr "" + +#: apps/utils/exceptions.py:126 +msgid "Incorrect login or password." +msgstr "Неправильный логин или пароль." + +#: apps/utils/exceptions.py:135 +msgid "Item is already in favorites." +msgstr "" + +#: apps/utils/exceptions.py:144 +msgid "Password reset request is already exists and valid." +msgstr "" + +#: apps/utils/models.py:21 +msgid "Date created" +msgstr "" + +#: apps/utils/models.py:23 +msgid "Date updated" +msgstr "" + +#: apps/utils/models.py:126 +msgid "created by" +msgstr "" + +#: apps/utils/models.py:130 +msgid "modified by" +msgstr "" + +#: apps/utils/models.py:186 +msgid "SVG image" +msgstr "" + +#: apps/utils/models.py:219 +msgid "Mobile" +msgstr "" + +#: apps/utils/models.py:220 +msgid "Web" +msgstr "" + +#: apps/utils/models.py:221 +msgid "All" +msgstr "" + +#: apps/utils/models.py:224 +msgid "Source" +msgstr "" + +#: project/templates/account/change_email.html:2 +#, python-format +msgid "" +"You're receiving this email because you want to change email address at " +"%(site_name)s." +msgstr "" + +#: project/templates/account/change_email.html:4 +msgid "Please go to the following page for confirmation new email address:" +msgstr "" + +#: project/templates/account/change_email.html:8 +#: project/templates/account/password_reset_email.html:8 +#: project/templates/authorization/confirm_email.html:7 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_email.html:10 +msgid "Thanks for using our site!" +msgstr "" + +#: project/templates/account/change_email.html:10 +#: project/templates/account/password_reset_email.html:10 +#: project/templates/authorization/confirm_email.html:9 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_email.html:12 +#, python-format +msgid "The %(site_name)s team" +msgstr "" + +#: project/templates/account/password_reset_email.html:2 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_email.html:2 +#, python-format +msgid "" +"You're receiving this email because you requested a password reset for your " +"user account at %(site_name)s." +msgstr "" + +#: project/templates/account/password_reset_email.html:4 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_email.html:4 +msgid "Please go to the following page and choose a new password:" +msgstr "" + +#: project/templates/authorization/confirm_email.html:2 +#, python-format +msgid "" +"You're receiving this email because you trying to register new account at " +"%(site_name)s." +msgstr "" + +#: project/templates/authorization/confirm_email.html:4 +msgid "Please confirm your email address to complete the registration:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/404.html:4 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/404.html:8 +msgid "Page not found" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/404.html:10 +msgid "We're sorry, but the requested page could not be found." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/500.html:6 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/app_index.html:10 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/change_password.html:19 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:163 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_form.html:22 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_list.html:32 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_confirmation.html:14 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_selected_confirmation.html:15 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/object_history.html:7 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_done.html:7 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_form.html:12 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_complete.html:7 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_confirm.html:7 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_done.html:7 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_form.html:7 +#: venv/lib/python3.6/site-packages/solo/templates/admin/solo/change_form.html:7 +#: venv/lib/python3.6/site-packages/solo/templates/admin/solo/object_history.html:6 +msgid "Home" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/500.html:7 +msgid "Server error" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/500.html:11 +msgid "Server error (500)" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/500.html:14 +msgid "Server Error (500)" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/500.html:15 +msgid "" +"There's been an error. It's been reported to the site administrators via " +"email and should be fixed shortly. Thanks for your patience." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/actions.html:7 +msgid "Run the selected action" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/actions.html:7 +msgid "Go" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/actions.html:19 +msgid "Click here to select the objects across all pages" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/actions.html:19 +#, python-format +msgid "Select all %(total_count)s %(module_name)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/actions.html:21 +msgid "Clear selection" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/add_form.html:7 +msgid "" +"First, enter a username and password. Then, you'll be able to edit more user " +"options." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/add_form.html:10 +msgid "Enter a username and password." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/change_password.html:31 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/change_password.html:75 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:143 +msgid "Change password" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/change_password.html:44 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_form.html:65 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_list.html:58 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/login.html:44 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_form.html:31 +msgid "Please correct the error below." +msgid_plural "Please correct the errors below." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/change_password.html:44 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_form.html:65 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_form.html:31 +msgid "Please correct the errors below." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/change_password.html:50 +#, python-format +msgid "Enter a new password for the user %(username)s." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/change_password.html:60 +msgid "Password" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/change_password.html:68 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_form.html:57 +msgid "Password (again)" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/auth/user/change_password.html:69 +msgid "Enter the same password as above, for verification." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:72 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:116 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:119 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base_site.html:9 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/login.html:31 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/login.html:36 +msgid "Django administration" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:81 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:52 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:58 +#, python-format +msgid "Models in the %(name)s application" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:87 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:93 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:143 +msgid "You don't have permission to edit anything." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:138 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:152 +msgid "Documentation" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:147 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:154 +msgid "Log out" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base.html:173 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_list.html:57 +msgid "Close" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/base_site.html:4 +msgid "Django site admin" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_form.html:39 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:65 +msgid "Add" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_form.html:114 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_form.html:116 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/object_history.html:18 +#: venv/lib/python3.6/site-packages/solo/templates/admin/solo/change_form.html:14 +#: venv/lib/python3.6/site-packages/solo/templates/admin/solo/object_history.html:9 +msgid "History" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_form.html:121 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_form.html:123 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/edit_inline/stacked.html:24 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/edit_inline/tabular.html:37 +#: venv/lib/python3.6/site-packages/solo/templates/admin/solo/change_form.html:15 +msgid "View on site" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_list.html:47 +msgid "Filter" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_list.html:81 +#, python-format +msgid "Add %(name)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_list.html:143 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:5 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:7 +msgid "Save" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_list_results.html:13 +msgid "Remove from sorting" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_list_results.html:16 +#, python-format +msgid "Sorting priority: %(priority_number)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/change_list_results.html:17 +msgid "Toggle sorting" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_confirmation.html:25 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:34 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:36 +#: venv/lib/python3.6/site-packages/django/forms/formsets.py:375 +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html:13 +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:38 +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/authorized-token-delete.html:7 +msgid "Delete" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_confirmation.html:34 +#, python-format +msgid "" +"Deleting the %(object_name)s '%(escaped_object)s' would result in deleting " +"related objects, but your account doesn't have permission to delete the " +"following types of objects:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_confirmation.html:48 +#, python-format +msgid "" +"Deleting the %(object_name)s '%(escaped_object)s' would require deleting the " +"following protected related objects:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_confirmation.html:62 +#, python-format +msgid "" +"Are you sure you want to delete the %(object_name)s \"%(escaped_object)s\"? " +"All of the following related items will be deleted:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_confirmation.html:70 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_confirmation.html:72 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_selected_confirmation.html:74 +msgid "Yes, I'm sure" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_selected_confirmation.html:23 +msgid "Delete multiple objects" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_selected_confirmation.html:32 +#, python-format +msgid "" +"Deleting the selected %(objects_name)s would result in deleting related " +"objects, but your account doesn't have permission to delete the following " +"types of objects:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_selected_confirmation.html:46 +#, python-format +msgid "" +"Deleting the selected %(objects_name)s would require deleting the following " +"protected related objects:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/delete_selected_confirmation.html:60 +#, python-format +msgid "" +"Are you sure you want to delete the selected %(objects_name)s? All of the " +"following objects and their related items will be deleted:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/edit_inline/tabular.html:22 +msgid "Delete?" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/filter.html:2 +#, python-format +msgid " By %(filter_title)s " +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/includes/object_delete_summary.html:2 +msgid "Summary" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:18 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:21 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:22 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:31 +msgid "Apps" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:37 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:71 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/search_form.html:11 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/search_form.html:13 +msgid "Search" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:42 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/object_history.html:31 +msgid "Action" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:67 +msgid "View" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:69 +#: venv/lib/python3.6/site-packages/django/forms/widgets.py:397 +msgid "Change" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:84 +msgid "Recent actions" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:89 +msgid "None available" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/index.html:115 +msgid "Unknown content" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/invalid_setup.html:5 +msgid "" +"Something's wrong with your database installation. Make sure the appropriate " +"database tables have been created, and make sure the database is readable by " +"the appropriate user." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/login.html:21 +#, python-format +msgid "" +"You are authenticated as %(username)s, but are not authorized to access this " +"page. Would you like to login to a different account?" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/login.html:85 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_complete.html:23 +msgid "Log in" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/login.html:96 +msgid "Forgotten your password or username?" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/object_history.html:29 +msgid "Date/time" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/object_history.html:45 +msgid "" +"This object doesn't have a change history. It probably wasn't added via this " +"admin site." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/pagination.html:19 +msgid "Show all" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/popup_response.html:3 +msgid "Popup closing..." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/related_widget_wrapper.html:9 +#, python-format +msgid "Change selected %(model)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/related_widget_wrapper.html:16 +#, python-format +msgid "Add another %(model)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/related_widget_wrapper.html:23 +#, python-format +msgid "Delete selected %(model)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/search_form.html:4 +#, python-format +msgid "%(counter)s result" +msgid_plural "%(counter)s results" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/search_form.html:4 +#, python-format +msgid "%(full_result_count)s total" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:12 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:14 +msgid "Save as new" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:19 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:21 +msgid "Save and add another" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:26 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/admin/submit_line.html:28 +msgid "Save and continue editing" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/logged_out.html:18 +msgid "Thanks for spending some quality time with the Web site today." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/logged_out.html:20 +msgid "Log in again" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_done.html:10 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_form.html:15 +msgid "Password change" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_done.html:20 +msgid "Your password was changed." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_form.html:37 +msgid "" +"Please enter your old password, for security's sake, and then enter your new " +"password twice so we can verify you typed it in correctly." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_form.html:43 +msgid "Old password" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_change_form.html:66 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_confirm.html:46 +msgid "Change my password" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_complete.html:10 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_done.html:10 +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_form.html:10 +msgid "Password reset" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_complete.html:20 +msgid "Your password has been set. You may go ahead and log in now." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_confirm.html:10 +msgid "Password reset confirmation" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_confirm.html:24 +msgid "" +"Please enter your new password twice so we can verify you typed it in " +"correctly." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_confirm.html:30 +msgid "New password:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_confirm.html:37 +msgid "Confirm password:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_confirm.html:55 +msgid "" +"The password reset link was invalid, possibly because it has already been " +"used. Please request a new password reset." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_done.html:19 +msgid "" +"We've emailed you instructions for setting your password. You should be " +"receiving them shortly." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_done.html:21 +msgid "" +"If you don't receive an email, please make sure you've entered the address " +"you registered with, and check your spam folder." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_email.html:8 +msgid "Your username, in case you've forgotten:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_form.html:23 +msgid "" +"Forgotten your password? Enter your email address below, and we'll email " +"instructions for setting a new one." +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_form.html:29 +msgid "Email address:" +msgstr "" + +#: venv/lib/python3.6/site-packages/bootstrap_admin/templates/registration/password_reset_form.html:38 +msgid "Reset my password" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/contrib/messages/apps.py:7 +msgid "Messages" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/contrib/sitemaps/apps.py:7 +msgid "Site Maps" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/contrib/staticfiles/apps.py:9 +msgid "Static Files" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/contrib/syndication/apps.py:7 +msgid "Syndication" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/paginator.py:45 +msgid "That page number is not an integer" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/paginator.py:47 +msgid "That page number is less than 1" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/paginator.py:52 +msgid "That page contains no results" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:31 +msgid "Enter a valid value." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:102 +#: venv/lib/python3.6/site-packages/django/forms/fields.py:658 +msgid "Enter a valid URL." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:154 +msgid "Enter a valid integer." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:165 +msgid "Enter a valid email address." +msgstr "" + +#. Translators: "letters" means latin letters: a-z and A-Z. +#: venv/lib/python3.6/site-packages/django/core/validators.py:239 +msgid "" +"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:246 +msgid "" +"Enter a valid 'slug' consisting of Unicode letters, numbers, underscores, or " +"hyphens." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:255 +#: venv/lib/python3.6/site-packages/django/core/validators.py:275 +msgid "Enter a valid IPv4 address." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:260 +#: venv/lib/python3.6/site-packages/django/core/validators.py:276 +msgid "Enter a valid IPv6 address." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:270 +#: venv/lib/python3.6/site-packages/django/core/validators.py:274 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:304 +msgid "Enter only digits separated by commas." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:310 +#, python-format +msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:342 +#, python-format +msgid "Ensure this value is less than or equal to %(limit_value)s." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:351 +#, python-format +msgid "Ensure this value is greater than or equal to %(limit_value)s." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:361 +#, python-format +msgid "" +"Ensure this value has at least %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at least %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:376 +#, python-format +msgid "" +"Ensure this value has at most %(limit_value)d character (it has " +"%(show_value)d)." +msgid_plural "" +"Ensure this value has at most %(limit_value)d characters (it has " +"%(show_value)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:395 +#: venv/lib/python3.6/site-packages/django/forms/fields.py:290 +#: venv/lib/python3.6/site-packages/django/forms/fields.py:325 +msgid "Enter a number." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:397 +#, python-format +msgid "Ensure that there are no more than %(max)s digit in total." +msgid_plural "Ensure that there are no more than %(max)s digits in total." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:402 +#, python-format +msgid "Ensure that there are no more than %(max)s decimal place." +msgid_plural "Ensure that there are no more than %(max)s decimal places." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:407 +#, python-format +msgid "" +"Ensure that there are no more than %(max)s digit before the decimal point." +msgid_plural "" +"Ensure that there are no more than %(max)s digits before the decimal point." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:469 +#, python-format +msgid "" +"File extension '%(extension)s' is not allowed. Allowed extensions are: " +"'%(allowed_extensions)s'." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/core/validators.py:521 +msgid "Null characters are not allowed." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/base.py:1162 +#: venv/lib/python3.6/site-packages/django/forms/models.py:756 +msgid "and" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/base.py:1164 +#, python-format +msgid "%(model_name)s with this %(field_labels)s already exists." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:104 +#, python-format +msgid "Value %(value)r is not a valid choice." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:105 +msgid "This field cannot be null." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:106 +msgid "This field cannot be blank." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:107 +#, python-format +msgid "%(model_name)s with this %(field_label)s already exists." +msgstr "" + +#. Translators: The 'lookup_type' is one of 'date', 'year' or 'month'. +#. Eg: "Title must be unique for pub_date year" +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:111 +#, python-format +msgid "" +"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:128 +#, python-format +msgid "Field of type: %(field_type)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:899 +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1766 +msgid "Integer" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:903 +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1764 +#, python-format +msgid "'%(value)s' value must be an integer." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:978 +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1844 +msgid "Big (8 byte) integer" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:990 +#, python-format +msgid "'%(value)s' value must be either True or False." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:991 +#, python-format +msgid "'%(value)s' value must be either True, False, or None." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:993 +msgid "Boolean (Either True or False)" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1034 +#, python-format +msgid "String (up to %(max_length)s)" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1098 +msgid "Comma-separated integers" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1147 +#, python-format +msgid "" +"'%(value)s' value has an invalid date format. It must be in YYYY-MM-DD " +"format." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1149 +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1292 +#, python-format +msgid "" +"'%(value)s' value has the correct format (YYYY-MM-DD) but it is an invalid " +"date." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1152 +msgid "Date (without time)" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1290 +#, python-format +msgid "" +"'%(value)s' value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." +"uuuuuu]][TZ] format." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1294 +#, python-format +msgid "" +"'%(value)s' value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" +"[TZ]) but it is an invalid date/time." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1298 +msgid "Date (with time)" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1446 +#, python-format +msgid "'%(value)s' value must be a decimal number." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1448 +msgid "Decimal number" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1587 +#, python-format +msgid "" +"'%(value)s' value has an invalid format. It must be in [DD] [HH:[MM:]]ss[." +"uuuuuu] format." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1590 +msgid "Duration" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1640 +msgid "Email address" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1663 +msgid "File path" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1729 +#, python-format +msgid "'%(value)s' value must be a float." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1731 +msgid "Floating point number" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1860 +msgid "IPv4 address" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1971 +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1972 +#, python-format +msgid "'%(value)s' value must be either None, True or False." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:1974 +msgid "Boolean (Either True, False or None)" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2009 +msgid "Positive integer" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2022 +msgid "Positive small integer" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2036 +#, python-format +msgid "Slug (up to %(max_length)s)" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2068 +msgid "Small integer" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2103 +#, python-format +msgid "" +"'%(value)s' value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " +"format." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2105 +#, python-format +msgid "" +"'%(value)s' value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " +"invalid time." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2108 +msgid "Time" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2256 +msgid "Raw binary data" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2306 +#, python-format +msgid "'%(value)s' is not a valid UUID." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/__init__.py:2308 +msgid "Universally unique identifier" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/files.py:221 +msgid "File" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/related.py:778 +#, python-format +msgid "%(model)s instance with %(field)s %(value)r does not exist." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/related.py:780 +msgid "Foreign Key (type determined by related field)" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/related.py:1007 +msgid "One-to-one relationship" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/related.py:1057 +#, python-format +msgid "%(from)s-%(to)s relationship" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/related.py:1058 +#, python-format +msgid "%(from)s-%(to)s relationships" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/db/models/fields/related.py:1100 +msgid "Many-to-many relationship" +msgstr "" + +#. Translators: If found as last label character, these punctuation +#. characters will prevent the default label_suffix to be appended to the label +#: venv/lib/python3.6/site-packages/django/forms/boundfield.py:146 +msgid ":?.!" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:245 +msgid "Enter a whole number." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:396 +#: venv/lib/python3.6/site-packages/django/forms/fields.py:1126 +msgid "Enter a valid date." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:420 +#: venv/lib/python3.6/site-packages/django/forms/fields.py:1127 +msgid "Enter a valid time." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:442 +msgid "Enter a valid date/time." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:471 +msgid "Enter a valid duration." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:472 +#, python-brace-format +msgid "The number of days must be between {min_days} and {max_days}." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:532 +msgid "No file was submitted. Check the encoding type on the form." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:533 +msgid "No file was submitted." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:534 +msgid "The submitted file is empty." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:536 +#, python-format +msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." +msgid_plural "" +"Ensure this filename has at most %(max)d characters (it has %(length)d)." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:539 +msgid "Please either submit a file or check the clear checkbox, not both." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:600 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:762 +#: venv/lib/python3.6/site-packages/django/forms/fields.py:852 +#: venv/lib/python3.6/site-packages/django/forms/models.py:1270 +#, python-format +msgid "Select a valid choice. %(value)s is not one of the available choices." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:853 +#: venv/lib/python3.6/site-packages/django/forms/fields.py:968 +#: venv/lib/python3.6/site-packages/django/forms/models.py:1269 +msgid "Enter a list of values." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:969 +msgid "Enter a complete value." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/fields.py:1185 +msgid "Enter a valid UUID." +msgstr "" + +#. Translators: This is the default suffix added to form field labels +#: venv/lib/python3.6/site-packages/django/forms/forms.py:86 +msgid ":" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/forms.py:212 +#, python-format +msgid "(Hidden field %(name)s) %(error)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/formsets.py:91 +msgid "ManagementForm data is missing or has been tampered with" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/formsets.py:338 +#, python-format +msgid "Please submit %d or fewer forms." +msgid_plural "Please submit %d or fewer forms." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/forms/formsets.py:345 +#, python-format +msgid "Please submit %d or more forms." +msgid_plural "Please submit %d or more forms." +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/forms/formsets.py:371 +#: venv/lib/python3.6/site-packages/django/forms/formsets.py:373 +msgid "Order" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/models.py:751 +#, python-format +msgid "Please correct the duplicate data for %(field)s." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/models.py:755 +#, python-format +msgid "Please correct the duplicate data for %(field)s, which must be unique." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/models.py:761 +#, python-format +msgid "" +"Please correct the duplicate data for %(field_name)s which must be unique " +"for the %(lookup)s in %(date_field)s." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/models.py:770 +msgid "Please correct the duplicate values below." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/models.py:1091 +msgid "The inline value did not match the parent instance." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/models.py:1158 +msgid "Select a valid choice. That choice is not one of the available choices." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/models.py:1272 +#, python-format +msgid "\"%(pk)s\" is not a valid value." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/utils.py:162 +#, python-format +msgid "" +"%(datetime)s couldn't be interpreted in time zone %(current_timezone)s; it " +"may be ambiguous or it may not exist." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/widgets.py:395 +msgid "Clear" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/widgets.py:396 +msgid "Currently" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/widgets.py:711 +msgid "Unknown" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/widgets.py:712 +msgid "Yes" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/forms/widgets.py:713 +msgid "No" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/template/defaultfilters.py:788 +msgid "yes,no,maybe" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/template/defaultfilters.py:817 +#: venv/lib/python3.6/site-packages/django/template/defaultfilters.py:834 +#, python-format +msgid "%(size)d byte" +msgid_plural "%(size)d bytes" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/template/defaultfilters.py:836 +#, python-format +msgid "%s KB" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/template/defaultfilters.py:838 +#, python-format +msgid "%s MB" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/template/defaultfilters.py:840 +#, python-format +msgid "%s GB" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/template/defaultfilters.py:842 +#, python-format +msgid "%s TB" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/template/defaultfilters.py:844 +#, python-format +msgid "%s PB" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dateformat.py:62 +msgid "p.m." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dateformat.py:63 +msgid "a.m." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dateformat.py:68 +msgid "PM" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dateformat.py:69 +msgid "AM" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dateformat.py:150 +msgid "midnight" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dateformat.py:152 +msgid "noon" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:10 +msgid "Mon" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:10 +msgid "Tue" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:10 +msgid "Wed" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:10 +msgid "Thu" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:10 +msgid "Fri" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:11 +msgid "Sat" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:11 +msgid "Sun" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:14 +msgid "January" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:14 +msgid "February" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:14 +msgid "March" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:14 +msgid "April" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:14 +msgid "May" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:14 +msgid "June" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:15 +msgid "July" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:15 +msgid "August" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:15 +msgid "September" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:15 +msgid "October" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:15 +msgid "November" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:16 +msgid "December" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:19 +msgid "jan" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:19 +msgid "feb" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:19 +msgid "mar" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:19 +msgid "apr" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:19 +msgid "may" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:19 +msgid "jun" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:20 +msgid "jul" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:20 +msgid "aug" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:20 +msgid "sep" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:20 +msgid "oct" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:20 +msgid "nov" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:20 +msgid "dec" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:23 +msgctxt "abbrev. month" +msgid "Jan." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:24 +msgctxt "abbrev. month" +msgid "Feb." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:25 +msgctxt "abbrev. month" +msgid "March" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:26 +msgctxt "abbrev. month" +msgid "April" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:27 +msgctxt "abbrev. month" +msgid "May" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:28 +msgctxt "abbrev. month" +msgid "June" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:29 +msgctxt "abbrev. month" +msgid "July" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:30 +msgctxt "abbrev. month" +msgid "Aug." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:31 +msgctxt "abbrev. month" +msgid "Sept." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:32 +msgctxt "abbrev. month" +msgid "Oct." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:33 +msgctxt "abbrev. month" +msgid "Nov." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:34 +msgctxt "abbrev. month" +msgid "Dec." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:37 +msgctxt "alt. month" +msgid "January" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:38 +msgctxt "alt. month" +msgid "February" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:39 +msgctxt "alt. month" +msgid "March" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:40 +msgctxt "alt. month" +msgid "April" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:41 +msgctxt "alt. month" +msgid "May" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:42 +msgctxt "alt. month" +msgid "June" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:43 +msgctxt "alt. month" +msgid "July" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:44 +msgctxt "alt. month" +msgid "August" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:45 +msgctxt "alt. month" +msgid "September" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:46 +msgctxt "alt. month" +msgid "October" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:47 +msgctxt "alt. month" +msgid "November" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/dates.py:48 +msgctxt "alt. month" +msgid "December" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/ipv6.py:8 +msgid "This is not a valid IPv6 address." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/text.py:67 +#, python-format +msgctxt "String to return when truncating text" +msgid "%(truncated_text)s…" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/text.py:233 +msgid "or" +msgstr "" + +#. Translators: This string is used as a separator between list elements +#: venv/lib/python3.6/site-packages/django/utils/text.py:252 +#: venv/lib/python3.6/site-packages/django/utils/timesince.py:83 +msgid ", " +msgstr "" + +#: venv/lib/python3.6/site-packages/django/utils/timesince.py:9 +#, python-format +msgid "%d year" +msgid_plural "%d years" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/utils/timesince.py:10 +#, python-format +msgid "%d month" +msgid_plural "%d months" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/utils/timesince.py:11 +#, python-format +msgid "%d week" +msgid_plural "%d weeks" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/utils/timesince.py:12 +#, python-format +msgid "%d day" +msgid_plural "%d days" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/utils/timesince.py:13 +#, python-format +msgid "%d hour" +msgid_plural "%d hours" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/utils/timesince.py:14 +#, python-format +msgid "%d minute" +msgid_plural "%d minutes" +msgstr[0] "" +msgstr[1] "" + +#: venv/lib/python3.6/site-packages/django/utils/timesince.py:72 +msgid "0 minutes" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/csrf.py:110 +msgid "Forbidden" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/csrf.py:111 +msgid "CSRF verification failed. Request aborted." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/csrf.py:115 +msgid "" +"You are seeing this message because this HTTPS site requires a 'Referer " +"header' to be sent by your Web browser, but none was sent. This header is " +"required for security reasons, to ensure that your browser is not being " +"hijacked by third parties." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/csrf.py:120 +msgid "" +"If you have configured your browser to disable 'Referer' headers, please re-" +"enable them, at least for this site, or for HTTPS connections, or for 'same-" +"origin' requests." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/csrf.py:124 +msgid "" +"If you are using the tag or " +"including the 'Referrer-Policy: no-referrer' header, please remove them. The " +"CSRF protection requires the 'Referer' header to do strict referer checking. " +"If you're concerned about privacy, use alternatives like for links to third-party sites." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/csrf.py:132 +msgid "" +"You are seeing this message because this site requires a CSRF cookie when " +"submitting forms. This cookie is required for security reasons, to ensure " +"that your browser is not being hijacked by third parties." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/csrf.py:137 +msgid "" +"If you have configured your browser to disable cookies, please re-enable " +"them, at least for this site, or for 'same-origin' requests." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/csrf.py:142 +msgid "More information is available with DEBUG=True." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:41 +msgid "No year specified" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:61 +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:111 +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:208 +msgid "Date out of range" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:90 +msgid "No month specified" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:142 +msgid "No day specified" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:188 +msgid "No week specified" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:338 +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:367 +#, python-format +msgid "No %(verbose_name_plural)s available" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:589 +#, python-format +msgid "" +"Future %(verbose_name_plural)s not available because %(class_name)s." +"allow_future is False." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/dates.py:623 +#, python-format +msgid "Invalid date string '%(datestr)s' given format '%(format)s'" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/detail.py:54 +#, python-format +msgid "No %(verbose_name)s found matching the query" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/list.py:67 +msgid "Page is not 'last', nor can it be converted to an int." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/list.py:72 +#, python-format +msgid "Invalid page (%(page_number)s): %(message)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/generic/list.py:154 +#, python-format +msgid "Empty list and '%(class_name)s.allow_empty' is False." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/static.py:40 +msgid "Directory indexes are not allowed here." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/static.py:42 +#, python-format +msgid "\"%(path)s\" does not exist" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/static.py:80 +#, python-format +msgid "Index of %(directory)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:6 +msgid "Django: the Web framework for perfectionists with deadlines." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:345 +#, python-format +msgid "" +"View release notes for Django %(version)s" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:367 +msgid "The install worked successfully! Congratulations!" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:368 +#, python-format +msgid "" +"You are seeing this page because DEBUG=True is in your settings file and you have not configured any " +"URLs." +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:383 +msgid "Django Documentation" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:384 +msgid "Topics, references, & how-to's" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:395 +msgid "Tutorial: A Polling App" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:396 +msgid "Get started with Django" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:407 +msgid "Django Community" +msgstr "" + +#: venv/lib/python3.6/site-packages/django/views/templates/default_urlconf.html:408 +msgid "Connect, get help, or contribute" +msgstr "" + +#: venv/lib/python3.6/site-packages/easy_select2/forms.py:7 +msgid "" +"Hold down \"Control\", or \"Command\" on a Mac, to select more than one." +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/admin.py:64 +#, python-format +msgid "Some messages were sent: %s" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/admin.py:66 +#, python-format +msgid "All messages were sent: %s" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/admin.py:72 +#, python-format +msgid "Some messages failed to send. %d devices were marked as inactive." +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/admin.py:80 +msgid "Send test notification" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/admin.py:85 +msgid "Send test notification in bulk" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/admin.py:90 +msgid "Send test data message" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/admin.py:96 +msgid "Send test data message in bulk" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/admin.py:101 +msgid "Enable selected devices" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/admin.py:106 +msgid "Disable selected devices" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/apps.py:7 +msgid "FCM Django" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/fields.py:52 +msgid "Enter a valid hexadecimal number" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/models.py:19 +msgid "Is active" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/models.py:20 +msgid "Inactive devices will not be sent notifications" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/models.py:25 +msgid "Creation date" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/models.py:161 +msgid "Device ID" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/models.py:162 +msgid "Unique device identifier" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/models.py:165 +msgid "Registration token" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/models.py:171 +#: venv/lib/python3.6/site-packages/fcm_django/models.py:255 +msgid "FCM device" +msgstr "" + +#: venv/lib/python3.6/site-packages/fcm_django/models.py:256 +msgid "FCM devices" +msgstr "" + +#: venv/lib/python3.6/site-packages/kombu/transport/qpid.py:1301 +#, python-format +msgid "Attempting to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv/lib/python3.6/site-packages/kombu/transport/qpid.py:1306 +#, python-format +msgid "Connected to qpid with SASL mechanism %s" +msgstr "" + +#: venv/lib/python3.6/site-packages/kombu/transport/qpid.py:1324 +#, python-format +msgid "Unable to connect to qpid with SASL mechanism %s" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/models.py:41 +msgid "Confidential" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/models.py:42 +msgid "Public" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/models.py:50 +msgid "Authorization code" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/models.py:51 +msgid "Implicit" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/models.py:52 +msgid "Resource owner password-based" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/models.py:53 +msgid "Client credentials" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/models.py:67 +msgid "Allowed URIs list, space separated" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/models.py:143 +#, python-brace-format +msgid "Unauthorized redirect scheme: {scheme}" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/models.py:148 +#, python-brace-format +msgid "redirect_uris cannot be empty with grant_type {grant_type}" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/oauth2_validators.py:166 +msgid "The access token is invalid." +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/oauth2_validators.py:171 +msgid "The access token has expired." +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/oauth2_validators.py:176 +msgid "The access token is valid but does not have enough scope." +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html:6 +msgid "Are you sure to delete the application" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_confirm_delete.html:12 +msgid "Cancel" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:10 +msgid "Client id" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:15 +msgid "Client secret" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:20 +msgid "Client type" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:25 +msgid "Authorization Grant Type" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:30 +msgid "Redirect Uris" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:36 +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_form.html:35 +msgid "Go Back" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_detail.html:37 +msgid "Edit" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_form.html:9 +msgid "Edit application" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_list.html:6 +msgid "Your applications" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_list.html:16 +msgid "No applications defined" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_list.html:16 +msgid "Click here" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_list.html:16 +msgid "if you want to register a new one" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/application_registration_form.html:5 +msgid "Register a new application" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/authorize.html:8 +msgid "Authorize" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/authorize.html:17 +msgid "Application requires following permissions" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/authorized-token-delete.html:6 +msgid "Are you sure you want to delete this token?" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/authorized-tokens.html:6 +msgid "Tokens" +msgstr "" + +#: venv/lib/python3.6/site-packages/oauth2_provider/templates/oauth2_provider/authorized-tokens.html:19 +msgid "There are no authorized tokens yet." +msgstr "" + +#: venv/lib/python3.6/site-packages/solo/admin.py:53 +#, python-format +msgid "%(obj)s was changed successfully." +msgstr "" + +#: venv/lib/python3.6/site-packages/solo/admin.py:55 +msgid "You may edit it again below." +msgstr "" + +#: venv/lib/python3.6/site-packages/solo/templatetags/solo_tags.py:22 +#, python-format +msgid "" +"Templatetag requires the model dotted path: 'app_label.ModelName'. Received " +"'%s'." +msgstr "" + +#: venv/lib/python3.6/site-packages/solo/templatetags/solo_tags.py:28 +#, python-format +msgid "" +"Could not get the model name '%(model)s' from the application named '%(app)s'" +msgstr "" diff --git a/project/settings/base.py b/project/settings/base.py index 5c32f263..448f6318 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -64,6 +64,7 @@ PROJECT_APPS = [ 'news.apps.NewsConfig', 'notification.apps.NotificationConfig', 'partner.apps.PartnerConfig', + # 'product.apps.ProductConfig', Uncomment after refining task and create migrations 'recipe.apps.RecipeConfig', 'search_indexes.apps.SearchIndexesConfig', 'translation.apps.TranslationConfig', @@ -73,6 +74,7 @@ PROJECT_APPS = [ 'comment.apps.CommentConfig', 'favorites.apps.FavoritesConfig', 'rating.apps.RatingConfig', + 'tag.apps.TagConfig', ] EXTERNAL_APPS = [ @@ -317,7 +319,10 @@ REDOC_SETTINGS = { } # CELERY -BROKER_URL = 'amqp://rabbitmq:5672' +# RabbitMQ +# BROKER_URL = 'amqp://rabbitmq:5672' +# Redis +BROKER_URL = 'redis://localhost:6379/1' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL CELERY_ACCEPT_CONTENT = ['application/json'] diff --git a/project/settings/local.py b/project/settings/local.py index a644e9b8..31fe88c2 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -6,7 +6,7 @@ ALLOWED_HOSTS = ['*', ] SEND_SMS = False SMS_CODE_SHOW = True -USE_CELERY = False +USE_CELERY = True SCHEMA_URI = 'http' DEFAULT_SUBDOMAIN = 'www' @@ -14,7 +14,10 @@ SITE_DOMAIN_URI = 'testserver.com:8000' DOMAIN_URI = '0.0.0.0:8000' # CELERY -BROKER_URL = 'amqp://rabbitmq:5672' +# RabbitMQ +# BROKER_URL = 'amqp://rabbitmq:5672' +# Redis +BROKER_URL = 'redis://redis:6379/1' CELERY_RESULT_BACKEND = BROKER_URL CELERY_BROKER_URL = BROKER_URL diff --git a/project/urls/back.py b/project/urls/back.py index 59758c66..40b3415a 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -3,11 +3,13 @@ from django.urls import path, include app_name = 'back' urlpatterns = [ - path('gallery/', include(('gallery.urls', 'gallery'), - namespace='gallery')), - path('establishments/', include('establishment.urls.back')), - path('location/', include('location.urls.back')), - path('news/', include('news.urls.back')), path('account/', include('account.urls.back')), path('comment/', include('comment.urls.back')), -] \ No newline at end of file + path('establishments/', include('establishment.urls.back')), + path('gallery/', include(('gallery.urls', 'gallery'), namespace='gallery')), + path('location/', include('location.urls.back')), + path('news/', include('news.urls.back')), + path('review/', include('review.urls.back')), + path('tags/', include(('tag.urls.back', 'tag'), namespace='tag')), +] + diff --git a/project/urls/mobile.py b/project/urls/mobile.py index 0bcbd31c..77d007dc 100644 --- a/project/urls/mobile.py +++ b/project/urls/mobile.py @@ -4,6 +4,8 @@ app_name = 'mobile' urlpatterns = [ path('establishments/', include('establishment.urls.mobile')), + path('main/', include('main.urls.mobile')), + path('location/', include('location.urls.mobile')) # path('account/', include('account.urls.web')), # path('advertisement/', include('advertisement.urls.web')), # path('collection/', include('collection.urls.web')), diff --git a/project/urls/web.py b/project/urls/web.py index 59c08a6f..872f0b9f 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -24,11 +24,13 @@ urlpatterns = [ path('collections/', include('collection.urls.web')), path('establishments/', include('establishment.urls.web')), path('news/', include('news.urls.web')), - path('notifications/', include(('notification.urls.web', "notification"), namespace='notification')), + path('notifications/', include(('notification.urls.web', "notification"), + namespace='notification')), path('partner/', include('partner.urls.web')), path('location/', include('location.urls.web')), - path('main/', include('main.urls')), + path('main/', include('main.urls.web')), path('recipes/', include('recipe.urls.web')), + path('tags/', include('tag.urls.web')), path('translation/', include('translation.urls')), path('comments/', include('comment.urls.web')), path('favorites/', include('favorites.urls')), diff --git a/requirements/base.txt b/requirements/base.txt index 716d21c6..5c734322 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -15,8 +15,6 @@ djangorestframework==3.9.4 markdown django-filter==2.1.0 djangorestframework-xml -celery -amqp>=2.4.0 geoip2==2.9.0 django-phonenumber-field[phonenumbers]==2.1.0 @@ -33,4 +31,9 @@ djangorestframework-simplejwt==4.3.0 django-elasticsearch-dsl>=7.0.0,<8.0.0 django-elasticsearch-dsl-drf==0.20.2 -sentry-sdk==0.11.2 \ No newline at end of file +sentry-sdk==0.11.2 + +# temp solution +redis==3.2.0 +amqp>=2.4.0 +celery==4.3.0rc2 \ No newline at end of file