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:
Russell Ballestrini 2024-06-19 13:57:23 -04:00
parent 068cc9b12d
commit f820210bb7
15 changed files with 221 additions and 221 deletions

View file

@ -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}"

View file

@ -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)

View file

@ -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", ""))

View file

@ -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 "&#8734;"
@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()

View file

@ -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):

View file

@ -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()

View file

@ -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)

View file

@ -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."""

View file

@ -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.
"""

View file

@ -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)

View file

@ -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")

View file

@ -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

View file

@ -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))

View file

@ -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()

View file

@ -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],