make_post_sell/make_post_sell/models/user.py
Russell Ballestrini c211df66a8 Time to open the sauce : )
Dead man switch activated. Good luck everyone, I wish you the best.

 - BDFL

	new file:   .gitignore
	new file:   MANIFEST.in
	new file:   Makefile
	new file:   README.rst
	new file:   development.ini
	new file:   journal.rst
	new file:   make_post_sell/__init__.py
	new file:   make_post_sell/config_jinja2.py
	new file:   make_post_sell/lib/__init__.py
	new file:   make_post_sell/lib/mail.py
	new file:   make_post_sell/lib/mail_messages.py
	new file:   make_post_sell/lib/render.py
	new file:   make_post_sell/models/__init__.py
	new file:   make_post_sell/models/cart.py
	new file:   make_post_sell/models/cart_coupon.py
	new file:   make_post_sell/models/coupon.py
	new file:   make_post_sell/models/coupon_redemption.py
	new file:   make_post_sell/models/invoice.py
	new file:   make_post_sell/models/market.py
	new file:   make_post_sell/models/meta.py
	new file:   make_post_sell/models/price.py
	new file:   make_post_sell/models/product.py
	new file:   make_post_sell/models/shop.py
	new file:   make_post_sell/models/shop_search_request.py
	new file:   make_post_sell/models/stripe_user_shop.py
	new file:   make_post_sell/models/user.py
	new file:   make_post_sell/models/user_product.py
	new file:   make_post_sell/models/user_shop.py
	new file:   make_post_sell/request_methods.py
	new file:   make_post_sell/routes.py
	new file:   make_post_sell/scripts/__init__.py
	new file:   make_post_sell/scripts/alembic/README
	new file:   make_post_sell/scripts/alembic/env.py
	new file:   make_post_sell/scripts/alembic/script.py.mako
	new file:   make_post_sell/scripts/alembic/versions/15751f5d7a03_add_active_cart_total_in_cents_to_user_.py
	new file:   make_post_sell/scripts/alembic/versions/24662905655f_create_new_active_shop_id_column_on_.py
	new file:   make_post_sell/scripts/alembic/versions/3e1e70bfe89d_google_analytics_id.py
	new file:   make_post_sell/scripts/alembic/versions/4d9da43b0e86_product_visibility_column.py
	new file:   make_post_sell/scripts/alembic/versions/5c40dfb56179_shop_ribbon_columns.py
	new file:   make_post_sell/scripts/alembic/versions/6da97f2f913f_add_updated_timestamp_to_cart.py
	new file:   make_post_sell/scripts/alembic/versions/96f851cbab01_terms_and_privacy.py
	new file:   make_post_sell/scripts/alembic/versions/ab7338eaedf1_add_price_in_cents_column.py
	new file:   make_post_sell/scripts/alembic/versions/af28cc35ed6f_product_bundles.py
	new file:   make_post_sell/scripts/alembic/versions/b0d5604d9255_add_active_cart_count_to_user_table.py
	new file:   make_post_sell/scripts/alembic/versions/bfc4195a7f56_product_is_sellable_coluumn.py
	new file:   make_post_sell/scripts/alembic/versions/c4d5566ec87d_coupon_stackable_and_couponredemption.py
	new file:   make_post_sell/scripts/alembic/versions/e12bbe39d87e_cart_coupon_table.py
	new file:   make_post_sell/scripts/alembic/versions/eadd0d69e133_shop_ribbon_text_color.py
	new file:   make_post_sell/scripts/alembic/versions/f3aa57777025_coupon_table.py
	new file:   make_post_sell/scripts/alembic/versions/f882f2255cd1_add_stripe_secret_and_public_key_to_shop.py
	new file:   make_post_sell/scripts/alembic/versions/fd9f7e2f2b78_user_full_name.py
	new file:   make_post_sell/scripts/initialize_db.py
	new file:   make_post_sell/scripts/pshell.py
	new file:   make_post_sell/static/css/alerts.css
	new file:   make_post_sell/static/css/common.css
	new file:   make_post_sell/static/css/lib.css
	new file:   make_post_sell/static/css/mps.css
	new file:   make_post_sell/static/favicon.ico
	new file:   make_post_sell/static/js/custom.js
	new file:   make_post_sell/static/js/jquery-3.4.1.min.js
	new file:   make_post_sell/static/js/jquery.min.js
	new file:   make_post_sell/static/makepostsell-3.png
	new file:   make_post_sell/static/makepostsell.png
	new file:   make_post_sell/static/mps.png
	new file:   make_post_sell/templates/actions_new.j2
	new file:   make_post_sell/templates/actions_view.j2
	new file:   make_post_sell/templates/base.j2
	new file:   make_post_sell/templates/billing.j2
	new file:   make_post_sell/templates/cart.j2
	new file:   make_post_sell/templates/cart_checkout.j2
	new file:   make_post_sell/templates/carts.j2
	new file:   make_post_sell/templates/content.j2
	new file:   make_post_sell/templates/content_new.j2
	new file:   make_post_sell/templates/coupon.j2
	new file:   make_post_sell/templates/coupon_new.j2
	new file:   make_post_sell/templates/coupons.j2
	new file:   make_post_sell/templates/home.j2
	new file:   make_post_sell/templates/join-or-log-in.j2
	new file:   make_post_sell/templates/markup_editor.j2
	new file:   make_post_sell/templates/markup_renderer.j2
	new file:   make_post_sell/templates/product.j2
	new file:   make_post_sell/templates/product_edit.j2
	new file:   make_post_sell/templates/product_new.j2
	new file:   make_post_sell/templates/products.j2
	new file:   make_post_sell/templates/shop.j2
	new file:   make_post_sell/templates/shop_about.j2
	new file:   make_post_sell/templates/shop_new.j2
	new file:   make_post_sell/templates/shop_products.j2
	new file:   make_post_sell/templates/shop_settings.j2
	new file:   make_post_sell/templates/shops.j2
	new file:   make_post_sell/templates/snippets/csrf.j2
	new file:   make_post_sell/templates/snippets/flash-alerts.j2
	new file:   make_post_sell/templates/snippets/google-analytics.j2
	new file:   make_post_sell/templates/snippets/optional-javascript.j2
	new file:   make_post_sell/templates/snippets/ribbon.j2
	new file:   make_post_sell/templates/snippets/search.j2
	new file:   make_post_sell/templates/snippets/stripe.j2
	new file:   make_post_sell/templates/update-card.j2
	new file:   make_post_sell/templates/user_settings.j2
	new file:   make_post_sell/tests/__init__.py
	new file:   make_post_sell/tests/test_functional.py
	new file:   make_post_sell/tests/test_models.py
	new file:   make_post_sell/views/__init__.py
	new file:   make_post_sell/views/actions.py
	new file:   make_post_sell/views/authentication.py
	new file:   make_post_sell/views/billing.py
	new file:   make_post_sell/views/cart.py
	new file:   make_post_sell/views/content.py
	new file:   make_post_sell/views/coupon.py
	new file:   make_post_sell/views/misc.py
	new file:   make_post_sell/views/product.py
	new file:   make_post_sell/views/shop.py
	new file:   make_post_sell/views/user.py
	new file:   pytest.ini
	new file:   requirements-dev.txt
	new file:   requirements-prod.txt
	new file:   requirements-test.txt
	new file:   requirements.py3.txt
	new file:   requirements.txt
	new file:   setup.py
	new file:   test.ini
