| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- 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 ./<products>/, 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_<size>w.jpg
- URL can also be a str in the format => <filename>.<ext>
- """
- 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_<size>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 <resource>'s modified time to the given <resource>,
- to make the web browser fetch the latest version of <resource>.
- Eg. soft cache-invalidation.
- => /<resource>-<file-modified-hash>.<ext>
- => /styles-<hash>.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:
- <item.path>__<item.options.size>--<item.options.style>--<item.quantity>
- """
- 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)
|