import random import hashlib from pathlib import Path from typing import Literal import pycountry from starlette.datastructures import URL from app.db import ( check_product_availability, get_docs_by_category, get_product_by_product_id, get_products_id_db, ) from app.parser import read_file from app.schema import ( CartSession, CheckoutItem, CheckoutItemUpdate, DocumentBlock, DocumentCheckout, DocumentProduct, Settings, ) from app.serializer import typeset_order_content, typeset_order_shipping_info from slugify import slugify def prepare_product(doc: DocumentProduct) -> dict[str, list[DocumentBlock]]: """Re-shape DocumentProductBlocks to the layout needs of ./templates/product.html """ blocks = { "texts": [], "images": [], } for block in doc.blocks: if block.type == "text": blocks["texts"].append(block) elif block.type == "image": blocks["images"].append(block) return blocks def prepare_related_products( docs: list[DocumentProduct], product_id: str ) -> list[DocumentProduct]: """Given a list of DocumentProduct coming from .//, remove the product index page and pick a random selection of 5 products to display in the Related Products section. """ random.seed() def generate_random_index(length): for i in range(0, length): N = 1 + random.randrange(3) for j in range(0, N): random_idx = random.randrange(length - j) return random_idx products = [] for doc in docs: if doc.meta.path not in products: if ( Path(doc.meta.path).stem != product_id and doc.meta.template == "product" ): doc.blocks = prepare_product(doc) # check if there's at least 1 image before adding this # item to the list of related products if len(doc.blocks["images"]) > 0: products.append(doc) random_selection = [] products_length = len(products) for i in range(0, products_length): if len(random_selection) == 5: break idx = generate_random_index(products_length) if products[idx] not in random_selection: random_selection.append(products[idx]) return random_selection def prepare_checkout( cart: CartSession, settings: Settings, doc: DocumentCheckout, product_id: str = None, js_update: bool = False, ) -> dict[str, list[str] | dict[str, str | int] | DocumentCheckout]: """Return dictionary with necessary data to display the Checkout view: - list of countries for final checkout form - list of checkout items, including whether they're still available (eg. meanwhile somebody else might have bought any of them); we update the cart itself while doing this to minimize the number of duplicate operations that otherwise would happen in a different yet similar function - checkout document """ # -- prepare list of countries countries = [country.name for country in pycountry.countries] # -- prepare checkout items and update cart show_checkout = [] # map over each item in the cart and create a new item object products = get_products_id_db(settings) checkout_items = [] for item in cart.items.values(): product = get_product_by_product_id(settings, item.product_id, products) checkout_item = { "path": product.meta.path, "title": product.meta.title, "image": {"url": "", "width": 0}, "product_id": item.product_id, "options": item.options, "price": item.price, "weight": product.meta.weight, "quantity": item.quantity, "total": item.price * item.quantity, "availability": False, "is_selectable": False, } item_image = [block for block in product.blocks if block.type == "image"] if len(item_image) > 0: item_image = item_image[0] checkout_item["image"]["url"] = item_image.url checkout_item["image"]["width"] = item_image.info.width checkout_item["image"]["height"] = item_image.info.height triplet = check_product_availability( product, Path(checkout_item["path"]).stem, checkout_item["quantity"], checkout_item["options"].size, checkout_item["options"].style, settings, ) is_product_available, is_product_selectable, inventory_amount = triplet show_checkout.append(is_product_available) checkout_item["availability"] = is_product_available checkout_item["is_selectable"] = is_product_selectable if js_update: checkout_item = CheckoutItemUpdate(**checkout_item) else: checkout_item = CheckoutItem(**checkout_item) checkout_items.append(checkout_item) return { "countries": countries, "checkout_items": checkout_items, "show_checkout": any(show_checkout), "doc": doc, } def prepare_sets_for_menu(settings: Settings) -> list[dict[str, str]]: """Get Lookbook pages and return only dict with title and path for each doc. """ lookbooks = get_docs_by_category("lookbook", settings) sets = [ {"title": lookbook.meta.title, "path": lookbook.meta.path} for lookbook in lookbooks ] return sets def prepare_email_order(order_id: str, settings: dict[str, list[str]]) -> dict[str]: """Prepare data for email order.""" filename = "orders" orders = read_file( settings["git_repo"], filename, settings["document_match"], settings["block_types"], "", ) order = [order for order in orders.blocks if order.order_id == order_id] if len(order) > 0: order = order[0] content = typeset_order_content( order.items, order.currency, order.subtotal, order.shipping, order.total ) shipping_info = typeset_order_shipping_info(order.shipping_info) return { "order_id": order.order_id, "content": content, "payment_provider": order.payment_provider, "payment_reference": order.payment_reference, "shipping_info": shipping_info, "email": order.shipping_info.email, } def make_srcset_url( selected_size_width: int, img_width: int, fileparent, parent, p, BASE_URL: str, size_attr: bool, ): """Return a srcset URL in the format.""" # check if img width is bigger than given srcset value # (eg. avoid to produce a srcset rule for an actual image file # that we did not produce as a thumbnail) if img_width > selected_size_width: # prepare a string for each srcset rule filename_new = f"{fileparent}/{Path(parent).stem}__{p.stem}_{selected_size_width}{p.suffix}" if size_attr: filename_new = f"{filename_new} {selected_size_width}w" new_url = f"{BASE_URL}{filename_new}" return new_url def to_srcset( url, template: Literal["main", "product-grid", "product", "page", "checkout"], img_width: int | None, parent: str, size: int | None = None, size_attr: bool = True, ) -> str: """Generate correct srcset-style URL to send to the backend and get back the desired resized image. Eg: => path/to/image.jpg path/to/image_w.jpg URL can also be a str in the format => . """ SIZES = { "main": [ (320,), (640,), (760,), (1024,), (1366,), (1600,), (1920,), (2400,), (2880,), (3840,), ], "product-grid": [ (320,), (640,), (380,), (760,), (256,), (512,), (340,), (683,), (400,), (800,), (480,), (960,), (460,), (920,), (560,), (1120,), ], "product": [ (320,), (640,), (760,), (1024,), (1200,), (1366,), (1920,), (2400,), ], "page": [ (320,), (640,), (760,), (1024,), (1366,), (1600,), (1920,), (2400,), ], "checkout": [(120, 120), (240, 240), (220, 220), (440, 440)], } if type(url) is URL: BASE_URL = f"{url.scheme}://{url.netloc}" # eg. /media/AJ5.jpg => /media/AJ5_w.jpg tokens = url.path.split("/") fileparent = "/".join(tokens[:-1]) filename = tokens[-1] p = Path(filename) template_sizes = SIZES[template] if size: selected_size_width = template_sizes[size][0] if img_width: return make_srcset_url( selected_size_width, img_width, fileparent, parent, p, BASE_URL, size_attr, ) else: return "" # -- srcset_list = [] for selected_size in template_sizes: selected_size_width = selected_size[0] if img_width: new_url = make_srcset_url( selected_size_width, img_width, fileparent, parent, p, BASE_URL, size_attr, ) srcset_list.append(new_url) # combine all srcset rules into one string srcset_list = [srcset for srcset in srcset_list if srcset] srcset_matrix = ",\n".join(srcset_list) return srcset_matrix elif type(url) is str: # sometimes if writing HTML wrongly for the jinja2 template # (?) starlette return a url string and not a url object. in # this case just return the url string as-is. return url def assets_hashing(resource: str) -> str: """ Append the hash of 's modified time to the given , to make the web browser fetch the latest version of . Eg. soft cache-invalidation. => /-. => /styles-.css """ fp = Path(f"{Path(__file__).parent.parent}/{resource.path}") filepath = Path(resource.path) if fp.exists: mtime = fp.stat().st_mtime # hash mtime with sha256 hash_mtime = hashlib.sha256(b"{mtime}") hex_digest = hash_mtime.hexdigest() return f"{filepath.parent}/{filepath.stem}-{hex_digest[:7]}{filepath.suffix}" else: return f"{resource}" def make_product_selected( size: str | None, style: str | None, quantity: int ) -> dict[str, str | int]: """Helper function to construct a dictionary with only not-None values. We use this as a JSON response, to let javascript update the current URL with a querystring with the selected product options. In case an option is None, we simply don't want to add it as query param. """ product_selected = {"quantity": quantity} if size: product_selected["size"] = size if style: product_selected["style"] = style return product_selected def make_product_info_code(item): """ Output a product info string in the format: __---- """ p = item.path o = "" q = item.quantity if item.options.size and item.options.style: o = f"{item.options.size}-{item.options.style}" return f"{p}__{o}--{q}" def make_slugify(text: str) -> str: return slugify(text)