Init Django-SHOP

This commit is contained in:
2021-02-23 22:57:30 +00:00
parent 8842939839
commit b4a79a6890
109 changed files with 5734 additions and 160 deletions

View File

View File

@@ -0,0 +1,37 @@
from django.core.management.base import BaseCommand
from cmsplugin_cascade.icon.utils import zipfile, unzip_archive
class Command(BaseCommand):
help = "Iterates over all files in Filer and creates an IconFont for all eligibles."
def handle(self, verbosity, *args, **options):
self.verbosity = verbosity
self.assign_files_to_iconfonts()
def assign_files_to_iconfonts(self):
from filer.models.filemodels import File
from cmsplugin_cascade.models import IconFont
for file in File.objects.all():
if file.label != 'Font Awesome':
continue
if self.verbosity >= 2:
self.stdout.write(
"Creating Icon Font from: {}".format(file.label))
try:
zip_ref = zipfile.ZipFile(file.file, 'r')
except zipfile.BadZipFile as exc:
self.stderr.write(
"Unable to unpack {}: {}".format(file.label, exc))
else:
if not IconFont.objects.filter(zip_file=file).exists():
font_folder, config_data = unzip_archive(
file.label, zip_ref)
IconFont.objects.create(
identifier=config_data['name'],
config_data=config_data,
zip_file=file,
font_folder=font_folder,
)
zip_ref.close()

View File

@@ -0,0 +1,41 @@
from cms.models.static_placeholder import StaticPlaceholder
from django.core.management.base import BaseCommand
from cmsplugin_cascade.models import CascadeClipboard
from shop.management.utils import deserialize_to_placeholder
class Command(BaseCommand):
help = "Iterates over all files in Filer and creates an IconFont for all eligibles."
def handle(self, verbosity, *args, **options):
self.verbosity = verbosity
self.create_social_icons()
def create_social_icons(self):
from cms.utils.i18n import get_public_languages
default_language = get_public_languages()[0]
try:
clipboard = CascadeClipboard.objects.get(identifier='social-icons')
except CascadeClipboard.DoesNotExist:
self.stderr.write(
"No Persisted Clipboard named 'social-icons' found.")
else:
static_placeholder = StaticPlaceholder.objects.create(
code='Social Icons')
deserialize_to_placeholder(
static_placeholder.public, clipboard.data, default_language)
deserialize_to_placeholder(
static_placeholder.draft, clipboard.data, default_language)
self.stdout.write("Added Social Icons to Static Placeholder")
def publish_in_all_languages(self, page):
from cms.api import copy_plugins_to_language
from cms.utils.i18n import get_public_languages
languages = get_public_languages()
for language in languages[1:]:
copy_plugins_to_language(page, languages[0], language)
for language in languages:
page.publish(language)

View File

@@ -0,0 +1,64 @@
import os
import requests
from io import BytesIO as StringIO
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
try:
import czipfile as zipfile
except ImportError:
import zipfile
from .spinner import Spinner
class Command(BaseCommand):
version = 18
help = "Download workdir to run a demo of django-SHOP."
download_url = 'https://downloads.django-shop.org/django-shop-workdir-{version}.zip'
def add_arguments(self, parser):
parser.add_argument(
'--noinput',
'--no-input',
action='store_false',
dest='interactive',
default=True,
help="Do NOT prompt the user for input of any kind.",
)
def set_options(self, **options):
self.interactive = options['interactive']
def handle(self, verbosity, *args, **options):
self.set_options(**options)
fixture1 = '{workdir}/fixtures/products-media.json'.format(
workdir=settings.WORK_DIR)
fixture2 = '{workdir}/fixtures/products-meta.json'.format(
workdir=settings.WORK_DIR)
if os.path.isfile(fixture1) or os.path.isfile(fixture2):
if self.interactive:
mesg = """
This will overwrite your workdir for your django-SHOP demo.
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel:
"""
if input(mesg) != 'yes':
raise CommandError(
"Downloading workdir has been cancelled.")
else:
self.stdout.write(self.style.WARNING(
"Can not override downloaded data in input-less mode."))
return
extract_to = os.path.join(settings.WORK_DIR, os.pardir)
msg = "Downloading workdir and extracting to {}. Please wait"
self.stdout.write(msg.format(extract_to), ending=' ')
with Spinner():
download_url = self.download_url.format(version=self.version)
response = requests.get(download_url, stream=True)
zip_ref = zipfile.ZipFile(StringIO(response.content))
try:
zip_ref.extractall(extract_to)
finally:
zip_ref.close()
self.stdout.write('- done.')

View File

@@ -0,0 +1,35 @@
import json
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand
from django.utils.module_loading import import_string
from django.utils.translation import activate
from weirdlittleempire.models import Product
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'-o',
'--output',
type=str,
dest='filename',
)
def handle(self, verbosity, filename, *args, **options):
activate(settings.LANGUAGE_CODE)
data = []
for product in Product.objects.all():
ProductModel = ContentType.objects.get(
app_label='weirdlittleempire', model=product.product_model)
class_name = 'weirdlittleempire.management.serializers.' + \
ProductModel.model_class().__name__ + 'Serializer'
serializer_class = import_string(class_name)
serializer = serializer_class(product, context={'request': None})
data.append(serializer.data)
dump = json.dumps(data, indent=2)
if filename:
with open(filename, 'w') as fh:
fh.write(dump)
else:
self.stdout.write(dump)

View File

@@ -0,0 +1,84 @@
import json
import os
from cms.utils.copy_plugins import copy_plugins_to
from cms.utils.i18n import get_public_languages
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.management.base import BaseCommand, CommandError
from django.utils.module_loading import import_string
from django.utils.translation import activate
from cmsplugin_cascade.models import CascadeClipboard
from shop.management.utils import deserialize_to_placeholder
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'filename',
nargs='?',
default='products-meta.json',
)
def handle(self, verbosity, filename, *args, **options):
activate(settings.LANGUAGE_CODE)
path = self.find_fixture(filename)
if verbosity >= 2:
self.stdout.write("Importing products from: {}".format(path))
with open(path, 'r') as fh:
data = json.load(fh)
for number, product in enumerate(data, 1):
product_model = product.pop('product_model')
if product.get('product_code') == "parklake-springfield":
continue
ProductModel = ContentType.objects.get(
app_label='weirdlittleempire', model=product_model)
class_name = 'weirdlittleempire.management.serializers.' + \
ProductModel.model_class().__name__ + 'Serializer'
serializer_class = import_string(class_name)
serializer = serializer_class(data=product)
assert serializer.is_valid(), serializer.errors
instance = serializer.save()
self.assign_product_to_catalog(instance)
self.stdout.write("{}. {}".format(number, instance))
if product_model == 'commodity':
languages = get_public_languages()
try:
clipboard = CascadeClipboard.objects.get(
identifier=instance.slug)
except CascadeClipboard.DoesNotExist:
pass
else:
deserialize_to_placeholder(
instance.placeholder, clipboard.data, languages[0])
plugins = list(instance.placeholder.get_plugins(
language=languages[0]).order_by('path'))
for language in languages[1:]:
copy_plugins_to(
plugins, instance.placeholder, language)
def find_fixture(self, filename):
if os.path.isabs(filename):
fixture_dirs = [os.path.dirname(filename)]
fixture_name = os.path.basename(filename)
else:
fixture_dirs = settings.FIXTURE_DIRS
if os.path.sep in os.path.normpath(filename):
fixture_dirs = [os.path.join(dir_, os.path.dirname(filename))
for dir_ in fixture_dirs]
fixture_name = os.path.basename(filename)
else:
fixture_name = filename
for fixture_dir in fixture_dirs:
path = os.path.join(fixture_dir, fixture_name)
if os.path.exists(path):
return path
raise CommandError(
"No such file in any fixture dir: {}".format(filename))
def assign_product_to_catalog(self, product):
from cms.models.pagemodel import Page
from shop.models.related import ProductPageModel
for page in Page.objects.published().filter(application_urls='CatalogListApp'):
ProductPageModel.objects.get_or_create(page=page, product=product)

View File

@@ -0,0 +1,26 @@
import random
from django.core.management.base import BaseCommand
from weirdlittleempire.models import Commodity, CommodityInventory, SmartCard, SmartCardInventory, SmartPhoneVariant, SmartPhoneInventory
class Command(BaseCommand):
help = "Create Inventories for all products using random values."
def handle(self, verbosity, *args, **options):
self.verbosity = verbosity
for commodity in Commodity.objects.all():
CommodityInventory.objects.create(
product=commodity,
quantity=random.randint(0, 15)
)
for smart_card in SmartCard.objects.all():
SmartCardInventory.objects.create(
product=smart_card,
quantity=random.randint(0, 55)
)
for smart_phone in SmartPhoneVariant.objects.all():
SmartPhoneInventory.objects.create(
product=smart_phone,
quantity=random.randint(0, 8)
)
self.stdout.write("Created inventories with random quantities.")

View File

@@ -0,0 +1,104 @@
import os
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command
from django.utils.translation import ugettext_lazy as _
try:
import czipfile as zipfile
except ImportError:
import zipfile
class Command(BaseCommand):
help = _("Initialize the workdir to run the demo of weirdlittleempire.")
def add_arguments(self, parser):
parser.add_argument(
'--noinput', '--no-input',
action='store_false',
dest='interactive',
default=True,
help="Do NOT prompt the user for input of any kind.",
)
def set_options(self, **options):
self.interactive = options['interactive']
def clear_compressor_cache(self):
from django.core.cache import caches
from django.core.cache.backends.base import InvalidCacheBackendError
from compressor.conf import settings
cache_dir = os.path.join(settings.STATIC_ROOT,
settings.COMPRESS_OUTPUT_DIR)
if settings.COMPRESS_ENABLED is False or not os.path.isdir(cache_dir) or os.listdir(cache_dir) != []:
return
try:
caches['compressor'].clear()
except InvalidCacheBackendError:
pass
def handle(self, verbosity, *args, **options):
self.set_options(**options)
self.clear_compressor_cache()
call_command('migrate')
initialize_file = os.path.join(settings.WORK_DIR, '.initialize')
if os.path.isfile(initialize_file):
self.stdout.write("Initializing project weirdlittleempire")
call_command('makemigrations', 'weirdlittleempire')
call_command('migrate')
os.remove(initialize_file)
call_command('loaddata', 'skeleton')
call_command('shop', 'check-pages', add_recommended=True)
call_command('assign_iconfonts')
call_command('create_social_icons')
call_command('download_workdir', interactive=self.interactive)
call_command('loaddata', 'products-media')
call_command('import_products')
self.create_polymorphic_subcategories()
try:
call_command('search_index', action='rebuild', force=True)
except:
pass
else:
self.stdout.write("Project weirdlittleempire already initialized")
call_command('migrate')
def create_polymorphic_subcategories(self):
from cms.models.pagemodel import Page
from shop.management.commands.shop import Command as ShopCommand
from weirdlittleempire.models import Commodity, SmartCard, SmartPhoneModel
apphook = ShopCommand.get_installed_apphook('CatalogListCMSApp')
catalog_pages = Page.objects.drafts().filter(
application_urls=apphook.__class__.__name__)
assert catalog_pages.count() == 1, "There should be only one catalog page"
self.create_subcategory(
apphook, catalog_pages.first(), "Earphones", Commodity)
self.create_subcategory(
apphook, catalog_pages.first(), "Smart Cards", SmartCard)
self.create_subcategory(
apphook, catalog_pages.first(), "Smart Phones", SmartPhoneModel)
def create_subcategory(self, apphook, parent_page, title, product_type):
from cms.api import create_page
from cms.constants import TEMPLATE_INHERITANCE_MAGIC
from cms.utils.i18n import get_public_languages
from shop.management.commands.shop import Command as ShopCommand
from shop.models.product import ProductModel
from shop.models.related import ProductPageModel
language = get_public_languages()[0]
page = create_page(
title,
TEMPLATE_INHERITANCE_MAGIC,
language,
apphook=apphook,
created_by="manage.py initialize_shop_demo",
in_navigation=True,
parent=parent_page,
)
ShopCommand.publish_in_all_languages(page)
page = page.get_public_object()
for product in ProductModel.objects.instance_of(product_type):
ProductPageModel.objects.create(page=page, product=product)

View File

@@ -0,0 +1,37 @@
import sys
import time
import threading
class Spinner:
busy = False
delay = 0.1
@staticmethod
def spinning_cursor():
while 1:
for cursor in '|/-\\':
yield cursor
def __init__(self, delay=None):
self.spinner_generator = self.spinning_cursor()
if delay and float(delay):
self.delay = delay
def spinner_task(self):
while self.busy:
sys.stdout.write(next(self.spinner_generator))
sys.stdout.flush()
time.sleep(self.delay)
sys.stdout.write('\b')
sys.stdout.flush()
def __enter__(self):
self.busy = True
threading.Thread(target=self.spinner_task).start()
def __exit__(self, exception, value, tb):
self.busy = False
time.sleep(self.delay)
if exception is not None:
return False

View File

