flynt and black
flynt turns python string format into f strings. modified: make_post_sell/lib/hex_color.py modified: make_post_sell/lib/mail.py modified: make_post_sell/lib/sanitize_html.py modified: make_post_sell/models/coupon.py modified: make_post_sell/models/meta.py modified: make_post_sell/models/product.py modified: make_post_sell/models/shop.py modified: make_post_sell/models/user.py modified: make_post_sell/tests/test_functional.py modified: make_post_sell/views/authentication.py modified: make_post_sell/views/cart.py modified: make_post_sell/views/content.py modified: make_post_sell/views/coupon.py modified: make_post_sell/views/product.py modified: make_post_sell/views/shop.py
This commit is contained in:
parent
068cc9b12d
commit
f820210bb7
15 changed files with 221 additions and 221 deletions
|
|
@ -39,5 +39,5 @@ def color_scale(hex_str, scale_factor):
|
|||
g = int(clamp(g * scale_factor))
|
||||
b = int(clamp(b * scale_factor))
|
||||
|
||||
#return "#%02x%02x%02x" % (r, g, b)
|
||||
return "#%02x%02x%02x" % (r, g, b)
|
||||
# return "#%02x%02x%02x" % (r, g, b)
|
||||
return f"#{r:02x}{g:02x}{b:02x}"
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ def send_email(
|
|||
dkim_selector="",
|
||||
dkim_signature_algorithm="ed25519-sha256",
|
||||
):
|
||||
|
||||
# the `email` library assumes it is working with string objects.
|
||||
# the `dkim` library assumes it is working with byte objects.
|
||||
# this function performs the acrobatics to make them both happy.
|
||||
|
|
@ -106,13 +105,15 @@ def send_email(
|
|||
|
||||
def send_pyramid_email(request, to_email, subject, message_text, message_html):
|
||||
"""Thin wrapper around `send_email` to customise settings using request object."""
|
||||
default_sender = "no-reply@{}".format(request.domain)
|
||||
default_sender = f"no-reply@{request.domain}"
|
||||
sender_email = request.app.get("email.sender", default_sender)
|
||||
subject = "{} | {}".format(subject, request.app.get("email.subject_postfix", request.domain))
|
||||
subject = f"{subject} | {request.app.get('email.subject_postfix', request.domain)}"
|
||||
relay = request.app.get("email.relay", "localhost")
|
||||
dkim_private_key_path = request.app.get("email.dkim_private_key_path", "")
|
||||
dkim_selector = request.app.get("email.dkim_selector", "")
|
||||
dkim_signature_algorithm = request.app.get("email.dkim_signature_algorithm", "ed25519-sha256")
|
||||
dkim_signature_algorithm = request.app.get(
|
||||
"email.dkim_signature_algorithm", "ed25519-sha256"
|
||||
)
|
||||
|
||||
send_email(
|
||||
to_email,
|
||||
|
|
@ -140,7 +141,7 @@ def send_verification_digits_to_email(request, to_email, raw_digits):
|
|||
raw_digits:
|
||||
the raw (unencrypted) digits the user may use to verify & authenticate.
|
||||
"""
|
||||
subject = "Verification Code | {}".format(raw_digits)
|
||||
subject = f"Verification Code | {raw_digits}"
|
||||
message_text = WELCOME_1_TEXT.format(raw_digits)
|
||||
message_html = WELCOME_1_HTML.format(subject, raw_digits)
|
||||
send_pyramid_email(request, to_email, subject, message_text, message_html)
|
||||
|
|
@ -174,17 +175,13 @@ def send_purchase_email(request, to_email, products, total_cost):
|
|||
)
|
||||
|
||||
product_text_list.append(
|
||||
'<p><a href="{}">{}<br/>{}</a></p>'.format(
|
||||
p.absolute_url(request), p.title, thumbnail
|
||||
)
|
||||
f'<p><a href="{p.absolute_url(request)}">{p.title}<br/>{thumbnail}</a></p>'
|
||||
)
|
||||
product_text = "<br/>".join(product_text_list)
|
||||
|
||||
message_text = PURCHASE_1_TEXT.format(
|
||||
"%.2f" % total_cost, request.host_url
|
||||
)
|
||||
message_text = PURCHASE_1_TEXT.format(f"{total_cost:.2f}", request.host_url)
|
||||
message_html = PURCHASE_1_HTML.format(
|
||||
subject, request.host_url, product_text, "%.2f" % total_cost
|
||||
subject, request.host_url, product_text, f"{total_cost:.2f}"
|
||||
)
|
||||
send_pyramid_email(request, to_email, subject, message_text, message_html)
|
||||
|
||||
|
|
@ -217,17 +214,15 @@ def send_sale_email(request, shop, products, total_cost):
|
|||
)
|
||||
|
||||
product_text_list.append(
|
||||
'<p><a href="{}">{}<br/>{}</a></p>'.format(
|
||||
p.absolute_url(request), p.title, thumbnail
|
||||
)
|
||||
f'<p><a href="{p.absolute_url(request)}">{p.title}<br/>{thumbnail}</a></p>'
|
||||
)
|
||||
product_text = "<br/>".join(product_text_list)
|
||||
|
||||
message_text = SALE_1_TEXT.format(
|
||||
"%.2f" % total_cost, shop.absolute_sales_url(request)
|
||||
f"{total_cost:.2f}", shop.absolute_sales_url(request)
|
||||
)
|
||||
message_html = SALE_1_HTML.format(
|
||||
subject, shop.absolute_sales_url(request), product_text, "%.2f" % total_cost
|
||||
subject, shop.absolute_sales_url(request), product_text, f"{total_cost:.2f}"
|
||||
)
|
||||
|
||||
# send a separate email for each shop owner.
|
||||
|
|
@ -241,7 +236,7 @@ def send_invite_email(request, to_email, user, shop):
|
|||
|
||||
request
|
||||
the request (of the successful log in attempt)
|
||||
|
||||
|
||||
to_email
|
||||
the email address to send the shop invitation to.
|
||||
|
||||
|
|
@ -251,7 +246,7 @@ def send_invite_email(request, to_email, user, shop):
|
|||
shop
|
||||
the shop the invitation is for.
|
||||
"""
|
||||
subject = "You have been invited to {}".format(shop.name)
|
||||
subject = f"You have been invited to {shop.name}"
|
||||
join_link = request.route_url("join-or-log-in")
|
||||
message_text = INVITE_1_TEXT.format(user.email, shop.name, join_link)
|
||||
message_html = INVITE_1_HTML.format(subject, user.email, shop.name, join_link)
|
||||
|
|
|
|||
|
|
@ -62,13 +62,10 @@ def default_cleaner(tag_acl=None):
|
|||
# to get past bleach.
|
||||
# We will conditionally filter tags using BeautifulSoup.
|
||||
for tag_name in tag_acl.keys():
|
||||
|
||||
# tag_name, for example "script".
|
||||
|
||||
for attr_name, attr_value, allow_or_deny in tag_acl[tag_name]:
|
||||
|
||||
if allow_or_deny == "allow":
|
||||
|
||||
# attr_name, for example "type".
|
||||
# attr_value, for example "math/tex; mode=display".
|
||||
|
||||
|
|
@ -84,7 +81,7 @@ def default_cleaner(tag_acl=None):
|
|||
|
||||
# doesn't do anything, but i used to be able to pass it via constructor.
|
||||
# https://github.com/yourcelf/bleach-allowlist/blob/main/bleach_allowlist/bleach_allowlist.py
|
||||
#cleaner.all_styles=all_styles
|
||||
# cleaner.all_styles=all_styles
|
||||
|
||||
# disable link_protection by default.
|
||||
cleaner.link_protection = False
|
||||
|
|
@ -122,17 +119,14 @@ def conditional_tag_filter(soup, cleaner):
|
|||
|
||||
# for each tag_name in the tag_acl ...
|
||||
for tag_name in cleaner.tag_acl.keys():
|
||||
|
||||
# find all the tags in the DOM that match the tag_name, for example "script".
|
||||
for tag in soup.find_all(tag_name):
|
||||
|
||||
# tag is a BeautifulSoup tag object.
|
||||
|
||||
# by default, assume we will extract tag (remove it from the DOM).
|
||||
extract_tag = True
|
||||
|
||||
for attr_name, attr_value, allow_or_deny in cleaner.tag_acl[tag_name]:
|
||||
|
||||
# tag attr_name:
|
||||
#
|
||||
# * "type"
|
||||
|
|
@ -151,10 +145,9 @@ def conditional_tag_filter(soup, cleaner):
|
|||
# * "allow"
|
||||
# * "deny"
|
||||
|
||||
log.info("{} {} {}".format(attr_name, attr_value, allow_or_deny))
|
||||
log.info(f"{attr_name} {attr_value} {allow_or_deny}")
|
||||
|
||||
if allow_or_deny == "allow":
|
||||
|
||||
if tag.attrs.get(attr_name) == attr_value:
|
||||
# this tag attr_name/attr_value is whitelisted.
|
||||
# do not extract tag (remove it from the DOM)
|
||||
|
|
@ -175,7 +168,6 @@ def conditional_tag_filter(soup, cleaner):
|
|||
|
||||
|
||||
def protect_links(soup, cleaner):
|
||||
|
||||
for a_tag in soup.find_all("a"):
|
||||
uri = miniuri.Uri(a_tag.attrs.get("href", ""))
|
||||
|
||||
|
|
|
|||
|
|
@ -50,9 +50,7 @@ class Coupon(RBase, Base):
|
|||
description = Column(Unicode(256), nullable=False)
|
||||
|
||||
# the type of action that this coupon applies to a cart.
|
||||
action_type = Column(
|
||||
Enum(*COUPON_ACTION_TYPES, name="action_type"), nullable=False
|
||||
)
|
||||
action_type = Column(Enum(*COUPON_ACTION_TYPES, name="action_type"), nullable=False)
|
||||
action_value = Column(BigInteger, nullable=False)
|
||||
|
||||
# The amount of times this code may be used.
|
||||
|
|
@ -77,13 +75,17 @@ class Coupon(RBase, Base):
|
|||
shop = relationship(argument="Shop", uselist=False, lazy="joined")
|
||||
|
||||
# many to many uses association_proxy.
|
||||
carts = association_proxy("coupon_carts", "cart", creator=lambda c: CartCoupon(cart=c))
|
||||
carts = association_proxy(
|
||||
"coupon_carts", "cart", creator=lambda c: CartCoupon(cart=c)
|
||||
)
|
||||
|
||||
# one to many.
|
||||
# returns this coupon's redemptions.
|
||||
# lazy="dynamic" returns a query object instread of an InstrumentedList.
|
||||
# this is ideal when we want to further filter the results.
|
||||
redemptions = relationship(argument="CouponRedemption", lazy="dynamic", back_populates="coupon")
|
||||
redemptions = relationship(
|
||||
argument="CouponRedemption", lazy="dynamic", back_populates="coupon"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -94,10 +96,9 @@ class Coupon(RBase, Base):
|
|||
action_value,
|
||||
max_redemptions=None,
|
||||
max_redemptions_per_user=None,
|
||||
expiration_date = None,
|
||||
cart_qualifier = None,
|
||||
expiration_date=None,
|
||||
cart_qualifier=None,
|
||||
):
|
||||
|
||||
self.id = uuid.uuid1()
|
||||
self.shop = shop
|
||||
self.created_timestamp = now_timestamp()
|
||||
|
|
@ -124,15 +125,12 @@ class Coupon(RBase, Base):
|
|||
self.expired_timestamp = None
|
||||
if expiration_date:
|
||||
# convert date string into datetime.
|
||||
expiration_datetime = datetime.strptime(
|
||||
expiration_date,
|
||||
'%Y-%m-%d'
|
||||
)
|
||||
expiration_datetime = datetime.strptime(expiration_date, "%Y-%m-%d")
|
||||
# convert datetime into timestamp.
|
||||
# adding 86399000 to get the end of day.
|
||||
self.expired_timestamp = datetime_to_timestamp(
|
||||
expiration_datetime
|
||||
) + 86399000
|
||||
self.expired_timestamp = (
|
||||
datetime_to_timestamp(expiration_datetime) + 86399000
|
||||
)
|
||||
|
||||
@property
|
||||
def expired_datetime(self):
|
||||
|
|
@ -160,34 +158,32 @@ class Coupon(RBase, Base):
|
|||
@property
|
||||
def human_discount(self):
|
||||
if self.action_type == "percent-off":
|
||||
return "{}% off".format(self.action_value)
|
||||
return "${0:.2f} off".format(
|
||||
cents_to_dollars(self.action_value)
|
||||
)
|
||||
return f"{self.action_value}% off"
|
||||
return f"${cents_to_dollars(self.action_value):.2f} off"
|
||||
|
||||
@property
|
||||
def human_max_redemptions(self):
|
||||
if self.max_redemptions:
|
||||
return "{}".format(self.max_redemptions)
|
||||
return f"{self.max_redemptions}"
|
||||
return "∞"
|
||||
|
||||
@property
|
||||
def human_limit_per_customer(self):
|
||||
if self.max_redemptions_per_user:
|
||||
return "Limit {} per customer.".format(self.max_redemptions_per_user)
|
||||
return f"Limit {self.max_redemptions_per_user} per customer."
|
||||
return ""
|
||||
|
||||
@property
|
||||
def human_valid_thru(self):
|
||||
if self.expired_timestamp:
|
||||
x = self.expired_datetime.strftime("%A, %b %d, %Y")
|
||||
return "Valid thru {}.".format(x)
|
||||
return f"Valid thru {x}."
|
||||
return ""
|
||||
|
||||
@property
|
||||
def human_minimum_cart(self):
|
||||
if self.cart_qualifier:
|
||||
return "any ${0:.0f} cart.".format(cents_to_dollars(self.cart_qualifier))
|
||||
return f"any ${cents_to_dollars(self.cart_qualifier):.0f} cart."
|
||||
return "any cart."
|
||||
|
||||
def compute_discount(self, cents):
|
||||
|
|
@ -221,7 +217,5 @@ def get_coupons_by_code(dbsession, code, shop=None):
|
|||
Remember a coupon code may not be unique across all shops.
|
||||
"""
|
||||
if shop is not None:
|
||||
return shop.coupons.filter(
|
||||
Coupon.code == code
|
||||
).all()
|
||||
return shop.coupons.filter(Coupon.code == code).all()
|
||||
return dbsession.query(Coupon).filter(Coupon.code == code).all()
|
||||
|
|
|
|||
|
|
@ -55,11 +55,11 @@ Base = declarative_base()
|
|||
|
||||
|
||||
COUPON_ACTION_TYPES = {
|
||||
"dollar-off",
|
||||
"percent-off",
|
||||
"dollar-off",
|
||||
"percent-off",
|
||||
}
|
||||
|
||||
VISIBILITY_INT_TO_HUMAN ={
|
||||
VISIBILITY_INT_TO_HUMAN = {
|
||||
0: "private",
|
||||
1: "public",
|
||||
2: "unlisted",
|
||||
|
|
@ -131,7 +131,6 @@ def get_objects_by_ids(dbsession, object_ids, cls):
|
|||
object_uuids = [id_to_uuid(object_id) for object_id in object_ids]
|
||||
|
||||
if object_uuids:
|
||||
|
||||
# reference: https://stackoverflow.com/q/444475
|
||||
return dbsession.query(cls).filter(cls.id.in_(object_uuids)).all()
|
||||
|
||||
|
|
@ -153,7 +152,7 @@ def now_timestamp():
|
|||
|
||||
|
||||
def foreign_key(class_name, column_name):
|
||||
return ForeignKey("{}.{}".format(CLASS_TO_TABLE[class_name], column_name))
|
||||
return ForeignKey(f"{CLASS_TO_TABLE[class_name]}.{column_name}")
|
||||
|
||||
|
||||
class RBase(object):
|
||||
|
|
|
|||
|
|
@ -251,21 +251,21 @@ class Product(RBase, Base):
|
|||
@property
|
||||
def s3_path(self):
|
||||
"""return the S3 path shop.id/product.id"""
|
||||
return "{}/{}".format(self.shop.id, self.id)
|
||||
return f"{self.shop.id}/{self.id}"
|
||||
|
||||
@property
|
||||
def s3_key(self):
|
||||
"""return the product S3 key"""
|
||||
return "{}/product".format(self.s3_path)
|
||||
return f"{self.s3_path}/product"
|
||||
|
||||
@property
|
||||
def s3_key_preview(self):
|
||||
"""return the product preview S3 key"""
|
||||
return "{}/preview".format(self.s3_path)
|
||||
return f"{self.s3_path}/preview"
|
||||
|
||||
def s3_key_thumbnail(self, file_key):
|
||||
"""return the thumbail S3 key"""
|
||||
return "{}/{}".format(self.s3_path, file_key)
|
||||
return f"{self.s3_path}/{file_key}"
|
||||
|
||||
@property
|
||||
def s3_key_thumbnails(self):
|
||||
|
|
@ -344,15 +344,15 @@ class Product(RBase, Base):
|
|||
* an inline image (thumbnails)
|
||||
* an attachment (file download)
|
||||
"""
|
||||
file_prefix = "" if file_key == "product" else "{}-".format(file_key)
|
||||
file_prefix = "" if file_key == "product" else f"{file_key}-"
|
||||
extension = self.extensions.get(file_key, "")
|
||||
filename = "{}{}.{}".format(file_prefix, self.slug, extension)
|
||||
content_disposition = "attachment; filename={}".format(filename)
|
||||
filename = f"{file_prefix}{self.slug}.{extension}"
|
||||
content_disposition = f"attachment; filename={filename}"
|
||||
|
||||
if file_key in self.file_thumbnail_keys:
|
||||
content_disposition = "inline; filename={}".format(filename)
|
||||
content_disposition = f"inline; filename={filename}"
|
||||
elif self.is_not_sellable:
|
||||
content_disposition = "inline; filename={}".format(filename)
|
||||
content_disposition = f"inline; filename={filename}"
|
||||
|
||||
return content_disposition
|
||||
|
||||
|
|
@ -375,8 +375,8 @@ class Product(RBase, Base):
|
|||
https://my.makepostsell.com/p/product-uuid
|
||||
"""
|
||||
if slug:
|
||||
return "{}/p/{}/{}".format(request.host_url, self.id, self.slug)
|
||||
return "{}/p/{}".format(request.host_url, self.id)
|
||||
return f"{request.host_url}/p/{self.id}/{self.slug}"
|
||||
return f"{request.host_url}/p/{self.id}"
|
||||
|
||||
def absolute_edit_url(self, request):
|
||||
"""
|
||||
|
|
@ -387,7 +387,7 @@ class Product(RBase, Base):
|
|||
https://makepostsell.com/p/product-uuid/edit
|
||||
"""
|
||||
|
||||
return "{}/edit".format(self.absolute_url(request, slug=False))
|
||||
return f"{self.absolute_url(request, slug=False)}/edit"
|
||||
|
||||
@property
|
||||
def shop_uuid_str(self):
|
||||
|
|
@ -518,7 +518,7 @@ 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))
|
||||
keyword_filter = Product.title.ilike(f"%{keyword}%")
|
||||
# the product _must_ be public (1).
|
||||
products = (
|
||||
product_query.filter(keyword_filter).filter(Product.visibility == 1).all()
|
||||
|
|
|
|||
|
|
@ -1,10 +1,18 @@
|
|||
import uuid
|
||||
|
||||
from sqlalchemy import Column, BigInteger, Boolean, Unicode, UnicodeText, func
|
||||
|
||||
|
||||
from sqlalchemy import and_
|
||||
|
||||
from .meta import Base, RBase, UUIDType, now_timestamp, foreign_key, get_object_by_id, get_objects_by_ids
|
||||
from .meta import (
|
||||
Base,
|
||||
RBase,
|
||||
UUIDType,
|
||||
now_timestamp,
|
||||
foreign_key,
|
||||
get_object_by_id,
|
||||
get_objects_by_ids,
|
||||
)
|
||||
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
|
||||
|
|
@ -62,8 +70,8 @@ class Shop(RBase, Base):
|
|||
|
||||
# these keys allow make_post_sell to charge
|
||||
# cards on behalf of the shop's stripe account.
|
||||
#stripe_secret_api_key = Column(Unicode(64), nullable=True)
|
||||
#stripe_public_api_key = Column(Unicode(64), nullable=True)
|
||||
# stripe_secret_api_key = Column(Unicode(64), nullable=True)
|
||||
# stripe_public_api_key = Column(Unicode(64), nullable=True)
|
||||
stripe_secret_api_key = Column(Unicode(128), nullable=True)
|
||||
stripe_public_api_key = Column(Unicode(128), nullable=True)
|
||||
|
||||
|
|
@ -101,7 +109,9 @@ class Shop(RBase, Base):
|
|||
|
||||
# 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")
|
||||
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
|
||||
|
|
@ -121,10 +131,14 @@ class Shop(RBase, Base):
|
|||
def get_usershop_for_user(self, user):
|
||||
"""Given a User object, return the UserShop object or None."""
|
||||
if self.dbsession is not None:
|
||||
return self.dbsession.query(UserShop).filter(
|
||||
UserShop.shop == self,
|
||||
UserShop.user == user,
|
||||
).one_or_none()
|
||||
return (
|
||||
self.dbsession.query(UserShop)
|
||||
.filter(
|
||||
UserShop.shop == self,
|
||||
UserShop.user == user,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
def add_user_to_shop(self, user, role_id=0):
|
||||
"""Add User object to UserShop."""
|
||||
|
|
@ -172,6 +186,7 @@ class Shop(RBase, Base):
|
|||
if hasattr(self, "_stripe") == False:
|
||||
if self.stripe_secret_api_key:
|
||||
import stripe
|
||||
|
||||
stripe.api_key = self.stripe_secret_api_key
|
||||
self._stripe = stripe
|
||||
else:
|
||||
|
|
@ -180,15 +195,19 @@ class Shop(RBase, Base):
|
|||
|
||||
def stripe_user_shop(self, user):
|
||||
"""Return the stripe_user_shop object from our database for this user, or None."""
|
||||
return self.dbsession.query(StripeUserShop).filter(
|
||||
StripeUserShop.user == user,
|
||||
StripeUserShop.shop == self,
|
||||
).one_or_none()
|
||||
return (
|
||||
self.dbsession.query(StripeUserShop)
|
||||
.filter(
|
||||
StripeUserShop.user == user,
|
||||
StripeUserShop.shop == self,
|
||||
)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
def stripe_customer_id(self, user):
|
||||
"""Return the stripe_customer id from our database for this user, or None."""
|
||||
stripe_user_shop = self.stripe_user_shop(user)
|
||||
|
||||
|
||||
if stripe_user_shop:
|
||||
return stripe_user_shop.cus_id
|
||||
|
||||
|
|
@ -201,7 +220,6 @@ class Shop(RBase, Base):
|
|||
stripe_customer_id = self.stripe_customer_id(user)
|
||||
|
||||
if stripe_customer_id is None:
|
||||
|
||||
if create == False:
|
||||
return None
|
||||
|
||||
|
|
@ -236,7 +254,7 @@ class Shop(RBase, Base):
|
|||
|
||||
@property
|
||||
def theme_link_color(self):
|
||||
return color_scale(self.theme_base_color, .75)
|
||||
return color_scale(self.theme_base_color, 0.75)
|
||||
|
||||
def absolute_url(self, request, slug=True):
|
||||
"""
|
||||
|
|
@ -247,8 +265,8 @@ class Shop(RBase, Base):
|
|||
https://my.makepostsell.com/s/<shop-uuid>
|
||||
"""
|
||||
if slug:
|
||||
return "{}/s/{}/{}".format(request.host_url, self.id, self.slug)
|
||||
return "{}/s/{}".format(request.host_url, self.id)
|
||||
return f"{request.host_url}/s/{self.id}/{self.slug}"
|
||||
return f"{request.host_url}/s/{self.id}"
|
||||
|
||||
def absolute_settings_url(self, request):
|
||||
"""
|
||||
|
|
@ -258,7 +276,7 @@ class Shop(RBase, Base):
|
|||
|
||||
https://my.makepostsell.com/s/<shop-uuid>/settings
|
||||
"""
|
||||
return "{}/settings".format(self.absolute_url(request, slug=False))
|
||||
return f"{self.absolute_url(request, slug=False)}/settings"
|
||||
|
||||
def absolute_about_url(self, request):
|
||||
"""
|
||||
|
|
@ -269,16 +287,16 @@ class Shop(RBase, Base):
|
|||
https://my.makepostsell.com/s/<shop-uuid>/about
|
||||
"""
|
||||
# why ure slug for this URL, is it for SEO?
|
||||
return "{}/about".format(self.absolute_url(request))
|
||||
return f"{self.absolute_url(request)}/about"
|
||||
|
||||
def absolute_terms_url(self, request):
|
||||
return "{}/terms".format(self.absolute_url(request, slug=False))
|
||||
|
||||
return f"{self.absolute_url(request, slug=False)}/terms"
|
||||
|
||||
def absolute_privacy_policy_url(self, request):
|
||||
return "{}/privacy-policy".format(self.absolute_url(request, slug=False))
|
||||
return f"{self.absolute_url(request, slug=False)}/privacy-policy"
|
||||
|
||||
def absolute_sales_url(self, request):
|
||||
return "{}/sales".format(self.absolute_url(request, slug=False))
|
||||
return f"{self.absolute_url(request, slug=False)}/sales"
|
||||
|
||||
def set_description(self, new_description):
|
||||
self.description = new_description
|
||||
|
|
@ -303,8 +321,9 @@ class Shop(RBase, Base):
|
|||
self.terms_of_service_html = markdown_to_html(self.terms_of_service_raw, self)
|
||||
|
||||
def get_carts_for_user(self, user):
|
||||
"""return all of this Shop's Cart objects for the given User."""
|
||||
"""return all of this Shop's Cart objects for the given User."""
|
||||
from .cart import Cart
|
||||
|
||||
return self.carts.filter(and_(Cart.shop == self, Cart.user == user))
|
||||
|
||||
def make_cart_active_for_user(self, user, cart):
|
||||
|
|
@ -324,6 +343,7 @@ class Shop(RBase, Base):
|
|||
|
||||
def create_new_cart_for_user(self, user):
|
||||
from .cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
cart.user = user
|
||||
cart.shop = self
|
||||
|
|
@ -382,7 +402,7 @@ def get_shop_by_id(dbsession, shop_id):
|
|||
"""Try to get Shop object by id or return None."""
|
||||
return get_object_by_id(dbsession, shop_id, Shop)
|
||||
|
||||
|
||||
def get_shops_by_ids(dbsession, shop_ids):
|
||||
"""Try to get Shop objects by ids or return None."""
|
||||
return get_objects_by_ids(dbsession, shop_ids, Shop)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ except:
|
|||
def generate_password(size=32):
|
||||
"""Return a system generated password"""
|
||||
from random import choice
|
||||
|
||||
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
digits = "0123456789"
|
||||
pool = letters + digits
|
||||
|
|
@ -56,7 +57,7 @@ class User(RBase, Base):
|
|||
name = Column(Unicode(64), unique=True, nullable=False)
|
||||
full_name = Column(Unicode(64))
|
||||
email = Column(Unicode(64), unique=True, nullable=False)
|
||||
#email_unverified = Column(Unicode(64))
|
||||
# email_unverified = Column(Unicode(64))
|
||||
password = Column(Unicode(64))
|
||||
password_attempts = Column(Integer, default=0)
|
||||
password_timestamp = Column(BigInteger)
|
||||
|
|
@ -81,7 +82,9 @@ class User(RBase, Base):
|
|||
shops = association_proxy("user_shops", "shop", creator=lambda s: UserShop(shop=s))
|
||||
|
||||
# many to many uses association_proxy.
|
||||
stripe_shops = association_proxy("stripe_shops", "shop", creator=lambda s: StripeUserShop(shop=s))
|
||||
stripe_shops = association_proxy(
|
||||
"stripe_shops", "shop", creator=lambda s: StripeUserShop(shop=s)
|
||||
)
|
||||
|
||||
# many to many uses association_proxy.
|
||||
# returns all the products that a user has purchased.
|
||||
|
|
@ -101,15 +104,16 @@ class User(RBase, Base):
|
|||
# this is ideal when we want to further filter the results.
|
||||
carts = relationship(argument="Cart", lazy="dynamic", back_populates="user")
|
||||
|
||||
addresses = relationship(argument="UserAddress", lazy="dynamic", back_populates="user")
|
||||
addresses = relationship(
|
||||
argument="UserAddress", lazy="dynamic", back_populates="user"
|
||||
)
|
||||
|
||||
def __init__(self, email):
|
||||
self.name = unicode("user-{}".format(generate_password(size=8)))
|
||||
self.name = unicode(f"user-{generate_password(size=8)}")
|
||||
self.created_timestamp = now_timestamp()
|
||||
self.id = uuid.uuid1()
|
||||
self.email = unicode(email)
|
||||
|
||||
|
||||
# TODO: remove this column.
|
||||
self.active_cart_id = uuid.uuid1()
|
||||
|
||||
|
|
@ -119,10 +123,11 @@ class User(RBase, Base):
|
|||
|
||||
def _generate_raw_password(self):
|
||||
"""Return a system generated password"""
|
||||
#return generate_password(size)
|
||||
# return generate_password(size)
|
||||
from random import choice
|
||||
numbers = '0123456789'
|
||||
return "".join([choice(numbers) for i in range(0,6)])
|
||||
|
||||
numbers = "0123456789"
|
||||
return "".join([choice(numbers) for i in range(0, 6)])
|
||||
|
||||
def new_password(self):
|
||||
"""Generate and return raw password, store password hash into DB."""
|
||||
|
|
@ -130,8 +135,7 @@ class User(RBase, Base):
|
|||
|
||||
# bcrypt works with bytes so we encode to utf-8.
|
||||
self.password = bcrypt.hashpw(
|
||||
raw_password.encode("utf-8"),
|
||||
bcrypt.gensalt()
|
||||
raw_password.encode("utf-8"), bcrypt.gensalt()
|
||||
).decode("utf-8")
|
||||
self.password_timestamp = now_timestamp()
|
||||
self.password_attempts = 0
|
||||
|
|
@ -159,7 +163,7 @@ class User(RBase, Base):
|
|||
stored_hash.encode("utf-8"),
|
||||
).decode("utf-8")
|
||||
|
||||
log.info("new_hash={} stored_hash={}".format(new_hash, stored_hash))
|
||||
log.info(f"new_hash={new_hash} stored_hash={stored_hash}")
|
||||
|
||||
if new_hash == stored_hash:
|
||||
return True
|
||||
|
|
@ -184,9 +188,11 @@ class User(RBase, Base):
|
|||
def active_shop(self):
|
||||
"""Return the user's active shop, or None."""
|
||||
if self.active_shop_id:
|
||||
return self.dbsession.query(Shop).filter(
|
||||
Shop.id == self.active_shop_id
|
||||
).one_or_none()
|
||||
return (
|
||||
self.dbsession.query(Shop)
|
||||
.filter(Shop.id == self.active_shop_id)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
elif self.shops:
|
||||
# if a user doesn't have an active shop but it has
|
||||
|
|
@ -244,7 +250,7 @@ class User(RBase, Base):
|
|||
for address in self.addresses:
|
||||
if address.id == self.active_address_id:
|
||||
return address
|
||||
|
||||
|
||||
|
||||
def _user_by_name_query(dbsession, name):
|
||||
"""query User by case insensitive name."""
|
||||
|
|
|
|||
|
|
@ -83,16 +83,17 @@ class UnauthenticatedFunctionalTests(FunctionalTests):
|
|||
|
||||
@patch("smtplib.SMTP")
|
||||
def test_user_log_in(self, mock_smtp):
|
||||
redirect_res1 = self.testapp.post("/join-or-log-in", {"email": "test@example.com"})
|
||||
redirect_res1 = self.testapp.post(
|
||||
"/join-or-log-in", {"email": "test@example.com"}
|
||||
)
|
||||
self.assertIn(
|
||||
b"Check email for a 6 digit verification code to log in. test@example.com",
|
||||
redirect_res1.follow().body)
|
||||
redirect_res1.follow().body,
|
||||
)
|
||||
|
||||
|
||||
class AuthenticatedFunctionalTests(FunctionalTests):
|
||||
|
||||
def setUp(self):
|
||||
|
||||
super(AuthenticatedFunctionalTests, self).setUp()
|
||||
|
||||
self.shop1_params = {
|
||||
|
|
@ -134,25 +135,21 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
}
|
||||
|
||||
# create test user1 and user2.
|
||||
self.user1 = get_or_create_user_by_email(
|
||||
self.dbsession, "test1@example.com"
|
||||
)
|
||||
self.user2 = get_or_create_user_by_email(
|
||||
self.dbsession, "test2@example.com"
|
||||
)
|
||||
self.user1 = get_or_create_user_by_email(self.dbsession, "test1@example.com")
|
||||
self.user2 = get_or_create_user_by_email(self.dbsession, "test2@example.com")
|
||||
|
||||
# capture user credentials needed for authentication.
|
||||
self.user1_creds = (
|
||||
"test1@example.com",
|
||||
# returns the raw auto-generated password used
|
||||
# in one-time-password links OTP
|
||||
self.user1.new_password()
|
||||
self.user1.new_password(),
|
||||
)
|
||||
self.user2_creds = (
|
||||
"test2@example.com",
|
||||
# returns the raw auto-generated password used
|
||||
# in one-time-password links OTP
|
||||
self.user2.new_password()
|
||||
self.user2.new_password(),
|
||||
)
|
||||
|
||||
# flush new users to database.
|
||||
|
|
@ -164,12 +161,8 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
transaction.manager.commit()
|
||||
|
||||
# requery User objects to avoid detached instance error.
|
||||
self.user1 = get_or_create_user_by_email(
|
||||
self.dbsession, "test1@example.com"
|
||||
)
|
||||
self.user2 = get_or_create_user_by_email(
|
||||
self.dbsession, "test2@example.com"
|
||||
)
|
||||
self.user1 = get_or_create_user_by_email(self.dbsession, "test1@example.com")
|
||||
self.user2 = get_or_create_user_by_email(self.dbsession, "test2@example.com")
|
||||
|
||||
def _clean_up_user(self, user):
|
||||
# print("deleteing user {} from dbsession.".format(user.name))
|
||||
|
|
@ -199,7 +192,6 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
# transaction.manager.commit()
|
||||
super(AuthenticatedFunctionalTests, self).tearDown()
|
||||
|
||||
|
||||
def log_in_user(self, user_creds):
|
||||
# log in user.
|
||||
res_login = self.testapp.post(
|
||||
|
|
@ -217,7 +209,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
valid_creds = self.user1_creds
|
||||
invalid_creds = (self.user1_creds[0], "invalid")
|
||||
|
||||
for i in range(0,12):
|
||||
for i in range(0, 12):
|
||||
self.log_in_user(invalid_creds)
|
||||
|
||||
self.log_in_user(valid_creds)
|
||||
|
|
@ -260,8 +252,9 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
res = redirect_res.follow()
|
||||
self.assertIn(b"You must have a shop editor role to access that.", res.body)
|
||||
|
||||
def test_create_new_shop(self, user_creds=None, shop_params=None, log_out_user=False):
|
||||
|
||||
def test_create_new_shop(
|
||||
self, user_creds=None, shop_params=None, log_out_user=False
|
||||
):
|
||||
if user_creds is None:
|
||||
user_creds = self.user1_creds
|
||||
|
||||
|
|
@ -287,7 +280,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
self.assertTrue(shop.is_not_ready)
|
||||
|
||||
# add stripe keys to the newly created shop to make it ready.
|
||||
res = self.testapp.post("/s/{}/settings".format(shop.id), shop_params)
|
||||
res = self.testapp.post(f"/s/{shop.id}/settings", shop_params)
|
||||
res_body = res.body.decode()
|
||||
self.assertIn(
|
||||
"You set the shop's stripe_public_api_key.",
|
||||
|
|
@ -337,23 +330,23 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
)
|
||||
|
||||
def test_new_product_missing_required_fields(self):
|
||||
|
||||
# log in and create a new shop.
|
||||
shop = self.test_create_new_shop(
|
||||
user_creds=self.user1_creds,
|
||||
shop_params=self.shop1_params,
|
||||
log_out_user=False
|
||||
log_out_user=False,
|
||||
)
|
||||
|
||||
product_params = self.product1_params
|
||||
del product_params["description"]
|
||||
#redirect_res1 = self.testapp.post("/p/new", product_params)
|
||||
#res = redirect_res1.follow()
|
||||
res = self.testapp.post("/p/new?shop_id={}".format(shop.id), product_params)
|
||||
# redirect_res1 = self.testapp.post("/p/new", product_params)
|
||||
# res = redirect_res1.follow()
|
||||
res = self.testapp.post(f"/p/new?shop_id={shop.id}", product_params)
|
||||
self.assertIn("You must fill out all fields.", res.body.decode())
|
||||
|
||||
def test_new_product(self, user_creds=None, shop_params=None, product_params=None, log_out_user=False):
|
||||
|
||||
def test_new_product(
|
||||
self, user_creds=None, shop_params=None, product_params=None, log_out_user=False
|
||||
):
|
||||
if user_creds is None:
|
||||
user_creds = self.user1_creds
|
||||
|
||||
|
|
@ -369,7 +362,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
shop_params=shop_params,
|
||||
)
|
||||
|
||||
redirect_res = self.testapp.post("/p/new?shop_id={}".format(shop.id), product_params)
|
||||
redirect_res = self.testapp.post(f"/p/new?shop_id={shop.id}", product_params)
|
||||
res = redirect_res.follow()
|
||||
self.assertIn("Great, next you may upload files.", res.body.decode())
|
||||
|
||||
|
|
@ -377,7 +370,6 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
self.testapp.get("/log-out")
|
||||
|
||||
def test_new_product_when_user_does_not_have_editor_role_on_shop(self):
|
||||
|
||||
# log in user1.
|
||||
self.log_in_user(self.user1_creds)
|
||||
|
||||
|
|
@ -396,9 +388,11 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
|
||||
# make user2 create a new product on a shop she doesn't own.
|
||||
params = self.product1_params
|
||||
redirect_res = self.testapp.post("/p/new?shop_id={}".format(shop_id), params)
|
||||
redirect_res = self.testapp.post(f"/p/new?shop_id={shop_id}", params)
|
||||
res = redirect_res.follow()
|
||||
self.assertIn("You must have a shop editor role to access that.", res.body.decode())
|
||||
self.assertIn(
|
||||
"You must have a shop editor role to access that.", res.body.decode()
|
||||
)
|
||||
|
||||
def test_new_product_price_history(self):
|
||||
self.test_new_product(
|
||||
|
|
@ -416,12 +410,10 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
self.assertEqual(product.price_history.count(), 1)
|
||||
self.assertEqual(product.price_in_cents, 350)
|
||||
|
||||
|
||||
# this patch requires an argument added to test method `mock_smtp`.
|
||||
@patch("smtplib.SMTP")
|
||||
@patch("make_post_sell.models.Product.is_ready", mock_always_true)
|
||||
def test_cart_checkout_for_shop(self, mock_smtp):
|
||||
|
||||
# 1. log in as user1.
|
||||
# 2. create new shop.
|
||||
# 3. make new shop ready for checkout.
|
||||
|
|
@ -446,12 +438,14 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
self.log_in_user(self.user2_creds)
|
||||
|
||||
# add product to cart.
|
||||
redirect_res1 = self.testapp.get("/cart/add?product_id={}&shop_id={}".format(product.id, shop.id))
|
||||
redirect_res1 = self.testapp.get(
|
||||
f"/cart/add?product_id={product.id}&shop_id={shop.id}"
|
||||
)
|
||||
redirect_res2 = redirect_res1.follow()
|
||||
res = redirect_res2.follow()
|
||||
res_body = res.body.decode()
|
||||
|
||||
self.assertIn("You added \"russell\'s product\" to your cart.", res_body)
|
||||
self.assertIn('You added "russell\'s product" to your cart.', res_body)
|
||||
|
||||
carts = get_all_carts(self.dbsession)
|
||||
|
||||
|
|
@ -472,10 +466,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
self.assertEqual(1, user_2_cart.count)
|
||||
|
||||
redirect_res = self.testapp.get(
|
||||
"/u/cart/{}/checkout?shop_id={}".format(
|
||||
user_2_cart.id,
|
||||
shop.id
|
||||
)
|
||||
f"/u/cart/{user_2_cart.id}/checkout?shop_id={shop.id}"
|
||||
)
|
||||
redirect_res2 = redirect_res1.follow()
|
||||
res = redirect_res2.follow()
|
||||
|
|
@ -529,7 +520,6 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
|
||||
@patch("make_post_sell.models.Product.is_ready", mock_always_true)
|
||||
def make_new_coupon_for_shop(self, coupon_params=None):
|
||||
|
||||
# 1. log in as user1.
|
||||
# 2. create new shop.
|
||||
# 3. make new shop ready for checkout.
|
||||
|
|
@ -553,7 +543,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
|
||||
# create a new shop coupon.
|
||||
res = self.testapp.post(
|
||||
"/s/{}/coupon/new".format(shop.id),
|
||||
f"/s/{shop.id}/coupon/new",
|
||||
coupon_params,
|
||||
)
|
||||
return res
|
||||
|
|
@ -572,7 +562,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
# this patch requires an argument added to test method `mock_smtp`.
|
||||
@patch("smtplib.SMTP")
|
||||
@patch("make_post_sell.models.Product.is_ready", mock_always_true)
|
||||
def test_checkout_with_coupon_code(self, mock_smtp):
|
||||
def test_checkout_with_coupon_code(self, mock_smtp):
|
||||
res = self.make_new_coupon_for_shop()
|
||||
res = res.follow()
|
||||
self.assertIn("You created a new coupon!", res.body.decode())
|
||||
|
|
@ -585,20 +575,15 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
|
||||
# query the new shop from database.
|
||||
shop = get_shop_by_name(self.dbsession, self.shop1_params["name"])
|
||||
|
||||
|
||||
coupons = get_coupons_by_code(self.dbsession, self.coupon1_params["code"])
|
||||
coupon = coupons[0]
|
||||
|
||||
# apply coupon to user2's active cart.
|
||||
self.testapp.post(
|
||||
"/coupon/apply?&coupon_id={}&shop_id={}".format(
|
||||
coupon.id,
|
||||
shop.id,
|
||||
)
|
||||
)
|
||||
self.testapp.post(f"/coupon/apply?&coupon_id={coupon.id}&shop_id={shop.id}")
|
||||
|
||||
# verify coupon was applied.
|
||||
res_redirect1 = self.testapp.get("/cart?shop_id={}".format(shop.id))
|
||||
res_redirect1 = self.testapp.get(f"/cart?shop_id={shop.id}")
|
||||
res = res_redirect1.follow()
|
||||
res_body = res.body.decode()
|
||||
self.assertIn("Slippery Halloween Party", res_body)
|
||||
|
|
@ -610,7 +595,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
product = all_products.one()
|
||||
|
||||
# add product to user2's active cart.
|
||||
self.testapp.get("/cart/add?product_id={}&shop_id={}".format(product.uuid_str, shop.id))
|
||||
self.testapp.get(f"/cart/add?product_id={product.uuid_str}&shop_id={shop.id}")
|
||||
|
||||
# the stripe migration to SetupIntents broke this end-to-end test routine.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -58,9 +58,7 @@ 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
|
||||
),
|
||||
f"Check email for a 6 digit verification code to log in. {user.email}",
|
||||
"info",
|
||||
)
|
||||
|
||||
|
|
@ -74,9 +72,7 @@ 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
|
||||
),
|
||||
f"Check email for a 6 digit verification code to log in. {user.email}",
|
||||
"info",
|
||||
)
|
||||
|
||||
|
|
@ -85,7 +81,7 @@ def join_or_log_in(request):
|
|||
email_encoded = urlencode(
|
||||
{"email": email, "next": request.params.get("next", "")}
|
||||
)
|
||||
return HTTPFound("/verification-challenge?{}".format(email_encoded))
|
||||
return HTTPFound(f"/verification-challenge?{email_encoded}")
|
||||
|
||||
return {
|
||||
"title": "join or log in",
|
||||
|
|
@ -113,7 +109,7 @@ def verification_challenge(request):
|
|||
name = ""
|
||||
if user.full_name:
|
||||
name = user.name
|
||||
msg = ("Welcome {}".format(name), "success")
|
||||
msg = (f"Welcome {name}", "success")
|
||||
request.session["authenticated_user_id"] = str(user.id)
|
||||
request.session.flash(msg)
|
||||
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ def cart_activate(request):
|
|||
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))
|
||||
return HTTPFound(f"/cart/{new_cart.id}")
|
||||
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
|
@ -162,7 +162,7 @@ def cart_delete(request):
|
|||
@view_config(route_name="cart")
|
||||
def cart(request):
|
||||
"""Load active cart from session. Create cart if not in session."""
|
||||
return HTTPFound("/cart/{}".format(request.active_cart.id))
|
||||
return HTTPFound(f"/cart/{request.active_cart.id}")
|
||||
|
||||
|
||||
@view_config(route_name="cart_by_id", renderer="cart.j2")
|
||||
|
|
@ -186,7 +186,7 @@ def cart_by_id(request):
|
|||
request.dbsession.add(cart)
|
||||
request.dbsession.flush()
|
||||
msg = (
|
||||
"Invalid coupon ({}) removed from cart.".format(coupon.code),
|
||||
f"Invalid coupon ({coupon.code}) removed from cart.",
|
||||
"info",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
|
|
@ -234,7 +234,7 @@ def cart_add_product(request):
|
|||
msg = ("that content is not for sale or purchase.", "error")
|
||||
|
||||
else:
|
||||
msg = ('You added "{}" to your cart.'.format(product.title), "success")
|
||||
msg = (f'You added "{product.title}" to your cart.', "success")
|
||||
request.active_cart.add_product(product)
|
||||
request.dbsession.add(request.active_cart)
|
||||
request.dbsession.flush()
|
||||
|
|
@ -273,7 +273,7 @@ def cart_remove_product(request):
|
|||
msg = ("That product_id does not exist.", "error")
|
||||
|
||||
else:
|
||||
msg = ('You removed "{}" from the cart.'.format(product.title), "success")
|
||||
msg = (f'You removed "{product.title}" from the cart.', "success")
|
||||
cart.remove_product(product)
|
||||
cart.remove_handling_if_no_physical_products()
|
||||
request.dbsession.add(cart)
|
||||
|
|
@ -335,7 +335,7 @@ def cart_quantity_product(request):
|
|||
return HTTPFound("/cart")
|
||||
else:
|
||||
msg = (
|
||||
'You updated the quantity of "{}" in the cart.'.format(product.title),
|
||||
f'You updated the quantity of "{product.title}" in the cart.',
|
||||
"success",
|
||||
)
|
||||
cart.set_product_quantity(product, quantity)
|
||||
|
|
@ -390,7 +390,7 @@ def cart_checkout(request):
|
|||
stripe_user_shop = request.shop.stripe_user_shop(request.user)
|
||||
|
||||
if "cart_id" not in request.matchdict:
|
||||
return HTTPFound("/u/cart/{}/checkout".format(request.active_cart.id))
|
||||
return HTTPFound(f"/u/cart/{request.active_cart.id}/checkout")
|
||||
|
||||
cart = get_cart_from_matchdict(request)
|
||||
|
||||
|
|
@ -420,12 +420,18 @@ def cart_checkout(request):
|
|||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
if not cart.handling_option:
|
||||
msg = ("Please select a handling option for your physical items.", "error")
|
||||
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")
|
||||
msg = (
|
||||
"Please add a shipping address to proceed with checkout.",
|
||||
"info",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/u/addresses")
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ def content(request):
|
|||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
if product.is_sellable:
|
||||
return HTTPFound("/p/{}/{}".format(product.id, product.slug))
|
||||
return HTTPFound(f"/p/{product.id}/{product.slug}")
|
||||
|
||||
if "slug" not in request.matchdict:
|
||||
return HTTPFound("/c/{}/{}".format(product.id, product.slug))
|
||||
return HTTPFound(f"/c/{product.id}/{product.slug}")
|
||||
|
||||
signed_get_object_url = None
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ def coupon_new(request):
|
|||
)
|
||||
request.dbsession.add(coupon)
|
||||
request.dbsession.flush()
|
||||
return HTTPFound("/s/{}/coupons".format(request.shop.id))
|
||||
return HTTPFound(f"/s/{request.shop.id}/coupons")
|
||||
|
||||
return request.params
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ def coupon_apply_to_cart(request):
|
|||
if coupon is None:
|
||||
msg = ("That coupon_id does not exist.", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/cart/{}".format(request.active_cart.id))
|
||||
return HTTPFound(f"/cart/{request.active_cart.id}")
|
||||
|
||||
if coupon not in request.active_cart.coupons:
|
||||
msg = ("The coupon was applied to your cart.", "success")
|
||||
|
|
@ -93,7 +93,7 @@ def coupon_apply_to_cart(request):
|
|||
msg = ("The coupon was already applied to your cart.", "success")
|
||||
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/cart/{}".format(request.active_cart.id))
|
||||
return HTTPFound(f"/cart/{request.active_cart.id}")
|
||||
|
||||
|
||||
@view_config(route_name="coupon_remove_from_cart", request_method="POST")
|
||||
|
|
@ -125,7 +125,7 @@ def coupon_remove_from_cart(request):
|
|||
else:
|
||||
msg = ("The coupon was already removed from your cart.", "success")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/cart/{}".format(cart_id))
|
||||
return HTTPFound(f"/cart/{cart_id}")
|
||||
|
||||
request.session.flash(msg)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ def product(request):
|
|||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
if product.is_not_sellable:
|
||||
return HTTPFound("/c/{}/{}".format(product.id, product.slug))
|
||||
return HTTPFound(f"/c/{product.id}/{product.slug}")
|
||||
|
||||
if "slug" not in request.matchdict:
|
||||
return HTTPFound("/p/{}/{}".format(product.id, product.slug))
|
||||
return HTTPFound(f"/p/{product.id}/{product.slug}")
|
||||
|
||||
signed_get_object_url = None
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ def product_new(request):
|
|||
request.dbsession.flush()
|
||||
msg = ("Great, next you may upload files.", "success")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/p/{}/edit".format(product.id))
|
||||
return HTTPFound(f"/p/{product.id}/edit")
|
||||
|
||||
elif title or description or price:
|
||||
msg = ("You must fill out all fields.", "error")
|
||||
|
|
@ -172,12 +172,10 @@ def product_edit_description(request):
|
|||
request.dbsession.flush()
|
||||
|
||||
return {
|
||||
"markup_subject": "Product Description for {}".format(product.title),
|
||||
"markup_subject": f"Product Description for {product.title}",
|
||||
"markup_rendered": product.description_html,
|
||||
"markup_raw": product.description,
|
||||
"markup_form_path": "{}/description".format(
|
||||
product.absolute_url(request, slug=False)
|
||||
),
|
||||
"markup_form_path": f"{product.absolute_url(request, slug=False)}/description",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -266,7 +264,7 @@ def product_edit(request):
|
|||
ContentDisposition=product.get_content_disposition(file_key),
|
||||
ContentType=product.get_content_type(file_key),
|
||||
CacheControl=cache_control,
|
||||
Key="{}/{}".format(product.s3_path, file_key),
|
||||
Key=f"{product.s3_path}/{file_key}",
|
||||
MetadataDirective="REPLACE",
|
||||
)
|
||||
|
||||
|
|
@ -279,7 +277,7 @@ def product_edit(request):
|
|||
# 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=f"{product.s3_path}/{file_key}",
|
||||
)
|
||||
|
||||
product_content_length = response["ContentLength"]
|
||||
|
|
@ -294,9 +292,7 @@ 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(f"/p/{product.id}/{product.slug}/edit?uploaded={file_key}")
|
||||
|
||||
if product_modified and product.error_message is None:
|
||||
request.dbsession.add(product)
|
||||
|
|
@ -305,7 +301,7 @@ def product_edit(request):
|
|||
signed_posts = {}
|
||||
|
||||
for file_key in product.file_keys:
|
||||
key_starts_with = "{}/{}.".format(product.s3_path, file_key)
|
||||
key_starts_with = f"{product.s3_path}/{file_key}."
|
||||
conditions = [
|
||||
{"success_action_redirect": product.absolute_edit_url(request)},
|
||||
["starts-with", "$key", key_starts_with],
|
||||
|
|
@ -374,18 +370,29 @@ def product_inventory_edit(request):
|
|||
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)
|
||||
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)
|
||||
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"))
|
||||
request.session.flash(
|
||||
(f"Invalid quantity for location {location.name}.", "error")
|
||||
)
|
||||
|
||||
if updated:
|
||||
request.dbsession.flush()
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ def home(request):
|
|||
def shop(request):
|
||||
shop = get_shop_from_matchdict(request)
|
||||
if "slug" not in request.matchdict:
|
||||
return HTTPFound("/s/{}/{}".format(shop.id, shop.slug))
|
||||
return HTTPFound(f"/s/{shop.id}/{shop.slug}")
|
||||
|
||||
return {
|
||||
"products": get_products_from_a_shop(shop),
|
||||
|
|
@ -106,7 +106,7 @@ def shop_products(request):
|
|||
def shop_about(request):
|
||||
shop = get_shop_from_matchdict(request)
|
||||
if "slug" not in request.matchdict:
|
||||
return HTTPFound("/s/{}/{}/about".format(shop.id, shop.slug))
|
||||
return HTTPFound(f"/s/{shop.id}/{shop.slug}/about")
|
||||
return {}
|
||||
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ def shop_new(request):
|
|||
"success",
|
||||
)
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/s/{}/settings".format(shop.id))
|
||||
return HTTPFound(f"/s/{shop.id}/settings")
|
||||
|
||||
elif name or description or billing_address or phone_number:
|
||||
msg = ("You must fill out all fields.", "error")
|
||||
|
|
@ -301,7 +301,7 @@ def shop_users(request):
|
|||
# send an invitation email to the new user.
|
||||
send_invite_email(request, email_to_invite, request.user, request.shop)
|
||||
|
||||
msg = ("You invited {} to the shop.".format(email_to_invite), "success")
|
||||
msg = (f"You invited {email_to_invite} to the shop.", "success")
|
||||
request.session.flash(msg)
|
||||
|
||||
return {
|
||||
|
|
@ -327,9 +327,9 @@ def shop_user_remove(request):
|
|||
else:
|
||||
user_shop = request.shop.get_usershop_for_user(user)
|
||||
request.dbsession.delete(user_shop)
|
||||
msg = ("You removed {} from the shop.".format(user.email), "success")
|
||||
msg = (f"You removed {user.email} from the shop.", "success")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/s/{}/users".format(request.shop.id))
|
||||
return HTTPFound(f"/s/{request.shop.id}/users")
|
||||
|
||||
|
||||
@view_config(route_name="shop_settings", renderer="shop_settings.j2")
|
||||
|
|
@ -376,7 +376,7 @@ def shop_settings(request):
|
|||
if uploaded_file_key:
|
||||
request.session.flash(
|
||||
(
|
||||
"You successfully uploaded a new {} file!".format(uploaded_file_key),
|
||||
f"You successfully uploaded a new {uploaded_file_key} file!",
|
||||
"success",
|
||||
)
|
||||
)
|
||||
|
|
@ -478,7 +478,7 @@ def shop_settings(request):
|
|||
shop.maint_mode = maint_mode
|
||||
request.session.flash(
|
||||
(
|
||||
"You turned {} maint_mode".format(maint_mode_checkbox),
|
||||
f"You turned {maint_mode_checkbox} maint_mode",
|
||||
"success",
|
||||
)
|
||||
)
|
||||
|
|
@ -508,10 +508,10 @@ def shop_settings(request):
|
|||
"Bucket": s3_webhook_bucket,
|
||||
"Key": s3_webhook_key,
|
||||
},
|
||||
ContentDisposition="inline; filename={}".format(file_key),
|
||||
ContentDisposition=f"inline; filename={file_key}",
|
||||
ContentType=content_type,
|
||||
CacheControl=cache_control,
|
||||
Key="{}/meta/{}".format(shop.id, file_key),
|
||||
Key=f"{shop.id}/meta/{file_key}",
|
||||
MetadataDirective="REPLACE",
|
||||
)
|
||||
|
||||
|
|
@ -531,7 +531,7 @@ def shop_settings(request):
|
|||
|
||||
# redirect back to this page to clear
|
||||
# the params posted by the s3 webhooks.
|
||||
return HTTPFound("/s/{}/settings?uploaded={}".format(shop.id, file_key))
|
||||
return HTTPFound(f"/s/{shop.id}/settings?uploaded={file_key}")
|
||||
|
||||
signed_posts = {}
|
||||
get_endpoints = {}
|
||||
|
|
@ -541,7 +541,7 @@ def shop_settings(request):
|
|||
# object path start's with the shop's s3 namespace (uuid) and only for
|
||||
# particular file keys. This prevents a user from writing files into
|
||||
# a shops namespace which is owned by a different user.
|
||||
key_starts_with = "{}/meta/{}.{}".format(shop.id, file_key, "$filename")
|
||||
key_starts_with = f"{shop.id}/meta/{file_key}.$filename"
|
||||
conditions = [
|
||||
{"success_action_redirect": shop.absolute_settings_url(request)},
|
||||
["starts-with", "$key", key_starts_with],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue