From f820210bb717a925dfd67809e2a00459022e03f1 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Wed, 19 Jun 2024 13:57:23 -0400 Subject: [PATCH] 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 --- make_post_sell/lib/hex_color.py | 4 +- make_post_sell/lib/mail.py | 33 ++++----- make_post_sell/lib/sanitize_html.py | 12 +--- make_post_sell/models/coupon.py | 46 ++++++------ make_post_sell/models/meta.py | 9 ++- make_post_sell/models/product.py | 26 +++---- make_post_sell/models/shop.py | 72 ++++++++++++------- make_post_sell/models/user.py | 36 ++++++---- make_post_sell/tests/test_functional.py | 93 +++++++++++-------------- make_post_sell/views/authentication.py | 12 ++-- make_post_sell/views/cart.py | 24 ++++--- make_post_sell/views/content.py | 4 +- make_post_sell/views/coupon.py | 8 +-- make_post_sell/views/product.py | 39 ++++++----- make_post_sell/views/shop.py | 24 +++---- 15 files changed, 221 insertions(+), 221 deletions(-) diff --git a/make_post_sell/lib/hex_color.py b/make_post_sell/lib/hex_color.py index b2865e4..0267340 100644 --- a/make_post_sell/lib/hex_color.py +++ b/make_post_sell/lib/hex_color.py @@ -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}" diff --git a/make_post_sell/lib/mail.py b/make_post_sell/lib/mail.py index 78aff7c..a74fa79 100644 --- a/make_post_sell/lib/mail.py +++ b/make_post_sell/lib/mail.py @@ -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( - '

{}
{}

'.format( - p.absolute_url(request), p.title, thumbnail - ) + f'

{p.title}
{thumbnail}

' ) product_text = "
".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( - '

{}
{}

'.format( - p.absolute_url(request), p.title, thumbnail - ) + f'

{p.title}
{thumbnail}

' ) product_text = "
".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) diff --git a/make_post_sell/lib/sanitize_html.py b/make_post_sell/lib/sanitize_html.py index cc5dea3..d2a5c6a 100644 --- a/make_post_sell/lib/sanitize_html.py +++ b/make_post_sell/lib/sanitize_html.py @@ -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", "")) diff --git a/make_post_sell/models/coupon.py b/make_post_sell/models/coupon.py index 862e09c..78e55de 100644 --- a/make_post_sell/models/coupon.py +++ b/make_post_sell/models/coupon.py @@ -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() diff --git a/make_post_sell/models/meta.py b/make_post_sell/models/meta.py index 7368a9e..814c7d2 100644 --- a/make_post_sell/models/meta.py +++ b/make_post_sell/models/meta.py @@ -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): diff --git a/make_post_sell/models/product.py b/make_post_sell/models/product.py index 706054c..7e11bcb 100644 --- a/make_post_sell/models/product.py +++ b/make_post_sell/models/product.py @@ -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() diff --git a/make_post_sell/models/shop.py b/make_post_sell/models/shop.py index 856f28b..8a2ce9a 100644 --- a/make_post_sell/models/shop.py +++ b/make_post_sell/models/shop.py @@ -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/ """ 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//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//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) - diff --git a/make_post_sell/models/user.py b/make_post_sell/models/user.py index 84d5e40..cdf57c4 100644 --- a/make_post_sell/models/user.py +++ b/make_post_sell/models/user.py @@ -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.""" diff --git a/make_post_sell/tests/test_functional.py b/make_post_sell/tests/test_functional.py index c3eaa9a..74e2a44 100644 --- a/make_post_sell/tests/test_functional.py +++ b/make_post_sell/tests/test_functional.py @@ -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. """ diff --git a/make_post_sell/views/authentication.py b/make_post_sell/views/authentication.py index 54f09d0..742309c 100644 --- a/make_post_sell/views/authentication.py +++ b/make_post_sell/views/authentication.py @@ -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) diff --git a/make_post_sell/views/cart.py b/make_post_sell/views/cart.py index d7202f2..a0c0809 100644 --- a/make_post_sell/views/cart.py +++ b/make_post_sell/views/cart.py @@ -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") diff --git a/make_post_sell/views/content.py b/make_post_sell/views/content.py index 22bd62e..495dd19 100644 --- a/make_post_sell/views/content.py +++ b/make_post_sell/views/content.py @@ -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 diff --git a/make_post_sell/views/coupon.py b/make_post_sell/views/coupon.py index 47dbf69..f15c6fd 100644 --- a/make_post_sell/views/coupon.py +++ b/make_post_sell/views/coupon.py @@ -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)) diff --git a/make_post_sell/views/product.py b/make_post_sell/views/product.py index 3f2b2b6..0c383b8 100644 --- a/make_post_sell/views/product.py +++ b/make_post_sell/views/product.py @@ -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() diff --git a/make_post_sell/views/shop.py b/make_post_sell/views/shop.py index dddb805..75d6b60 100644 --- a/make_post_sell/views/shop.py +++ b/make_post_sell/views/shop.py @@ -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],