@@ -0,0 +1,68 @@
"""
These serializers are used exclusively to import the file ``workdir/fixtures/products-meta.json``.
They are not intended for general purpose and can be deleted thereafter.
"""
from rest_framework import serializers
from shop.serializers.catalog import CMSPagesField, ImagesField, ValueRelatedField
from weirdlittleempire.models import (Commodity, SmartCard, SmartPhoneModel, SmartPhoneVariant,
Manufacturer, OperatingSystem, ProductPage, ProductImage)
from .translation import TranslatedFieldsField, TranslatedField, TranslatableModelSerializerMixin
class ProductSerializer(serializers.ModelSerializer):
product_model = serializers.CharField(read_only=True)
manufacturer = ValueRelatedField(model=Manufacturer)
images = ImagesField()
caption = TranslatedField()
cms_pages = CMSPagesField()
class Meta:
exclude = ['id', 'polymorphic_ctype', 'updated_at']
def create(self, validated_data):
cms_pages = validated_data.pop('cms_pages')
images = validated_data.pop('images')
product = super().create(validated_data)
for page in cms_pages:
ProductPage.objects.create(product=product, page=page)
for image in images:
ProductImage.objects.create(product=product, image=image)
return product
class CommoditySerializer(TranslatableModelSerializerMixin, ProductSerializer):
class Meta(ProductSerializer.Meta):
model = Commodity
exclude = ['id', 'placeholder', 'polymorphic_ctype', 'updated_at']
class SmartCardSerializer(TranslatableModelSerializerMixin, ProductSerializer):
multilingual = TranslatedFieldsField(
help_text="Helper to convert multilingual data into single field.",
)
class Meta(ProductSerializer.Meta):
model = SmartCard
class SmartphoneVariantSerializer(serializers.ModelSerializer):
class Meta:
model = SmartPhoneVariant
fields = ['product_code', 'unit_price', 'storage', 'quantity']
class SmartPhoneModelSerializer(TranslatableModelSerializerMixin, ProductSerializer):
multilingual = TranslatedFieldsField()
operating_system = ValueRelatedField(model=OperatingSystem)
variants = SmartphoneVariantSerializer(many=True)
class Meta(ProductSerializer.Meta):
model = SmartPhoneModel
def create(self, validated_data):
variants = validated_data.pop('variants')
product = super().create(validated_data)
for variant in variants:
SmartPhoneVariant.objects.create(product=product, **variant)
return product

View File

@@ -0,0 +1,73 @@
"""
These serializer mixinins and fields are used exclusively to import the file
``workdir/fixtures/products-meta.json``. They are not intended for general
purpose and can be deleted thereafter.
"""
from rest_framework import serializers
class TranslatableModelSerializerMixin:
"""
Pseudo class mimicking the behaviour of :class:`parler_rest.TranslatableModelSerializerMixin`.
It converts the content for fields of type TranslatedFieldsField to simple serializer
fields.
"""
def to_internal_value(self, data):
data = self._unify_translated_data(data)
result = super().to_internal_value(data)
return result
def _unify_translated_data(self, data):
"""
Unify translated data to be used by simple serializer fields.
"""
for field_name, field in self.get_fields().items():
if isinstance(field, TranslatedFieldsField):
key = field.source or field_name
translations = data.pop(key, None)
if isinstance(translations, dict):
data.update(translations.get('en', {}))
return data
class TranslatedFieldsField(serializers.Field):
"""
Pseudo class mimicking the behaviour of :class:`parler_rest.TranslatedFieldsField`, where only
the English translation is used.
"""
def to_representation(self, value):
raise NotImplementedError(
"If USE_I18N is False, do not use {cls}.to_representation() for field '{field_name}'. "
"It thwarts the possibility to reuse that string in a multi language environment.".format(
cls=self.__class__.__name__,
field_name=self.field_name,
)
)
def validate_empty_values(self, data):
raise serializers.SkipField()
class TranslatedField(serializers.Field):
"""
Pseudo class mimicking the behaviour of :class:`parler_rest.TranslatedField`, where only
the English translation is used.
"""
def to_representation(self, value):
raise NotImplementedError(
"If USE_I18N is False, do not use {cls}.to_representation() for field '{field_name}'. "
"It thwarts the possibility to reuse that string in a multi language environment.".format(
cls=self.__class__.__name__,
field_name=self.field_name,
)
)
def to_internal_value(self, data):
return data.get('en')
__all__ = ['TranslatedFieldsField', 'TranslatedField',
'TranslatableModelSerializerMixin']