diff --git a/apps/account/management/commands/add_account.py b/apps/account/management/commands/add_account.py index 2b72767c..2e5842c5 100644 --- a/apps/account/management/commands/add_account.py +++ b/apps/account/management/commands/add_account.py @@ -1,6 +1,7 @@ from django.core.management.base import BaseCommand from django.db import connections -from django.db.models import Q +from django.db.models import Q, F, Value +from django.db.models.functions import ConcatPair from establishment.management.commands.add_position import namedtuplefetchall from account.models import User @@ -11,33 +12,29 @@ class Command(BaseCommand): def account_sql(self): with connections['legacy'].cursor() as cursor: cursor.execute(''' - select a.email, a.id as account_id, a.encrypted_password + select a.email, a.id as account_id, a.encrypted_password, + case when a.confirmed_at is not null then true else false end as confirmed_at, + case when a.confirmed_at is null then true else false end as unconfirmed_email, + nickname from accounts as a - where a.email is not null - and a.email not in ('cyril@tomatic.net', - 'cyril2@tomatic.net', - 'd.sadykova@id-east.ru', - 'd.sadykova@octopod.ru', - 'n.yurchenko@id-east.ru' - ) - and a.confirmed_at is not null + where a.email is not null and a.nickname!='admin' ''') return namedtuplefetchall(cursor) def handle(self, *args, **kwargs): objects = [] for a in self.account_sql(): - count = User.objects.filter(Q(email=a.email) | Q(old_id=a.account_id)).count() - if count == 0: + users = User.objects.filter(Q(email=a.email) | Q(old_id=a.account_id)) + if not users.exists(): objects.append(User(email=a.email, - unconfirmed_email=False, - email_confirmed=True, + unconfirmed_email=a.unconfirmed_email, + email_confirmed=a.confirmed_at, old_id=a.account_id, - password='bcrypt$'+a.encrypted_password + password=a.encrypted_password, + username=a.nickname )) - else: - user = User.objects.filter(Q(email=a.email) | Q(old_id=a.account_id)) - user.update(password='bcrypt$'+a.encrypted_password) User.objects.bulk_create(objects) - self.stdout.write(self.style.WARNING(f'Created accounts objects.')) \ No newline at end of file + user = User.objects.filter(old_id__isnull=False) + user.update(password=ConcatPair(Value('bcrypt$'), F('password'))) + self.stdout.write(self.style.WARNING(f'Created accounts objects.')) diff --git a/apps/account/management/commands/add_social.py b/apps/account/management/commands/add_social.py index c7894c32..ecf86d60 100644 --- a/apps/account/management/commands/add_social.py +++ b/apps/account/management/commands/add_social.py @@ -20,14 +20,7 @@ class Command(BaseCommand): ( select a.email, a.id as account_id from accounts as a - where a.email is not null - and a.email not in ('cyril@tomatic.net', - 'cyril2@tomatic.net', - 'd.sadykova@id-east.ru', - 'd.sadykova@octopod.ru', - 'n.yurchenko@id-east.ru' - ) - and a.confirmed_at is not null + where a.email is not null ) t join identities i on i.account_id = t.account_id ''') @@ -39,13 +32,12 @@ class Command(BaseCommand): user = User.objects.filter(old_id=s.account_id) if user.count() > 0: social = UserSocialAuth.objects.filter(user=user.first(), - provider=s.provider, - uid=s.uid) + provider=s.provider, + uid=s.uid) if social.count() == 0: objects.append(UserSocialAuth(user=user.first(), provider=s.provider, uid=s.uid) - ) - print('INSERT') + ) UserSocialAuth.objects.bulk_create(objects) self.stdout.write(self.style.WARNING(f'Created social objects.')) \ No newline at end of file diff --git a/apps/account/transfer_data.py b/apps/account/transfer_data.py index 75e0d8ee..0f199173 100644 --- a/apps/account/transfer_data.py +++ b/apps/account/transfer_data.py @@ -7,11 +7,11 @@ from transfer.serializers.account import UserSerializer from transfer.serializers.user_social_auth import UserSocialAuthSerializer STOP_LIST = ( - 'cyril@tomatic.net', - 'cyril2@tomatic.net', - 'd.sadykova@id-east.ru', - 'd.sadykova@octopod.ru', - 'n.yurchenko@id-east.ru', + # 'cyril@tomatic.net', + # 'cyril2@tomatic.net', + # 'd.sadykova@id-east.ru', + # 'd.sadykova@octopod.ru', + # 'n.yurchenko@id-east.ru', ) @@ -20,7 +20,7 @@ def transfer_user(): # queryset = queryset.annotate(nickname=F('account__nickname')) # queryset = queryset.annotate(email=F('account__email')) - queryset = Accounts.objects.filter(confirmed_at__isnull=False).exclude(email__in=STOP_LIST) + queryset = Accounts.objects.exclude(email__in=STOP_LIST) serialized_data = UserSerializer(data=list(queryset.values()), many=True) @@ -33,7 +33,7 @@ def transfer_user(): def transfer_identities(): queryset = Identities.objects.exclude( Q(account_id__isnull=True) | - Q(account__confirmed_at__isnull=True) | + # Q(account__confirmed_at__isnull=True) | Q(account__email__in=STOP_LIST) ).values_list( 'account_id', diff --git a/apps/collection/serializers/back.py b/apps/collection/serializers/back.py new file mode 100644 index 00000000..45eaec73 --- /dev/null +++ b/apps/collection/serializers/back.py @@ -0,0 +1,9 @@ +from rest_framework import serializers +from collection import models + + +class CollectionSerializer(serializers.ModelSerializer): + """Collection serializer.""" + class Meta: + model = models.Collection.objects.all() + fields = '__all__' diff --git a/apps/collection/urls/back.py b/apps/collection/urls/back.py new file mode 100644 index 00000000..3ac8a073 --- /dev/null +++ b/apps/collection/urls/back.py @@ -0,0 +1,11 @@ +"""Collection common urlpaths.""" +from django.urls import path + +from collection.views import back as views + +app_name = 'collection' + +urlpatterns = [ + path('', views.CollectionListCreateView.as_view(), name='list-create'), + +] \ No newline at end of file diff --git a/apps/collection/views/back.py b/apps/collection/views/back.py new file mode 100644 index 00000000..af08aff5 --- /dev/null +++ b/apps/collection/views/back.py @@ -0,0 +1,19 @@ +from rest_framework import generics, permissions +from collection import models +from collection.serializers import common, back + + +class CollectionListCreateView(generics.ListCreateAPIView): + """Collection list-create view.""" + queryset = models.Collection.objects.all() + serializer_class = back.CollectionSerializer + # todo: conf. permissions by TT + permission_classes = (permissions.IsAuthenticated, ) + + +class CollectionRUDView(generics.RetrieveUpdateDestroyAPIView): + """Collection list-create view.""" + queryset = models.Collection.objects.all() + serializer_class = back.CollectionSerializer + # todo: conf. permissions by TT + permission_classes = (permissions.IsAuthenticated, ) \ No newline at end of file diff --git a/apps/establishment/transfer_data.py b/apps/establishment/transfer_data.py index 71ca9f0a..77bdecab 100644 --- a/apps/establishment/transfer_data.py +++ b/apps/establishment/transfer_data.py @@ -16,7 +16,6 @@ def transfer_establishment(): old_establishments = Establishments.objects.exclude( id__in=list(Establishment.objects.all().values_list('old_id', flat=True)) ).exclude( - Q(type='Wineyard') | Q(location__timezone__isnull=True), ).prefetch_related( 'establishmentinfos_set', diff --git a/apps/location/admin.py b/apps/location/admin.py index a52fa14e..a7610a65 100644 --- a/apps/location/admin.py +++ b/apps/location/admin.py @@ -19,22 +19,6 @@ class CityAdmin(admin.ModelAdmin): """City admin.""" -class WineAppellationInline(admin.TabularInline): - model = models.WineAppellation - extra = 0 - - -@admin.register(models.WineRegion) -class WineRegionAdmin(admin.ModelAdmin): - """WineRegion admin.""" - inlines = [WineAppellationInline, ] - - -@admin.register(models.WineAppellation) -class WineAppellationAdmin(admin.ModelAdmin): - """WineAppellation admin.""" - - @admin.register(models.Address) class AddressAdmin(admin.OSMGeoAdmin): """Address admin.""" diff --git a/apps/location/migrations/0012_data_migrate.py b/apps/location/migrations/0012_data_migrate.py index b61c43df..a88723a1 100644 --- a/apps/location/migrations/0012_data_migrate.py +++ b/apps/location/migrations/0012_data_migrate.py @@ -21,5 +21,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(load_data_from_sql, revert_data), + # migrations.RunPython(load_data_from_sql, revert_data), ] diff --git a/apps/location/migrations/0021_auto_20191111_0731.py b/apps/location/migrations/0021_auto_20191111_0731.py new file mode 100644 index 00000000..8585fa7f --- /dev/null +++ b/apps/location/migrations/0021_auto_20191111_0731.py @@ -0,0 +1,69 @@ +# Generated by Django 2.2.4 on 2019-11-11 07:31 + +import django.contrib.gis.db.models.fields +from django.db import migrations, models +import django.db.models.deletion +import utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0020_merge_20191030_1714'), + ] + + operations = [ + migrations.CreateModel( + name='WineSubRegion', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('old_id', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id')), + ], + options={ + 'verbose_name': 'wine region', + 'verbose_name_plural': 'wine regions', + }, + ), + migrations.CreateModel( + name='WineVillage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('old_id', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id')), + ], + options={ + 'verbose_name': 'wine village', + 'verbose_name_plural': 'wine villages', + }, + ), + migrations.RemoveField( + model_name='wineappellation', + name='wine_region', + ), + migrations.AddField( + model_name='wineregion', + name='coordinates', + field=django.contrib.gis.db.models.fields.PointField(blank=True, default=None, null=True, srid=4326, verbose_name='Coordinates'), + ), + migrations.AddField( + model_name='wineregion', + name='description', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='description'), + ), + migrations.AddField( + model_name='wineregion', + name='old_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'), + ), + migrations.AlterField( + model_name='wineregion', + name='country', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='location.Country', verbose_name='country'), + ), + migrations.AlterField( + model_name='wineregion', + name='name', + field=models.CharField(max_length=255, verbose_name='name'), + ), + ] diff --git a/apps/location/migrations/0022_auto_20191111_0731.py b/apps/location/migrations/0022_auto_20191111_0731.py new file mode 100644 index 00000000..378ed1a3 --- /dev/null +++ b/apps/location/migrations/0022_auto_20191111_0731.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.4 on 2019-11-11 07:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0021_auto_20191111_0731'), + ('product', '0009_auto_20191111_0731'), + ] + + operations = [ + migrations.DeleteModel( + name='WineAppellation', + ), + migrations.AddField( + model_name='winevillage', + name='wine_region', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='location.WineRegion', verbose_name='wine region'), + ), + migrations.AddField( + model_name='winesubregion', + name='wine_region', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='location.WineRegion', verbose_name='wine sub region'), + ), + ] diff --git a/apps/location/models.py b/apps/location/models.py index 9f059e34..cca1371a 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -160,14 +160,19 @@ class WineRegionQuerySet(models.QuerySet): """Wine region queryset.""" -class WineRegion(TranslatedFieldsMixin, models.Model): +class WineRegion(models.Model): """Wine region model.""" - STR_FIELD_NAME = 'name' - - name = TJSONField(verbose_name=_('Name'), - help_text='{"en-GB":"some text"}') + name = models.CharField(_('name'), max_length=255) country = models.ForeignKey(Country, on_delete=models.PROTECT, + blank=True, null=True, default=None, verbose_name=_('country')) + coordinates = models.PointField( + _('Coordinates'), blank=True, null=True, default=None) + old_id = models.PositiveIntegerField(_('old id'), default=None, + blank=True, null=True) + description = TJSONField(blank=True, null=True, default=None, + verbose_name=_('description'), + help_text='{"en-GB":"some text"}') objects = WineRegionQuerySet.as_manager() @@ -177,26 +182,47 @@ class WineRegion(TranslatedFieldsMixin, models.Model): verbose_name = _('wine region') -class WineAppellationQuerySet(models.QuerySet): - """Wine appellation queryset.""" +class WineSubRegionQuerySet(models.QuerySet): + """Wine sub region QuerySet.""" -class WineAppellation(TranslatedFieldsMixin, models.Model): - """Wine appellation model.""" - STR_FIELD_NAME = 'name' - - name = TJSONField(verbose_name=_('Name'), - help_text='{"en-GB":"some text"}') +class WineSubRegion(models.Model): + """Wine sub region model.""" + name = models.CharField(_('name'), max_length=255) wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT, - related_name='appellations', - verbose_name=_('wine region')) + verbose_name=_('wine sub region')) + old_id = models.PositiveIntegerField(_('old id'), default=None, + blank=True, null=True) - objects = WineAppellationQuerySet.as_manager() + objects = WineSubRegionQuerySet.as_manager() class Meta: """Meta class.""" - verbose_name_plural = _('wine appellations') - verbose_name = _('wine appellation') + verbose_name_plural = _('wine regions') + verbose_name = _('wine region') + + +class WineVillageQuerySet(models.QuerySet): + """Wine village QuerySet.""" + + +class WineVillage(models.Model): + """ + Wine village. + Description: Imported from legacy DB. + """ + name = models.CharField(_('name'), max_length=255) + wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT, + verbose_name=_('wine region')) + old_id = models.PositiveIntegerField(_('old id'), default=None, + blank=True, null=True) + + objects = WineVillageQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('wine village') + verbose_name_plural = _('wine villages') # todo: Make recalculate price levels diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index 3d412ec6..378e4912 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -152,19 +152,6 @@ class AddressDetailSerializer(AddressBaseSerializer): ) -class WineAppellationBaseSerializer(serializers.ModelSerializer): - """Wine appellations.""" - name_translated = TranslatedField() - - class Meta: - """Meta class.""" - model = models.WineAppellation - fields = [ - 'id', - 'name_translated', - ] - - class WineRegionBaseSerializer(serializers.ModelSerializer): """Wine region serializer.""" name_translated = TranslatedField() diff --git a/apps/location/transfer_data.py b/apps/location/transfer_data.py index cb8402ad..0d90461a 100644 --- a/apps/location/transfer_data.py +++ b/apps/location/transfer_data.py @@ -1,14 +1,13 @@ -from transfer.serializers.location import CountrySerializer, RegionSerializer, \ - CitySerializer, AddressSerializer, \ - Country -from transfer.models import Cities, Locations +from transfer.serializers import location as location_serializers +from transfer import models as transfer_models +from location.models import Country from pprint import pprint from requests import get def transfer_countries(): - queryset = Cities.objects.raw(""" + queryset = transfer_models.Cities.objects.raw(""" SELECT cities.id, cities.country_code_2 FROM cities WHERE country_code_2 IS NOT NULL AND @@ -18,7 +17,7 @@ def transfer_countries(): queryset = [vars(query) for query in queryset] - serialized_data = CountrySerializer(data=queryset, many=True) + serialized_data = location_serializers.CountrySerializer(data=queryset, many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -26,7 +25,7 @@ def transfer_countries(): def transfer_regions(): - regions_without_subregion_queryset = Cities.objects.raw(""" + regions_without_subregion_queryset = transfer_models.Cities.objects.raw(""" SELECT cities.id, cities.region_code, cities.country_code_2, @@ -47,13 +46,14 @@ def transfer_regions(): regions_without_subregion_queryset = [vars(query) for query in regions_without_subregion_queryset] - serialized_without_subregion = RegionSerializer(data=regions_without_subregion_queryset, many=True) + serialized_without_subregion = location_serializers.RegionSerializer(data=regions_without_subregion_queryset, + many=True) if serialized_without_subregion.is_valid(): serialized_without_subregion.save() else: pprint(f"Parent regions serializer errors: {serialized_without_subregion.errors}") - regions_with_subregion_queryset = Cities.objects.raw(""" + regions_with_subregion_queryset = transfer_models.Cities.objects.raw(""" SELECT cities.id, cities.region_code, @@ -84,7 +84,8 @@ def transfer_regions(): regions_with_subregion_queryset = [vars(query) for query in regions_with_subregion_queryset] - serialized_with_subregion = RegionSerializer(data=regions_with_subregion_queryset, many=True) + serialized_with_subregion = location_serializers.RegionSerializer(data=regions_with_subregion_queryset, + many=True) if serialized_with_subregion.is_valid(): serialized_with_subregion.save() else: @@ -92,7 +93,7 @@ def transfer_regions(): def transfer_cities(): - queryset = Cities.objects.raw("""SELECT cities.id, cities.name, cities.country_code_2, cities.zip_code, + queryset = transfer_models.Cities.objects.raw("""SELECT cities.id, cities.name, cities.country_code_2, cities.zip_code, cities.is_island, cities.region_code, cities.subregion_code FROM cities WHERE region_code IS NOT NULL AND @@ -103,7 +104,7 @@ def transfer_cities(): queryset = [vars(query) for query in queryset] - serialized_data = CitySerializer(data=queryset, many=True) + serialized_data = location_serializers.CitySerializer(data=queryset, many=True) if serialized_data.is_valid(): serialized_data.save() else: @@ -111,7 +112,7 @@ def transfer_cities(): def transfer_addresses(): - queryset = Locations.objects.raw("""SELECT locations.id, locations.zip_code, locations.longitude, + queryset = transfer_models.Locations.objects.raw("""SELECT locations.id, locations.zip_code, locations.longitude, locations.latitude, locations.address, locations.city_id FROM locations WHERE locations.address != "" AND @@ -126,13 +127,46 @@ def transfer_addresses(): queryset = [vars(query) for query in queryset] - serialized_data = AddressSerializer(data=queryset, many=True) + serialized_data = location_serializers.AddressSerializer(data=queryset, many=True) if serialized_data.is_valid(): serialized_data.save() else: pprint(f"Address serializer errors: {serialized_data.errors}") +def transfer_wine_region(): + queryset = transfer_models.WineLocations.objects.filter(type='WineRegion') + serialized_data = location_serializers.WineRegion( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"WineStandardClassificationSerializer errors: {serialized_data.errors}") + + +def transfer_wine_sub_region(): + queryset = transfer_models.WineLocations.objects.filter(type='WineSubRegion') + serialized_data = location_serializers.WineSubRegion( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"WineStandardClassificationSerializer errors: {serialized_data.errors}") + + +def transfer_wine_village(): + queryset = transfer_models.WineLocations.objects.filter(type='Village') + serialized_data = location_serializers.WineVillage( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"WineStandardClassificationSerializer errors: {serialized_data.errors}") + + def update_flags(): queryset = Country.objects.only("id", "code", "svg_image").filter(old_id__isnull=False) link_to_request = "https://s3.eu-central-1.amazonaws.com/gm-test.com/media" @@ -150,9 +184,12 @@ data_types = { transfer_countries, transfer_regions, transfer_cities, - transfer_addresses + transfer_addresses, + transfer_wine_region, + transfer_wine_sub_region, + transfer_wine_village, ], "update_country_flag": [ update_flags - ] + ], } diff --git a/apps/product/admin.py b/apps/product/admin.py index b77f2cbc..3ad78409 100644 --- a/apps/product/admin.py +++ b/apps/product/admin.py @@ -1,12 +1,17 @@ """Product admin conf.""" from django.contrib import admin from utils.admin import BaseModelAdminMixin -from .models import Product, ProductType, ProductSubType +from .models import Product, ProductType, ProductSubType, Unit @admin.register(Product) class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin): """Admin page for model Product.""" + search_fields = ('name', ) + list_filter = ('available', 'product_type') + list_display = ('id', '__str__', 'get_category_display', 'product_type') + raw_id_fields = ('subtypes', 'classifications', 'standards', + 'tags', 'gallery') @admin.register(ProductType) @@ -17,3 +22,8 @@ class ProductTypeAdmin(admin.ModelAdmin): @admin.register(ProductSubType) class ProductSubTypeAdmin(admin.ModelAdmin): """Admin page for model ProductSubType.""" + + +@admin.register(Unit) +class UnitAdmin(admin.ModelAdmin): + """Admin page for model Unit.""" diff --git a/apps/product/management/commands/add_product.py b/apps/product/management/commands/add_product.py deleted file mode 100644 index 981487ec..00000000 --- a/apps/product/management/commands/add_product.py +++ /dev/null @@ -1,54 +0,0 @@ -from django.core.management.base import BaseCommand -from product.models import ProductType, ProductSubType - - -def add_type(): - product_type = ProductType.objects.create( - name={'"en-GB"': "Wine"}, - index_name=ProductType.WINE - ) - return product_type.save() - - -def add_subtype(id_type): - subtypes = ProductSubType.objects.bulk_create([ - ProductSubType(product_type=id_type, - name={"en-GB", ProductSubType.EXTRA_BRUT}, - index_name=ProductSubType.EXTRA_BRUT), - ProductSubType(product_type=id_type, - name={"en-GB", ProductSubType.BRUT}, - index_name=ProductSubType.BRUT), - ProductSubType(product_type=id_type, - name={"en-GB", ProductSubType.BRUT_NATURE}, - index_name=ProductSubType.BRUT_NATURE), - ProductSubType(product_type=id_type, - name={"en-GB", ProductSubType.DEMI_SEC}, - index_name=ProductSubType.DEMI_SEC), - ProductSubType(product_type=id_type, - name={"en-GB", ProductSubType.EXTRA_DRY}, - index_name=ProductSubType.EXTRA_DRY), - ProductSubType(product_type=id_type, - name={"en-GB", ProductSubType.DOSAGE_ZERO}, - index_name=ProductSubType.DOSAGE_ZERO), - ProductSubType(product_type=id_type, - name={"en-GB", ProductSubType.SEC}, - index_name=ProductSubType.SEC), - ProductSubType(product_type=id_type, - name={"en-GB", ProductSubType.SEC}, - index_name=ProductSubType.SEC), - ProductSubType(product_type=id_type, - name={"en-GB", ProductSubType.MOELLEUX}, - index_name=ProductSubType.MOELLEUX), - ]) - - -class Command(BaseCommand): - help = 'Add product data' - - def handle(self, *args, **kwarg): - product_type = add_type() - add_subtype(product_type.id) - - - - diff --git a/apps/product/management/commands/add_product_sub_type.py b/apps/product/management/commands/add_product_sub_type.py new file mode 100644 index 00000000..9601020c --- /dev/null +++ b/apps/product/management/commands/add_product_sub_type.py @@ -0,0 +1,34 @@ +from django.conf import settings +from django.core.management.base import BaseCommand + +from product.models import ProductType, ProductSubType + + +class Command(BaseCommand): + help = 'Add product sub type data' + + def handle(self, *args, **kwarg): + error_counter = 0 + create_counter = 0 + + product_subtypes = { + 'plate': { + 'product_type_index_name': 'souvenir', + 'name': 'plate', + 'index_name': 'plate', + } + } + for product_subtype in product_subtypes.values(): + product_type_qs = ProductType.objects.filter( + index_name=product_subtype.get('product_type_index_name')) + if product_type_qs: + product_type = product_type_qs.first() + subtype, status = ProductSubType.objects.get_or_create(**{ + 'name': {settings.FALLBACK_LOCALE: product_subtype.get('name')}, + 'index_name': product_subtype.get('index_name'), + 'product_type': product_type + }) + create_counter += 1 if status else 0 + else: + error_counter += 1 + self.stdout.write(self.style.WARNING(f'Errors occurred: {error_counter}\nCreated: {create_counter}')) diff --git a/apps/product/management/commands/add_product_type.py b/apps/product/management/commands/add_product_type.py new file mode 100644 index 00000000..14ba53d3 --- /dev/null +++ b/apps/product/management/commands/add_product_type.py @@ -0,0 +1,20 @@ +from django.conf import settings +from django.core.management.base import BaseCommand + +from product.models import ProductType + + +class Command(BaseCommand): + help = 'Add product type data' + + def handle(self, *args, **kwarg): + product_types = ['wine', 'souvenir'] + create_counter = 0 + for product_type in product_types: + subtype, created = ProductType.objects.get_or_create(**{ + 'name': {settings.FALLBACK_LOCALE: product_type}, + 'index_name': product_type + }) + if created: + created += 1 + self.stdout.write(self.style.WARNING(f'Created: {create_counter}')) diff --git a/apps/product/migrations/0009_auto_20191111_0731.py b/apps/product/migrations/0009_auto_20191111_0731.py new file mode 100644 index 00000000..53a79c0b --- /dev/null +++ b/apps/product/migrations/0009_auto_20191111_0731.py @@ -0,0 +1,179 @@ +# Generated by Django 2.2.4 on 2019-11-11 07:31 + +import django.contrib.gis.db.models.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0006_merge_20191027_1758'), + ('location', '0021_auto_20191111_0731'), + ('tag', '0009_auto_20191111_0731'), + ('product', '0008_auto_20191031_1410'), + ] + + operations = [ + migrations.CreateModel( + name='ProductStandard', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('standard_type', models.PositiveSmallIntegerField(choices=[(0, 'Appellation'), (1, 'Wine quality'), (2, 'Yard classification')], verbose_name='standard type')), + ('coordinates', django.contrib.gis.db.models.fields.PointField(blank=True, default=None, null=True, srid=4326, verbose_name='Coordinates')), + ('old_id', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id')), + ], + options={ + 'verbose_name': 'wine standard', + 'verbose_name_plural': 'wine standards', + }, + ), + migrations.CreateModel( + name='Unit', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, verbose_name='name')), + ('value', models.CharField(max_length=255, verbose_name='value')), + ], + options={ + 'verbose_name': 'unit', + 'verbose_name_plural': 'units', + }, + ), + migrations.RemoveField( + model_name='product', + name='characteristics', + ), + migrations.RemoveField( + model_name='product', + name='wine_appellation', + ), + migrations.AddField( + model_name='product', + name='old_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'), + ), + migrations.AddField( + model_name='product', + name='old_unique_key', + field=models.CharField(blank=True, default=None, help_text='attribute from legacy db', max_length=255, null=True, unique=True), + ), + migrations.AddField( + model_name='product', + name='state', + field=models.PositiveIntegerField(choices=[(0, 'Published'), (1, 'Out_of_production'), (2, 'Waiting')], default=2, help_text='attribute from legacy db', verbose_name='state'), + ), + migrations.AddField( + model_name='product', + name='tags', + field=models.ManyToManyField(related_name='products', to='tag.Tag', verbose_name='Tag'), + ), + migrations.AddField( + model_name='product', + name='vintage', + field=models.IntegerField(blank=True, default=None, null=True, validators=[django.core.validators.MinValueValidator(1700), django.core.validators.MaxValueValidator(2100)], verbose_name='vintage year'), + ), + migrations.AddField( + model_name='product', + name='wine_sub_region', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='wines', to='location.WineSubRegion', verbose_name='wine sub region'), + ), + migrations.AddField( + model_name='product', + name='wine_village', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='location.WineVillage', verbose_name='wine appellation'), + ), + migrations.AddField( + model_name='producttype', + name='tag_categories', + field=models.ManyToManyField(related_name='product_types', to='tag.TagCategory', verbose_name='Tag'), + ), + migrations.AlterField( + model_name='product', + name='establishment', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='establishment.Establishment', verbose_name='establishment'), + ), + migrations.AlterField( + model_name='product', + name='name', + field=models.CharField(default=None, max_length=255, null=True, verbose_name='name'), + ), + migrations.AlterField( + model_name='product', + name='product_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='product.ProductType', verbose_name='Type'), + ), + migrations.AlterField( + model_name='product', + name='wine_region', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='wines', to='location.WineRegion', verbose_name='wine region'), + ), + migrations.AlterField( + model_name='productsubtype', + name='index_name', + field=models.CharField(choices=[('rum', 'Rum'), ('plate', 'Plate'), ('other', 'Other')], db_index=True, max_length=50, unique=True, verbose_name='Index name'), + ), + migrations.AlterField( + model_name='producttype', + name='index_name', + field=models.CharField(choices=[('food', 'Food'), ('wine', 'Wine'), ('liquor', 'Liquor'), ('souvenir', 'Souvenir'), ('book', 'Book')], db_index=True, max_length=50, unique=True, verbose_name='Index name'), + ), + migrations.CreateModel( + name='ProductGallery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_main', models.BooleanField(default=False, verbose_name='Is the main image')), + ('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_gallery', to='gallery.Image', verbose_name='gallery')), + ('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_gallery', to='product.Product', verbose_name='product')), + ], + options={ + 'verbose_name': 'product gallery', + 'verbose_name_plural': 'product galleries', + 'unique_together': {('product', 'image'), ('product', 'is_main')}, + }, + ), + migrations.CreateModel( + name='ProductClassificationType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True, verbose_name='classification type')), + ('product_sub_type', models.ForeignKey(blank=True, default=None, help_text='Legacy attribute - possible_type (product type).Product type in our case is product subtype.', null=True, on_delete=django.db.models.deletion.PROTECT, to='product.ProductSubType', verbose_name='product subtype')), + ('product_type', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='product.ProductType', verbose_name='product type')), + ], + options={ + 'verbose_name': 'wine classification type', + 'verbose_name_plural': 'wine classification types', + }, + ), + migrations.CreateModel( + name='ProductClassification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('old_id', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id')), + ('classification_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='product.ProductClassificationType', verbose_name='classification type')), + ('standard', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='product.ProductStandard', verbose_name='standard')), + ('tags', models.ManyToManyField(related_name='product_classifications', to='tag.Tag', verbose_name='Tag')), + ], + options={ + 'verbose_name': 'product classification', + 'verbose_name_plural': 'product classifications', + }, + ), + migrations.AddField( + model_name='product', + name='classifications', + field=models.ManyToManyField(blank=True, to='product.ProductClassification', verbose_name='classifications'), + ), + migrations.AddField( + model_name='product', + name='gallery', + field=models.ManyToManyField(through='product.ProductGallery', to='gallery.Image'), + ), + migrations.AddField( + model_name='product', + name='standards', + field=models.ManyToManyField(blank=True, help_text='attribute from legacy db', to='product.ProductStandard', verbose_name='standards'), + ), + ] diff --git a/apps/product/migrations/0010_auto_20191111_1227.py b/apps/product/migrations/0010_auto_20191111_1227.py new file mode 100644 index 00000000..774422c5 --- /dev/null +++ b/apps/product/migrations/0010_auto_20191111_1227.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.7 on 2019-11-11 12:27 + +from django.db import migrations, models +import utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0009_auto_20191111_0731'), + ] + + operations = [ + migrations.RemoveField( + model_name='product', + name='country', + ), + migrations.AlterField( + model_name='product', + name='available', + field=models.BooleanField(default=True, verbose_name='available'), + ), + migrations.AlterField( + model_name='product', + name='description', + field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='description'), + ), + migrations.AlterField( + model_name='product', + name='slug', + field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Slug'), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index 19d8443c..775da7d2 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -1,9 +1,11 @@ """Product app models.""" -from django.db import models from django.contrib.contenttypes import fields as generic +from django.contrib.gis.db import models as gis_models from django.core.exceptions import ValidationError -from django.contrib.postgres.fields import JSONField +from django.db import models from django.utils.translation import gettext_lazy as _ +from django.core.validators import MaxValueValidator, MinValueValidator + from utils.models import (BaseAttributes, ProjectBaseMixin, TranslatedFieldsMixin, TJSONField) @@ -17,11 +19,15 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): FOOD = 'food' WINE = 'wine' LIQUOR = 'liquor' + SOUVENIR = 'souvenir' + BOOK = 'book' INDEX_NAME_TYPES = ( (FOOD, _('Food')), (WINE, _('Wine')), (LIQUOR, _('Liquor')), + (SOUVENIR, _('Souvenir')), + (BOOK, _('Book')), ) name = TJSONField(blank=True, null=True, default=None, @@ -30,6 +36,9 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): 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='product_types', + verbose_name=_('Tag')) class Meta: """Meta class.""" @@ -45,29 +54,13 @@ class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin): # INDEX NAME CHOICES RUM = 'rum' + PLATE = 'plate' OTHER = 'other' - EXTRA_BRUT = 'extra brut' - BRUT = 'brut' - BRUT_NATURE = 'brut nature' - DEMI_SEC = 'demi-sec' - EXTRA_DRY = 'Extra Dry' - DOSAGE_ZERO = 'dosage zero' - SEC = 'sec' - DOUX = 'doux' - MOELLEUX= 'moelleux' INDEX_NAME_TYPES = ( (RUM, _('Rum')), + (PLATE, _('Plate')), (OTHER, _('Other')), - (EXTRA_BRUT, _('extra brut')), - (BRUT, _('brut')), - (BRUT_NATURE, _('brut nature')), - (DEMI_SEC, _('demi-sec')), - (EXTRA_DRY, _('Extra Dry')), - (DOSAGE_ZERO, _('dosage zero')), - (SEC, _('sec')), - (DOUX, _('doux')), - (MOELLEUX, _('moelleux')) ) product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE, @@ -99,7 +92,7 @@ class ProductQuerySet(models.QuerySet): def with_base_related(self): return self.select_related('product_type', 'establishment') \ - .prefetch_related('product_type__subtypes', 'country') + .prefetch_related('product_type__subtypes') def common(self): return self.filter(category=self.model.COMMON) @@ -122,7 +115,8 @@ class ProductQuerySet(models.QuerySet): class Product(TranslatedFieldsMixin, BaseAttributes): """Product models.""" - STR_FIELD_NAME = 'name' + EARLIEST_VINTAGE_YEAR = 1700 + LATEST_VINTAGE_YEAR = 2100 COMMON = 0 ONLINE = 1 @@ -132,38 +126,74 @@ class Product(TranslatedFieldsMixin, BaseAttributes): (ONLINE, _('Online')), ) + PUBLISHED = 0 + OUT_OF_PRODUCTION = 1 + WAITING = 2 + + STATE_CHOICES = ( + (PUBLISHED, _('Published')), + (OUT_OF_PRODUCTION, _('Out_of_production')), + (WAITING, _('Waiting')), + ) + 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, + name = models.CharField(max_length=255, + default=None, null=True, + verbose_name=_('name')) + description = TJSONField(_('description'), null=True, blank=True, default=None, help_text='{"en-GB":"some text"}') - #TODO set null=False - characteristics = JSONField(_('Characteristics'), null=True) - country = models.ManyToManyField('location.Country', - verbose_name=_('Country')) - available = models.BooleanField(_('Available'), default=True) + available = models.BooleanField(_('available'), default=True) product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT, + null=True, related_name='products', verbose_name=_('Type')) subtypes = models.ManyToManyField(ProductSubType, blank=True, related_name='products', verbose_name=_('Subtypes')) - establishment = models.ForeignKey('establishment.Establishment', - on_delete=models.PROTECT, + establishment = models.ForeignKey('establishment.Establishment', on_delete=models.PROTECT, + blank=True, null=True, related_name='products', verbose_name=_('establishment')) public_mark = models.PositiveIntegerField(blank=True, null=True, default=None, verbose_name=_('public mark'),) wine_region = models.ForeignKey('location.WineRegion', on_delete=models.PROTECT, related_name='wines', - blank=True, null=True, + blank=True, null=True, default=None, verbose_name=_('wine region')) - wine_appellation = models.ForeignKey('location.WineAppellation', on_delete=models.PROTECT, - blank=True, null=True, - verbose_name=_('wine appellation')) + wine_sub_region = models.ForeignKey('location.WineSubRegion', on_delete=models.PROTECT, + related_name='wines', + blank=True, null=True, default=None, + verbose_name=_('wine sub region')) + classifications = models.ManyToManyField('ProductClassification', + blank=True, + verbose_name=_('classifications')) + standards = models.ManyToManyField('ProductStandard', + blank=True, + verbose_name=_('standards'), + help_text=_('attribute from legacy db')) + wine_village = models.ForeignKey('location.WineVillage', on_delete=models.PROTECT, + blank=True, null=True, + verbose_name=_('wine appellation')) slug = models.SlugField(unique=True, max_length=255, null=True, - verbose_name=_('Establishment slug')) + verbose_name=_('Slug')) favorites = generic.GenericRelation(to='favorites.Favorites') + old_id = models.PositiveIntegerField(_('old id'), default=None, + blank=True, null=True) + state = models.PositiveIntegerField(choices=STATE_CHOICES, + default=WAITING, + verbose_name=_('state'), + help_text=_('attribute from legacy db')) + tags = models.ManyToManyField('tag.Tag', related_name='products', + verbose_name=_('Tag')) + old_unique_key = models.CharField(max_length=255, unique=True, + blank=True, null=True, default=None, + help_text=_('attribute from legacy db')) + vintage = models.IntegerField(verbose_name=_('vintage year'), + null=True, blank=True, default=None, + validators=[MinValueValidator(EARLIEST_VINTAGE_YEAR), + MaxValueValidator(LATEST_VINTAGE_YEAR)]) + gallery = models.ManyToManyField('gallery.Image', through='ProductGallery') + reviews = generic.GenericRelation(to='review.Review') objects = ProductManager.from_queryset(ProductQuerySet)() @@ -173,15 +203,19 @@ class Product(TranslatedFieldsMixin, BaseAttributes): verbose_name = _('Product') verbose_name_plural = _('Products') + def __str__(self): + """Override str dunder method.""" + return f'{self.name}' + def clean_fields(self, exclude=None): super().clean_fields(exclude=exclude) if self.product_type.index_name == ProductType.WINE and not self.wine_region: raise ValidationError(_('wine_region field must be specified.')) if not self.product_type.index_name == ProductType.WINE and self.wine_region: raise ValidationError(_('wine_region field must not be specified.')) - if (self.wine_region and self.wine_appellation) and \ - self.wine_appellation not in self.wine_region.appellations.all(): - raise ValidationError(_('Wine appellation not exists in wine region.')) + # if (self.wine_region and self.wine_appellation) and \ + # self.wine_appellation not in self.wine_region.appellations.all(): + # raise ValidationError(_('Wine appellation not exists in wine region.')) class OnlineProductManager(ProductManager): @@ -203,3 +237,129 @@ class OnlineProduct(Product): proxy = True verbose_name = _('Online product') verbose_name_plural = _('Online products') + + +class Unit(models.Model): + """Product unit model.""" + name = models.CharField(max_length=255, + verbose_name=_('name')) + value = models.CharField(max_length=255, + verbose_name=_('value')) + + class Meta: + """Meta class.""" + verbose_name = _('unit') + verbose_name_plural = _('units') + + def __str__(self): + """Overridden dunder method.""" + return self.name + + +class ProductStandardQuerySet(models.QuerySet): + """Product standard queryset.""" + + +class ProductStandard(models.Model): + """Product standard model.""" + + APPELLATION = 0 + WINEQUALITY = 1 + YARDCLASSIFICATION = 2 + + STANDARDS = ( + (APPELLATION, _('Appellation')), + (WINEQUALITY, _('Wine quality')), + (YARDCLASSIFICATION, _('Yard classification')), + ) + + name = models.CharField(_('name'), max_length=255) + standard_type = models.PositiveSmallIntegerField(choices=STANDARDS, + verbose_name=_('standard type')) + coordinates = gis_models.PointField( + _('Coordinates'), blank=True, null=True, default=None) + old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) + + objects = ProductStandardQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name_plural = _('wine standards') + verbose_name = _('wine standard') + + +class ProductGalleryQuerySet(models.QuerySet): + """QuerySet for model Product""" + + def main_image(self): + """Return objects with flag is_main is True""" + return self.filter(is_main=True) + + +class ProductGallery(models.Model): + product = models.ForeignKey(Product, null=True, + related_name='product_gallery', + on_delete=models.CASCADE, + verbose_name=_('product')) + image = models.ForeignKey('gallery.Image', null=True, + related_name='product_gallery', + on_delete=models.CASCADE, + verbose_name=_('gallery')) + is_main = models.BooleanField(default=False, + verbose_name=_('Is the main image')) + + objects = ProductGalleryQuerySet.as_manager() + + class Meta: + """ProductGallery meta class.""" + verbose_name = _('product gallery') + verbose_name_plural = _('product galleries') + unique_together = (('product', 'is_main'), ('product', 'image')) + + +class ProductClassificationType(models.Model): + """Product classification type.""" + + name = models.CharField(max_length=255, unique=True, + verbose_name=_('classification type')) + product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT, + null=True, default=None, + verbose_name=_('product type')) + product_sub_type = models.ForeignKey(ProductSubType, on_delete=models.PROTECT, + blank=True, null=True, default=None, + verbose_name=_('product subtype'), + help_text=_('Legacy attribute - possible_type (product type).' + 'Product type in our case is product subtype.')) + + class Meta: + """Meta class.""" + verbose_name = _('wine classification type') + verbose_name_plural = _('wine classification types') + + def __str__(self): + """Override str dunder.""" + return self.name + + +class ProductClassificationQuerySet(models.QuerySet): + """Product classification QuerySet.""" + + +class ProductClassification(models.Model): + """Product classification model.""" + + classification_type = models.ForeignKey(ProductClassificationType, on_delete=models.PROTECT, + verbose_name=_('classification type')) + standard = models.ForeignKey(ProductStandard, on_delete=models.PROTECT, + null=True, blank=True, default=None, + verbose_name=_('standard')) + tags = models.ManyToManyField('tag.Tag', related_name='product_classifications', + verbose_name=_('Tag')) + old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) + + objects = ProductClassificationQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name = _('product classification') + verbose_name_plural = _('product classifications') diff --git a/apps/product/serializers/__init__.py b/apps/product/serializers/__init__.py index c564831e..7390ae2b 100644 --- a/apps/product/serializers/__init__.py +++ b/apps/product/serializers/__init__.py @@ -1,3 +1,4 @@ from .common import * from .web import * from .mobile import * +from .back import * diff --git a/apps/product/serializers/back.py b/apps/product/serializers/back.py new file mode 100644 index 00000000..3ffe5579 --- /dev/null +++ b/apps/product/serializers/back.py @@ -0,0 +1,44 @@ +"""Product app back-office serializers.""" +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from product import models +from gallery.models import Image + + +class ProductBackOfficeGallerySerializer(serializers.ModelSerializer): + """Serializer class for model ProductGallery.""" + + class Meta: + """Meta class""" + + model = models.ProductGallery + fields = [ + 'id', + 'is_main', + ] + + def get_request_kwargs(self): + """Get url kwargs from request.""" + return self.context.get('request').parser_context.get('kwargs') + + def validate(self, attrs): + """Override validate method.""" + product_pk = self.get_request_kwargs().get('pk') + image_id = self.get_request_kwargs().get('image_id') + + product_qs = models.Product.objects.filter(pk=product_pk) + image_qs = Image.objects.filter(id=image_id) + + if not product_qs.exists(): + raise serializers.ValidationError({'detail': _('Product not found')}) + if not image_qs.exists(): + raise serializers.ValidationError({'detail': _('Image not found')}) + + product = product_qs.first() + image = image_qs.first() + + attrs['product'] = product + attrs['image'] = image + + return attrs diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 7ebfaa28..d6b22157 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -4,8 +4,8 @@ from utils.serializers import TranslatedField, FavoritesCreateSerializer from product.models import Product, ProductSubType, ProductType from utils import exceptions as utils_exceptions from django.utils.translation import gettext_lazy as _ -from location.serializers import (WineRegionBaseSerializer, WineAppellationBaseSerializer, - CountrySimpleSerializer) +from location.serializers import WineRegionBaseSerializer, CountrySimpleSerializer +from gallery.models import Image class ProductSubTypeBaseSerializer(serializers.ModelSerializer): @@ -44,7 +44,6 @@ class ProductBaseSerializer(serializers.ModelSerializer): product_type = ProductTypeBaseSerializer() subtypes = ProductSubTypeBaseSerializer(many=True) wine_region = WineRegionBaseSerializer(allow_null=True) - wine_appellation = WineAppellationBaseSerializer(allow_null=True) available_countries = CountrySimpleSerializer(source='country', many=True) class Meta: @@ -61,7 +60,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): 'subtypes', 'public_mark', 'wine_region', - 'wine_appellation', + 'standards', 'available_countries', ] @@ -93,4 +92,76 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer): 'user': self.user, 'content_object': validated_data.pop('product') }) - return super().create(validated_data) \ No newline at end of file + return super().create(validated_data) + + +# 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() +# tile_horizontal_web_url = serializers.SerializerMethodField() +# tile_horizontal_mobile_url = serializers.SerializerMethodField() +# tile_vertical_web_url = serializers.SerializerMethodField() +# highlight_vertical_web_url = serializers.SerializerMethodField() +# editor_web_url = serializers.SerializerMethodField() +# editor_mobile_url = serializers.SerializerMethodField() +# +# def get_preview_url(self, obj): +# """Get crop preview.""" +# return obj.instance.get_image_url('news_preview') +# +# def get_promo_horizontal_web_url(self, obj): +# """Get crop promo_horizontal_web.""" +# return obj.instance.get_image_url('news_promo_horizontal_web') +# +# def get_promo_horizontal_mobile_url(self, obj): +# """Get crop promo_horizontal_mobile.""" +# return obj.instance.get_image_url('news_promo_horizontal_mobile') +# +# def get_tile_horizontal_web_url(self, obj): +# """Get crop tile_horizontal_web.""" +# return obj.instance.get_image_url('news_tile_horizontal_web') +# +# def get_tile_horizontal_mobile_url(self, obj): +# """Get crop tile_horizontal_mobile.""" +# return obj.instance.get_image_url('news_tile_horizontal_mobile') +# +# def get_tile_vertical_web_url(self, obj): +# """Get crop tile_vertical_web.""" +# return obj.instance.get_image_url('news_tile_vertical_web') +# +# def get_highlight_vertical_web_url(self, obj): +# """Get crop highlight_vertical_web.""" +# return obj.instance.get_image_url('news_highlight_vertical_web') +# +# def get_editor_web_url(self, obj): +# """Get crop editor_web.""" +# return obj.instance.get_image_url('news_editor_web') +# +# def get_editor_mobile_url(self, obj): +# """Get crop editor_mobile.""" +# return obj.instance.get_image_url('news_editor_mobile') + + +class ProductImageSerializer(serializers.ModelSerializer): + """Serializer for returning crop images of product image.""" + + orientation_display = serializers.CharField(source='get_orientation_display', + read_only=True) + original_url = serializers.URLField(source='image.url') + # auto_crop_images = CropImageSerializer(source='image', allow_null=True) + + class Meta: + model = Image + fields = [ + 'id', + 'title', + 'orientation_display', + 'original_url', + # 'auto_crop_images', + ] + extra_kwargs = { + 'orientation': {'write_only': True} + } diff --git a/apps/product/transfer_data.py b/apps/product/transfer_data.py new file mode 100644 index 00000000..1e52b362 --- /dev/null +++ b/apps/product/transfer_data.py @@ -0,0 +1,157 @@ +from pprint import pprint + +from transfer import models as transfer_models +from transfer.serializers import product as product_serializers +from transfer.serializers.partner import PartnerSerializer + + +def transfer_partner(): + queryset = transfer_models.EstablishmentBacklinks.objects.filter(type="Partner") + + serialized_data = PartnerSerializer(data=list(queryset.values()), many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"News serializer errors: {serialized_data.errors}") + + +def transfer_wine_color(): + queryset = transfer_models.WineColor.objects.all() + serialized_data = product_serializers.WineColorSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"transfer_wine_color errors: {serialized_data.errors}") + + +def transfer_wine_sugar_content(): + queryset = transfer_models.WineType.objects.all() + serialized_data = product_serializers.WineTypeSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"transfer_wine_sugar_content errors: {serialized_data.errors}") + + +def transfer_wine_bottles_produced(): + raw_queryset = transfer_models.Products.objects.raw( + """ + SELECT + DISTINCT bottles_produced, + 1 as id + FROM products + WHERE bottles_produced IS NOT NULL + AND bottles_produced !=''; + """ + ) + queryset = [vars(query) for query in raw_queryset] + serialized_data = product_serializers.WineBottlesProducedSerializer( + data=queryset, + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"transfer_wine_bottles_produced errors: {serialized_data.errors}") + + +def transfer_wine_classification_type(): + raw_queryset = transfer_models.ProductClassification.objects.raw( + """ + SELECT + DISTINCT name, + 1 as id + FROM wine_classifications; + """ + ) + queryset = [vars(query) for query in raw_queryset] + serialized_data = product_serializers.WineClassificationTypeSerializer( + data=queryset, + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"transfer_classification errors: {serialized_data.errors}") + + +def transfer_wine_standard(): + queryset = transfer_models.ProductClassification.objects.filter(parent_id__isnull=True) \ + .exclude(type='Classification') + serialized_data = product_serializers.ProductStandardSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"transfer_wine_standard errors: {serialized_data.errors}") + + +def transfer_wine_classifications(): + queryset = transfer_models.ProductClassification.objects.filter(type='Classification') + serialized_data = product_serializers.ProductClassificationSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + pprint(f"transfer_wine_classifications errors: {serialized_data.errors}") + + +def transfer_product(): + queryset = transfer_models.Products.objects.all() + serialized_data = product_serializers.ProductSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + errors = [] + for d in serialized_data.errors: errors.append(d) if d else None + pprint(f"transfer_product errors: {errors}") + + +def transfer_plate(): + queryset = transfer_models.Merchandise.objects.all() + serialized_data = product_serializers.PlateSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + errors = [] + for d in serialized_data.errors: errors.append(d) if d else None + pprint(f"transfer_plates errors: {errors}") + + +def transfer_plate_image(): + queryset = transfer_models.Merchandise.objects.all() + serialized_data = product_serializers.PlateImageSerializer( + data=list(queryset.values()), + many=True) + if serialized_data.is_valid(): + serialized_data.save() + else: + errors = [] + for d in serialized_data.errors: errors.append(d) if d else None + pprint(f"transfer_plates_images errors: {errors}") + + +data_types = { + "partner": [transfer_partner], + "wine_characteristics": [ + transfer_wine_sugar_content, + transfer_wine_color, + transfer_wine_bottles_produced, + transfer_wine_classification_type, + transfer_wine_standard, + transfer_wine_classifications, + ], + "product": [ + transfer_product, + transfer_plate, + transfer_plate_image, + ], +} diff --git a/apps/product/urls/back.py b/apps/product/urls/back.py index e69de29b..3330cdd5 100644 --- a/apps/product/urls/back.py +++ b/apps/product/urls/back.py @@ -0,0 +1,13 @@ +"""Product backoffice url patterns.""" +from django.urls import path +from product.urls.common import urlpatterns as common_urlpatterns +from product import views + +urlpatterns = [ + path('/gallery/', views.ProductBackOfficeGalleryListView.as_view(), + name='gallery-list'), + path('/gallery//', views.ProductBackOfficeGalleryCreateDestroyView.as_view(), + name='gallery-create-destroy'), +] + +urlpatterns.extend(common_urlpatterns) diff --git a/apps/product/urls/common.py b/apps/product/urls/common.py index d0dbb8a9..ea75bf7d 100644 --- a/apps/product/urls/common.py +++ b/apps/product/urls/common.py @@ -7,6 +7,7 @@ app_name = 'product' urlpatterns = [ path('', views.ProductListView.as_view(), name='list'), + path('slug/', views.ProductDetailView.as_view(), name='detail'), path('slug//favorites/', views.CreateFavoriteProductView.as_view(), name='create-destroy-favorites') ] diff --git a/apps/product/views/back.py b/apps/product/views/back.py index e69de29b..42c1ea71 100644 --- a/apps/product/views/back.py +++ b/apps/product/views/back.py @@ -0,0 +1,75 @@ +"""Product app back-office views.""" +from django.conf import settings +from django.db.transaction import on_commit +from django.shortcuts import get_object_or_404 +from rest_framework import generics, status, permissions +from rest_framework.response import Response + +from gallery.tasks import delete_image +from product import serializers, models + + +class ProductBackOfficeMixinView: + """Product back-office mixin view.""" + + permission_classes = (permissions.IsAuthenticated,) + queryset = models.Product.objects.with_base_related() \ + .order_by('-created', ) + + +class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView, + generics.CreateAPIView, + generics.DestroyAPIView): + """Resource for a create gallery for product for back-office users.""" + serializer_class = serializers.ProductBackOfficeGallerySerializer + + def get_object(self): + """ + Returns the object the view is displaying. + """ + product_qs = self.filter_queryset(self.get_queryset()) + + product = get_object_or_404(product_qs, pk=self.kwargs['pk']) + gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs['image_id']) + + # May raise a permission denied + self.check_object_permissions(self.request, gallery) + + return gallery + + def create(self, request, *args, **kwargs): + """Overridden create method""" + super().create(request, *args, **kwargs) + return Response(status=status.HTTP_201_CREATED) + + def destroy(self, request, *args, **kwargs): + """Override destroy method.""" + gallery_obj = self.get_object() + if settings.USE_CELERY: + on_commit(lambda: delete_image.delay(image_id=gallery_obj.image.id, + completely=False)) + else: + on_commit(lambda: delete_image(image_id=gallery_obj.image.id, + completely=False)) + # Delete an instances of ProductGallery model + gallery_obj.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.ListAPIView): + """Resource for returning gallery for product for back-office users.""" + serializer_class = serializers.ProductImageSerializer + + def get_object(self): + """Override get_object method.""" + qs = super(ProductBackOfficeGalleryListView, self).get_queryset() + product = get_object_or_404(qs, pk=self.kwargs['pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, product) + + return product + + def get_queryset(self): + """Override get_queryset method.""" + return self.get_object().gallery.all() diff --git a/apps/product/views/common.py b/apps/product/views/common.py index 63b9677f..c8fdcd2b 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -8,6 +8,7 @@ from product import filters class ProductBaseView(generics.GenericAPIView): """Product base view""" + permission_classes = (permissions.AllowAny, ) def get_queryset(self): """Override get_queryset method.""" @@ -16,11 +17,16 @@ class ProductBaseView(generics.GenericAPIView): class ProductListView(ProductBaseView, generics.ListAPIView): """List view for model Product.""" - permission_classes = (permissions.AllowAny, ) serializer_class = serializers.ProductBaseSerializer filter_class = filters.ProductFilterSet +class ProductDetailView(ProductBaseView, generics.RetrieveAPIView): + """Detail view fro model Product.""" + lookup_field = 'slug' + serializer_class = serializers.ProductBaseSerializer + + class CreateFavoriteProductView(generics.CreateAPIView, generics.DestroyAPIView): """View for create/destroy product in favorites.""" diff --git a/apps/tag/migrations/0009_auto_20191111_0731.py b/apps/tag/migrations/0009_auto_20191111_0731.py new file mode 100644 index 00000000..f4b775b4 --- /dev/null +++ b/apps/tag/migrations/0009_auto_20191111_0731.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2019-11-11 07:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tag', '0008_auto_20191101_1244'), + ] + + operations = [ + migrations.AddField( + model_name='tag', + name='old_id', + field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'), + ), + migrations.AddField( + model_name='tag', + name='priority', + field=models.PositiveIntegerField(default=0, null=True), + ), + ] diff --git a/apps/tag/models.py b/apps/tag/models.py index 7aa0a5ec..064def73 100644 --- a/apps/tag/models.py +++ b/apps/tag/models.py @@ -34,8 +34,9 @@ class Tag(TranslatedFieldsMixin, models.Model): category = models.ForeignKey('TagCategory', on_delete=models.CASCADE, null=True, related_name='tags', verbose_name=_('Category')) - chosen_tag_settings = models.ManyToManyField(Country, through='ChosenTagSettings') + priority = models.PositiveIntegerField(null=True, default=0) + old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) objects = TagQuerySet.as_manager() diff --git a/apps/transfer/management/commands/transfer.py b/apps/transfer/management/commands/transfer.py index 24da538c..e24bc28c 100644 --- a/apps/transfer/management/commands/transfer.py +++ b/apps/transfer/management/commands/transfer.py @@ -30,6 +30,9 @@ class Command(BaseCommand): 'update_country_flag', 'comment', 'inquiries', # №6 - перенос запросов оценок + 'wine_characteristics', + 'product', + 'comment', ] def handle(self, *args, **options): diff --git a/apps/transfer/mixins.py b/apps/transfer/mixins.py index a8e8287a..fb85725e 100644 --- a/apps/transfer/mixins.py +++ b/apps/transfer/mixins.py @@ -1,5 +1,10 @@ from django.db import models from django.forms.models import model_to_dict +from rest_framework import serializers +from tag import models as tag_models +from django.conf import settings +from product.models import ProductType, ProductSubType +from django.utils.text import slugify class SecondDbManager(models.Manager): @@ -24,3 +29,68 @@ class MigrateMixin(models.Model): class Meta: abstract = True + + +class TransferSerializerMixin(serializers.ModelSerializer): + """Mixin for transferring legacy db models.""" + + def create(self, validated_data): + qs = self.Meta.model.objects.filter(**validated_data) + if not qs.exists(): + return super().create(validated_data) + + @property + def tag_category(self): + if self.CATEGORY_LABEL and self.CATEGORY_INDEX_NAME: + tag_category, _ = tag_models.TagCategory.objects.get_or_create( + index_name=self.CATEGORY_INDEX_NAME, + defaults={ + 'label': {settings.FALLBACK_LOCALE: self.CATEGORY_LABEL}, + 'value_type': tag_models.TagCategory.STRING, + 'index_name': self.CATEGORY_INDEX_NAME, + 'public': True + }) + return tag_category + + def get_vintage_year(self, vintage): + earliest_year = self.Meta.model.EARLIEST_VINTAGE_YEAR + latest_year = self.Meta.model.LATEST_VINTAGE_YEAR + if vintage: + if vintage.isdigit(): + if len(vintage) == 2: + if vintage == '16': + return 2016 + elif len(vintage) == 4: + if earliest_year < int(vintage) < latest_year: + return int(vintage) + elif vintage == '1584': + return 1984 + elif vintage == '1017': + return 2017 + elif len(vintage) == 5: + if vintage == '20115': + return 2015 + elif vintage == '20174': + return 2017 + elif vintage.endswith('er'): + return self.get_vintage_year(vintage[:-2]) + + def get_product_type(self, index_name): + if index_name: + qs = ProductType.objects.filter( + index_name__icontains=index_name) + if qs.exists(): + return qs.first() + + def get_product_sub_type(self, product_type, product_sub_type): + if not isinstance(product_type, ProductType): + product_type = self.get_product_type(product_type) + if product_type and product_sub_type: + qs = ProductSubType.objects.filter( + product_type=product_type, + index_name__icontains=product_sub_type) + if qs.exists(): + return qs.first() + + def get_slug(self, name, old_id): + return slugify(f'{name}-{old_id}') \ No newline at end of file diff --git a/apps/transfer/models.py b/apps/transfer/models.py index bbd09319..026f7a27 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -899,47 +899,76 @@ class KeyValueMetadatumKeyValueMetadatumEstablishments(MigrateMixin): db_table = 'key_value_metadatum_key_value_metadatum_establishments' -# class Products(models.Model): -# establishment = models.ForeignKey('Establishments', models.DO_NOTHING, blank=True, null=True) -# brand = models.CharField(max_length=255, blank=True, null=True) -# name = models.CharField(max_length=255, blank=True, null=True) -# vintage = models.CharField(max_length=255, blank=True, null=True) -# type = models.CharField(max_length=255, blank=True, null=True) -# unique_key = models.CharField(max_length=255, blank=True, null=True) -# price = models.FloatField(blank=True, null=True) -# average_price_in_shops = models.FloatField(blank=True, null=True) -# fra_encima_id = models.IntegerField(blank=True, null=True) -# wine_sub_region_id = models.IntegerField(blank=True, null=True) -# classification = models.ForeignKey('WineClassifications', models.DO_NOTHING, blank=True, null=True) -# wine_region = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True) -# wine_type = models.ForeignKey('WineTypes', models.DO_NOTHING, blank=True, null=True) -# wine_color = models.ForeignKey('WineColors', models.DO_NOTHING, blank=True, null=True) -# appellation = models.ForeignKey('WineClassifications', models.DO_NOTHING, blank=True, null=True) -# created_at = models.DateTimeField() -# updated_at = models.DateTimeField() -# state = models.CharField(max_length=255, blank=True, null=True) -# wine_style = models.ForeignKey('WineStyles', models.DO_NOTHING, blank=True, null=True) -# village = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True) -# vineyard = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True) -# yard_classification = models.ForeignKey('WineClassifications', models.DO_NOTHING, blank=True, null=True) -# wine_quality = models.ForeignKey('WineClassifications', models.DO_NOTHING, blank=True, null=True) -# bottles_produced = models.CharField(max_length=3000, blank=True, null=True) -# deu_import_id = models.IntegerField(blank=True, null=True) -# -# class Meta: -# managed = False -# db_table = 'products' -# -# -# class WineTypes(models.Model): -# name = models.CharField(max_length=255, blank=True, null=True) -# fra_encima_id = models.IntegerField(blank=True, null=True) -# created_at = models.DateTimeField() -# updated_at = models.DateTimeField() -# -# class Meta: -# managed = False -# db_table = 'wine_types' +class WineColor(MigrateMixin): + using = 'legacy' + + name = models.CharField(max_length=255) + order_number = models.IntegerField(null=True, blank=True) + + class Meta: + managed = False + db_table = 'wine_colors' + + +class WineType(MigrateMixin): + using = 'legacy' + + name = models.CharField(max_length=255) + + class Meta: + managed = False + db_table = 'wine_types' + + +class ProductClassification(MigrateMixin): + using = 'legacy' + + name = models.CharField(max_length=255) + desc = models.TextField() + latitude = models.FloatField(blank=True, null=True) + longitude = models.FloatField(blank=True, null=True) + type = models.CharField(max_length=255) + parent_id = models.IntegerField() + possible_type_id = models.IntegerField(null=True, blank=True) + possible_color_id = models.IntegerField(null=True, blank=True) + fra_encima_id = models.IntegerField(null=True, blank=True) + + class Meta: + managed = False + db_table = 'wine_classifications' + + +class Products(MigrateMixin): + using = 'legacy' + + establishment = models.ForeignKey('Establishments', models.DO_NOTHING, null=True) + brand = models.CharField(max_length=255, null=True) + name = models.CharField(max_length=255, null=True) + vintage = models.CharField(max_length=255, null=True) + type = models.CharField(max_length=255, null=True) + price = models.FloatField(null=True) + average_price_in_shops = models.FloatField(null=True) + wine_sub_region_id = models.IntegerField(null=True) + classification = models.ForeignKey('ProductClassification', models.DO_NOTHING, null=True, + related_name='product_classification') + wine_region = models.ForeignKey('WineLocations', models.DO_NOTHING, null=True) + wine_type = models.ForeignKey('WineType', models.DO_NOTHING, null=True) + wine_color = models.ForeignKey('WineColor', models.DO_NOTHING, null=True) + appellation = models.ForeignKey('ProductClassification', models.DO_NOTHING, null=True) + state = models.CharField(max_length=255) + village = models.ForeignKey('WineLocations', models.DO_NOTHING, null=True, + related_name='product_village') + vineyard = models.ForeignKey('WineLocations', models.DO_NOTHING, null=True, + related_name='product_vineyard') + wine_quality = models.ForeignKey('ProductClassification', models.DO_NOTHING, null=True, + related_name='product_wine_quality') + bottles_produced = models.CharField(max_length=3000, null=True) + unique_key = models.CharField(max_length=255, null=True) + + class Meta: + managed = False + db_table = 'products' + class HomePages(models.Model): using = 'legacy' @@ -1004,6 +1033,35 @@ class Identities(MigrateMixin): db_table = 'identities' +class WineLocations(MigrateMixin): + using = 'legacy' + + name = models.CharField(max_length=255) + desc = models.TextField(null=True) + latitude = models.FloatField(null=True) + longitude = models.FloatField(null=True) + type = models.CharField(max_length=255) + parent_id = models.IntegerField() + + class Meta: + managed = False + db_table = 'wine_locations' + + +class Merchandise(MigrateMixin): + using = 'legacy' + + name = models.CharField(max_length=255) + vintage = models.CharField(max_length=255) + highlighted = models.CharField(max_length=255) + site = models.ForeignKey('Sites', models.DO_NOTHING) + attachment_suffix_url = models.CharField(max_length=255) + + class Meta: + managed = False + db_table = 'merchandises' + + class Inquiries(MigrateMixin): using = 'legacy' diff --git a/apps/transfer/serializers/account.py b/apps/transfer/serializers/account.py index 5b0e1369..86b28e03 100644 --- a/apps/transfer/serializers/account.py +++ b/apps/transfer/serializers/account.py @@ -5,7 +5,7 @@ from account.models import User class UserSerializer(serializers.ModelSerializer): nickname = serializers.CharField() email = serializers.CharField() - confirmed_at = serializers.DateTimeField() + confirmed_at = serializers.DateTimeField(allow_null=True) id = serializers.CharField() class Meta: @@ -30,8 +30,11 @@ class UserSerializer(serializers.ModelSerializer): # использовать get_or_create User.objects.create(**validated_data) - def get_email_confirmed(self, obj): - return True + def get_email_confirmed(self, data): + if data.get("confirmed_at"): + return True + else: + return False def get_username(self, obj): return obj["email"] diff --git a/apps/transfer/serializers/establishment.py b/apps/transfer/serializers/establishment.py index cee07b79..f9127d6e 100644 --- a/apps/transfer/serializers/establishment.py +++ b/apps/transfer/serializers/establishment.py @@ -1,13 +1,16 @@ +from django.conf import settings from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.db import transaction from rest_framework import serializers -from establishment.models import Establishment, ContactEmail, ContactPhone, EstablishmentType +from establishment.models import Establishment, ContactEmail, ContactPhone, EstablishmentType, \ + EstablishmentSubType from location.models import Address from timetable.models import Timetable from utils.legacy_parser import parse_legacy_schedule_content from utils.serializers import TimeZoneChoiceField from utils.slug_generator import generate_unique_slug +from django.utils.text import slugify class EstablishmentSerializer(serializers.ModelSerializer): @@ -53,14 +56,16 @@ class EstablishmentSerializer(serializers.ModelSerializer): ) def validate(self, data): + old_type = data.pop('type', None) + data.update({ 'slug': generate_unique_slug(Establishment, data['slug'] if data['slug'] else data['name']), 'address_id': self.get_address(data['location']), - 'establishment_type_id': self.get_type(data), + 'establishment_type_id': self.get_type(old_type), 'is_publish': data.get('state') == 'published', + 'subtype': self.get_subtype(old_type), }) data.pop('location') - data.pop('type') data.pop('state') return data @@ -69,6 +74,7 @@ class EstablishmentSerializer(serializers.ModelSerializer): email = validated_data.pop('email') phone = validated_data.pop('phone') schedules = validated_data.pop('schedules') + subtypes = [validated_data.pop('subtype', None)] establishment = Establishment.objects.create(**validated_data) if email: @@ -86,6 +92,8 @@ class EstablishmentSerializer(serializers.ModelSerializer): for schedule in new_schedules: establishment.schedule.add(schedule) establishment.save() + if subtypes: + establishment.establishment_subtypes.add(*[i for i in subtypes if i]) return establishment @@ -97,13 +105,16 @@ class EstablishmentSerializer(serializers.ModelSerializer): return None @staticmethod - def get_type(data): + def get_type(old_type): types = { 'Restaurant': EstablishmentType.RESTAURANT, 'Shop': EstablishmentType.ARTISAN, + 'Wineyard': EstablishmentType.PRODUCER, } - obj, _ = EstablishmentType.objects.get_or_create(index_name=types[data['type']]) - return obj.id + index_name = types.get(old_type) + if index_name: + obj, _ = EstablishmentType.objects.get_or_create(index_name=index_name) + return obj.id @staticmethod def get_schedules(schedules): @@ -152,3 +163,13 @@ class EstablishmentSerializer(serializers.ModelSerializer): result.append(obj) return result + + def get_subtype(self, old_type): + if old_type == 'Wineyard': + subtype_name = 'Winery' + establishment_type_id = self.get_type(old_type) + subtype, _ = EstablishmentSubType.objects.get_or_create( + name={settings.FALLBACK_LOCALE: subtype_name}, + index_name=slugify(subtype_name), + establishment_type_id=establishment_type_id) + return subtype diff --git a/apps/transfer/serializers/location.py b/apps/transfer/serializers/location.py index 4e05897d..b3f49f3c 100644 --- a/apps/transfer/serializers/location.py +++ b/apps/transfer/serializers/location.py @@ -1,8 +1,10 @@ -from rest_framework import serializers -from location.models import Country, Region, City, Address +from django.conf import settings from django.contrib.gis.geos import Point -from django.contrib.gis.geos import GEOSGeometry -import json +from rest_framework import serializers + +from location import models +from transfer.mixins import TransferSerializerMixin +from utils.methods import get_point_from_coordinates class CountrySerializer(serializers.ModelSerializer): @@ -10,7 +12,7 @@ class CountrySerializer(serializers.ModelSerializer): id = serializers.IntegerField() class Meta: - model = Country + model = models.Country fields = ( "id", "country_code_2", @@ -27,9 +29,9 @@ class CountrySerializer(serializers.ModelSerializer): def create(self, validated_data): # Some countries already in database try: - country = Country.objects.get(code=validated_data['code']) - except Country.DoesNotExist: - country = Country.objects.create(**validated_data) + country = models.Country.objects.get(code=validated_data['code']) + except models.Country.DoesNotExist: + country = models.Country.objects.create(**validated_data) return country def get_country_code(self, obj): @@ -43,7 +45,7 @@ class RegionSerializer(serializers.ModelSerializer): id = serializers.IntegerField() class Meta: - model = Region + model = models.Region fields = ( "region_code", "country_code_2", @@ -60,9 +62,9 @@ class RegionSerializer(serializers.ModelSerializer): def create(self, validated_data): # Some regions may be already in database try: - region = Region.objects.get(old_id=validated_data['old_id']) - except Region.DoesNotExist: - region = Region.objects.create(**validated_data) + region = models.Region.objects.get(old_id=validated_data['old_id']) + except models.Region.DoesNotExist: + region = models.Region.objects.create(**validated_data) except Exception as e: raise ValueError(f"REGION ERROR: {validated_data}: {e}") return region @@ -71,7 +73,7 @@ class RegionSerializer(serializers.ModelSerializer): print(data) if "subregion_code" in data and data["subregion_code"] is not None and data["subregion_code"].strip() != "": try: - parent_region = Region.objects.filter(code=str(data['region_code'])).first() + parent_region = models.Region.objects.filter(code=str(data['region_code'])).first() except Exception as e: raise ValueError(f"Parent region error with {data}: {e}") @@ -86,7 +88,7 @@ class RegionSerializer(serializers.ModelSerializer): def set_country(self, data): try: - country = Country.objects.get(code=data['country_code_2']) + country = models.Country.objects.get(code=data['country_code_2']) except Exception as e: raise ValueError(f"Country error with {data}: {e}") @@ -110,7 +112,7 @@ class CitySerializer(serializers.ModelSerializer): id = serializers.IntegerField() class Meta: - model = City + model = models.City fields = ( "country_code_2", "region_code", @@ -130,7 +132,7 @@ class CitySerializer(serializers.ModelSerializer): return data def create(self, validated_data): - return City.objects.create(**validated_data) + return models.City.objects.create(**validated_data) def set_is_island(self, data): data['is_island'] = True if "is_island" in data \ @@ -145,19 +147,19 @@ class CitySerializer(serializers.ModelSerializer): def set_relations(self, data): try: - region = Region.objects.filter(code=data['region_code']).first() - except Region.DoesNotExist as e: + region = models.Region.objects.filter(code=data['region_code']).first() + except models.Region.DoesNotExist as e: try: - region = Region.objects.filter(code=data['subregion_code']).first() - except Region.DoesNotExist as e: + region = models.Region.objects.filter(code=data['subregion_code']).first() + except models.Region.DoesNotExist as e: raise ValueError(f"Region not found with {data}: {e}") data['region'] = region del(data['subregion_code']) try: - country = Country.objects.get(code=data['country_code_2']) - except Country.DoesNotExist as e: + country = models.Country.objects.get(code=data['country_code_2']) + except models.Country.DoesNotExist as e: raise ValueError(f"Country not found with {data}: {e}") data['country'] = country @@ -185,7 +187,7 @@ class AddressSerializer(serializers.ModelSerializer): address = serializers.CharField() class Meta: - model = Address + model = models.Address fields = ( "id", "city_id", @@ -204,7 +206,7 @@ class AddressSerializer(serializers.ModelSerializer): return data def create(self, validated_data): - return Address.objects.create(**validated_data) + return models.Address.objects.create(**validated_data) def set_old_id(self, data): data['old_id'] = data.pop("id") @@ -218,8 +220,8 @@ class AddressSerializer(serializers.ModelSerializer): def set_city(self, data): try: - city = City.objects.filter(old_id=data['city_id']).first() - except City.DoesNotExist as e: + city = models.City.objects.filter(old_id=data['city_id']).first() + except models.City.DoesNotExist as e: raise ValueError(f"City not found with {data}: {e}") data['city'] = city @@ -266,3 +268,84 @@ class AddressSerializer(serializers.ModelSerializer): del(data['longitude']) return data + + +class WineRegion(TransferSerializerMixin): + + id = serializers.IntegerField() + name = serializers.CharField() + desc = serializers.CharField(allow_null=True) + latitude = serializers.FloatField(allow_null=True) + longitude = serializers.FloatField(allow_null=True) + + class Meta: + model = models.WineRegion + fields = ( + 'id', + 'name', + 'desc', + 'latitude', + 'longitude', + ) + + def validate(self, attrs): + latitude = attrs.pop('latitude', None) + longitude = attrs.pop('longitude', None) + + attrs['old_id'] = attrs.pop('id') + attrs['description'] = {settings.FALLBACK_LOCALE: attrs.pop('desc', None)} + attrs['coordinates'] = get_point_from_coordinates(latitude, longitude) + return attrs + + +class WineSubRegion(WineRegion): + + id = serializers.IntegerField() + name = serializers.CharField() + parent_id = serializers.IntegerField() + + class Meta: + model = models.WineSubRegion + fields = ( + 'id', + 'name', + 'parent_id', + ) + + def validate(self, attrs): + parent_id = attrs.pop('parent_id', None) + + attrs['old_id'] = attrs.pop('id') + attrs['wine_region'] = self.get_wine_region(parent_id) + return attrs + + def get_wine_region(self, parent_id): + qs = models.WineRegion.objects.filter(old_id=parent_id) + if qs.exists(): + return qs.first() + + +class WineVillage(TransferSerializerMixin): + + id = serializers.IntegerField() + name = serializers.CharField() + parent_id = serializers.IntegerField() + + class Meta: + model = models.WineVillage + fields = ( + 'id', + 'name', + 'parent_id', + ) + + def validate(self, attrs): + parent_id = attrs.pop('parent_id', None) + attrs['old_id'] = attrs.pop('id') + attrs['wine_region'] = self.get_wine_region(parent_id) + return attrs + + def get_wine_region(self, parent_id): + qs = models.WineRegion.objects.filter(old_id=parent_id) + if qs.exists(): + return qs.first() diff --git a/apps/transfer/serializers/product.py b/apps/transfer/serializers/product.py new file mode 100644 index 00000000..5bd5f450 --- /dev/null +++ b/apps/transfer/serializers/product.py @@ -0,0 +1,519 @@ +from rest_framework import serializers +from django.utils.text import slugify + +from product import models +from location import models as location_models +from tag import models as tag_models +from establishment import models as establishment_models +from transfer import models as transfer_models +from utils.methods import get_point_from_coordinates +from transfer.mixins import TransferSerializerMixin +from django.conf import settings +from functools import reduce +from gallery.models import Image + + +class WineColorSerializer(TransferSerializerMixin): + CATEGORY_LABEL = 'Wine color' + CATEGORY_INDEX_NAME = slugify(CATEGORY_LABEL) + + id = serializers.IntegerField() + name = serializers.CharField(allow_null=True) + order_number = serializers.IntegerField(allow_null=True) + + class Meta: + model = tag_models.Tag + fields = ( + 'id', + 'name', + 'order_number', + ) + + def validate(self, attrs): + value = attrs.pop('name') + attrs['old_id'] = attrs.pop('id', None) + attrs['label'] = {settings.FALLBACK_LOCALE: value} + attrs['value'] = slugify(value) + attrs['priority'] = attrs.pop('order_number') + attrs['category'] = self.tag_category + return attrs + + +class WineTypeSerializer(TransferSerializerMixin): + CATEGORY_LABEL = 'Sugar content' + CATEGORY_INDEX_NAME = slugify(CATEGORY_LABEL) + + id = serializers.IntegerField() + name = serializers.CharField() + + class Meta: + model = tag_models.Tag + fields = ( + 'id', + 'name', + ) + + def validate(self, attrs): + value = attrs.pop('name') + attrs['old_id'] = attrs.pop('id', None) + attrs['label'] = {settings.FALLBACK_LOCALE: value} + attrs['value'] = slugify(value) + attrs['category'] = self.tag_category + return attrs + + +class WineBottlesProducedSerializer(TransferSerializerMixin): + CATEGORY_LABEL = 'Bottles produced' + CATEGORY_INDEX_NAME = slugify(CATEGORY_LABEL) + + bottles_produced = serializers.CharField(allow_null=True) + + class Meta: + model = tag_models.Tag + fields = ( + 'bottles_produced', + ) + + def validate(self, attrs): + value = attrs.pop('bottles_produced') + parsed_value = self.parsed_value(value) + attrs['label'] = {settings.FALLBACK_LOCALE: parsed_value} + attrs['value'] = slugify(parsed_value) + attrs['category'] = self.tag_category + return attrs + + def parsed_value(self, value): + if value.isdigit(): + return int(value) + + parted = value.split(' ') + if len(parted) > 1: + values = [int(i) for i in parted if i.isdigit()] + return reduce(lambda a, b: a + b, values) + + lowered = value.lower() + if 'magnum' in lowered: + return 1 + + return 0 + + +class ProductStandardSerializer(TransferSerializerMixin): + id = serializers.IntegerField() + name = serializers.CharField() + type = serializers.CharField(allow_null=True) + longitude = serializers.FloatField(allow_null=True) + latitude = serializers.FloatField(allow_null=True) + + class Meta: + model = models.ProductStandard + fields = ( + 'id', + 'name', + 'type', + 'longitude', + 'latitude', + ) + + def validate(self, attrs): + latitude = attrs.pop('latitude', None) + longitude = attrs.pop('longitude', None) + standard_type = attrs.pop('type', None) + + attrs['coordinates'] = get_point_from_coordinates(latitude, longitude) + attrs['old_id'] = attrs.get('id') + attrs['standard_type'] = self.get_standard_type(standard_type) + return attrs + + def get_standard_type(self, type: str): + if type == 'Appellation': + return models.ProductStandard.APPELLATION + elif type == 'YardClassification': + return models.ProductStandard.WINEQUALITY + elif type == 'WineQuality': + return models.ProductStandard.YARDCLASSIFICATION + + +class WineClassificationTypeSerializer(TransferSerializerMixin): + + name = serializers.CharField() + + class Meta: + model = models.ProductClassificationType + fields = ( + 'name', + ) + + def validate(self, attrs): + attrs['product_type'] = self.wine_type + return attrs + + @property + def wine_type(self): + qs = models.ProductType.objects.filter(index_name=models.ProductType.WINE) + if qs.exists(): + return qs.first() + + +class ProductClassificationSerializer(TransferSerializerMixin): + + id = serializers.IntegerField() + name = serializers.CharField() + possible_type_id = serializers.IntegerField(allow_null=True) + possible_color_id = serializers.IntegerField(allow_null=True) + parent_id = serializers.IntegerField(allow_null=True) + + class Meta: + model = models.ProductClassification + fields = ( + 'id', + 'name', + 'possible_type_id', + 'possible_color_id', + 'parent_id', + ) + + def validate(self, attrs): + classification_name = attrs.pop('name') + possible_type_id = attrs.pop('possible_type_id', None) + possible_color_id = attrs.pop('possible_color_id', None) + parent_id = attrs.pop('parent_id', None) + + attrs['old_id'] = attrs.pop('id') + attrs['classification_type'] = self.get_classification_type(classification_name) + attrs['possible_subtype_tag'] = self.get_possible_type_tag(possible_type_id) + attrs['possible_color_tag'] = self.get_possible_color_tag(possible_color_id) + attrs['standard'] = self.get_wine_standard(parent_id) + return attrs + + def create(self, validated_data): + possible_color_tag = validated_data.pop('possible_color_tag', None) + possible_subtype_tag = validated_data.pop('possible_subtype_tag', None) + obj, _ = self.Meta.model.objects.get_or_create(**validated_data) + if possible_color_tag: + obj.tags.add(possible_color_tag) + if possible_subtype_tag: + obj.tags.add(possible_subtype_tag) + return obj + + def get_classification_type(self, classification_name): + qs = models.ProductClassificationType.objects.filter(name__icontains=classification_name) + if qs.exists(): + return qs.first() + + def get_possible_type_tag(self, possible_type_id): + if possible_type_id: + qs = tag_models.Tag.objects.filter( + old_id=possible_type_id, + category__index_name=WineTypeSerializer.CATEGORY_INDEX_NAME) + if qs.exists(): + return qs.first() + else: + import ipdb; ipdb.set_trace() + + def get_possible_color_tag(self, possible_color_id): + if possible_color_id: + qs = tag_models.Tag.objects.filter( + old_id=possible_color_id, + category__index_name=WineColorSerializer.CATEGORY_INDEX_NAME) + if qs.exists(): + return qs.first() + + def get_wine_standard(self, parent_id): + if parent_id: + qs = models.ProductStandard.objects.filter(old_id=parent_id) + if qs.exists(): + return qs.first() + + +class ProductSerializer(TransferSerializerMixin): + + establishment_id = serializers.PrimaryKeyRelatedField( + queryset=transfer_models.Establishments.objects.all(), + allow_null=True) + classification_id = serializers.PrimaryKeyRelatedField( + queryset=transfer_models.ProductClassification.objects.all(), + allow_null=True) + wine_region_id = serializers.PrimaryKeyRelatedField( + queryset=transfer_models.WineLocations.objects.all(), + allow_null=True) + wine_type_id = serializers.PrimaryKeyRelatedField( + queryset=transfer_models.WineType.objects.all(), + allow_null=True) + wine_color_id = serializers.PrimaryKeyRelatedField( + queryset=transfer_models.WineColor.objects.all(), + allow_null=True) + appellation_id = serializers.PrimaryKeyRelatedField( + queryset=transfer_models.ProductClassification.objects.all(), + allow_null=True) + village_id = serializers.PrimaryKeyRelatedField( + queryset=transfer_models.WineLocations.objects.all(), + allow_null=True) + # same as wine_village + # vineyard_id = serializers.PrimaryKeyRelatedField( + # queryset=transfer_models.WineLocations.objects.all(), + # allow_null=True) + wine_quality_id = serializers.PrimaryKeyRelatedField( + queryset=transfer_models.ProductClassification.objects.all(), + allow_null=True) + + # same as establishment name + id = serializers.IntegerField() + brand = serializers.CharField(allow_null=True, allow_blank=True) + name = serializers.CharField(allow_null=True, allow_blank=True) + vintage = serializers.CharField(allow_null=True) + type = serializers.CharField(allow_null=True) + wine_sub_region_id = serializers.CharField(allow_null=True) + state = serializers.CharField() + bottles_produced = serializers.CharField(allow_null=True, allow_blank=True) + unique_key = serializers.CharField(allow_null=True) + + class Meta: + model = models.Product + fields = ( + 'id', + 'establishment_id', # done + 'classification_id', # done + 'wine_region_id', # done + 'wine_type_id', # done + 'wine_color_id', # done + 'appellation_id', # done + 'village_id', # done + # 'vineyard_id', + 'wine_quality_id', # done + 'brand', + 'name', # done + 'vintage', # done + 'type', # done + 'wine_sub_region_id', # done + 'state', # done + 'bottles_produced', # done + 'unique_key', # done + ) + + def validate(self, attrs): + name = attrs.pop('name', None) + establishment = attrs.pop('establishment_id', None) + brand = attrs.pop('brand', None) + classification = attrs.pop('classification_id', None) + vintage = attrs.pop('vintage', None) + product_type = attrs.pop('type') + wine_region = attrs.pop('wine_region_id', None) + wine_sub_region_id = attrs.pop('wine_sub_region_id', None) + bottles_produced = attrs.pop('bottles_produced', None) + wine_type = attrs.pop('wine_type_id', None) + wine_color = attrs.pop('wine_color_id', None) + appellation = attrs.pop('appellation_id', None) + village = attrs.pop('village_id', None) + wine_quality = attrs.pop('wine_quality_id', None) + name = self.get_name(name, brand) + old_id = attrs.pop('id') + state = self.get_state(attrs.pop('state', None)) + + attrs['old_id'] = old_id + attrs['name'] = name + attrs['old_unique_key'] = attrs.pop('unique_key') + attrs['establishment'] = self.get_establishment(establishment) + attrs['state'] = state + # attrs['brand'] = self.get_brand(brand) + attrs['wine_classification'] = self.get_wine_classification(classification) + attrs['wine_quality'] = self.get_wine_standard(wine_quality, + models.ProductStandard.WINEQUALITY) + attrs['wine_appellation'] = self.get_wine_standard(appellation, + models.ProductStandard.APPELLATION) + attrs['product_type'] = self.get_product_type(product_type) + attrs['vintage'] = self.get_vintage_year(vintage) + attrs['wine_region'] = self.get_wine_region(wine_region) + attrs['wine_sub_region'] = self.get_wine_sub_region(wine_sub_region_id) + attrs['bottles_produced_tag'] = self.get_tag(bottles_produced, + WineBottlesProducedSerializer.CATEGORY_INDEX_NAME) + attrs['wine_type_tag'] = self.get_tag(wine_type, + WineTypeSerializer.CATEGORY_INDEX_NAME) + attrs['wine_color_tag'] = self.get_tag(wine_color, + WineColorSerializer.CATEGORY_INDEX_NAME) + attrs['wine_village'] = self.get_wine_village(village) + attrs['available'] = self.get_availability(state) + attrs['slug'] = self.get_slug(name, old_id) + return attrs + + def create(self, validated_data): + qs = self.Meta.model.objects.filter(old_id=validated_data.get('old_id')) + # classifications + classifications = [validated_data.pop('wine_classification', None)] + # standards + standards = [validated_data.pop('wine_quality', None), + validated_data.pop('wine_appellation', None)] + # tags + tags = [validated_data.pop('bottles_produced_tag', None), + validated_data.pop('wine_type_tag', None), + validated_data.pop('wine_color_tag', None)] + + if not qs.exists(): + obj = super().create(validated_data) + else: + obj = qs.first() + + # adding classification + obj.classifications.add(*[i for i in classifications if i]) + # adding standard + obj.standards.add(*[i for i in standards if i]) + # adding tags + obj.tags.add(*[i for i in tags if i]) + return obj + + def get_name(self, name, brand): + if name: + return name + if brand and not name: + return brand + + def get_establishment(self, establishment): + if establishment: + qs = establishment_models.Establishment.objects.filter( + old_id=establishment.id) + if qs.exists(): + return qs.first() + + def get_state(self, state): + if state == 'published': + return models.Product.PUBLISHED + elif state == 'out_of_production': + return models.Product.OUT_OF_PRODUCTION + return models.Product.WAITING + + def get_wine_classification(self, classification): + if classification: + classification_qs = models.ProductClassification.objects.filter( + old_id=classification.id) + if classification_qs.exists(): + return classification_qs.first() + + def get_wine_region(self, wine_region): + if wine_region: + wine_region_qs = location_models.WineRegion.objects.filter( + old_id=wine_region.id) + if wine_region_qs.exists(): + return wine_region_qs.first() + + def get_wine_sub_region(self, wine_sub_region_id): + if wine_sub_region_id: + sub_region_qs = location_models.WineSubRegion.objects.filter( + old_id=wine_sub_region_id) + if sub_region_qs.exists(): + return sub_region_qs.first() + + def get_wine_village(self, village): + if village: + village_qs = location_models.WineVillage.objects.filter( + old_id=village.id) + if village_qs.exists(): + return village_qs.first() + + def get_tag(self, tag, category_index_name: str): + tag = tag.value if hasattr(tag, 'value') else tag + if tag: + qs = tag_models.Tag.objects.filter( + value=tag, + category__index_name=category_index_name) + if qs.exists(): + return qs.first() + + def get_wine_standard(self, standard, standard_type): + if standard: + if isinstance(standard, transfer_models.ProductClassification): + standard = standard.id + standard_qs = models.ProductStandard.objects.filter( + standard_type=standard_type, + old_id=standard) + if standard_qs.exists(): + return standard_qs.first() + + def get_availability(self, state: int): + if state == models.Product.PUBLISHED: + return True + return False + + +class PlateSerializer(TransferSerializerMixin): + + PRODUCT_TYPE_INDEX_NAME = 'souvenir' + PRODUCT_SUB_TYPE_INDEX_NAME = 'plate' + + id = serializers.IntegerField() + name = serializers.CharField() + vintage = serializers.CharField() + + class Meta(ProductSerializer.Meta): + fields = ( + 'id', + 'name', + 'vintage', + ) + + def validate(self, attrs): + old_id = attrs.pop('id') + name = attrs.get('name') + product_type = self.get_product_type(self.PRODUCT_TYPE_INDEX_NAME) + + attrs['old_id'] = old_id + attrs['vintage'] = self.get_vintage_year(attrs.pop('vintage')) + attrs['product_type'] = product_type + attrs['state'] = self.Meta.model.PUBLISHED + attrs['subtype'] = self.get_product_sub_type(product_type, + self.PRODUCT_SUB_TYPE_INDEX_NAME) + attrs['slug'] = self.get_slug(name, old_id) + return attrs + + def create(self, validated_data): + qs = self.Meta.model.objects.filter(old_id=validated_data.get('old_id')) + # subtypes + subtypes = [validated_data.pop('subtype', None)] + + if not qs.exists(): + obj = super().create(validated_data) + else: + obj = qs.first() + + # adding classification + obj.subtypes.add(*[i for i in subtypes if i]) + return obj + + +class PlateImageSerializer(serializers.ModelSerializer): + + id = serializers.IntegerField() + name = serializers.CharField() + attachment_suffix_url = serializers.CharField() + + class Meta: + model = models.ProductGallery + fields = ( + 'id', + 'name', + 'attachment_suffix_url', + ) + + def create(self, validated_data): + image = self.get_image(validated_data.pop('attachment_suffix_url', None), + validated_data.pop('name', None)) + product = self.get_product(validated_data.pop('id')) + + if product and image: + obj, created = models.ProductGallery.objects.get_or_create( + product=product, + image=image, + is_main=True) + return obj + + def get_image(self, image_url, name): + if image_url: + obj, created = Image.objects.get_or_create( + title=name, + image=image_url) + return obj if created else None + + def get_product(self, product_id): + if product_id: + product_qs = models.Product.objects.filter(old_id=product_id) + if product_qs.exists(): + return product_qs.first() diff --git a/apps/utils/methods.py b/apps/utils/methods.py index ed4fe7bc..dc027317 100644 --- a/apps/utils/methods.py +++ b/apps/utils/methods.py @@ -7,6 +7,7 @@ import string import requests from django.conf import settings from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.geos import Point from django.http.request import HttpRequest from django.utils.timezone import datetime from rest_framework import status @@ -118,3 +119,8 @@ def absolute_url_decorator(func): else: return url_path return get_absolute_image_url + + +def get_point_from_coordinates(latitude: str, longitude: str): + if latitude and longitude: + return Point(x=longitude, y=latitude, srid=4326) diff --git a/docker-compose.mysql.yml b/docker-compose.mysql.yml index 4d4e553e..c325fce1 100644 --- a/docker-compose.mysql.yml +++ b/docker-compose.mysql.yml @@ -14,8 +14,7 @@ services: volumes: - .:/code - gm-mysql_db:/var/lib/mysql - networks: - - mysql_network + # PostgreSQL database db: @@ -31,8 +30,7 @@ services: - "5436:5432" volumes: - gm-db:/var/lib/postgresql/data/ - networks: - - database_network + elasticsearch: image: elasticsearch:7.3.1 @@ -46,14 +44,12 @@ services: - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - discovery.type=single-node - xpack.security.enabled=false - networks: - - elasticsearch_network + # Redis redis: image: redis:alpine - networks: - - redis_network + # Celery worker: @@ -71,8 +67,7 @@ services: links: - db - redis - networks: - - redis_network + worker_beat: build: . @@ -89,8 +84,7 @@ services: links: - db - redis - networks: - - redis_network + # App: G&M gm_app: @@ -115,11 +109,7 @@ services: - gm-media:/media-data ports: - "8000:8000" - networks: - - redis_network - - database_network - - mysql_network - - elasticsearch_network + volumes: gm-mysql_db: @@ -130,12 +120,3 @@ volumes: name: gm-media gm-esdata: -networks: - database_network: - driver: bridge - mysql_network: - driver: bridge - elasticsearch_network: - driver: bridge - redis_network: - driver: bridge diff --git a/project/settings/base.py b/project/settings/base.py index 4352590f..ad96a2fe 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -64,7 +64,7 @@ PROJECT_APPS = [ 'news.apps.NewsConfig', 'notification.apps.NotificationConfig', 'partner.apps.PartnerConfig', - # 'product.apps.ProductConfig', Uncomment after refining task and create migrations + 'product.apps.ProductConfig', 'recipe.apps.RecipeConfig', 'search_indexes.apps.SearchIndexesConfig', 'translation.apps.TranslationConfig', @@ -74,9 +74,7 @@ PROJECT_APPS = [ 'comment.apps.CommentConfig', 'favorites.apps.FavoritesConfig', 'rating.apps.RatingConfig', - 'transfer.apps.TransferConfig', 'tag.apps.TagConfig', - 'product.apps.ProductConfig', ] EXTERNAL_APPS = [ @@ -157,16 +155,6 @@ DATABASES = { 'HOST': os.environ.get('DB_HOSTNAME'), 'PORT': os.environ.get('DB_PORT'), }, - # 'legacy': { - # 'ENGINE': 'django.db.backends.mysql', - # # 'HOST': '172.17.0.1', - # # 'HOST': '172.23.0.1', - # 'HOST': 'mysql_db', - # 'PORT': 3306, - # 'NAME': 'dev', - # 'USER': 'dev', - # 'PASSWORD': 'octosecret123' - # } } diff --git a/project/settings/development.py b/project/settings/development.py index fd832329..b9203259 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -38,18 +38,9 @@ sentry_sdk.init( integrations=[DjangoIntegration()] ) -# Database -# https://docs.djangoproject.com/en/2.2/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': os.environ.get('DB_NAME'), - 'USER': os.environ.get('DB_USERNAME'), - 'PASSWORD': os.environ.get('DB_PASSWORD'), - 'HOST': os.environ.get('DB_HOSTNAME'), - 'PORT': os.environ.get('DB_PORT'), - }, +# DATABASE +DATABASES.update({ 'legacy': { 'ENGINE': 'django.db.backends.mysql', 'HOST': os.environ.get('MYSQL_HOSTNAME'), @@ -57,9 +48,13 @@ DATABASES = { 'NAME': os.environ.get('MYSQL_DATABASE'), 'USER': os.environ.get('MYSQL_USER'), 'PASSWORD': os.environ.get('MYSQL_PASSWORD') - } -} + }}) + + +# INSTALLED APPS +INSTALLED_APPS.append('transfer.apps.TransferConfig') + BROKER_URL = 'redis://localhost:6379/1' CELERY_RESULT_BACKEND = BROKER_URL -CELERY_BROKER_URL = BROKER_URL \ No newline at end of file +CELERY_BROKER_URL = BROKER_URL diff --git a/project/settings/local.py b/project/settings/local.py index b1775ebd..62298faa 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -35,6 +35,21 @@ MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION) THUMBNAIL_DEBUG = True +# ADDED TRANSFER APP +INSTALLED_APPS.append('transfer.apps.TransferConfig') + + +# DATABASES +DATABASES.update({ + 'legacy': { + 'ENGINE': 'django.db.backends.mysql', + 'HOST': 'mysql_db', + 'PORT': 3306, + 'NAME': 'dev', + 'USER': 'dev', + 'PASSWORD': 'octosecret123'}}) + + # LOGGING LOGGING = { 'version': 1, @@ -73,28 +88,6 @@ LOGGING = { } - -DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': os.environ.get('DB_NAME'), - 'USER': os.environ.get('DB_USERNAME'), - 'PASSWORD': os.environ.get('DB_PASSWORD'), - 'HOST': os.environ.get('DB_HOSTNAME'), - 'PORT': os.environ.get('DB_PORT'), - }, - 'legacy': { - 'ENGINE': 'django.db.backends.mysql', - # 'HOST': '172.17.0.1', - # 'HOST': '172.23.0.1', - 'HOST': 'mysql_db', - 'PORT': 3306, - 'NAME': 'dev', - 'USER': 'dev', - 'PASSWORD': 'octosecret123' - } -} - # ELASTICSEARCH SETTINGS ELASTICSEARCH_DSL = { 'default': { @@ -107,6 +100,7 @@ ELASTICSEARCH_INDEX_NAMES = { 'search_indexes.documents.establishment': 'local_establishment', } + TESTING = sys.argv[1:2] == ['test'] if TESTING: ELASTICSEARCH_INDEX_NAMES = {} diff --git a/project/settings/production.py b/project/settings/production.py index a531a2ae..fe8618dd 100644 --- a/project/settings/production.py +++ b/project/settings/production.py @@ -40,19 +40,6 @@ sentry_sdk.init( integrations=[DjangoIntegration()] ) -# Database -# https://docs.djangoproject.com/en/2.2/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.contrib.gis.db.backends.postgis', - 'NAME': os.environ.get('DB_NAME'), - 'USER': os.environ.get('DB_USERNAME'), - 'PASSWORD': os.environ.get('DB_PASSWORD'), - 'HOST': os.environ.get('DB_HOSTNAME'), - 'PORT': os.environ.get('DB_PORT'), - }, -} BROKER_URL = 'redis://redis:6379/1' CELERY_RESULT_BACKEND = BROKER_URL diff --git a/project/urls/back.py b/project/urls/back.py index 40b3415a..8e983ebc 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -6,10 +6,12 @@ urlpatterns = [ path('account/', include('account.urls.back')), path('comment/', include('comment.urls.back')), path('establishments/', include('establishment.urls.back')), + path('collections/', include('collection.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')), + path('products/', include(('product.urls.back', 'product'), namespace='product')), ] diff --git a/requirements/base.txt b/requirements/base.txt index 38c68179..a1408fc5 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -43,7 +43,6 @@ django-storages==1.7.2 sorl-thumbnail==12.5.0 -# mysqlclient==1.4.4 PyYAML==5.1.2 # temp solution