2021-12-31 21:39:40 -05:00

268 lines
8.3 KiB
Python

import uuid
import bcrypt
from sqlalchemy import Column, BigInteger, Integer, Boolean, Unicode, func
from .meta import Base, RBase, UUIDType, now_timestamp, foreign_key, get_object_by_id
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship
from .user_shop import UserShop
from .stripe_user_shop import StripeUserShop
from .user_product import UserProduct
from .shop import Shop
from .cart import Cart
from .cart import get_cart_by_id
from miscutils import generate_password
import logging
log = logging.getLogger(__name__)
try:
unicode("")
except:
from six import u as unicode
class User(RBase, Base):
"""This class represents a user account."""
# was this User object authenticated?
authenticated = False
id = Column(UUIDType, primary_key=True, index=True)
active_cart_id = Column(UUIDType, nullable=False)
active_shop_id = Column(UUIDType, default=None)
name = Column(Unicode(64), unique=True, nullable=False)
full_name = Column(Unicode(64))
email = Column(Unicode(64), unique=True, nullable=False)
password = Column(Unicode(64))
password_timestamp = Column(BigInteger)
created_timestamp = Column(BigInteger, nullable=False)
gravatar = Column(Boolean, default=False)
verified = Column(Boolean, default=False)
disabled = Column(Boolean, default=False)
active_cart_count = Column(Integer, default=0, nullable=False)
active_cart_total_in_cents = Column(BigInteger, default=0, nullable=False)
# many to many uses association_proxy.
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))
# many to many uses association_proxy.
# returns all the products that a user has purchased.
"""
_products = relationship(UserProduct, order_by=[UserProduct.created_timestamp.desc()])
products = association_proxy(
"_products", "product", creator=lambda p: UserProduct(product=p)
)
"""
products = association_proxy(
"user_products", "product", creator=lambda p: UserProduct(product=p)
)
# one to many.
# returns this user's carts.
# lazy="dynamic" returns a query object instread of an InstrumentedList.
# this is ideal when we want to further filter the results.
carts = relationship(argument="Cart", lazy="dynamic", back_populates="user")
def __init__(self, email):
self.name = unicode("user-{}".format(generate_password(size=8)))
self.created_timestamp = now_timestamp()
self.id = uuid.uuid1()
self.email = unicode(email)
self.new_password()
# don't password throttle new User objects.
self.password_timestamp = 0
def _generate_raw_password(self, size=32):
"""Return a system generated password"""
return generate_password(size)
def new_password(self):
"""Generate and return raw password, store password hash into DB."""
raw_password = self._generate_raw_password()
# bcrypt works with bytes so we encode to utf-8.
self.password = bcrypt.hashpw(
raw_password.encode("utf-8"),
bcrypt.gensalt()
).decode("utf-8")
self.password_timestamp = now_timestamp()
return raw_password
def check_password(self, password):
"""Accept plain-text raw password, create hash, compare with DB."""
stored_hash = self.password
# bcrypt works with bytes so we encode to utf-8.
new_hash = bcrypt.hashpw(
password.encode("utf-8"),
stored_hash.encode("utf-8"),
).decode("utf-8")
# log.info("new_hash={} stored_hash={}".format(new_hash, stored_hash))
if new_hash == stored_hash:
return True
return False
def throttle_password(self, needed_delta=90000):
"""Return True when throttled, else False"""
if self.password_timestamp_delta <= needed_delta:
return True
return False
@property
def password_timestamp_delta(self):
return now_timestamp() - self.password_timestamp
def set_active_shop(self, shop):
self.active_shop_id = shop.id
self.dbsession.add(self)
self.dbsession.flush()
@property
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()
elif self.shops:
# if a user doesn't have an active shop but it has
# at least one shop, set them an active shop.
shop = self.shops[0]
self.set_active_shop(shop)
return shop
def can_edit_product(self, product):
"""Return True or False"""
if product and self in product.shop.users:
return True
return False
def can_not_edit_product(self, product):
"""Return True or False"""
return not self.can_edit_product(product)
def can_download_product(self, product):
"""Return True or False"""
if product:
if self.can_edit_product(product) or self in product.users:
return True
for bundle in product.bundles_with_this_product:
if self in bundle.users:
return True
return False
def can_not_download_product(self, product):
"""Return True or False"""
return not self.can_download_product(product)
def can_edit_shop(self, shop):
if shop and self in shop.users:
return True
return False
def can_not_edit_shop(self, shop):
return not self.can_edit_shop(shop)
@property
def active_cart(self):
if hasattr(self, "_active_cart") == False:
self._active_cart = get_cart_by_id(self.dbsession, self.active_cart_id)
return self._active_cart
def new_active_cart(self):
self._active_cart = Cart(user=self)
self.active_cart_id = self._active_cart.id
return self._active_cart
def owns_cart(self, cart):
return self == cart.user
def does_not_own_cart(self, cart):
return not self.owns_cart(cart)
def _user_by_name_query(dbsession, name):
"""query User by case insensitive name."""
return dbsession.query(User).filter(func.lower(User.name) == unicode(name.lower()))
def is_user_name_available(dbsession, name):
return not _user_by_name_query(dbsession, unicode(name)).count()
def generate_user_name(dbsession, desired_name=""):
size = 8
sep = ""
if desired_name:
size = 3
sep = "-"
name = sep.join([desired_name, generate_password(size)])
while is_user_name_available(dbsession, name) == False:
name = sep.join([desired_name, generate_password(size)])
return name
def is_user_name_valid(name):
"""Test if user name meets our criteria for validity."""
# Choices here are subject to change.
# allow dashes.
name = name.replace("-", "")
# allow period.
name = name.replace(".", "")
# allow spaces.
name = name.replace(" ", "")
return name.isalnum()
def get_user_by_name(dbsession, name):
"""Try to get User object by name or return None"""
if name:
return _user_by_name_query(dbsession, name).one_or_none()
def get_user_by_id(dbsession, user_id):
"""Try to get User object by id or return None."""
return get_object_by_id(dbsession, user_id, User)
def get_user_by_email(dbsession, email):
"""Try to get User object by email or return None"""
if email:
return dbsession.query(User).filter(User.email == unicode(email)).one_or_none()
def get_or_create_user_by_email(dbsession, email):
"""Try to get User object by email or return new User"""
user = get_user_by_email(dbsession, email)
if user is None:
# create new User for unverified user.
user = User(email)
cart = user.new_active_cart()
if not is_user_name_available(dbsession, user.name):
user.name = generate_user_name(dbsession)
# make sure the new user's default active cart exists.
dbsession.add(cart)
dbsession.flush()
return user