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 c7c3e0b4..a8fd93d2 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/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/migrations/0039_establishmentsubtype_index_name.py b/apps/establishment/migrations/0039_establishmentsubtype_index_name.py index 5473600a..a29b1ae0 100644 --- a/apps/establishment/migrations/0039_establishmentsubtype_index_name.py +++ b/apps/establishment/migrations/0039_establishmentsubtype_index_name.py @@ -24,7 +24,7 @@ class Migration(migrations.Migration): 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(fill_establishment_subtype, migrations.RunPython.noop), migrations.AlterField( model_name='establishmentsubtype', name='index_name', diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 37108e34..e900993e 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -466,6 +466,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.""" @@ -600,6 +614,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') 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/views/back.py b/apps/establishment/views/back.py index d87baf6d..e490d576 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -2,6 +2,7 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics +from utils.permissions import IsCountryAdmin, IsEstablishmentManager from establishment import models, serializers from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer @@ -18,11 +19,13 @@ 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): @@ -57,12 +60,14 @@ 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): @@ -70,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): @@ -83,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): @@ -96,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): @@ -109,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): diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index cd83fed5..8ee9ddf7 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -10,7 +10,7 @@ from establishment import models, serializers from main import methods from main.models import MetaDataContent from utils.pagination import EstablishmentPortionPagination - +from utils.permissions import IsCountryAdmin class EstablishmentMixinView: """Establishment mixin.""" 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 e05f5b26..99761e1e 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -23,6 +23,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.""" @@ -119,6 +123,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/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/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/models.py b/apps/news/models.py index ed1d3a7a..ee90b897 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 @@ -38,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""" @@ -61,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.""" @@ -140,6 +164,14 @@ class News(BaseAttributes, TranslatedFieldsMixin): gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery') ratings = generic.GenericRelation(Rating) + agenda = models.ForeignKey('news.Agenda', blank=True, null=True, + on_delete=models.SET_NULL, + verbose_name=_('agenda')) + + banner = models.ForeignKey('news.NewsBanner', blank=True, null=True, + on_delete=models.SET_NULL, + verbose_name=_('banner')) + objects = NewsQuerySet.as_manager() class Meta: diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 25590f02..aee179f9 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -5,7 +5,7 @@ from rest_framework import serializers from account.serializers.common import UserBaseSerializer from gallery.models import Image from location import models as location_models -from location.serializers import CountrySimpleSerializer +from location.serializers import CountrySimpleSerializer, AddressBaseSerializer from news import models from tag.serializers import TagBaseSerializer from utils.serializers import TranslatedField, ProjectModelSerializer @@ -13,6 +13,7 @@ from utils.serializers import TranslatedField, ProjectModelSerializer class CropImageSerializer(serializers.Serializer): """Serializer for crop images for News object.""" + preview_url = serializers.SerializerMethodField() promo_horizontal_web_url = serializers.SerializerMethodField() promo_horizontal_mobile_url = serializers.SerializerMethodField() @@ -81,6 +82,40 @@ class NewsImageSerializer(serializers.ModelSerializer): } +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.""" @@ -95,7 +130,7 @@ class NewsBaseSerializer(ProjectModelSerializer): """Base serializer for News model.""" # read only fields - title_translated = TranslatedField() + title_translated = TranslatedField(source='title') subtitle_translated = TranslatedField() # related fields @@ -151,6 +186,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.""" @@ -158,6 +195,8 @@ class NewsDetailWebSerializer(NewsDetailSerializer): fields = NewsDetailSerializer.Meta.fields + ( 'same_theme', 'should_read', + 'agenda', + 'banner' ) @@ -191,11 +230,11 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, fields = NewsBackOfficeBaseSerializer.Meta.fields + \ NewsDetailSerializer.Meta.fields + ( - 'description', - 'news_type_id', - 'country_id', - 'template', - 'template_display', + 'description', + 'news_type_id', + 'country_id', + 'template', + 'template_display', ) 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 e712abfb..e962c99e 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from gallery.tasks import delete_image from news import filters, models, serializers from rating.tasks import add_rating +from utils.permissions import IsCountryAdmin, IsContentPageManager class NewsMixinView: @@ -65,6 +66,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.""" @@ -140,6 +142,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/account/permissions.py b/apps/products/urls/back.py similarity index 100% rename from apps/account/permissions.py rename to apps/products/urls/back.py diff --git a/apps/products/views/back.py b/apps/products/views/back.py new file mode 100644 index 00000000..e69de29b 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/signals.py b/apps/search_indexes/signals.py index 0f6a071f..77660a2c 100644 --- a/apps/search_indexes/signals.py +++ b/apps/search_indexes/signals.py @@ -29,16 +29,20 @@ 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 == 'tag': if model_name == 'tag': 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 bc9fbfbf..cb0729ea 100644 --- a/apps/translation/models.py +++ b/apps/translation/models.py @@ -22,7 +22,7 @@ class Language(models.Model): title = models.CharField(max_length=255, verbose_name=_('Language title')) - locale = models.CharField(max_length=10, unique=True, + locale = models.CharField(max_length=10, verbose_name=_('Locale identifier')) objects = LanguageQuerySet.as_manager() @@ -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/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/project/urls/back.py b/project/urls/back.py index 206d359c..40b3415a 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -9,5 +9,7 @@ urlpatterns = [ 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')), ] +