Merge branch 'shop-locations-with-inventory' into 'master'
stub in new shop_location and inventory models testing CodeRabbit See merge request engineering/make-post-sell/make_post_sell!10
This commit is contained in:
commit
29bcc12ea5
36 changed files with 1691 additions and 373 deletions
|
|
@ -7,6 +7,7 @@ import zope.sqlalchemy
|
|||
# Base.metadata prior to any initialization routines.
|
||||
from .product import *
|
||||
from .shop import *
|
||||
from .shop_location import *
|
||||
from .user import *
|
||||
from .user_shop import *
|
||||
from .user_product import *
|
||||
|
|
@ -18,6 +19,7 @@ from .market import *
|
|||
from .coupon import *
|
||||
from .coupon_redemption import *
|
||||
from .invoice import *
|
||||
from .inventory import *
|
||||
|
||||
from .stripe_user_shop import *
|
||||
|
||||
|
|
|
|||
|
|
@ -6,22 +6,16 @@ from sqlalchemy.orm import relationship
|
|||
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
Boolean,
|
||||
BigInteger,
|
||||
Unicode,
|
||||
UnicodeText
|
||||
)
|
||||
from sqlalchemy import Column, Boolean, BigInteger, Unicode, UnicodeText
|
||||
|
||||
from .meta import (
|
||||
Base,
|
||||
RBase,
|
||||
UUIDType,
|
||||
foreign_key,
|
||||
now_timestamp,
|
||||
get_object_by_id,
|
||||
bucket_objects_by_id,
|
||||
Base,
|
||||
RBase,
|
||||
UUIDType,
|
||||
foreign_key,
|
||||
now_timestamp,
|
||||
get_object_by_id,
|
||||
bucket_objects_by_id,
|
||||
)
|
||||
|
||||
from .product import get_products_by_ids
|
||||
|
|
@ -29,6 +23,8 @@ from .shop import get_shops_by_ids
|
|||
|
||||
from .cart_coupon import CartCoupon
|
||||
|
||||
from .inventory import get_inventory_by_product_and_shop_location
|
||||
|
||||
from ..lib.currency import cents_to_dollars
|
||||
|
||||
try:
|
||||
|
|
@ -40,16 +36,20 @@ except:
|
|||
def timestamp_to_datetime(timestamp):
|
||||
"""Accepts a milliseconds timestamp integer and returns a datetime"""
|
||||
from datetime import datetime
|
||||
|
||||
return datetime.fromtimestamp(timestamp / 1000.0)
|
||||
|
||||
|
||||
def timestamp_to_ago_string(timestamp):
|
||||
"""Accepts a timestamp and returns a human readable string"""
|
||||
from ago import human
|
||||
|
||||
return human(timestamp_to_datetime(timestamp), 2, abbreviate=True)
|
||||
|
||||
|
||||
class Cart(RBase, Base):
|
||||
"""This class tracks a User's shopping carts."""
|
||||
|
||||
id = Column(UUIDType, primary_key=True, index=True)
|
||||
user_id = Column(UUIDType, foreign_key("User", "id"), nullable=True)
|
||||
shop_id = Column(UUIDType, foreign_key("Shop", "id"), nullable=True)
|
||||
|
|
@ -57,13 +57,18 @@ class Cart(RBase, Base):
|
|||
public = Column(Boolean, default=False)
|
||||
json_cart = Column(UnicodeText, default=unicode("{}"))
|
||||
|
||||
handling_option = Column(Unicode(64), nullable=True)
|
||||
handling_cost_in_cents = Column(BigInteger, nullable=True, default=0)
|
||||
|
||||
created_timestamp = Column(BigInteger, nullable=False)
|
||||
updated_timestamp = Column(BigInteger, nullable=False)
|
||||
|
||||
active = Column(Boolean, default=False)
|
||||
|
||||
# many to many uses association_proxy.
|
||||
coupons = association_proxy("cart_coupons", "coupon", creator=lambda c: CartCoupon(coupon=c))
|
||||
coupons = association_proxy(
|
||||
"cart_coupons", "coupon", creator=lambda c: CartCoupon(coupon=c)
|
||||
)
|
||||
|
||||
user = relationship(argument="User", uselist=False, lazy="joined")
|
||||
shop = relationship(argument="Shop", uselist=False, lazy="joined")
|
||||
|
|
@ -85,6 +90,8 @@ class Cart(RBase, Base):
|
|||
del self._line_totals_in_cents
|
||||
if hasattr(self, "_products"):
|
||||
del self._products
|
||||
if hasattr(self, "_physical_products"):
|
||||
del self._physical_products
|
||||
if hasattr(self, "_shops"):
|
||||
del self._shops
|
||||
if hasattr(self, "_shop_product_dict"):
|
||||
|
|
@ -122,13 +129,13 @@ class Cart(RBase, Base):
|
|||
|
||||
def add_product(self, product):
|
||||
quantity = 1
|
||||
if str(product.id) in self.cart:
|
||||
quantity = self.cart[str(product.id)] + 1
|
||||
if product.uuid_str in self.cart:
|
||||
quantity = self.cart[product.uuid_str] + 1
|
||||
self.set_product_quantity(product, quantity)
|
||||
|
||||
def remove_product(self, product):
|
||||
tmp_cart = self.cart
|
||||
del tmp_cart[str(product.id)]
|
||||
del tmp_cart[product.uuid_str]
|
||||
self.cart = tmp_cart
|
||||
|
||||
def set_product_quantity(self, product, quantity):
|
||||
|
|
@ -136,7 +143,7 @@ class Cart(RBase, Base):
|
|||
if quantity >= 999:
|
||||
quantity = 999
|
||||
tmp_cart = self.cart
|
||||
tmp_cart[str(product.id)] = quantity
|
||||
tmp_cart[product.uuid_str] = quantity
|
||||
self.cart = tmp_cart
|
||||
|
||||
def get_product_quantity(self, product):
|
||||
|
|
@ -149,6 +156,15 @@ class Cart(RBase, Base):
|
|||
tmp_cart[product_id] += count
|
||||
else:
|
||||
tmp_cart[product_id] = count
|
||||
|
||||
self.handling_option = other.handling_option
|
||||
self.handling_cost_in_cents = self.handling_cost_in_cents
|
||||
|
||||
for coupon in other.coupons:
|
||||
if coupon not in self.coupons:
|
||||
self.coupons.append(coupon)
|
||||
|
||||
# this busts memoization.
|
||||
self.cart = tmp_cart
|
||||
|
||||
@property
|
||||
|
|
@ -161,7 +177,7 @@ class Cart(RBase, Base):
|
|||
def products(self):
|
||||
"""
|
||||
Return a dictionary of unique Product objects in cart.
|
||||
|
||||
|
||||
The key is the product UUID, and the value is the Product object.
|
||||
"""
|
||||
if hasattr(self, "_products") == False:
|
||||
|
|
@ -170,6 +186,20 @@ class Cart(RBase, Base):
|
|||
self._products = bucket_objects_by_id(products)
|
||||
return self._products
|
||||
|
||||
@property
|
||||
def physical_products(self):
|
||||
"""
|
||||
Return a dictionary of physical Product objects in cart.
|
||||
|
||||
The key is the product UUID, and the value is the Product object.
|
||||
"""
|
||||
if not hasattr(self, "_physical_products"):
|
||||
self._physical_products = {}
|
||||
for product_id, product in self.products.items():
|
||||
if product.is_physical:
|
||||
self._physical_products[product_id] = product
|
||||
return self._physical_products
|
||||
|
||||
@property
|
||||
def shops(self):
|
||||
"""
|
||||
|
|
@ -178,7 +208,6 @@ class Cart(RBase, Base):
|
|||
The key is the Shop UUID, and the value is the Shop object.
|
||||
"""
|
||||
if hasattr(self, "_shops") == False:
|
||||
|
||||
self._shops = {}
|
||||
|
||||
# loop over Products in cart, aggregate a unique set of shop_ids.
|
||||
|
|
@ -195,18 +224,17 @@ class Cart(RBase, Base):
|
|||
@property
|
||||
def shop_product_dict(self):
|
||||
"""
|
||||
Return a dict where keys are shop_ids and values are a list of product_ids.
|
||||
Return a dict where keys are shop_ids (as strings) and values are a list of tuples (product, quantity).
|
||||
"""
|
||||
if hasattr(self, "_shop_product_dict") == False:
|
||||
|
||||
if not hasattr(self, "_shop_product_dict"):
|
||||
self._shop_product_dict = {}
|
||||
|
||||
for shop_id in self.shops.keys():
|
||||
self._shop_product_dict[shop_id] = []
|
||||
|
||||
for product in self.products.values():
|
||||
shop_id = product.id_to_uuid_str(product.shop_id)
|
||||
self._shop_product_dict[shop_id].append(product.uuid_str)
|
||||
for product_id, quantity in self.cart.items():
|
||||
product = self.products[product_id]
|
||||
shop_id = product.shop.uuid_str
|
||||
if shop_id not in self._shop_product_dict:
|
||||
self._shop_product_dict[shop_id] = []
|
||||
self._shop_product_dict[shop_id].append((product, quantity))
|
||||
|
||||
return self._shop_product_dict
|
||||
|
||||
|
|
@ -231,12 +259,14 @@ class Cart(RBase, Base):
|
|||
|
||||
@property
|
||||
def shop_totals_in_cents(self):
|
||||
if hasattr(self, "_shop_totals_in_cents") == False:
|
||||
if not hasattr(self, "_shop_totals_in_cents"):
|
||||
self._shop_totals_in_cents = {}
|
||||
for shop_id, product_ids in self.shop_product_dict.items():
|
||||
for shop_id, product_quantity_tuple in self.shop_product_dict.items():
|
||||
self._shop_totals_in_cents[shop_id] = 0
|
||||
for product_id in product_ids:
|
||||
self._shop_totals_in_cents[shop_id] += self.line_totals_in_cents[product_id]
|
||||
for product, _ in product_quantity_tuple:
|
||||
self._shop_totals_in_cents[shop_id] += self.line_totals_in_cents[
|
||||
product.uuid_str
|
||||
]
|
||||
return self._shop_totals_in_cents
|
||||
|
||||
@property
|
||||
|
|
@ -252,38 +282,65 @@ class Cart(RBase, Base):
|
|||
"""Discounted shop totals in cents after applying coupons."""
|
||||
if hasattr(self, "_discounted_shop_totals_in_cents") == False:
|
||||
from copy import deepcopy
|
||||
|
||||
self._discounted_shop_totals_in_cents = deepcopy(self.shop_totals_in_cents)
|
||||
if len(self.coupons) > 0:
|
||||
for coupon in sorted(self.coupons):
|
||||
for shop_uuid, shop_total_in_cents in self.shop_totals_in_cents.items():
|
||||
for coupon in sorted(self.coupons, key=lambda c: c.id):
|
||||
for (
|
||||
shop_uuid,
|
||||
shop_total_in_cents,
|
||||
) in self.shop_totals_in_cents.items():
|
||||
if coupon.shop_uuid_str == shop_uuid:
|
||||
self._discounted_shop_totals_in_cents[shop_uuid] = coupon.compute_discount(shop_total_in_cents)
|
||||
self._discounted_shop_totals_in_cents[
|
||||
shop_uuid
|
||||
] = coupon.compute_discount(shop_total_in_cents)
|
||||
return self._discounted_shop_totals_in_cents
|
||||
|
||||
@property
|
||||
def discounted_shop_totals(self):
|
||||
if hasattr(self, "_discounted_shop_totals") == False:
|
||||
self._discounted_shop_totals = {}
|
||||
for shop_id, discounted_shop_total_in_cents in self.discounted_shop_totals_in_cents.items():
|
||||
self._discounted_shop_totals[shop_id] = cents_to_dollars(discounted_shop_total_in_cents)
|
||||
for (
|
||||
shop_id,
|
||||
discounted_shop_total_in_cents,
|
||||
) in self.discounted_shop_totals_in_cents.items():
|
||||
self._discounted_shop_totals[shop_id] = cents_to_dollars(
|
||||
discounted_shop_total_in_cents
|
||||
)
|
||||
return self._discounted_shop_totals
|
||||
|
||||
@property
|
||||
def total_price_in_cents(self):
|
||||
# TODO: memoize?
|
||||
return sum(self.line_totals_in_cents.values())
|
||||
"""
|
||||
Calculate the total price in cents, including handling cost if applicable.
|
||||
"""
|
||||
total = sum(self.line_totals_in_cents.values())
|
||||
if self.handling_cost_in_cents:
|
||||
total += self.handling_cost_in_cents
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_price(self):
|
||||
"""
|
||||
Total price in dollars, including handling cost if applicable.
|
||||
"""
|
||||
return cents_to_dollars(self.total_price_in_cents)
|
||||
|
||||
@property
|
||||
def total_discounted_price_in_cents(self):
|
||||
# TODO: memoize?
|
||||
return sum(self.discounted_shop_totals_in_cents.values())
|
||||
"""
|
||||
Calculate the total discounted price in cents, including handling cost if applicable.
|
||||
"""
|
||||
total = sum(self.discounted_shop_totals_in_cents.values())
|
||||
if self.handling_cost_in_cents:
|
||||
total += self.handling_cost_in_cents
|
||||
return total
|
||||
|
||||
@property
|
||||
def total_discounted_price(self):
|
||||
"""
|
||||
Total discounted price in dollars, including handling cost if applicable.
|
||||
"""
|
||||
return cents_to_dollars(self.total_discounted_price_in_cents)
|
||||
|
||||
@property
|
||||
|
|
@ -294,14 +351,18 @@ class Cart(RBase, Base):
|
|||
|
||||
@property
|
||||
def total(self):
|
||||
"""The total (after discounts) to be charged to user via an Invoice."""
|
||||
"""
|
||||
The total in dollars (after discounts) to be charged to user via an Invoice, including handling cost if applicable.
|
||||
"""
|
||||
if self.is_discounted:
|
||||
return self.total_discounted_price
|
||||
return self.total_price
|
||||
|
||||
@property
|
||||
def total_in_cents(self):
|
||||
"""The total in cents (after discounts)."""
|
||||
"""
|
||||
The total in cents (after discounts) to be charged to user via an Invoice, including handling cost if applicable.
|
||||
"""
|
||||
if self.is_discounted:
|
||||
return self.total_discounted_price_in_cents
|
||||
return self.total_price_in_cents
|
||||
|
|
@ -339,25 +400,99 @@ class Cart(RBase, Base):
|
|||
for coupon in self.coupons:
|
||||
# test if the shop's cart total qualifies for this coupon.
|
||||
if coupon.is_not_valid:
|
||||
error_messages.append("Sorry that coupon is not valid (expired or disabled).")
|
||||
elif self.shop_totals_in_cents[coupon.shop_uuid_str] < coupon.cart_qualifier:
|
||||
error_messages.append("Please review coupon terms: shop total not met.")
|
||||
elif coupon.max_redemptions and coupon.redemptions.count() >= coupon.max_redemptions:
|
||||
error_messages.append("That coupon was used too many times. Please remove it.")
|
||||
elif coupon.max_redemptions_per_user and coupon.redemptions.filter_by(user_id=self.user_id).count() >= coupon.max_redemptions_per_user:
|
||||
error_messages.append("You have already used this coupon. Please remove it.")
|
||||
error_messages.append(
|
||||
"Sorry that coupon is not valid (expired or disabled)."
|
||||
)
|
||||
elif (
|
||||
self.shop_totals_in_cents[coupon.shop_uuid_str]
|
||||
< coupon.cart_qualifier
|
||||
):
|
||||
error_messages.append(
|
||||
"Please review coupon terms: shop total not met."
|
||||
)
|
||||
elif (
|
||||
coupon.max_redemptions
|
||||
and coupon.redemptions.count() >= coupon.max_redemptions
|
||||
):
|
||||
error_messages.append(
|
||||
"That coupon was used too many times. Please remove it."
|
||||
)
|
||||
elif (
|
||||
coupon.max_redemptions_per_user
|
||||
and coupon.redemptions.filter_by(user_id=self.user_id).count()
|
||||
>= coupon.max_redemptions_per_user
|
||||
):
|
||||
error_messages.append(
|
||||
"You have already used this coupon. Please remove it."
|
||||
)
|
||||
return error_messages
|
||||
|
||||
def check_inventory(self, shop_location):
|
||||
"""
|
||||
Check if the shop location has enough quantity for each physical product in the cart.
|
||||
Returns a list of error messages if any product is out of stock.
|
||||
"""
|
||||
error_messages = []
|
||||
for product_id, product in self.physical_products.items():
|
||||
quantity_needed = self.cart[product_id]
|
||||
inventory = get_inventory_by_product_and_shop_location(
|
||||
self.dbsession, product_id, shop_location.uuid_str
|
||||
)
|
||||
if inventory is None or inventory.quantity < quantity_needed:
|
||||
error_messages.append(
|
||||
f"Not enough stock for {product.title}. Switch Shop Location and try again. Needed: {quantity_needed}, Available: {inventory.quantity if inventory else 0}"
|
||||
)
|
||||
return error_messages
|
||||
|
||||
def remove_handling_if_no_physical_products(self):
|
||||
"""
|
||||
Remove the handling option and cost if there are no physical products in the cart.
|
||||
"""
|
||||
if not self.physical_products:
|
||||
self.handling_option = None
|
||||
self.handling_cost_in_cents = 0
|
||||
|
||||
def update_handling_cost(self, shop_location):
|
||||
"""
|
||||
Update the handling cost based on the provided shop location.
|
||||
"""
|
||||
if self.handling_option:
|
||||
if self.handling_option == "local_pickup":
|
||||
self.handling_cost_in_cents = 0
|
||||
elif self.handling_option == "local_delivery":
|
||||
self.handling_cost_in_cents = shop_location.local_delivery_rate_in_cents
|
||||
elif self.handling_option == "local_shipping":
|
||||
self.handling_cost_in_cents = shop_location.local_shipping_rate_in_cents
|
||||
elif self.handling_option == "international_shipping":
|
||||
self.handling_cost_in_cents = (
|
||||
shop_location.international_shipping_rate_in_cents
|
||||
)
|
||||
|
||||
def update_inventory(self, shop_location):
|
||||
"""
|
||||
Update the inventory quantity for physical products in the cart.
|
||||
"""
|
||||
changed = False
|
||||
for product_id, product in self.physical_products.items():
|
||||
quantity_needed = self.cart[product_id]
|
||||
inventory = get_inventory_by_product_and_shop_location(
|
||||
self.dbsession, product_id, shop_location.uuid_str
|
||||
)
|
||||
if inventory:
|
||||
inventory.quantity -= quantity_needed
|
||||
self.dbsession.add(inventory)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
self.dbsession.flush()
|
||||
|
||||
|
||||
def get_all_carts(dbsession):
|
||||
"""
|
||||
Return all Cart objects in descending order by
|
||||
created timestamp (newest first).
|
||||
"""
|
||||
return (
|
||||
dbsession.query(Cart)
|
||||
.order_by(Cart.created_timestamp.desc())
|
||||
)
|
||||
return dbsession.query(Cart).order_by(Cart.created_timestamp.desc())
|
||||
|
||||
|
||||
def get_cart_by_id(dbsession, cart_id):
|
||||
|
|
|
|||
50
make_post_sell/models/inventory.py
Normal file
50
make_post_sell/models/inventory.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import uuid
|
||||
from sqlalchemy import Column, Integer, BigInteger
|
||||
from .meta import Base, RBase, UUIDType, now_timestamp, foreign_key
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
class Inventory(RBase, Base):
|
||||
"""This class represents the inventory of a product at a specific location."""
|
||||
|
||||
id = Column(UUIDType, primary_key=True, index=True)
|
||||
shop_location_id = Column(
|
||||
UUIDType, foreign_key("ShopLocation", "id"), nullable=False
|
||||
)
|
||||
product_id = Column(UUIDType, foreign_key("Product", "id"), nullable=False)
|
||||
quantity = Column(Integer, nullable=False, default=0)
|
||||
created_timestamp = Column(BigInteger, nullable=False)
|
||||
updated_timestamp = Column(BigInteger, nullable=False)
|
||||
|
||||
shop_location = relationship("ShopLocation", uselist=False)
|
||||
product = relationship("Product", uselist=False)
|
||||
|
||||
def __init__(self, shop_location, product, quantity=0):
|
||||
if not product.is_physical:
|
||||
raise ValueError("Cannot create inventory for a digital product")
|
||||
self.id = uuid.uuid1()
|
||||
self.shop_location = shop_location
|
||||
self.product = product
|
||||
self.quantity = quantity
|
||||
self.created_timestamp = now_timestamp()
|
||||
self.updated_timestamp = now_timestamp()
|
||||
|
||||
def update_quantity(self, quantity):
|
||||
self.quantity = quantity
|
||||
self.updated_timestamp = now_timestamp()
|
||||
|
||||
|
||||
def get_inventory_by_product_and_shop_location(dbsession, product_id, shop_location_id):
|
||||
"""
|
||||
Query the Inventory table by product_id and shop_location_id.
|
||||
|
||||
:param dbsession: The SQLAlchemy session object.
|
||||
:param product_id: The UUID of the product.
|
||||
:param shop_location_id: The UUID of the shop location.
|
||||
:return: The Inventory object if found, else None.
|
||||
"""
|
||||
return (
|
||||
dbsession.query(Inventory)
|
||||
.filter_by(product_id=product_id, shop_location_id=shop_location_id)
|
||||
.first()
|
||||
)
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import uuid
|
||||
|
||||
from sqlalchemy import Column, BigInteger, Boolean, Integer, UnicodeText
|
||||
from sqlalchemy import Column, BigInteger, Boolean, Integer, UnicodeText, Unicode
|
||||
|
||||
from .meta import Base, RBase, UUIDType, foreign_key, now_timestamp
|
||||
from .meta import Base, RBase, UUIDType, foreign_key, now_timestamp, get_object_by_id
|
||||
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
|
||||
from .coupon_redemption import CouponRedemption
|
||||
|
||||
from ..lib.currency import cents_to_dollars
|
||||
|
||||
|
||||
class InvoiceLineItem(RBase, Base):
|
||||
"""
|
||||
|
|
@ -15,6 +17,7 @@ class InvoiceLineItem(RBase, Base):
|
|||
We should do our best to treat these line items as immutable.
|
||||
This will let us rely on them as a proof of transactional record.
|
||||
"""
|
||||
|
||||
id = Column(UUIDType, primary_key=True, index=True)
|
||||
# a way to group multiple transactions into a single reference.
|
||||
invoice_id = Column(UUIDType, foreign_key("Invoice", "id"), nullable=False)
|
||||
|
|
@ -57,9 +60,8 @@ class InvoiceLineItem(RBase, Base):
|
|||
shop=None,
|
||||
market=None,
|
||||
quantity=1,
|
||||
credit=False
|
||||
credit=False,
|
||||
):
|
||||
|
||||
if price is None:
|
||||
price = product.current_price
|
||||
if shop is None:
|
||||
|
|
@ -81,6 +83,7 @@ class Invoice(RBase, Base):
|
|||
A user may have zero or many invoices
|
||||
which relate to a purchase transaction.
|
||||
"""
|
||||
|
||||
id = Column(UUIDType, primary_key=True, index=True)
|
||||
# the user making the purchase.
|
||||
user_id = Column(UUIDType, foreign_key("User", "id"), nullable=False)
|
||||
|
|
@ -93,6 +96,10 @@ class Invoice(RBase, Base):
|
|||
# the timestamp when the invoice was created.
|
||||
created_timestamp = Column(BigInteger, nullable=False)
|
||||
|
||||
# optional physical product handling.
|
||||
handling_option = Column(Unicode(64), nullable=True)
|
||||
handling_cost_in_cents = Column(BigInteger, nullable=True, default=0)
|
||||
|
||||
# the user's address to ship the item to.
|
||||
delivery_address = Column(UnicodeText, nullable=True)
|
||||
|
||||
|
|
@ -109,18 +116,20 @@ class Invoice(RBase, Base):
|
|||
# returns all the InvoiceLineItems.
|
||||
# lazy="dynamic" returns a query object instead of an InstrumentedList.
|
||||
# this is ideal when we want to further filter the results.
|
||||
line_items = relationship(argument="InvoiceLineItem", lazy="dynamic", back_populates="invoice")
|
||||
line_items = relationship(
|
||||
argument="InvoiceLineItem", lazy="dynamic", back_populates="invoice"
|
||||
)
|
||||
|
||||
# one to many.
|
||||
# returns all the CouponRedemptions for this Invoice.
|
||||
# lazy="dynamic" returns a query object instead of an InstrumentedList.
|
||||
# this is ideal when we want to further filter the results.
|
||||
coupon_redemptions = relationship(argument="CouponRedemption", lazy="dynamic", back_populates="invoice")
|
||||
|
||||
# TODO: We need a method that returns InvoiceLineItem grouped by shop.
|
||||
# we will need this when we start supporting market places.
|
||||
#line_items_grouped_by_shop
|
||||
coupon_redemptions = relationship(
|
||||
argument="CouponRedemption", lazy="dynamic", back_populates="invoice"
|
||||
)
|
||||
|
||||
# maybe not needed anymore, an invoice is only good for one shop.
|
||||
# so multiple shops in a cart will mean multiple invoices.
|
||||
# TODO: We need a way to relate none or many CouponRedemption
|
||||
# to an Invoice. This will allow us to keep track of where the
|
||||
# coupons are utilized and how they affect the invoice total.
|
||||
|
|
@ -186,5 +195,38 @@ class Invoice(RBase, Base):
|
|||
invoice=self,
|
||||
shop=coupon.shop,
|
||||
user=self.user,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def total_in_cents(self):
|
||||
"""Calculate the total amount in cents for the invoice, including handling fee."""
|
||||
line_items_total = sum(
|
||||
item.price.price_in_cents * item.quantity for item in self.line_items
|
||||
)
|
||||
return line_items_total + (self.handling_cost_in_cents or 0)
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
"""Total in dollars."""
|
||||
return cents_to_dollars(self.total_in_cents)
|
||||
|
||||
@property
|
||||
def requires_payment(self):
|
||||
"""Return True if we should charge a card, else False."""
|
||||
# Only charge a credit card if invoice total is greater than $0.64!
|
||||
# Credit card "gas" payment costs $0.30 + 2.9%
|
||||
# We gift the customer up to $0.32 when we bypass credit card gas.
|
||||
if self.total_in_cents > 64:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def human_created_timestamp(self):
|
||||
"""Return the created timestamp in a human-readable format."""
|
||||
from datetime import datetime
|
||||
return datetime.fromtimestamp(self.created_timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def get_invoice_by_id(dbsession, invoice_id):
|
||||
"""Try to get Invoice object by id or return None."""
|
||||
return get_object_by_id(dbsession, invoice_id, Invoice)
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ UUIDType = TempUUIDType(binary=False)
|
|||
|
||||
|
||||
CLASS_TO_TABLE = {
|
||||
"Address": "mps_address",
|
||||
"Shop": "mps_shop",
|
||||
"ShopLocation": "mps_shop_location",
|
||||
"Inventory": "mps_inventory",
|
||||
"Product": "mps_product",
|
||||
"Coupon": "mps_coupon",
|
||||
"CouponRedemption": "mps_coupon_redemption",
|
||||
|
|
|
|||
|
|
@ -104,6 +104,12 @@ class Product(RBase, Base):
|
|||
# this value should be treated as immutable after object creation.
|
||||
is_sellable = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
# this value should be treated as immutable after object creation.
|
||||
is_physical = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Add relationship to inventories
|
||||
inventories = relationship("Inventory", lazy="joined", back_populates="product")
|
||||
|
||||
# lazy="joined" performs a left join to reduce queries, it's magic.
|
||||
# set uselist=False creates one-to-one relationship.
|
||||
shop = relationship(argument="Shop", uselist=False, lazy="joined")
|
||||
|
|
@ -121,13 +127,6 @@ class Product(RBase, Base):
|
|||
"product_users", "user", creator=lambda u: UserProduct(user=u)
|
||||
)
|
||||
|
||||
"""
|
||||
_users = relationship(UserProduct, order_by=[UserProduct.created_timestamp.desc()])
|
||||
users = association_proxy(
|
||||
"_users", "user", creator=lambda u: UserProduct(user=u)
|
||||
)
|
||||
"""
|
||||
|
||||
file_keys = [
|
||||
"product",
|
||||
"preview",
|
||||
|
|
@ -203,13 +202,15 @@ class Product(RBase, Base):
|
|||
|
||||
@property
|
||||
def is_ready(self):
|
||||
"""internal safety checks prior to allowing the sale of this product."""
|
||||
"""Internal safety checks prior to allowing the sale of this product."""
|
||||
if self.is_sellable:
|
||||
if self.is_bundle:
|
||||
return True if len(self.bundle_metadata["product-ids"]) >= 1 else False
|
||||
if self.is_physical:
|
||||
return "thumbnail1" in self.extensions
|
||||
return self.has_product_file
|
||||
|
||||
# content is always ready, for example a blog post only needs a title & description.
|
||||
# Content is always ready, for example a blog post only needs a title & description.
|
||||
return True
|
||||
|
||||
@property
|
||||
|
|
@ -241,6 +242,12 @@ class Product(RBase, Base):
|
|||
"""return slug from title"""
|
||||
return slugify(self.title)
|
||||
|
||||
@property
|
||||
def total_inventory_quantity(self):
|
||||
if not self.is_physical:
|
||||
return None
|
||||
return sum(inventory.quantity for inventory in self.inventories)
|
||||
|
||||
@property
|
||||
def s3_path(self):
|
||||
"""return the S3 path shop.id/product.id"""
|
||||
|
|
@ -340,7 +347,7 @@ class Product(RBase, Base):
|
|||
file_prefix = "" if file_key == "product" else "{}-".format(file_key)
|
||||
extension = self.extensions.get(file_key, "")
|
||||
filename = "{}{}.{}".format(file_prefix, self.slug, extension)
|
||||
content_disposition = "attachment; filename={}".format(filename)
|
||||
content_disposition = "attachment; filename={}".format(filename)
|
||||
|
||||
if file_key in self.file_thumbnail_keys:
|
||||
content_disposition = "inline; filename={}".format(filename)
|
||||
|
|
@ -502,7 +509,6 @@ def get_products_from_a_shop(shop, visibility=1):
|
|||
|
||||
|
||||
def get_products_by_keywords(dbsession, keywords, shop=None):
|
||||
|
||||
scores = {}
|
||||
hits = {}
|
||||
|
||||
|
|
@ -512,7 +518,6 @@ def get_products_by_keywords(dbsession, keywords, shop=None):
|
|||
product_query = dbsession.query(Product)
|
||||
|
||||
for keyword in keywords:
|
||||
|
||||
keyword_filter = Product.title.ilike("%{}%".format(keyword))
|
||||
# the product _must_ be public (1).
|
||||
products = (
|
||||
|
|
@ -520,7 +525,6 @@ def get_products_by_keywords(dbsession, keywords, shop=None):
|
|||
)
|
||||
|
||||
for product in products:
|
||||
|
||||
if product.id not in scores:
|
||||
scores[product.id] = 0
|
||||
scores[product.id] += 1
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ class Shop(RBase, Base):
|
|||
order_by="desc(Coupon.created_timestamp)",
|
||||
)
|
||||
|
||||
# lazy='dynamic' returns a query object instead of collection.
|
||||
# Reference: http://docs.sqlalchemy.org/en/latest/orm/collections.html
|
||||
shop_locations = relationship(argument="ShopLocation", lazy="dynamic", back_populates="shop")
|
||||
|
||||
# lazy='dynamic' returns a query object instead of collection.
|
||||
# Reference: http://docs.sqlalchemy.org/en/latest/orm/collections.html
|
||||
search_requests = relationship(
|
||||
|
|
|
|||
75
make_post_sell/models/shop_location.py
Normal file
75
make_post_sell/models/shop_location.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import uuid
|
||||
from sqlalchemy import Column, Unicode, BigInteger, Boolean
|
||||
from .meta import Base, RBase, UUIDType, now_timestamp, foreign_key, get_object_by_id
|
||||
from sqlalchemy.orm import relationship
|
||||
from ..lib.currency import cents_to_dollars, dollars_to_cents
|
||||
|
||||
class ShopLocation(RBase, Base):
|
||||
"""This class represents a physical location of a shop."""
|
||||
|
||||
id = Column(UUIDType, primary_key=True, index=True)
|
||||
shop_id = Column(UUIDType, foreign_key("Shop", "id"), nullable=False)
|
||||
name = Column(Unicode(64), nullable=False)
|
||||
address = Column(Unicode(256), nullable=False)
|
||||
city = Column(Unicode(64), nullable=False)
|
||||
state = Column(Unicode(64), nullable=False)
|
||||
country = Column(Unicode(64), nullable=False)
|
||||
postal_code = Column(Unicode(16), nullable=False)
|
||||
created_timestamp = Column(BigInteger, nullable=False)
|
||||
|
||||
# Delivery options
|
||||
local_pickup = Column(Boolean, default=False, nullable=False)
|
||||
local_delivery = Column(Boolean, default=False, nullable=False)
|
||||
local_shipping = Column(Boolean, default=False, nullable=False)
|
||||
international_shipping = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
# Delivery rates in cents
|
||||
local_delivery_rate_in_cents = Column(BigInteger, nullable=True)
|
||||
local_shipping_rate_in_cents = Column(BigInteger, nullable=True)
|
||||
international_shipping_rate_in_cents = Column(BigInteger, nullable=True)
|
||||
|
||||
# Operating hours
|
||||
days_open = Column(Unicode(64), nullable=True) # e.g., "Mon-Fri"
|
||||
hours_open = Column(Unicode(64), nullable=True) # e.g., "9am-5pm"
|
||||
|
||||
shop = relationship("Shop", uselist=False)
|
||||
|
||||
def __init__(self, shop, name, address, city, state, country, postal_code):
|
||||
self.id = uuid.uuid1()
|
||||
self.shop = shop
|
||||
self.name = name
|
||||
self.address = address
|
||||
self.city = city
|
||||
self.state = state
|
||||
self.country = country
|
||||
self.postal_code = postal_code
|
||||
self.created_timestamp = now_timestamp()
|
||||
|
||||
@property
|
||||
def local_delivery_rate(self):
|
||||
return cents_to_dollars(self.local_delivery_rate_in_cents) if self.local_delivery_rate_in_cents else None
|
||||
|
||||
@local_delivery_rate.setter
|
||||
def local_delivery_rate(self, value):
|
||||
self.local_delivery_rate_in_cents = dollars_to_cents(value)
|
||||
|
||||
@property
|
||||
def local_shipping_rate(self):
|
||||
return cents_to_dollars(self.local_shipping_rate_in_cents) if self.local_shipping_rate_in_cents else None
|
||||
|
||||
@local_shipping_rate.setter
|
||||
def local_shipping_rate(self, value):
|
||||
self.local_shipping_rate_in_cents = dollars_to_cents(value)
|
||||
|
||||
@property
|
||||
def international_shipping_rate(self):
|
||||
return cents_to_dollars(self.international_shipping_rate_in_cents) if self.international_shipping_rate_in_cents else None
|
||||
|
||||
@international_shipping_rate.setter
|
||||
def international_shipping_rate(self, value):
|
||||
self.international_shipping_rate_in_cents = dollars_to_cents(value)
|
||||
|
||||
|
||||
def get_shop_location_by_id(dbsession, shop_location_id):
|
||||
"""Try to get ShopLocation object by id or return None."""
|
||||
return get_object_by_id(dbsession, shop_location_id, ShopLocation)
|
||||
|
|
@ -3,6 +3,7 @@ from .models.cart import get_cart_by_id
|
|||
from .models.user import get_user_by_id
|
||||
from .models.shop import get_shop_by_id
|
||||
from .models.shop import get_shop_by_domain_name
|
||||
from .models.shop_location import get_shop_location_by_id
|
||||
from .models.product import get_product_by_id
|
||||
|
||||
from . import get_children_settings
|
||||
|
|
@ -30,7 +31,6 @@ def includeme(config):
|
|||
|
||||
return user
|
||||
|
||||
|
||||
def add_product(request):
|
||||
"""Return Product object or None from route."""
|
||||
product_id = request.matchdict.get("product_id")
|
||||
|
|
@ -55,7 +55,6 @@ def includeme(config):
|
|||
return get_shop_by_domain_name(request.dbsession, request.domain)
|
||||
|
||||
def add_active_cart(request):
|
||||
|
||||
"""
|
||||
Load active cart for user for shop or
|
||||
create cart and add it to the user as active.
|
||||
|
|
@ -85,6 +84,12 @@ def includeme(config):
|
|||
request.dbsession.flush()
|
||||
return cart
|
||||
|
||||
def add_shop_location(request):
|
||||
"""Return ShopLocation from session or None."""
|
||||
shop_location_id = request.session.get("shop_location_id")
|
||||
if shop_location_id is not None:
|
||||
return get_shop_location_by_id(request.dbsession, shop_location_id)
|
||||
|
||||
def add_market(request):
|
||||
"""Return Market object or None."""
|
||||
return None
|
||||
|
|
@ -155,6 +160,8 @@ def includeme(config):
|
|||
|
||||
config.add_request_method(add_shop, "shop", reify=True)
|
||||
|
||||
config.add_request_method(add_shop_location, "shop_location", reify=True)
|
||||
|
||||
config.add_request_method(add_market, "market", reify=True)
|
||||
|
||||
config.add_request_method(add_spam, "spam", reify=True)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ def includeme(config):
|
|||
config.add_route("user_cart_public", "/u/cart/{cart_id}/public")
|
||||
config.add_route("user_cart_unpublic", "/u/cart/{cart_id}/unpublic")
|
||||
|
||||
|
||||
# cart routes.
|
||||
config.add_route("cart", "/cart")
|
||||
config.add_route("cart_checkout", "/cart/checkout")
|
||||
|
|
@ -59,6 +60,10 @@ def includeme(config):
|
|||
config.add_route("cart_remove_product", "/cart/{cart_id}/remove")
|
||||
config.add_route("cart_quantity_product", "/cart/{cart_id}/quantity")
|
||||
|
||||
config.add_route("cart_handling_option", "/cart/{cart_id}/handling-option")
|
||||
|
||||
config.add_route("view_invoice", "/invoices/{invoice_id}")
|
||||
|
||||
# coupon routes.
|
||||
config.add_route("coupon_new1", "/coupon/new")
|
||||
config.add_route("coupon_new2", "/s/{shop_id}/coupon/new")
|
||||
|
|
@ -70,6 +75,14 @@ def includeme(config):
|
|||
config.add_route("coupon1", "/s/{shop_id}/coupon/{coupon_id}")
|
||||
config.add_route("coupon2", "/s/{shop_id}/coupon/{coupon_id}/{slug:.*}")
|
||||
|
||||
# shop_location routes.
|
||||
config.add_route("shop_location_new", "/s/{shop_id}/locations/new")
|
||||
config.add_route("shop_location_edit", "/s/{shop_id}/locations/{location_id}/edit")
|
||||
config.add_route("shop_locations", "/s/{shop_id}/locations")
|
||||
config.add_route(
|
||||
"shop_location_switch", "/s/{shop_id}/location/{location_id}/switch"
|
||||
)
|
||||
|
||||
# shop routes.
|
||||
config.add_route("shop_new", "/s/new")
|
||||
|
||||
|
|
@ -103,9 +116,14 @@ def includeme(config):
|
|||
config.add_route("product_new", "/p/new")
|
||||
config.add_route("product", "/p/{product_id}")
|
||||
config.add_route("product_description_edit", "/p/{product_id}/description/edit")
|
||||
config.add_route("product_inventory_edit", "/p/{product_id}/inventory/edit")
|
||||
config.add_route("product_bundle_edit", "/p/{product_id}/bundle/edit")
|
||||
config.add_route("product_bundle_edit2", "/p/{product_id}/{slug:.*}/bundle/edit")
|
||||
config.add_route("product_remove_bundled_product", "/p/{product_id}/bundle/remove/{bundled_product_id}")
|
||||
config.add_route(
|
||||
"product_remove_bundled_product",
|
||||
"/p/{product_id}/bundle/remove/{bundled_product_id}",
|
||||
)
|
||||
|
||||
config.add_route("product_edit", "/p/{product_id}/edit")
|
||||
config.add_route("product_edit2", "/p/{product_id}/{slug:.*}/edit")
|
||||
config.add_route("product_slug", "/p/{product_id}/{slug:.*}")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
"""handling options
|
||||
|
||||
Revision ID: 1b3ecdde9e65
|
||||
Revises: ddfbd6d96915
|
||||
Create Date: 2024-06-16 14:30:57.649239
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1b3ecdde9e65'
|
||||
down_revision = 'ddfbd6d96915'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from make_post_sell.models.meta import UUIDType
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('mps_cart', sa.Column('handling_option', sa.Unicode(length=64), nullable=True))
|
||||
op.add_column('mps_cart', sa.Column('handling_cost_in_cents', sa.BigInteger(), nullable=True))
|
||||
op.add_column('mps_invoice', sa.Column('handling_option', sa.Unicode(length=64), nullable=True))
|
||||
op.add_column('mps_invoice', sa.Column('handling_cost_in_cents', sa.BigInteger(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('mps_invoice', 'handling_cost_in_cents')
|
||||
op.drop_column('mps_invoice', 'handling_option')
|
||||
op.drop_column('mps_cart', 'handling_cost_in_cents')
|
||||
op.drop_column('mps_cart', 'handling_option')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
"""physical products shop_locations and inventory
|
||||
|
||||
Revision ID: cc6211ec360e
|
||||
Revises: 7217f39a20a8
|
||||
Create Date: 2024-06-08 09:40:24.200194
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'cc6211ec360e'
|
||||
down_revision = '7217f39a20a8'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from make_post_sell.models.meta import UUIDType
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('mps_shop_location',
|
||||
sa.Column('id', UUIDType, nullable=False),
|
||||
sa.Column('shop_id', UUIDType, nullable=False),
|
||||
sa.Column('name', sa.Unicode(length=64), nullable=False),
|
||||
sa.Column('address', sa.Unicode(length=256), nullable=False),
|
||||
sa.Column('city', sa.Unicode(length=64), nullable=False),
|
||||
sa.Column('state', sa.Unicode(length=64), nullable=False),
|
||||
sa.Column('country', sa.Unicode(length=64), nullable=False),
|
||||
sa.Column('postal_code', sa.Unicode(length=16), nullable=False),
|
||||
sa.Column('created_timestamp', sa.BigInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['shop_id'], ['mps_shop.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_mps_shop_location_id'), 'mps_shop_location', ['id'], unique=False)
|
||||
op.create_table('mps_inventory',
|
||||
sa.Column('id', UUIDType, nullable=False),
|
||||
sa.Column('shop_location_id', UUIDType, nullable=False),
|
||||
sa.Column('product_id', UUIDType, nullable=False),
|
||||
sa.Column('quantity', sa.Integer(), nullable=False),
|
||||
sa.Column('created_timestamp', sa.BigInteger(), nullable=False),
|
||||
sa.Column('updated_timestamp', sa.BigInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['mps_product.id'], ),
|
||||
sa.ForeignKeyConstraint(['shop_location_id'], ['mps_shop_location.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_mps_inventory_id'), 'mps_inventory', ['id'], unique=False)
|
||||
|
||||
op.add_column('mps_product', sa.Column('is_physical', sa.Boolean(), nullable=False, server_default=sa.false()))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('mps_product', 'is_physical')
|
||||
op.drop_index(op.f('ix_mps_inventory_id'), table_name='mps_inventory')
|
||||
op.drop_table('mps_inventory')
|
||||
op.drop_index(op.f('ix_mps_shop_location_id'), table_name='mps_shop_location')
|
||||
op.drop_table('mps_shop_location')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
"""shop_location rates and hours
|
||||
|
||||
Revision ID: ddfbd6d96915
|
||||
Revises: cc6211ec360e
|
||||
Create Date: 2024-06-08 11:38:38.812584
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ddfbd6d96915'
|
||||
down_revision = 'cc6211ec360e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from make_post_sell.models.meta import UUIDType
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('mps_shop_location', sa.Column('local_pickup', sa.Boolean(), nullable=False, server_default=sa.false()))
|
||||
op.add_column('mps_shop_location', sa.Column('local_delivery', sa.Boolean(), nullable=False, server_default=sa.false()))
|
||||
op.add_column('mps_shop_location', sa.Column('local_shipping', sa.Boolean(), nullable=False, server_default=sa.false()))
|
||||
op.add_column('mps_shop_location', sa.Column('international_shipping', sa.Boolean(), nullable=False, server_default=sa.false()))
|
||||
op.add_column('mps_shop_location', sa.Column('local_delivery_rate_in_cents', sa.BigInteger(), nullable=True))
|
||||
op.add_column('mps_shop_location', sa.Column('local_shipping_rate_in_cents', sa.BigInteger(), nullable=True))
|
||||
op.add_column('mps_shop_location', sa.Column('international_shipping_rate_in_cents', sa.BigInteger(), nullable=True))
|
||||
op.add_column('mps_shop_location', sa.Column('days_open', sa.Unicode(length=64), nullable=True))
|
||||
op.add_column('mps_shop_location', sa.Column('hours_open', sa.Unicode(length=64), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('mps_shop_location', 'hours_open')
|
||||
op.drop_column('mps_shop_location', 'days_open')
|
||||
op.drop_column('mps_shop_location', 'international_shipping_rate_in_cents')
|
||||
op.drop_column('mps_shop_location', 'local_shipping_rate_in_cents')
|
||||
op.drop_column('mps_shop_location', 'local_delivery_rate_in_cents')
|
||||
op.drop_column('mps_shop_location', 'international_shipping')
|
||||
op.drop_column('mps_shop_location', 'local_shipping')
|
||||
op.drop_column('mps_shop_location', 'local_delivery')
|
||||
op.drop_column('mps_shop_location', 'local_pickup')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -28,6 +28,10 @@
|
|||
<br/>
|
||||
<a href="/s/{{ request.shop.id }}/settings" class="product-edit-button mps-button">⚙   Shop Settings</a>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="/s/{{ request.shop.id }}/locations" class="product-edit-button mps-button">🏠   Shop Locations</a>
|
||||
|
||||
{% if request.is_saas_domain %}
|
||||
<br/>
|
||||
<br/>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,11 @@
|
|||
</section>
|
||||
|
||||
<section class="log-in">
|
||||
|
||||
|
||||
{% if request.shop and request.shop.shop_locations.all()|length > 1 %}
|
||||
<a class="cart-and-count" href="{{ request.route_url('shop_locations', shop_id=request.shop.id, _query={'next': request.url}) }}">Shop Locations</a>
|
||||
{% endif %}
|
||||
|
||||
{% if request.user and request.user.authenticated %}
|
||||
|
||||
{% if request.user.full_name %}
|
||||
|
|
@ -67,7 +71,7 @@
|
|||
|
||||
|
||||
{% else %}
|
||||
<a href="/join-or-log-in">My Account</a>
|
||||
<a href="{{ request.route_url('join-or-log-in', _query={'next': request.url}) }}">My Account</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="/cart" class="cart-and-count">🛒 Cart ${{ '%0.2f' % request.active_cart.total }} ({{ request.active_cart.count }})</a>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
{% else %}
|
||||
|
||||
|
||||
{% for shop_id, product_ids in shop_product_dict.items() %}
|
||||
{% for shop_id, product_quantity_tuples in shop_product_dict.items() %}
|
||||
|
||||
{% set shop = shops[shop_id] %}
|
||||
|
||||
|
|
@ -90,11 +90,10 @@
|
|||
<b style="font-size: 1.5em;">{{ shop.name }}</b>
|
||||
</div>
|
||||
|
||||
{% for product_id in product_ids %}
|
||||
{% for product, quantity in product_quantity_tuples %}
|
||||
|
||||
{% set product = products[product_id] %}
|
||||
{% set product_quantity = cart.cart[product_id] %}
|
||||
{% set line_total = cart.line_totals[product_id] %}
|
||||
{% set product_quantity = quantity %}
|
||||
{% set line_total = cart.line_totals[product.uuid_str] %}
|
||||
|
||||
<div class="cart-item">
|
||||
{% if "thumbnail1" in product.extensions %}
|
||||
|
|
@ -131,6 +130,31 @@
|
|||
|
||||
{% endfor %}
|
||||
|
||||
<!-- Handling Options -->
|
||||
{% if cart.physical_products %}
|
||||
<h3>Physical Handling Options</h3>
|
||||
<form action="{{ request.route_url('cart_handling_option', cart_id=cart.id) }}" method="POST">
|
||||
{% if request.shop_location.local_pickup %}
|
||||
<input type="radio" id="local_pickup" name="handling_option" value="local_pickup" {% if cart.handling_option == "local_pickup" %}checked{% endif %} required>
|
||||
<label for="local_pickup" style="display: inline;">Local Pickup (Free)</label><br>
|
||||
{% endif %}
|
||||
{% if request.shop_location.local_delivery %}
|
||||
<input type="radio" id="local_delivery" name="handling_option" value="local_delivery" {% if cart.handling_option == "local_delivery" %}checked{% endif %} required>
|
||||
<label for="local_delivery" style="display: inline;">Local Delivery (${{ '%0.2f' % request.shop_location.local_delivery_rate }})</label><br>
|
||||
{% endif %}
|
||||
{% if request.shop_location.local_shipping %}
|
||||
<input type="radio" id="local_shipping" name="handling_option" value="local_shipping" {% if cart.handling_option == "local_shipping" %}checked{% endif %} required>
|
||||
<label for="local_shipping" style="display: inline;">Local Shipping (${{ '%0.2f' % request.shop_location.local_shipping_rate }})</label><br>
|
||||
{% endif %}
|
||||
{% if request.shop_location.international_shipping %}
|
||||
<input type="radio" id="international_shipping" name="handling_option" value="international_shipping" {% if cart.handling_option == "international_shipping" %}checked{% endif %} required>
|
||||
<label for="international_shipping" style="display: inline;">International Shipping (${{ '%0.2f' % request.shop_location.international_shipping_rate }})</label><br>
|
||||
{% endif %}
|
||||
<input type="submit" value="Set Handling Option">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div style="text-align: right; grid-column: span 3">
|
||||
|
||||
{% if cart.is_discounted %}
|
||||
|
|
@ -148,7 +172,6 @@
|
|||
|
||||
{% endfor %}
|
||||
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
|
@ -173,7 +196,7 @@
|
|||
<h2>Total: ${{ '%0.2f' % cart.total }}</h2>
|
||||
{% endif %}
|
||||
|
||||
{% if not cart.is_empty %}
|
||||
{% if not cart.is_empty and request.user == cart.user %}
|
||||
|
||||
<a href="/u/cart/{{ cart.id }}/checkout" class="cart-checkout-button mps-button">Checkout</a>
|
||||
|
||||
|
|
@ -193,9 +216,9 @@
|
|||
<a href="/u/cart/save" class="cart-save-button mps-button">Save Cart</a>
|
||||
<br/>
|
||||
|
||||
{% elif request.user and request.active_cart != cart %}
|
||||
{% elif request.active_cart != cart %}
|
||||
|
||||
{% if request.user == cart.user %}
|
||||
{% if request.user and request.user == cart.user %}
|
||||
<a href="/u/cart/{{ cart.id }}/activate" class="cart-activate-button mps-button">Make Cart Active</a>
|
||||
<br/>
|
||||
{% else %}
|
||||
|
|
|
|||
40
make_post_sell/templates/invoice.j2
Normal file
40
make_post_sell/templates/invoice.j2
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{% extends "base.j2" -%}
|
||||
|
||||
{% block content -%}
|
||||
|
||||
<h1>Invoice Details</h1>
|
||||
|
||||
<section class="invoice-details">
|
||||
<h2>Invoice ID: {{ invoice.id }}</h2>
|
||||
<p><strong>User:</strong> {{ invoice.user.name }}</p>
|
||||
<p><strong>Shop:</strong> {{ invoice.shop.name }}</p>
|
||||
<p><strong>Created:</strong> {{ invoice.human_created_timestamp }}</p>
|
||||
<p><strong>Delivery Address:</strong> {{ invoice.delivery_address }}</p>
|
||||
<p><strong>Handling Option:</strong> {{ invoice.handling_option }}</p>
|
||||
<p><strong>Handling Cost:</strong> ${{ '%0.2f' % (invoice.handling_cost_in_cents / 100) }}</p>
|
||||
<p><strong>Total:</strong> ${{ '%0.2f' % invoice.total }}</p>
|
||||
|
||||
<h3>Line Items</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>Quantity</th>
|
||||
<th>Price</th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in line_items %}
|
||||
<tr>
|
||||
<td>{{ item.product.title }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>${{ '%0.2f' % (item.price.price_in_cents / 100) }}</td>
|
||||
<td>${{ '%0.2f' % ((item.price.price_in_cents * item.quantity) / 100) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -27,6 +27,11 @@
|
|||
required
|
||||
autofocus />
|
||||
|
||||
<input
|
||||
name = "next"
|
||||
type = "hidden"
|
||||
value = "{{ next }}" />
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
|
|
|||
|
|
@ -58,24 +58,41 @@
|
|||
<h1>${{ '%0.2f' % product.price }}</h1>
|
||||
|
||||
{% if product.is_ready %}
|
||||
|
||||
{% if signed_get_object_url is not none %}
|
||||
<a href="{{ signed_get_object_url }}" class="product-download-button mps-button" download>⭳ Download</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<hr/>
|
||||
<br/>
|
||||
|
||||
{% if not product.is_physical %}
|
||||
|
||||
{% if signed_get_object_url is not none %}
|
||||
<a href="{{ signed_get_object_url }}" class="product-download-button mps-button" download>⭳ Download</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<hr/>
|
||||
<br/>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<a href="/cart/add?product_id={{ product.id }}" class="product-download-button mps-button">Add To Cart</a>
|
||||
<br/>
|
||||
{% if (not request.user or not request.user.authenticated) and signed_get_object_url is none %}
|
||||
<br/>
|
||||
Already purchased? <a href="/join-or-log-in">log in</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
{% if product.is_physical %}
|
||||
{% set inventory = product.inventories | selectattr('shop_location_id', 'equalto', request.shop_location.id) | first %}
|
||||
{% if inventory and inventory.quantity > 0 %}
|
||||
<a href="/cart/add?product_id={{ product.id }}" class="product-download-button mps-button">Add To Cart</a>
|
||||
{% else %}
|
||||
<button class="product-download-button mps-button" style="background-color: red; cursor: not-allowed;" disabled>Sold Out</button>
|
||||
{% if request.shop.shop_locations.all()|length > 1 %}
|
||||
<p>This item is sold out at this location.
|
||||
<br> You can <a href="{{ request.route_url('shop_locations', shop_id=request.shop.id, _query={'next': request.url}) }}">switch shop locations</a> to check availability.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="/cart/add?product_id={{ product.id }}" class="product-download-button mps-button">Add To Cart</a>
|
||||
{% endif %}
|
||||
|
||||
<br/>
|
||||
{% if (not request.user or not request.user.authenticated) and signed_get_object_url is none %}
|
||||
<br/>
|
||||
Already purchased? <a href="/join-or-log-in">log in</a>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
{% endif %}
|
||||
|
||||
{% if "preview" in product.extensions %}
|
||||
<a href="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/preview?ts={{ product.updated_timestamp }}" class="product-preview-button mps-button" download>View Preview</a>
|
||||
|
|
|
|||
|
|
@ -33,8 +33,7 @@ permanent link: <a href="{{ product.absolute_url(request) }}">{{ product.absolut
|
|||
<li>
|
||||
<form method="post" action="/p/{{ product.id }}/bundle/remove/{{ p.id }}" onsubmit="submit.disabled = true; return true;">
|
||||
{{p.title}}
|
||||
<input type="submit" name="submit" class="mps-submit" value="remove" style
|
||||
="width: 80px;"/>
|
||||
<input type="submit" name="submit" class="mps-submit" value="remove" style="width: 80px;"/>
|
||||
</form>
|
||||
</li>
|
||||
<br>
|
||||
|
|
@ -42,6 +41,38 @@ permanent link: <a href="{{ product.absolute_url(request) }}">{{ product.absolut
|
|||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% elif product.is_physical %}
|
||||
|
||||
<h3>Inventory Management</h3>
|
||||
|
||||
<form method="post" action="/p/{{ product.id }}/inventory/edit" onsubmit="submit.disabled = true; return true;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th>Quantity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for location in locations %}
|
||||
<tr>
|
||||
<td>{{ location.name }}</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
name="inventory_{{ location.id }}"
|
||||
value="{{ inventories[location.id] if location.id in inventories else 0 }}"
|
||||
min="0"
|
||||
required
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<input type="submit" name="submit" class="mps-submit" value="Update Inventory" />
|
||||
</form>
|
||||
|
||||
{% else %}
|
||||
|
||||
<h3>Upload or Replace Product File</h3>
|
||||
|
|
@ -63,8 +94,6 @@ permanent link: <a href="{{ product.absolute_url(request) }}">{{ product.absolut
|
|||
</form>
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if product.is_sellable %}
|
||||
|
||||
<hr/>
|
||||
|
|
@ -88,12 +117,11 @@ permanent link: <a href="{{ product.absolute_url(request) }}">{{ product.absolut
|
|||
<input type="submit" name="submit" class="mps-upload" value="Upload Preview" />
|
||||
</form>
|
||||
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section class="upload-thumbnails well2">
|
||||
<h3>Upload or Replace Thumbnail Files</h3>
|
||||
|
||||
|
|
@ -103,7 +131,6 @@ Your product cover (<code>thumbnail1</code>) will show up on search pages.
|
|||
<br />
|
||||
</section>
|
||||
|
||||
|
||||
{% set s3_key_thumbnails = product.s3_key_thumbnails %}
|
||||
|
||||
{% for thumbnail_key in product.file_thumbnail_keys %}
|
||||
|
|
@ -130,7 +157,6 @@ Your product cover (<code>thumbnail1</code>) will show up on search pages.
|
|||
|
||||
</div>
|
||||
|
||||
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
id = "description_input"
|
||||
class = "mps-product-description"
|
||||
placeholder = "Product Description"
|
||||
required />{{ description }}</textarea>
|
||||
required>{{ description }}</textarea>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
|
@ -57,16 +57,47 @@
|
|||
<br />
|
||||
|
||||
<label for="is_bundle_input">Product Bundle?</label>
|
||||
<input type="checkbox" name="is_bundle" id="is_bundle_input"> If checked, this product will bundle other existing products.
|
||||
<input type="checkbox" name="is_bundle" id="is_bundle_input" {% if is_bundle %}checked{% endif %}> If checked, this product will bundle other existing products.
|
||||
<br>(playlist, gallery, album, collection, series, etc.)
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label>Product Type</label>
|
||||
<div class="radio-group">
|
||||
<input type="radio" id="digital" name="is_physical" value="false" {% if not is_physical %}checked{% endif %}>
|
||||
<label for="digital">Digital</label>
|
||||
{% if shop_has_locations %}
|
||||
<input type="radio" id="physical" name="is_physical" value="true" {% if is_physical %}checked{% endif %}>
|
||||
<label for="physical">Physical</label>
|
||||
{% else %}
|
||||
<input type="radio" id="physical" name="is_physical" value="true" disabled>
|
||||
<label for="physical">Physical (Add a <a href="{{ create_location_url }}">location</a> first)</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<input type="hidden" name="is_sellable" id="is_sellable_input" value="on" required />
|
||||
<input type="submit" name="submit" class="mps-submit" value="Continue" />
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.radio-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-group input[type="radio"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.radio-group label {
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
{%- endblock -%}
|
||||
|
|
|
|||
200
make_post_sell/templates/shop_location_form.j2
Normal file
200
make_post_sell/templates/shop_location_form.j2
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
{% extends "base.j2" -%}
|
||||
|
||||
{%- block call_to_action -%}
|
||||
{%- endblock call_to_action -%}
|
||||
|
||||
{% block content -%}
|
||||
|
||||
<section class="one-column">
|
||||
<section class="shop-name-and-description well">
|
||||
<h3>{{ "Edit" if location_id else "New" }} Shop Location</h3>
|
||||
<form method="post" action="{{ action_url }}" onsubmit="submit.disabled = true; return true;">
|
||||
|
||||
<label for="name_input">Location Name</label>
|
||||
<input
|
||||
name = "name"
|
||||
type = "text"
|
||||
id = "name_input"
|
||||
class = "mps-location-name"
|
||||
value = "{{ name }}"
|
||||
placeholder = "Location Name"
|
||||
required />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="address_input">Address</label>
|
||||
<textarea
|
||||
name = "address"
|
||||
id = "address_input"
|
||||
class = "mps-address"
|
||||
placeholder = "Address"
|
||||
required />{{ address }}</textarea>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="city_input">City</label>
|
||||
<input
|
||||
name = "city"
|
||||
type = "text"
|
||||
id = "city_input"
|
||||
class = "mps-city"
|
||||
value = "{{ city }}"
|
||||
placeholder = "City"
|
||||
required />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="state_input">State</label>
|
||||
<input
|
||||
name = "state"
|
||||
type = "text"
|
||||
id = "state_input"
|
||||
class = "mps-state"
|
||||
value = "{{ state }}"
|
||||
placeholder = "State"
|
||||
required />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="country_input">Country</label>
|
||||
<input
|
||||
name = "country"
|
||||
type = "text"
|
||||
id = "country_input"
|
||||
class = "mps-country"
|
||||
value = "{{ country }}"
|
||||
placeholder = "Country"
|
||||
required />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="postal_code_input">Postal Code</label>
|
||||
<input
|
||||
name = "postal_code"
|
||||
type = "text"
|
||||
id = "postal_code_input"
|
||||
class = "mps-postal-code"
|
||||
value = "{{ postal_code }}"
|
||||
placeholder = "Postal Code"
|
||||
required />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="days_open_input">Days Open</label>
|
||||
<input
|
||||
name = "days_open"
|
||||
type = "text"
|
||||
id = "days_open_input"
|
||||
class = "mps-days-open"
|
||||
value = "{{ days_open }}"
|
||||
placeholder = "Mon-Fri"
|
||||
required />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="hours_open_input">Hours Open</label>
|
||||
<input
|
||||
name = "hours_open"
|
||||
type = "text"
|
||||
id = "hours_open_input"
|
||||
class = "mps-hours-open"
|
||||
value = "{{ hours_open }}"
|
||||
placeholder = "9am-5pm"
|
||||
required />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<input
|
||||
name = "local_pickup"
|
||||
type = "checkbox"
|
||||
id = "local_pickup_input"
|
||||
class = "mps-local-pickup"
|
||||
{% if local_pickup %}checked{% endif %} />
|
||||
<label for="local_pickup_input" style="display: inline;">Local Pickup</label>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<input
|
||||
name = "local_delivery"
|
||||
type = "checkbox"
|
||||
id = "local_delivery_input"
|
||||
class = "mps-local-delivery"
|
||||
{% if local_delivery %}checked{% endif %} />
|
||||
<label for="local_delivery_input" style="display: inline;">Local Delivery</label>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="local_delivery_rate_input">Local Delivery Rate</label>
|
||||
<input
|
||||
name = "local_delivery_rate"
|
||||
type = "text"
|
||||
id = "local_delivery_rate_input"
|
||||
class = "mps-local-delivery-rate"
|
||||
value = "{{ local_delivery_rate }}"
|
||||
placeholder = "0.00" />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<input
|
||||
name = "local_shipping"
|
||||
type = "checkbox"
|
||||
id = "local_shipping_input"
|
||||
class = "mps-local-shipping"
|
||||
{% if local_shipping %}checked{% endif %} />
|
||||
<label for="local_shipping_input" style="display: inline;">Local Shipping</label>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="local_shipping_rate_input">Local Shipping Rate</label>
|
||||
<input
|
||||
name = "local_shipping_rate"
|
||||
type = "text"
|
||||
id = "local_shipping_rate_input"
|
||||
class = "mps-local-shipping-rate"
|
||||
value = "{{ local_shipping_rate }}"
|
||||
placeholder = "0.00" />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<input
|
||||
name = "international_shipping"
|
||||
type = "checkbox"
|
||||
id = "international_shipping_input"
|
||||
class = "mps-international-shipping"
|
||||
{% if international_shipping %}checked{% endif %} />
|
||||
<label for="international_shipping_input" style="display: inline;">International Shipping</label>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<label for="international_shipping_rate_input">International Shipping Rate</label>
|
||||
<input
|
||||
name = "international_shipping_rate"
|
||||
type = "text"
|
||||
id = "international_shipping_rate_input"
|
||||
class = "mps-international-shipping-rate"
|
||||
value = "{{ international_shipping_rate }}"
|
||||
placeholder = "0.00" />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<input type="submit" name="submit" class="mps-submit" value="{{ "Update Location" if location_id else "Create Location" }}" />
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{%- endblock -%}
|
||||
27
make_post_sell/templates/shop_locations.j2
Normal file
27
make_post_sell/templates/shop_locations.j2
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{% extends "base.j2" -%}
|
||||
|
||||
{%- block call_to_action -%}
|
||||
{%- endblock call_to_action -%}
|
||||
|
||||
{% block content -%}
|
||||
|
||||
<section class="one-column">
|
||||
<section class="shop-name-and-description well">
|
||||
<h3>Shop Locations</h3>
|
||||
<ul>
|
||||
{% for location in locations %}
|
||||
<li>
|
||||
<a href="{{ request.route_url('shop_location_switch', shop_id=shop.id, location_id=location.id, _query={'next': request.params.get('next', request.url)}) }}">{{ location.name }}</a>
|
||||
{% if request.user and request.user.can_edit_shop(request.shop) %}
|
||||
<a href="{{ request.route_url('shop_location_edit', shop_id=shop.id, location_id=location.id) }}" class="button">⚙ edit</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if request.user and request.user.can_edit_shop(request.shop) %}
|
||||
<a href="{{ request.route_url('shop_location_new', shop_id=shop.id) }}" class="shop-new-button mps-button">Add New Location</a>
|
||||
{% endif %}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{%- endblock -%}
|
||||
|
|
@ -10,7 +10,7 @@ tr { text-align: left;}
|
|||
|
||||
<table style="width=100%; max-width: 100%; table-layout : fixed;">
|
||||
<thead>
|
||||
<tr> <th> Sellable <th> Price <th> Visibility <th> Bundle <th> Ready <th> Title <th> Size
|
||||
<tr> <th> Sellable <th> Price <th> Visibility <th> Bundle <th> Physical <th> Ready <th> Title <th> Size
|
||||
<tbody style="width=100%; max-width: 100%;">
|
||||
|
||||
{% for product in products %}
|
||||
|
|
@ -20,6 +20,7 @@ tr { text-align: left;}
|
|||
<td><a href="{{ product.absolute_url(request) }}" rel="nofollow">${{ '%0.2f' % product.price }}</a></td>
|
||||
<td>{{ product.human_visibility }}</td>
|
||||
<td>{{ product.is_bundle }}</td>
|
||||
<td>{{ product.is_physical }}</td>
|
||||
<td>{{ product.is_ready }}</td>
|
||||
<td><b><a href="{{ product.absolute_url(request) }}">{{ product.title }}</a></b></td>
|
||||
<td>{{ product.human_total_file_bytes }}</td>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,11 @@
|
|||
required
|
||||
autofocus />
|
||||
|
||||
<input
|
||||
name = "next"
|
||||
type = "hidden"
|
||||
value = "{{ next }}" />
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ def get_referer_or_home(request):
|
|||
"""return referer or or '/'"""
|
||||
return request.referer if request.referer is not None else "/"
|
||||
|
||||
|
||||
# view decorator.
|
||||
def user_required(
|
||||
flash_msg="You must log in to access that area.",
|
||||
|
|
@ -14,16 +15,19 @@ def user_required(
|
|||
"""This view requires that the request has a user."""
|
||||
|
||||
def wrapped(fn):
|
||||
|
||||
def inner(request):
|
||||
if request.user and request.user.authenticated:
|
||||
return fn(request)
|
||||
request.session.flash((flash_msg, flash_level))
|
||||
next_url = request.url
|
||||
if redirect_to_route_name:
|
||||
return HTTPFound(request.route_url(redirect_to_route_name))
|
||||
return HTTPFound(
|
||||
request.route_url(redirect_to_route_name, _query={"next": next_url})
|
||||
)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
return inner
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
|
|
@ -35,7 +39,6 @@ def shop_is_ready_required(
|
|||
"""This view requires that the request has a shop and that show is_ready."""
|
||||
|
||||
def wrapped(fn):
|
||||
|
||||
def inner(request):
|
||||
if request.shop and request.shop.is_ready:
|
||||
return fn(request)
|
||||
|
|
@ -65,11 +68,11 @@ def shop_owner_required(
|
|||
):
|
||||
return fn(request)
|
||||
|
||||
request.session.flash(
|
||||
(flash_msg, flash_level)
|
||||
)
|
||||
request.session.flash((flash_msg, flash_level))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
return inner
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
|
|
@ -91,9 +94,9 @@ def shop_editor_required(
|
|||
):
|
||||
return fn(request)
|
||||
|
||||
request.session.flash(
|
||||
(flash_msg, flash_level)
|
||||
)
|
||||
request.session.flash((flash_msg, flash_level))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
return inner
|
||||
|
||||
return wrapped
|
||||
|
|
|
|||
|
|
@ -58,7 +58,9 @@ def join_or_log_in(request):
|
|||
|
||||
if user.throttle_password():
|
||||
msg = (
|
||||
"Check email for a 6 digit verification code to log in. {}".format(user.email),
|
||||
"Check email for a 6 digit verification code to log in. {}".format(
|
||||
user.email
|
||||
),
|
||||
"info",
|
||||
)
|
||||
|
||||
|
|
@ -72,23 +74,27 @@ def join_or_log_in(request):
|
|||
send_verification_digits_to_email(request, user.email, raw_otp)
|
||||
|
||||
msg = (
|
||||
"Check email for a 6 digit verification code to log in. {}".format(user.email),
|
||||
"Check email for a 6 digit verification code to log in. {}".format(
|
||||
user.email
|
||||
),
|
||||
"info",
|
||||
)
|
||||
|
||||
request.session.flash(msg)
|
||||
|
||||
email_encoded = urlencode({"email": email})
|
||||
email_encoded = urlencode(
|
||||
{"email": email, "next": request.params.get("next", "")}
|
||||
)
|
||||
return HTTPFound("/verification-challenge?{}".format(email_encoded))
|
||||
|
||||
return {
|
||||
"title": "join or log in",
|
||||
"next": request.params.get("next", request.route_url("home")),
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="verification-challenge", renderer="verification-challenge.j2")
|
||||
def verification_challenge(request):
|
||||
|
||||
# get the raw OTP (one-time-password) from posted parameters.
|
||||
raw_otp = request.params.get("raw-otp", "")
|
||||
|
||||
|
|
@ -124,9 +130,14 @@ def verification_challenge(request):
|
|||
# save user and user's active cart.
|
||||
request.dbsession.add(user)
|
||||
request.dbsession.add(request.active_cart)
|
||||
|
||||
request.dbsession.flush()
|
||||
return HTTPFound("/")
|
||||
|
||||
if request.active_cart.count > 0:
|
||||
next_url = "/cart"
|
||||
else:
|
||||
next_url = request.params.get("next", request.route_url("home"))
|
||||
|
||||
return HTTPFound(next_url)
|
||||
|
||||
else:
|
||||
msg = ("Invalid Verification Code", "error")
|
||||
|
|
@ -136,4 +147,5 @@ def verification_challenge(request):
|
|||
"title": "Please Enter Verification Code",
|
||||
"email": email,
|
||||
"raw_otp": raw_otp,
|
||||
"next": request.params.get("next", request.route_url("home")),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ from pyramid.view import view_config
|
|||
from pyramid.httpexceptions import HTTPFound, HTTPBadRequest
|
||||
|
||||
from . import (
|
||||
user_required,
|
||||
shop_is_ready_required,
|
||||
user_required,
|
||||
shop_is_ready_required,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ from . import (
|
|||
@user_required(
|
||||
flash_msg="Verify your email to access Payment Preferences.",
|
||||
flash_level="info",
|
||||
redirect_to_route_name="join-or-log-in"
|
||||
redirect_to_route_name="join-or-log-in",
|
||||
)
|
||||
@shop_is_ready_required()
|
||||
def billing(request):
|
||||
|
|
@ -31,9 +31,8 @@ def billing(request):
|
|||
if card.id == active_card.id:
|
||||
del cards[i]
|
||||
|
||||
setup_intent = request.shop.stripe.SetupIntent.create(
|
||||
payment_method_types=["card"],
|
||||
customer=stripe_user_shop.cus_id
|
||||
setup_intent = request.shop.stripe.SetupIntent.create(
|
||||
payment_method_types=["card"], customer=stripe_user_shop.cus_id
|
||||
)
|
||||
return {
|
||||
"client_secret": setup_intent.client_secret,
|
||||
|
|
@ -41,18 +40,19 @@ def billing(request):
|
|||
"active_card": active_card,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="add-card")
|
||||
@user_required()
|
||||
@shop_is_ready_required()
|
||||
def add_card(request):
|
||||
stripe_setup_intent = request.params.get("setup_intent")
|
||||
stripe_setup_intent_client_secret = request.params.get("setup_intent_client_secret")
|
||||
|
||||
|
||||
stripe_user_shop = request.shop.stripe_user_shop(request.user)
|
||||
|
||||
if stripe_setup_intent:
|
||||
if stripe_setup_intent:
|
||||
setup_intent = request.shop.stripe.SetupIntent.retrieve(
|
||||
stripe_setup_intent,
|
||||
stripe_setup_intent,
|
||||
)
|
||||
|
||||
card_ids = stripe_user_shop.stripe_card_ids
|
||||
|
|
@ -84,6 +84,7 @@ def confirm_update_card(request):
|
|||
"the_title": action_human.title(),
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="update-card")
|
||||
@user_required()
|
||||
@shop_is_ready_required()
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ def save_cart(request):
|
|||
@user_required(
|
||||
flash_msg="To <b>save</b> your cart, please verify your email address below.",
|
||||
flash_level="info",
|
||||
redirect_to_route_name="join-or-log-in"
|
||||
redirect_to_route_name="join-or-log-in",
|
||||
)
|
||||
def cart_save(request):
|
||||
save_cart(request)
|
||||
|
|
@ -67,7 +67,10 @@ def cart_public(request):
|
|||
msg = ("You do not own that cart.", "error")
|
||||
|
||||
else:
|
||||
msg = ("You made that cart public, you may now share the cart's link with others.", "success")
|
||||
msg = (
|
||||
"You made that cart public, you may now share the cart's link with others.",
|
||||
"success",
|
||||
)
|
||||
cart.public = True
|
||||
request.dbsession.add(cart)
|
||||
request.dbsession.flush()
|
||||
|
|
@ -98,7 +101,6 @@ def cart_unpublic(request):
|
|||
|
||||
|
||||
@view_config(route_name="user_cart_activate")
|
||||
@user_required()
|
||||
def cart_activate(request):
|
||||
cart = get_cart_from_matchdict(request)
|
||||
|
||||
|
|
@ -116,7 +118,7 @@ def cart_activate(request):
|
|||
msg = ("This cart is already your active cart.", "success")
|
||||
|
||||
else:
|
||||
if request.user.owns_cart(cart):
|
||||
if request.user and request.user.owns_cart(cart):
|
||||
msg = ("You activated that cart.", "success")
|
||||
request.shop.make_cart_active_for_user(request.user, cart)
|
||||
else:
|
||||
|
|
@ -125,6 +127,7 @@ def cart_activate(request):
|
|||
new_cart.merge_in_cart(cart)
|
||||
request.dbsession.add(new_cart)
|
||||
request.session.flash(msg)
|
||||
request.session["active_cart_id"] = new_cart.uuid_str
|
||||
return HTTPFound("/cart/{}".format(new_cart.id))
|
||||
|
||||
request.session.flash(msg)
|
||||
|
|
@ -164,21 +167,28 @@ def cart(request):
|
|||
|
||||
@view_config(route_name="cart_by_id", renderer="cart.j2")
|
||||
def cart_by_id(request):
|
||||
|
||||
cart = get_cart_from_matchdict(request)
|
||||
|
||||
if cart is None:
|
||||
msg = ("That cart_id does not exist.", "error")
|
||||
|
||||
elif cart.active or cart.public or (request.user and request.user.authenticated and request.user.owns_cart(cart)):
|
||||
|
||||
elif (
|
||||
cart.user is None
|
||||
or cart.public
|
||||
or (
|
||||
request.user and request.user.authenticated and request.user.owns_cart(cart)
|
||||
)
|
||||
):
|
||||
# remove invalid coupons from cart, if any.
|
||||
for coupon in cart.coupons:
|
||||
if coupon.is_not_valid:
|
||||
cart.coupons.remove(coupon)
|
||||
request.dbsession.add(cart)
|
||||
request.dbsession.flush()
|
||||
msg = ("Invalid coupon ({}) removed from cart.".format(coupon.code), "info")
|
||||
msg = (
|
||||
"Invalid coupon ({}) removed from cart.".format(coupon.code),
|
||||
"info",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
|
||||
return {
|
||||
|
|
@ -191,7 +201,10 @@ def cart_by_id(request):
|
|||
"total_discounted_price": cart.total_discounted_price,
|
||||
}
|
||||
else:
|
||||
msg = ("You may not view that cart. It is not public and you do not own it.", "error")
|
||||
msg = (
|
||||
"You may not view that cart. It is not public and you do not own it.",
|
||||
"error",
|
||||
)
|
||||
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
|
@ -199,7 +212,6 @@ def cart_by_id(request):
|
|||
|
||||
@view_config(route_name="cart_add_product")
|
||||
def cart_add_product(request):
|
||||
|
||||
product_id = request.params.get("product_id", None)
|
||||
|
||||
if product_id is None:
|
||||
|
|
@ -209,32 +221,32 @@ def cart_add_product(request):
|
|||
|
||||
product = get_product_by_id(request.dbsession, product_id)
|
||||
|
||||
if product is None:
|
||||
if request.user and request.user.does_not_own_cart(request.active_cart):
|
||||
msg = ("You do not own this cart.", "error")
|
||||
|
||||
elif product is None:
|
||||
msg = ("invalid product_id.", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
elif product.is_not_ready:
|
||||
msg = ("that product is not ready for purchase.", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
elif product.is_not_sellable:
|
||||
msg = ("that content is not for sale or purchase.", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
msg = ('You added "{}" to your cart.'.format(product.title), "success")
|
||||
request.active_cart.add_product(product)
|
||||
request.dbsession.add(request.active_cart)
|
||||
request.dbsession.flush()
|
||||
else:
|
||||
msg = ('You added "{}" to your cart.'.format(product.title), "success")
|
||||
request.active_cart.add_product(product)
|
||||
request.dbsession.add(request.active_cart)
|
||||
request.dbsession.flush()
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/cart")
|
||||
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/cart")
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
|
||||
@view_config(route_name="cart_remove_product")
|
||||
def cart_remove_product(request):
|
||||
|
||||
product_id = request.params.get("product_id", None)
|
||||
|
||||
if product_id is None:
|
||||
|
|
@ -248,25 +260,31 @@ def cart_remove_product(request):
|
|||
if cart is None:
|
||||
msg = ("That cart_id does not exist.", "error")
|
||||
|
||||
elif request.user and request.user.does_not_own_cart(cart):
|
||||
msg = ("You do not own this cart.", "error")
|
||||
|
||||
elif request.user is None and cart.user is not None:
|
||||
msg = ("You do not own this cart.", "error")
|
||||
|
||||
elif request.user is None and cart.user is not None:
|
||||
msg = ("You do not own this cart.", "error")
|
||||
|
||||
elif product is None:
|
||||
msg = ("That product_id does not exist.", "error")
|
||||
|
||||
elif cart.active or (request.user and request.user.authenticated and request.user.owns_cart(cart)):
|
||||
msg = ("You removed \"{}\" from the cart.".format(product.title), "success")
|
||||
else:
|
||||
msg = ('You removed "{}" from the cart.'.format(product.title), "success")
|
||||
cart.remove_product(product)
|
||||
cart.remove_handling_if_no_physical_products()
|
||||
request.dbsession.add(cart)
|
||||
request.dbsession.flush()
|
||||
|
||||
else:
|
||||
msg = ("You do not own that cart.", "error")
|
||||
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
|
||||
@view_config(route_name="cart_quantity_product")
|
||||
def cart_quantity_product(request):
|
||||
|
||||
product_id = request.params.get("product_id", None)
|
||||
quantity = request.params.get("quantity", None)
|
||||
|
||||
|
|
@ -278,7 +296,6 @@ def cart_quantity_product(request):
|
|||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
|
||||
if product_id is None or quantity is None or quantity == "":
|
||||
msg = ("missing product_id or quantity.", "error")
|
||||
request.session.flash(msg)
|
||||
|
|
@ -294,6 +311,16 @@ def cart_quantity_product(request):
|
|||
if cart is None:
|
||||
msg = ("That cart_id does not exist.", "error")
|
||||
|
||||
elif request.user and request.user.does_not_own_cart(cart):
|
||||
msg = ("You do not own this cart.", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
elif request.user is None and cart.user is not None:
|
||||
msg = ("You do not own this cart.", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
elif product is None:
|
||||
msg = ("That product_id does not exist.", "error")
|
||||
request.session.flash(msg)
|
||||
|
|
@ -302,32 +329,64 @@ def cart_quantity_product(request):
|
|||
# then refuse to add a product of another shop.
|
||||
# we could keep a simple allow list of marketplace domains.
|
||||
elif not request.is_saas_domain and request.shop.uuid_str != product.shop_uuid_str:
|
||||
request.session.flash(("Refusing to add a product from another shop to cart.", "error"))
|
||||
request.session.flash(
|
||||
("Refusing to add a product from another shop to cart.", "error")
|
||||
)
|
||||
return HTTPFound("/cart")
|
||||
|
||||
elif cart.active or (request.user and request.user.authenticated and request.user.owns_cart(cart)):
|
||||
msg = ("You updated the quantity of \"{}\" in the cart.".format(product.title), "success")
|
||||
else:
|
||||
msg = (
|
||||
'You updated the quantity of "{}" in the cart.'.format(product.title),
|
||||
"success",
|
||||
)
|
||||
cart.set_product_quantity(product, quantity)
|
||||
request.dbsession.add(cart)
|
||||
request.dbsession.flush()
|
||||
|
||||
else:
|
||||
msg = ("You do not own that cart.", "error")
|
||||
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
|
||||
@view_config(route_name="cart_handling_option", request_method="POST")
|
||||
def cart_handling_option(request):
|
||||
cart = get_cart_from_matchdict(request)
|
||||
handling_option = request.params.get("handling_option")
|
||||
|
||||
if request.user and request.user.does_not_own_cart(cart):
|
||||
request.session.flash(("You do not own this cart.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
if request.user is None and cart.user is not None:
|
||||
request.session.flash(("You do not own this cart.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
if handling_option not in [
|
||||
"local_pickup",
|
||||
"local_delivery",
|
||||
"local_shipping",
|
||||
"international_shipping",
|
||||
]:
|
||||
request.session.flash(("Invalid handling option selected.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
cart.handling_option = handling_option
|
||||
cart.update_handling_cost(request.shop_location)
|
||||
|
||||
request.dbsession.add(cart)
|
||||
request.dbsession.flush()
|
||||
|
||||
request.session.flash(("Handling option set successfully.", "success"))
|
||||
return HTTPFound(f"/cart/{cart.id}")
|
||||
|
||||
|
||||
@view_config(route_name="cart_checkout", renderer="cart_checkout.j2")
|
||||
@view_config(route_name="user_cart_checkout", renderer="cart_checkout.j2")
|
||||
@user_required(
|
||||
flash_msg="To <b>checkout</b> your cart, please verify your email address below.",
|
||||
flash_level="info",
|
||||
redirect_to_route_name="join-or-log-in"
|
||||
redirect_to_route_name="join-or-log-in",
|
||||
)
|
||||
@shop_is_ready_required()
|
||||
def cart_checkout(request):
|
||||
|
||||
stripe_user_shop = request.shop.stripe_user_shop(request.user)
|
||||
|
||||
if "cart_id" not in request.matchdict:
|
||||
|
|
@ -344,30 +403,49 @@ def cart_checkout(request):
|
|||
elif cart.is_empty:
|
||||
msg = ("That cart is empty, you cannot checkout.", "error")
|
||||
|
||||
elif cart.requires_payment and stripe_user_shop is None:
|
||||
msg = ("Please enter your payment information.", "info")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/billing")
|
||||
|
||||
elif stripe_user_shop.active_card is None:
|
||||
msg = ("Please make a payment method active.", "info")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/billing")
|
||||
|
||||
else:
|
||||
# make sure all coupon terms are met before proceeding.
|
||||
# Validate coupons first
|
||||
error_messages = cart.validate_attached_coupons()
|
||||
if error_messages:
|
||||
for error_message in error_messages:
|
||||
request.session.flash((error_message, "error"))
|
||||
request.session.flash((error_message, "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
# Check handling options, inventory, and address for physical products
|
||||
if cart.physical_products:
|
||||
inventory_errors = cart.check_inventory(request.shop_location)
|
||||
if inventory_errors:
|
||||
for error_message in inventory_errors:
|
||||
request.session.flash((error_message, "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
if not cart.handling_option:
|
||||
msg = ("Please select a handling option for your physical items.", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
if not request.user.active_address:
|
||||
msg = ("Please add a shipping address to proceed with checkout.", "info")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/u/addresses")
|
||||
|
||||
# Check for payment information
|
||||
if cart.requires_payment and stripe_user_shop is None:
|
||||
msg = ("Please enter your payment information.", "info")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/billing")
|
||||
|
||||
if stripe_user_shop.active_card is None:
|
||||
msg = ("Please make a payment method active.", "info")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/billing")
|
||||
|
||||
msg = ("Please confirm your order.", "info")
|
||||
request.session.flash(msg)
|
||||
return {
|
||||
"cart": cart,
|
||||
"products": cart.products,
|
||||
"active_card": stripe_user_shop.active_card
|
||||
"active_card": stripe_user_shop.active_card,
|
||||
}
|
||||
|
||||
request.session.flash(msg)
|
||||
|
|
@ -399,74 +477,99 @@ def cart_complete_checkout(request):
|
|||
error_messages = cart.validate_attached_coupons()
|
||||
if error_messages:
|
||||
for error_message in error_messages:
|
||||
request.session.flash((error_message, "error"))
|
||||
request.session.flash((error_message, "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
# TODO: currently this routine is naive to "market" invoices, an Invoice
|
||||
# which has InvoiceLineItems for Products derived from more than one Shop.
|
||||
invoice = Invoice(user=request.user)
|
||||
|
||||
invoice.shop = request.shop
|
||||
invoice.market = request.market
|
||||
|
||||
for product_id, quantity in cart.cart.items():
|
||||
product = cart.products[product_id]
|
||||
product.unlock_for_user(request.user)
|
||||
request.dbsession.add(product)
|
||||
invoice.new_line_item(
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
market=invoice.market,
|
||||
)
|
||||
|
||||
for coupon in cart.coupons:
|
||||
invoice.new_coupon_redemption(coupon)
|
||||
|
||||
request.dbsession.add(invoice)
|
||||
|
||||
invoices = []
|
||||
successful_checkout = True
|
||||
|
||||
# only create a real charge if the cart has a positive price.
|
||||
# this conditional allows us to support freebies.
|
||||
if cart.requires_payment:
|
||||
for shop_id, product_quantity_tuple in cart.shop_product_dict.items():
|
||||
shop = cart.shops[shop_id]
|
||||
invoice = Invoice(user=request.user)
|
||||
invoice.shop = shop
|
||||
invoice.shop_id = shop.id
|
||||
invoice.handling_option = cart.handling_option
|
||||
invoice.handling_cost_in_cents = cart.handling_cost_in_cents
|
||||
|
||||
stripe_user_shop = request.shop.stripe_user_shop(request.user)
|
||||
|
||||
try:
|
||||
request.shop.stripe.PaymentIntent.create(
|
||||
amount=cart.total_in_cents,
|
||||
currency='usd',
|
||||
customer=stripe_user_shop.cus_id,
|
||||
payment_method=stripe_user_shop.active_card_id,
|
||||
off_session=True,
|
||||
confirm=True,
|
||||
# Copy the user's active address to the invoice
|
||||
if cart.physical_products:
|
||||
invoice.delivery_address = request.user.active_address.data
|
||||
|
||||
for product, quantity in product_quantity_tuple:
|
||||
product.unlock_for_user(request.user)
|
||||
request.dbsession.add(product)
|
||||
invoice.new_line_item(
|
||||
product=product,
|
||||
quantity=quantity,
|
||||
)
|
||||
except request.shop.stripe.error.CardError as e:
|
||||
|
||||
# rollback changes to database because charge was NOT successful!
|
||||
request.dbsession.rollback()
|
||||
for coupon in cart.coupons:
|
||||
invoice.new_coupon_redemption(coupon)
|
||||
|
||||
err = e.error
|
||||
# Error code will be authentication_required if authentication is needed
|
||||
payment_intent_id = err.payment_intent['id']
|
||||
payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id)
|
||||
request.flash("Payment Error: {} {} - {}".format(
|
||||
payment_intent.last_payment_error.code,
|
||||
payment_intent.last_payment_error.decline_code,
|
||||
payment_intent.last_payment_error.message,
|
||||
), "error")
|
||||
successful_checkout = False
|
||||
|
||||
request.dbsession.add(invoice)
|
||||
invoices.append(invoice)
|
||||
|
||||
# Loop over each invoice to handle payments
|
||||
for invoice in invoices:
|
||||
shop = invoice.shop
|
||||
|
||||
# only create a real charge if the invoice has a positive price.
|
||||
if invoice.requires_payment:
|
||||
stripe_user_shop = shop.stripe_user_shop(request.user)
|
||||
|
||||
try:
|
||||
shop.stripe.PaymentIntent.create(
|
||||
amount=invoice.total_in_cents,
|
||||
currency="usd",
|
||||
customer=stripe_user_shop.cus_id,
|
||||
payment_method=stripe_user_shop.active_card_id,
|
||||
off_session=True,
|
||||
confirm=True,
|
||||
)
|
||||
except shop.stripe.error.CardError as e:
|
||||
# rollback changes to database because charge was NOT successful!
|
||||
request.dbsession.rollback()
|
||||
|
||||
err = e.error
|
||||
# Error code will be authentication_required if authentication is needed
|
||||
payment_intent_id = err.payment_intent["id"]
|
||||
payment_intent = shop.stripe.PaymentIntent.retrieve(
|
||||
payment_intent_id
|
||||
)
|
||||
request.flash(
|
||||
"Payment Error: {} {} - {}".format(
|
||||
payment_intent.last_payment_error.code,
|
||||
payment_intent.last_payment_error.decline_code,
|
||||
payment_intent.last_payment_error.message,
|
||||
),
|
||||
"error",
|
||||
)
|
||||
successful_checkout = False
|
||||
break
|
||||
|
||||
if successful_checkout:
|
||||
# Update inventory for physical products
|
||||
cart.update_inventory(request.shop_location)
|
||||
|
||||
# flush changes to database because charge was successful!
|
||||
request.dbsession.flush()
|
||||
msg = ("Success, you have completed the purchase!", "success")
|
||||
request.session.flash(msg)
|
||||
# email the authenticated user regarding their purchase.
|
||||
send_purchase_email(request, request.user.email, cart.products.values(), cart.total)
|
||||
# email the shop owners regarding their sale.
|
||||
send_sale_email(request, request.shop, cart.products.values(), cart.total)
|
||||
for invoice in invoices:
|
||||
send_purchase_email(
|
||||
request,
|
||||
request.user.email,
|
||||
[item.product for item in invoice.line_items],
|
||||
invoice.total,
|
||||
)
|
||||
# email the shop owners regarding their sale.
|
||||
send_sale_email(
|
||||
request,
|
||||
invoice.shop,
|
||||
[item.product for item in invoice.line_items],
|
||||
invoice.total,
|
||||
)
|
||||
# save the cart, so that we end up with a new empty cart.
|
||||
save_cart(request)
|
||||
return HTTPFound("/u/purchases")
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ from pyramid.httpexceptions import HTTPFound
|
|||
@view_config(route_name="content", renderer="content.j2")
|
||||
@view_config(route_name="content_slug", renderer="content.j2")
|
||||
def content(request):
|
||||
|
||||
product = request.product
|
||||
|
||||
if not request.is_saas_domain and request.domain != request.shop.domain_name:
|
||||
|
|
@ -31,8 +30,11 @@ def content(request):
|
|||
|
||||
bucket_name = request.app["bucket.secure_uploads"]
|
||||
|
||||
if request.user and request.user.authenticated and request.user.can_download_product(product):
|
||||
|
||||
if (
|
||||
request.user
|
||||
and request.user.authenticated
|
||||
and request.user.can_download_product(product)
|
||||
):
|
||||
# Params: Bucket, IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince,
|
||||
# Key, Range, ResponseCacheControl, ResponseContentDisposition, ResponseProductEncoding,
|
||||
# ResponseContentLanguage, ResponseContentType, ResponseExpires, VersionId,
|
||||
|
|
@ -58,10 +60,10 @@ def content(request):
|
|||
params["ResponseContentType"] = content_type
|
||||
|
||||
signed_get_object_url = request.secure_uploads_client.generate_presigned_url(
|
||||
ClientMethod='get_object',
|
||||
ClientMethod="get_object",
|
||||
Params=params,
|
||||
# 15 minutes.
|
||||
ExpiresIn=900
|
||||
ExpiresIn=900,
|
||||
)
|
||||
|
||||
product_size = ""
|
||||
|
|
|
|||
|
|
@ -27,18 +27,14 @@ def get_coupon_from_matchdict(request):
|
|||
@view_config(route_name="coupons", renderer="coupons.j2")
|
||||
@shop_owner_required()
|
||||
def coupons(request):
|
||||
return {
|
||||
"coupons": request.shop.coupons.all()
|
||||
}
|
||||
return {"coupons": request.shop.coupons.all()}
|
||||
|
||||
|
||||
@view_config(route_name="coupon1", renderer="coupon.j2")
|
||||
@view_config(route_name="coupon2", renderer="coupon.j2")
|
||||
def coupon(request):
|
||||
coupon = get_coupon_from_matchdict(request)
|
||||
return {
|
||||
"coupon": coupon
|
||||
}
|
||||
return {"coupon": coupon}
|
||||
|
||||
|
||||
@view_config(route_name="coupon_new1", renderer="coupon_new.j2")
|
||||
|
|
@ -55,26 +51,25 @@ def coupon_new(request):
|
|||
cart_qualifier = request.params.get("cart_qualifier")
|
||||
|
||||
if request.params:
|
||||
|
||||
if not code or not description or not action_type or not action_type:
|
||||
request.session.flash(("Please submit all required fields.", "error"))
|
||||
else:
|
||||
request.session.flash(("You created a new coupon!", "success"))
|
||||
coupon = Coupon(
|
||||
shop = request.shop,
|
||||
code = code,
|
||||
description = description,
|
||||
action_type = action_type,
|
||||
action_value = action_value,
|
||||
max_redemptions = max_redemptions,
|
||||
max_redemptions_per_user = max_redemptions_per_user,
|
||||
expiration_date = expiration_date,
|
||||
cart_qualifier = cart_qualifier,
|
||||
shop=request.shop,
|
||||
code=code,
|
||||
description=description,
|
||||
action_type=action_type,
|
||||
action_value=action_value,
|
||||
max_redemptions=max_redemptions,
|
||||
max_redemptions_per_user=max_redemptions_per_user,
|
||||
expiration_date=expiration_date,
|
||||
cart_qualifier=cart_qualifier,
|
||||
)
|
||||
request.dbsession.add(coupon)
|
||||
request.dbsession.flush()
|
||||
return HTTPFound("/s/{}/coupons".format(request.shop.id))
|
||||
|
||||
|
||||
return request.params
|
||||
|
||||
|
||||
|
|
@ -102,7 +97,6 @@ def coupon_apply_to_cart(request):
|
|||
|
||||
|
||||
@view_config(route_name="coupon_remove_from_cart", request_method="POST")
|
||||
@user_required()
|
||||
def coupon_remove_from_cart(request):
|
||||
cart_id = request.params.get("cart_id")
|
||||
coupon_id = request.params.get("coupon_id")
|
||||
|
|
@ -116,9 +110,12 @@ def coupon_remove_from_cart(request):
|
|||
elif coupon is None:
|
||||
msg = ("That coupon_id does not exist.", "error")
|
||||
|
||||
elif request.user.does_not_own_cart(cart):
|
||||
elif request.user and request.user.does_not_own_cart(cart):
|
||||
msg = ("You do not own that cart.", "error")
|
||||
|
||||
elif request.user is None and cart.user is not None:
|
||||
msg = ("You do not own this cart.", "error")
|
||||
|
||||
else:
|
||||
if coupon in cart.coupons:
|
||||
msg = ("The coupon was removed from your cart.", "success")
|
||||
|
|
|
|||
29
make_post_sell/views/invoice.py
Normal file
29
make_post_sell/views/invoice.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from pyramid.view import view_config
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
from ..models.invoice import get_invoice_by_id
|
||||
|
||||
from . import (
|
||||
user_required,
|
||||
get_referer_or_home,
|
||||
)
|
||||
|
||||
@view_config(route_name="view_invoice", renderer="invoice.j2")
|
||||
@user_required()
|
||||
def view_invoice(request):
|
||||
invoice_id = request.matchdict.get("invoice_id")
|
||||
invoice = get_invoice_by_id(request.dbsession, invoice_id)
|
||||
|
||||
if invoice is None:
|
||||
request.session.flash(("Invoice not found.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
# Check if the user is the owner of the invoice or a shop editor
|
||||
if request.user != invoice.user and not request.user.can_edit_shop(invoice.shop):
|
||||
request.session.flash(("You do not have permission to view this invoice.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
return {
|
||||
"invoice": invoice,
|
||||
"line_items": invoice.line_items.all(),
|
||||
}
|
||||
|
||||
|
|
@ -9,6 +9,8 @@ from . import (
|
|||
)
|
||||
|
||||
from ..models.product import Product
|
||||
from ..models.shop_location import ShopLocation
|
||||
from ..models.inventory import Inventory
|
||||
|
||||
from ..lib.currency import dollars_to_cents
|
||||
|
||||
|
|
@ -20,7 +22,6 @@ def checkbox_to_bool(checkbox):
|
|||
@view_config(route_name="product", renderer="product.j2")
|
||||
@view_config(route_name="product_slug", renderer="product.j2")
|
||||
def product(request):
|
||||
|
||||
product = request.product
|
||||
|
||||
if not request.is_saas_domain and request.domain != request.shop.domain_name:
|
||||
|
|
@ -43,8 +44,11 @@ def product(request):
|
|||
|
||||
bucket_name = request.app["bucket.secure_uploads"]
|
||||
|
||||
if request.user and request.user.authenticated and request.user.can_download_product(product):
|
||||
|
||||
if (
|
||||
request.user
|
||||
and request.user.authenticated
|
||||
and request.user.can_download_product(product)
|
||||
):
|
||||
# Params: Bucket, IfMatch, IfModifiedSince, IfNoneMatch, IfUnmodifiedSince,
|
||||
# Key, Range, ResponseCacheControl, ResponseContentDisposition, ResponseContentEncoding,
|
||||
# ResponseContentLanguage, ResponseContentType, ResponseExpires, VersionId,
|
||||
|
|
@ -70,10 +74,10 @@ def product(request):
|
|||
params["ResponseContentType"] = content_type
|
||||
|
||||
signed_get_object_url = request.secure_uploads_client.generate_presigned_url(
|
||||
ClientMethod='get_object',
|
||||
ClientMethod="get_object",
|
||||
Params=params,
|
||||
# 15 minutes.
|
||||
ExpiresIn=900
|
||||
ExpiresIn=900,
|
||||
)
|
||||
|
||||
product_size = ""
|
||||
|
|
@ -92,7 +96,6 @@ def product(request):
|
|||
@user_required()
|
||||
@shop_editor_required()
|
||||
def product_new(request):
|
||||
|
||||
title = request.params.get("title", "")
|
||||
description = request.params.get("description", "")
|
||||
price = request.params.get("price", "")
|
||||
|
|
@ -103,26 +106,35 @@ def product_new(request):
|
|||
is_sellable_checkbox = request.params.get("is_sellable", "off")
|
||||
is_sellable = checkbox_to_bool(is_sellable_checkbox)
|
||||
|
||||
is_physical = bool(request.params.get("is_physical", "false") == "true")
|
||||
|
||||
if title and description:
|
||||
|
||||
product = Product(title, description)
|
||||
product.shop = request.shop
|
||||
product.is_bundle = is_bundle
|
||||
product.is_sellable = is_sellable
|
||||
|
||||
if product.error_message:
|
||||
request.session.flash((product.error_message, "error"))
|
||||
|
||||
else:
|
||||
if is_sellable and price:
|
||||
product_price = product.set_price(price)
|
||||
request.dbsession.add(product_price)
|
||||
|
||||
request.dbsession.add(product)
|
||||
request.dbsession.flush()
|
||||
msg = ("Great, next you may upload files.", "success")
|
||||
# prevent trying to create a bundle and also physical product.
|
||||
if is_bundle and is_physical:
|
||||
msg = (
|
||||
"The bundle feature only works with digital goods. Please uncheck the bundle option or mark the product as digital.",
|
||||
"error",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/p/{}/edit".format(product.id))
|
||||
else:
|
||||
product = Product(title, description)
|
||||
product.shop = request.shop
|
||||
product.is_bundle = is_bundle
|
||||
product.is_sellable = is_sellable
|
||||
product.is_physical = is_physical
|
||||
|
||||
if product.error_message:
|
||||
request.session.flash((product.error_message, "error"))
|
||||
else:
|
||||
if is_sellable and price:
|
||||
product_price = product.set_price(price)
|
||||
request.dbsession.add(product_price)
|
||||
|
||||
request.dbsession.add(product)
|
||||
request.dbsession.flush()
|
||||
msg = ("Great, next you may upload files.", "success")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/p/{}/edit".format(product.id))
|
||||
|
||||
elif title or description or price:
|
||||
msg = ("You must fill out all fields.", "error")
|
||||
|
|
@ -132,6 +144,12 @@ def product_new(request):
|
|||
"title": title,
|
||||
"description": description,
|
||||
"price": price,
|
||||
"is_bundle": is_bundle,
|
||||
"is_physical": is_physical,
|
||||
"create_location_url": request.route_url(
|
||||
"shop_location_new", shop_id=request.shop.id
|
||||
),
|
||||
"shop_has_locations": len(request.shop.shop_locations.all()) > 0,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -143,7 +161,7 @@ def product_edit_description(request):
|
|||
raw_markup_data = request.params.get(
|
||||
"markup-editor-textarea",
|
||||
# TODO: rename this column to description_raw.
|
||||
product.description
|
||||
product.description,
|
||||
)
|
||||
|
||||
if raw_markup_data and raw_markup_data != product.description:
|
||||
|
|
@ -159,7 +177,7 @@ def product_edit_description(request):
|
|||
"markup_raw": product.description,
|
||||
"markup_form_path": "{}/description".format(
|
||||
product.absolute_url(request, slug=False)
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -167,7 +185,6 @@ def product_edit_description(request):
|
|||
@view_config(route_name="product_edit2", renderer="product_edit.j2")
|
||||
@shop_editor_required()
|
||||
def product_edit(request):
|
||||
|
||||
product_modified = False
|
||||
product = request.product
|
||||
|
||||
|
|
@ -177,11 +194,13 @@ def product_edit(request):
|
|||
visibility = int(request.params.get("visibility", product.visibility))
|
||||
uploaded_file_key = request.params.get("uploaded", "")
|
||||
|
||||
if uploaded_file_key:
|
||||
if uploaded_file_key:
|
||||
request.session.flash(
|
||||
(
|
||||
"<span class=\"avoid-wrap\">You successfully uploaded</span> <span class=\"avoid-wrap\">a new {} file!</span>".format(uploaded_file_key),
|
||||
"success"
|
||||
'<span class="avoid-wrap">You successfully uploaded</span> <span class="avoid-wrap">a new {} file!</span>'.format(
|
||||
uploaded_file_key
|
||||
),
|
||||
"success",
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -201,7 +220,7 @@ def product_edit(request):
|
|||
product_modified = True
|
||||
product.title = title
|
||||
request.session.flash(("You updated the product's title.", "success"))
|
||||
|
||||
|
||||
if description != product.description:
|
||||
product_modified = True
|
||||
product.set_description(description)
|
||||
|
|
@ -224,7 +243,7 @@ def product_edit(request):
|
|||
# Store the uploaded user's file_metadata in our database.
|
||||
file_key = product.store_file_metadata(s3_webhook_key)
|
||||
|
||||
# This is how we protect our digital downloads while using the
|
||||
# This is how we protect our digital downloads while using the
|
||||
# same bucket for thumbnails and previews related to the product.
|
||||
acl = "private"
|
||||
if file_key in product.file_public_keys:
|
||||
|
|
@ -238,7 +257,7 @@ def product_edit(request):
|
|||
# copy upload to our system defined s3 location.
|
||||
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.copy_object
|
||||
request.secure_uploads_client.copy_object(
|
||||
ACL = acl,
|
||||
ACL=acl,
|
||||
Bucket=request.app["bucket.secure_uploads"],
|
||||
CopySource={
|
||||
"Bucket": s3_webhook_bucket,
|
||||
|
|
@ -256,11 +275,11 @@ def product_edit(request):
|
|||
Bucket=s3_webhook_bucket,
|
||||
Key=s3_webhook_key,
|
||||
)
|
||||
|
||||
# get file size & store in our database.
|
||||
|
||||
# get file size & store in our database.
|
||||
response = request.secure_uploads_client.head_object(
|
||||
Bucket=request.app["bucket.secure_uploads"],
|
||||
Key="{}/{}".format(product.s3_path, file_key)
|
||||
Key="{}/{}".format(product.s3_path, file_key),
|
||||
)
|
||||
|
||||
product_content_length = response["ContentLength"]
|
||||
|
|
@ -275,7 +294,9 @@ def product_edit(request):
|
|||
|
||||
# redirect back to this page to clear
|
||||
# the params posted by the s3 webhooks.
|
||||
return HTTPFound("/p/{}/{}/edit?uploaded={}".format(product.id, product.slug, file_key))
|
||||
return HTTPFound(
|
||||
"/p/{}/{}/edit?uploaded={}".format(product.id, product.slug, file_key)
|
||||
)
|
||||
|
||||
if product_modified and product.error_message is None:
|
||||
request.dbsession.add(product)
|
||||
|
|
@ -286,7 +307,7 @@ def product_edit(request):
|
|||
for file_key in product.file_keys:
|
||||
key_starts_with = "{}/{}.".format(product.s3_path, file_key)
|
||||
conditions = [
|
||||
{ "success_action_redirect": product.absolute_edit_url(request) },
|
||||
{"success_action_redirect": product.absolute_edit_url(request)},
|
||||
["starts-with", "$key", key_starts_with],
|
||||
]
|
||||
|
||||
|
|
@ -310,6 +331,18 @@ def product_edit(request):
|
|||
continue
|
||||
signed_posts[file_key]["fields"].update(condition)
|
||||
|
||||
# Fetch locations and inventory quantities for physical products
|
||||
locations = []
|
||||
if product.is_physical:
|
||||
locations = (
|
||||
request.dbsession.query(ShopLocation)
|
||||
.filter_by(shop_id=product.shop_id)
|
||||
.all()
|
||||
)
|
||||
inventories = {
|
||||
inv.shop_location.id: inv.quantity for inv in product.inventories
|
||||
}
|
||||
|
||||
return {
|
||||
"product": product,
|
||||
"title": title,
|
||||
|
|
@ -317,14 +350,54 @@ def product_edit(request):
|
|||
"price": price,
|
||||
"visibility": visibility,
|
||||
"signed_posts": signed_posts,
|
||||
"locations": locations,
|
||||
"inventories": inventories if product.is_physical else {},
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="product_inventory_edit")
|
||||
@shop_editor_required()
|
||||
def product_inventory_edit(request):
|
||||
product = request.product
|
||||
|
||||
if not product.is_physical:
|
||||
request.session.flash(("This product is not physical.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
# Get all shop locations for the product's shop
|
||||
shop_locations = request.shop.shop_locations
|
||||
|
||||
updated = False
|
||||
|
||||
for location in shop_locations:
|
||||
quantity = request.params.get(f"inventory_{location.id}", None)
|
||||
if quantity is not None:
|
||||
try:
|
||||
quantity = int(quantity)
|
||||
inventory = next((inv for inv in product.inventories if inv.shop_location_id == location.id), None)
|
||||
if inventory:
|
||||
if inventory.quantity != quantity:
|
||||
inventory.update_quantity(quantity)
|
||||
request.dbsession.add(inventory)
|
||||
updated = True
|
||||
else:
|
||||
inventory = Inventory(shop_location=location, product=product, quantity=quantity)
|
||||
request.dbsession.add(inventory)
|
||||
updated = True
|
||||
except ValueError:
|
||||
request.session.flash((f"Invalid quantity for location {location.name}.", "error"))
|
||||
|
||||
if updated:
|
||||
request.dbsession.flush()
|
||||
request.session.flash(("Inventory updated successfully.", "success"))
|
||||
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
|
||||
@view_config(route_name="product_bundle_edit")
|
||||
@view_config(route_name="product_bundle_edit2")
|
||||
@shop_editor_required()
|
||||
def product_bundle_edit(request):
|
||||
|
||||
from ..models.product import get_products_by_ids
|
||||
|
||||
from ..lib.uuid_validation import UUID_ALL_PATTERN
|
||||
|
|
@ -332,14 +405,15 @@ def product_bundle_edit(request):
|
|||
product = request.product
|
||||
|
||||
if product.is_bundle == False:
|
||||
request.session.flash(("You may only add products to a Product bundle.", "error"))
|
||||
request.session.flash(
|
||||
("You may only add products to a Product bundle.", "error")
|
||||
)
|
||||
|
||||
bundle_data = request.params.get("bundle-data", None)
|
||||
|
||||
if bundle_data:
|
||||
products = get_products_by_ids(
|
||||
request.dbsession,
|
||||
UUID_ALL_PATTERN.findall(bundle_data)
|
||||
request.dbsession, UUID_ALL_PATTERN.findall(bundle_data)
|
||||
)
|
||||
for product_to_bundle in products:
|
||||
if product.add_product_to_bundle(product_to_bundle) == False:
|
||||
|
|
@ -353,7 +427,6 @@ def product_bundle_edit(request):
|
|||
@view_config(route_name="product_remove_bundled_product")
|
||||
@shop_editor_required()
|
||||
def product_bundle_remove_product(request):
|
||||
|
||||
from ..models.product import get_product_by_id
|
||||
|
||||
product = request.product
|
||||
|
|
@ -362,7 +435,9 @@ def product_bundle_remove_product(request):
|
|||
bundled_product = get_product_by_id(request.dbsession, bundled_product_id)
|
||||
|
||||
if product.is_bundle == False:
|
||||
request.session.flash(("You may only remove products from a Product bundle.", "error"))
|
||||
request.session.flash(
|
||||
("You may only remove products from a Product bundle.", "error")
|
||||
)
|
||||
|
||||
if product.remove_product_from_bundle(bundled_product) == False:
|
||||
request.session.flash((product.error_message, "error"))
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from ..models.user import (
|
|||
)
|
||||
|
||||
from ..models.shop_search_request import (
|
||||
ShopSearchRequest,
|
||||
ShopSearchRequest,
|
||||
)
|
||||
|
||||
from ..lib.mail import send_invite_email
|
||||
|
|
@ -37,10 +37,10 @@ from ..lib.phone_numbers import is_phone_number_valid
|
|||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
|
||||
|
||||
def checkbox_to_bool(checkbox):
|
||||
return True if checkbox == "on" else False
|
||||
|
||||
|
||||
def bool_to_checkbox(boolean):
|
||||
return "on" if boolean == True else "off"
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ def get_shop_from_matchdict(request, prefetched_shop=None):
|
|||
# returns None if shop_id is an invalid uuid.
|
||||
return get_shop_by_id(request.dbsession, request.matchdict["shop_id"])
|
||||
|
||||
|
||||
|
||||
@view_config(route_name="home", renderer="home.j2")
|
||||
def home(request):
|
||||
products = None
|
||||
|
|
@ -92,7 +92,7 @@ def shop_products(request):
|
|||
if shop is None:
|
||||
msg = ("That shop_id does not exist.", "error")
|
||||
|
||||
if msg:
|
||||
if msg:
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
|
|
@ -118,9 +118,7 @@ def search(request):
|
|||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
products = get_products_by_keywords(
|
||||
request.dbsession,
|
||||
keywords.split(" "),
|
||||
request.shop
|
||||
request.dbsession, keywords.split(" "), request.shop
|
||||
)
|
||||
|
||||
hit_count = len(products)
|
||||
|
|
@ -129,8 +127,8 @@ def search(request):
|
|||
search_request = ShopSearchRequest(
|
||||
keywords,
|
||||
hit_count,
|
||||
shop = request.shop,
|
||||
user = request.user,
|
||||
shop=request.shop,
|
||||
user=request.user,
|
||||
)
|
||||
request.dbsession.add(search_request)
|
||||
request.dbsession.flush()
|
||||
|
|
@ -148,7 +146,6 @@ def search(request):
|
|||
@view_config(route_name="shop_new", renderer="shop_new.j2")
|
||||
@user_required()
|
||||
def shop_new(request):
|
||||
|
||||
name = request.params.get("name", "")
|
||||
phone_number = request.params.get("phone_number", "")
|
||||
billing_address = request.params.get("billing_address", "")
|
||||
|
|
@ -156,7 +153,10 @@ def shop_new(request):
|
|||
|
||||
if name and description and billing_address and phone_number:
|
||||
if is_shop_name_valid(name) == False:
|
||||
msg = ("Invalid shop name, only use alpha numeric, spaces, dashes, or periods.", "error")
|
||||
msg = (
|
||||
"Invalid shop name, only use alpha numeric, spaces, dashes, or periods.",
|
||||
"error",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
elif is_shop_name_available(request.dbsession, name) == False:
|
||||
msg = ("That shop name is already in use. Please pick another.", "error")
|
||||
|
|
@ -172,7 +172,10 @@ def shop_new(request):
|
|||
request.dbsession.add(shop)
|
||||
request.dbsession.add(request.user)
|
||||
request.dbsession.flush()
|
||||
msg = ("Great work, you created a shop! You may continue to setup your shop or start posting products!", "success")
|
||||
msg = (
|
||||
"Great work, you created a shop! You may continue to setup your shop or start posting products!",
|
||||
"success",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/s/{}/settings".format(shop.id))
|
||||
|
||||
|
|
@ -191,9 +194,7 @@ def shop_new(request):
|
|||
@view_config(route_name="user_shop_activate")
|
||||
@shop_editor_required()
|
||||
def shop_activate(request):
|
||||
shop = get_shop_from_matchdict(
|
||||
request, prefetched_shop=request.user.active_shop
|
||||
)
|
||||
shop = get_shop_from_matchdict(request, prefetched_shop=request.user.active_shop)
|
||||
|
||||
if shop is None:
|
||||
msg = ("That shop_id does not exist.", "error")
|
||||
|
|
@ -237,8 +238,7 @@ def shop_edit_terms_of_service(request):
|
|||
shop = request.shop
|
||||
|
||||
raw_markup_data = request.params.get(
|
||||
"markup-editor-textarea",
|
||||
shop.terms_of_service_raw
|
||||
"markup-editor-textarea", shop.terms_of_service_raw
|
||||
)
|
||||
|
||||
if raw_markup_data and raw_markup_data != shop.terms_of_service_raw:
|
||||
|
|
@ -252,7 +252,7 @@ def shop_edit_terms_of_service(request):
|
|||
"markup_subject": "Terms of Service",
|
||||
"markup_rendered": shop.terms_of_service_html,
|
||||
"markup_raw": shop.terms_of_service_raw,
|
||||
"markup_form_path": shop.absolute_terms_url(request)
|
||||
"markup_form_path": shop.absolute_terms_url(request),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -264,8 +264,7 @@ def shop_edit_privacy_policy(request):
|
|||
shop = request.shop
|
||||
|
||||
raw_markup_data = request.params.get(
|
||||
"markup-editor-textarea",
|
||||
shop.privacy_policy_raw
|
||||
"markup-editor-textarea", shop.privacy_policy_raw
|
||||
)
|
||||
|
||||
if raw_markup_data and raw_markup_data != shop.privacy_policy_raw:
|
||||
|
|
@ -279,14 +278,13 @@ def shop_edit_privacy_policy(request):
|
|||
"markup_subject": "Privacy Policy",
|
||||
"markup_rendered": shop.privacy_policy_html,
|
||||
"markup_raw": shop.privacy_policy_raw,
|
||||
"markup_form_path": shop.absolute_privacy_policy_url(request)
|
||||
"markup_form_path": shop.absolute_privacy_policy_url(request),
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="shop_users", renderer="shop_users.j2")
|
||||
@shop_owner_required()
|
||||
def shop_users(request):
|
||||
|
||||
email_to_invite = request.params.get("email-to-invite")
|
||||
role_id = request.params.get("role-id")
|
||||
|
||||
|
|
@ -306,7 +304,6 @@ def shop_users(request):
|
|||
msg = ("You invited {} to the shop.".format(email_to_invite), "success")
|
||||
request.session.flash(msg)
|
||||
|
||||
|
||||
return {
|
||||
"owners": request.shop.owners,
|
||||
"editors": request.shop.editors,
|
||||
|
|
@ -322,7 +319,10 @@ def shop_user_remove(request):
|
|||
user = get_user_by_id(request.dbsession, user_id)
|
||||
if user:
|
||||
if user == request.user:
|
||||
msg = ("Sheepishly refusing to remove myself!".format(user.email), "error")
|
||||
msg = (
|
||||
f"Sheepishly refusing to remove myself! {user.email}",
|
||||
"error",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
else:
|
||||
user_shop = request.shop.get_usershop_for_user(user)
|
||||
|
|
@ -335,7 +335,6 @@ def shop_user_remove(request):
|
|||
@view_config(route_name="shop_settings", renderer="shop_settings.j2")
|
||||
@shop_owner_required()
|
||||
def shop_settings(request):
|
||||
|
||||
shop = request.shop
|
||||
|
||||
name = request.params.get("name", shop.name)
|
||||
|
|
@ -350,13 +349,21 @@ def shop_settings(request):
|
|||
ribbon_color_1 = request.params.get("ribbon_color_1", shop.ribbon_color_1)
|
||||
ribbon_color_2 = request.params.get("ribbon_color_2", shop.ribbon_color_2)
|
||||
|
||||
google_analytics_id = request.params.get("google_analytics_id", shop.google_analytics_id)
|
||||
plausible_domain_name = request.params.get("plausible_domain_name", shop.plausible_domain_name)
|
||||
google_analytics_id = request.params.get(
|
||||
"google_analytics_id", shop.google_analytics_id
|
||||
)
|
||||
plausible_domain_name = request.params.get(
|
||||
"plausible_domain_name", shop.plausible_domain_name
|
||||
)
|
||||
|
||||
stripe_public_api_key = request.params.get("stripe_public_api_key", shop.stripe_public_api_key)
|
||||
stripe_secret_api_key = request.params.get("stripe_secret_api_key", shop.stripe_secret_api_key)
|
||||
stripe_public_api_key = request.params.get(
|
||||
"stripe_public_api_key", shop.stripe_public_api_key
|
||||
)
|
||||
stripe_secret_api_key = request.params.get(
|
||||
"stripe_secret_api_key", shop.stripe_secret_api_key
|
||||
)
|
||||
|
||||
#maint_mode_checkbox = request.params.get("maint-mode-checkbox", bool_to_checkbox(shop.maint_mode))
|
||||
# maint_mode_checkbox = request.params.get("maint-mode-checkbox", bool_to_checkbox(shop.maint_mode))
|
||||
maint_mode_checkbox = request.params.get("maint-mode-checkbox", "off")
|
||||
maint_mode = checkbox_to_bool(maint_mode_checkbox)
|
||||
|
||||
|
|
@ -370,12 +377,11 @@ def shop_settings(request):
|
|||
request.session.flash(
|
||||
(
|
||||
"You successfully uploaded a new {} file!".format(uploaded_file_key),
|
||||
"success"
|
||||
"success",
|
||||
)
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
if name != shop.name:
|
||||
shop.name = name
|
||||
msg = ("You set the shop's name.", "success")
|
||||
|
|
@ -416,12 +422,18 @@ def shop_settings(request):
|
|||
|
||||
if ribbon_color_1 != shop.ribbon_color_1:
|
||||
shop.ribbon_color_1 = ribbon_color_1
|
||||
msg = ("You set the shop's announcement ribbon background color 1.", "success")
|
||||
msg = (
|
||||
"You set the shop's announcement ribbon background color 1.",
|
||||
"success",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
|
||||
if ribbon_color_2 != shop.ribbon_color_2:
|
||||
shop.ribbon_color_2 = ribbon_color_2
|
||||
msg = ("You set the shop's announcement ribbon background color 2.", "success")
|
||||
msg = (
|
||||
"You set the shop's announcement ribbon background color 2.",
|
||||
"success",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
|
||||
if google_analytics_id != shop.google_analytics_id:
|
||||
|
|
@ -434,36 +446,45 @@ def shop_settings(request):
|
|||
msg = ("You set the shop's Plausible Analytics Domain Name.", "success")
|
||||
request.session.flash(msg)
|
||||
|
||||
if stripe_public_api_key and stripe_public_api_key != shop.stripe_public_api_key:
|
||||
if (
|
||||
stripe_public_api_key
|
||||
and stripe_public_api_key != shop.stripe_public_api_key
|
||||
):
|
||||
if stripe_public_api_key.startswith("pk_"):
|
||||
shop.stripe_public_api_key = stripe_public_api_key
|
||||
msg = ("You set the shop's stripe_public_api_key.", "success")
|
||||
else:
|
||||
msg = ("The shop's stripe_public_api_key must start with 'pk_'.", "error")
|
||||
msg = (
|
||||
"The shop's stripe_public_api_key must start with 'pk_'.",
|
||||
"error",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
|
||||
if stripe_secret_api_key and stripe_secret_api_key != shop.stripe_secret_api_key:
|
||||
if (
|
||||
stripe_secret_api_key
|
||||
and stripe_secret_api_key != shop.stripe_secret_api_key
|
||||
):
|
||||
if stripe_secret_api_key.startswith("sk_"):
|
||||
shop.stripe_secret_api_key = stripe_secret_api_key
|
||||
msg = ("You set the shop's stripe_secret_api_key.", "success")
|
||||
else:
|
||||
msg = ("The shop's stripe_secret_api_key must start with 'sk_'.", "error")
|
||||
msg = (
|
||||
"The shop's stripe_secret_api_key must start with 'sk_'.",
|
||||
"error",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
|
||||
if shop.maint_mode != maint_mode:
|
||||
shop.maint_mode = maint_mode
|
||||
request.session.flash(
|
||||
(
|
||||
"You turned {} maint_mode".format(
|
||||
maint_mode_checkbox
|
||||
),
|
||||
"You turned {} maint_mode".format(maint_mode_checkbox),
|
||||
"success",
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: Dry out this block, it's a copy pasta from views/product.py
|
||||
if s3_webhook_key and s3_webhook_bucket and s3_webhook_etag:
|
||||
|
||||
# get file_from S3 service posted params.
|
||||
file_key = s3_webhook_key.split("/")[-1].split(".")[0]
|
||||
|
||||
|
|
@ -474,20 +495,20 @@ def shop_settings(request):
|
|||
|
||||
# determine content type from extension.
|
||||
file_ext = s3_webhook_key.split(".")[-1]
|
||||
#print(file_ext)
|
||||
# print(file_ext)
|
||||
content_type = mimetypes.types_map.get("." + file_ext)
|
||||
#print(content_type)
|
||||
# print(content_type)
|
||||
|
||||
# copy upload to our system defined s3 location.
|
||||
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.copy_object
|
||||
request.secure_uploads_client.copy_object(
|
||||
ACL = acl,
|
||||
ACL=acl,
|
||||
Bucket=request.app["bucket.secure_uploads"],
|
||||
CopySource={
|
||||
"Bucket": s3_webhook_bucket,
|
||||
"Key": s3_webhook_key,
|
||||
},
|
||||
ContentDisposition= "inline; filename={}".format(file_key),
|
||||
ContentDisposition="inline; filename={}".format(file_key),
|
||||
ContentType=content_type,
|
||||
CacheControl=cache_control,
|
||||
Key="{}/meta/{}".format(shop.id, file_key),
|
||||
|
|
@ -522,7 +543,7 @@ def shop_settings(request):
|
|||
# a shops namespace which is owned by a different user.
|
||||
key_starts_with = "{}/meta/{}.{}".format(shop.id, file_key, "$filename")
|
||||
conditions = [
|
||||
{ "success_action_redirect": shop.absolute_settings_url(request) },
|
||||
{"success_action_redirect": shop.absolute_settings_url(request)},
|
||||
["starts-with", "$key", key_starts_with],
|
||||
]
|
||||
|
||||
|
|
|
|||
206
make_post_sell/views/shop_location.py
Normal file
206
make_post_sell/views/shop_location.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
from pyramid.view import view_config
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
from . import shop_owner_required
|
||||
from ..models.shop import get_shop_by_id
|
||||
from ..models.shop_location import ShopLocation
|
||||
|
||||
|
||||
def checkbox_to_bool(checkbox):
|
||||
return True if checkbox == "on" else False
|
||||
|
||||
|
||||
def bool_to_checkbox(boolean):
|
||||
return "on" if boolean else "off"
|
||||
|
||||
|
||||
@view_config(route_name="shop_location_new", renderer="shop_location_form.j2")
|
||||
@shop_owner_required()
|
||||
def shop_location_new(request):
|
||||
shop = get_shop_by_id(request.dbsession, request.matchdict["shop_id"])
|
||||
|
||||
if not shop:
|
||||
request.session.flash(("Shop not found.", "error"))
|
||||
return HTTPFound(location=request.route_url("home"))
|
||||
|
||||
if request.method == "POST":
|
||||
name = request.params.get("name", "")
|
||||
address = request.params.get("address", "")
|
||||
city = request.params.get("city", "")
|
||||
state = request.params.get("state", "")
|
||||
country = request.params.get("country", "")
|
||||
postal_code = request.params.get("postal_code", "")
|
||||
days_open = request.params.get("days_open", "")
|
||||
hours_open = request.params.get("hours_open", "")
|
||||
local_pickup = checkbox_to_bool(request.params.get("local_pickup", "off"))
|
||||
local_delivery = checkbox_to_bool(request.params.get("local_delivery", "off"))
|
||||
local_delivery_rate = request.params.get("local_delivery_rate", "0.00")
|
||||
local_shipping = checkbox_to_bool(request.params.get("local_shipping", "off"))
|
||||
local_shipping_rate = request.params.get("local_shipping_rate", "0.00")
|
||||
international_shipping = checkbox_to_bool(
|
||||
request.params.get("international_shipping", "off")
|
||||
)
|
||||
international_shipping_rate = request.params.get(
|
||||
"international_shipping_rate", "0.00"
|
||||
)
|
||||
|
||||
shop_location = ShopLocation(
|
||||
shop=shop,
|
||||
name=name,
|
||||
address=address,
|
||||
city=city,
|
||||
state=state,
|
||||
country=country,
|
||||
postal_code=postal_code,
|
||||
)
|
||||
shop_location.days_open = days_open
|
||||
shop_location.hours_open = hours_open
|
||||
shop_location.local_pickup = local_pickup
|
||||
shop_location.local_delivery = local_delivery
|
||||
shop_location.local_delivery_rate = local_delivery_rate
|
||||
shop_location.local_shipping = local_shipping
|
||||
shop_location.local_shipping_rate = local_shipping_rate
|
||||
shop_location.international_shipping = international_shipping
|
||||
shop_location.international_shipping_rate = international_shipping_rate
|
||||
|
||||
request.dbsession.add(shop_location)
|
||||
request.dbsession.flush()
|
||||
|
||||
request.session.flash(("Shop location created successfully.", "success"))
|
||||
return HTTPFound(location=request.route_url("shop_locations", shop_id=shop.id))
|
||||
|
||||
return {
|
||||
"action_url": request.route_url("shop_location_new", shop_id=shop.id),
|
||||
"name": "",
|
||||
"address": "",
|
||||
"city": "",
|
||||
"state": "",
|
||||
"country": "",
|
||||
"postal_code": "",
|
||||
"days_open": "",
|
||||
"hours_open": "",
|
||||
"local_pickup": False,
|
||||
"local_delivery": False,
|
||||
"local_delivery_rate": "0.00",
|
||||
"local_shipping": False,
|
||||
"local_shipping_rate": "0.00",
|
||||
"international_shipping": False,
|
||||
"international_shipping_rate": "0.00",
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="shop_location_edit", renderer="shop_location_form.j2")
|
||||
@shop_owner_required()
|
||||
def shop_location_edit(request):
|
||||
shop = get_shop_by_id(request.dbsession, request.matchdict["shop_id"])
|
||||
location_id = request.matchdict["location_id"]
|
||||
shop_location = (
|
||||
request.dbsession.query(ShopLocation)
|
||||
.filter_by(id=location_id, shop_id=shop.id)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
if not shop_location:
|
||||
request.session.flash(("Shop location not found.", "error"))
|
||||
return HTTPFound(location=request.route_url("shop_locations", shop_id=shop.id))
|
||||
|
||||
if request.method == "POST":
|
||||
shop_location.name = request.params.get("name", "")
|
||||
shop_location.address = request.params.get("address", "")
|
||||
shop_location.city = request.params.get("city", "")
|
||||
shop_location.state = request.params.get("state", "")
|
||||
shop_location.country = request.params.get("country", "")
|
||||
shop_location.postal_code = request.params.get("postal_code", "")
|
||||
shop_location.days_open = request.params.get("days_open", "")
|
||||
shop_location.hours_open = request.params.get("hours_open", "")
|
||||
shop_location.local_pickup = checkbox_to_bool(
|
||||
request.params.get("local_pickup", "off")
|
||||
)
|
||||
shop_location.local_delivery = checkbox_to_bool(
|
||||
request.params.get("local_delivery", "off")
|
||||
)
|
||||
shop_location.local_delivery_rate = request.params.get(
|
||||
"local_delivery_rate", "0.00"
|
||||
)
|
||||
shop_location.local_shipping = checkbox_to_bool(
|
||||
request.params.get("local_shipping", "off")
|
||||
)
|
||||
shop_location.local_shipping_rate = request.params.get(
|
||||
"local_shipping_rate", "0.00"
|
||||
)
|
||||
shop_location.international_shipping = checkbox_to_bool(
|
||||
request.params.get("international_shipping", "off")
|
||||
)
|
||||
shop_location.international_shipping_rate = request.params.get(
|
||||
"international_shipping_rate", "0.00"
|
||||
)
|
||||
|
||||
request.dbsession.add(shop_location)
|
||||
request.dbsession.flush()
|
||||
|
||||
request.session.flash(("Shop location updated successfully.", "success"))
|
||||
return HTTPFound(location=request.route_url("shop_locations", shop_id=shop.id))
|
||||
|
||||
return {
|
||||
"action_url": request.route_url(
|
||||
"shop_location_edit", shop_id=shop.id, location_id=location_id
|
||||
),
|
||||
"name": shop_location.name,
|
||||
"address": shop_location.address,
|
||||
"city": shop_location.city,
|
||||
"state": shop_location.state,
|
||||
"country": shop_location.country,
|
||||
"postal_code": shop_location.postal_code,
|
||||
"days_open": shop_location.days_open,
|
||||
"hours_open": shop_location.hours_open,
|
||||
"local_pickup": shop_location.local_pickup,
|
||||
"local_delivery": shop_location.local_delivery,
|
||||
"local_delivery_rate": shop_location.local_delivery_rate,
|
||||
"local_shipping": shop_location.local_shipping,
|
||||
"local_shipping_rate": shop_location.local_shipping_rate,
|
||||
"international_shipping": shop_location.international_shipping,
|
||||
"international_shipping_rate": shop_location.international_shipping_rate,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="shop_locations", renderer="shop_locations.j2")
|
||||
def shop_locations(request):
|
||||
return {
|
||||
"shop": request.shop,
|
||||
"locations": request.shop.shop_locations,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="shop_location_switch")
|
||||
def shop_location_switch(request):
|
||||
shop_id = request.shop.id
|
||||
location_id = request.matchdict.get("location_id")
|
||||
next_url = request.params.get("next", request.route_url("shop", shop_id=shop_id))
|
||||
|
||||
# Fetch the location to ensure it exists and belongs to the shop
|
||||
location = (
|
||||
request.dbsession.query(ShopLocation)
|
||||
.filter_by(id=location_id, shop_id=shop_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not location:
|
||||
request.session.flash(
|
||||
("Location not found or does not belong to this shop.", "error")
|
||||
)
|
||||
return HTTPFound(request.route_url("shop_locations", shop_id=shop_id))
|
||||
|
||||
# Update the session with the selected location
|
||||
request.session["shop_location_id"] = str(location.id)
|
||||
request.session.flash(
|
||||
(f"You are now viewing the {location.name} shop location", "success")
|
||||
)
|
||||
|
||||
# Recalculate the handling cost based on the new location
|
||||
cart = request.active_cart
|
||||
cart.update_handling_cost(location)
|
||||
request.dbsession.add(cart)
|
||||
request.dbsession.flush()
|
||||
|
||||
# Redirect to the next URL or the shop home page
|
||||
return HTTPFound(next_url)
|
||||
|
|
@ -3,13 +3,13 @@ from pyramid.view import view_config
|
|||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
from . import (
|
||||
user_required,
|
||||
shop_is_ready_required,
|
||||
user_required,
|
||||
shop_is_ready_required,
|
||||
)
|
||||
|
||||
from ..models.user import (
|
||||
is_user_name_available,
|
||||
is_user_name_valid,
|
||||
is_user_name_available,
|
||||
is_user_name_valid,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -36,7 +36,6 @@ def user_products(request):
|
|||
@view_config(route_name="user_settings", renderer="user_settings.j2")
|
||||
@user_required()
|
||||
def user_settings(request):
|
||||
|
||||
user = request.user
|
||||
|
||||
name = request.params.get("name", user.name)
|
||||
|
|
@ -44,10 +43,16 @@ def user_settings(request):
|
|||
|
||||
if name and name != user.name:
|
||||
if not is_user_name_valid(name):
|
||||
msg = ("Sorry your display name is invalid. Please try again with only letters, numbers, spaces, dashes, and periods.", "error")
|
||||
msg = (
|
||||
"Sorry your display name is invalid. Please try again with only letters, numbers, spaces, dashes, and periods.",
|
||||
"error",
|
||||
)
|
||||
name = user.name
|
||||
elif not is_user_name_available(request.dbsession, name):
|
||||
msg = ("Sorry that display name is already taken. Please try again.", "error")
|
||||
msg = (
|
||||
"Sorry that display name is already taken. Please try again.",
|
||||
"error",
|
||||
)
|
||||
name = user.name
|
||||
else:
|
||||
user.name = name
|
||||
|
|
@ -81,7 +86,7 @@ def user_address_save(request):
|
|||
request.dbsession.flush()
|
||||
msg = ("You saved that address.", "success")
|
||||
request.session.flash(msg)
|
||||
|
||||
|
||||
return HTTPFound("/u/addresses")
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue