try it out.

modified:   make_post_sell/models/product.py
	modified:   make_post_sell/models/shop.py
	modified:   make_post_sell/models/user.py
	new file:   make_post_sell/scripts/alembic/versions/c263b4fba9e7_new_ux_columns.py
	deleted:    make_post_sell/static/css/alerts.css
	modified:   make_post_sell/static/css/common.css
	deleted:    make_post_sell/static/css/lib.css
	deleted:    make_post_sell/static/css/mps.css
	modified:   make_post_sell/templates/base.j2
	modified:   make_post_sell/templates/cart.j2
	modified:   make_post_sell/templates/product_edit.j2
	modified:   make_post_sell/templates/shop_settings.j2
	modified:   make_post_sell/templates/snippets/search.j2
	modified:   make_post_sell/templates/user_settings.j2
	modified:   make_post_sell/views/shop.py
	modified:   requirements.txt
This commit is contained in:
Russell Ballestrini 2022-04-29 10:58:09 -04:00
parent 759501ad25
commit 4142476f7e
16 changed files with 343 additions and 167 deletions

View file

@ -470,6 +470,7 @@ def get_products_by_keywords(dbsession, keywords, shop=None):
for keyword in keywords:
keyword_filter = Product.title.ilike("%{}%".format(keyword))
# the product _must_ be public (1).
products = (
product_query.filter(keyword_filter).filter(Product.visibility == 1).all()
)

View file

@ -67,6 +67,9 @@ class Shop(RBase, Base):
disabled = Column(Boolean, default=False)
maint_mode = Column(Boolean, default=False)
favicon = Column(Boolean, default=False)
# many to many uses association_proxy.
users = association_proxy("shop_users", "user", creator=lambda u: UserShop(user=u))

View file

@ -56,6 +56,9 @@ class User(RBase, Base):
active_cart_count = Column(Integer, default=0, nullable=False)
active_cart_total_in_cents = Column(BigInteger, default=0, nullable=False)
# 0 is dark, 1 is light, could have more themes when h@ckz0rs unite.
theme_id = Column(BigInteger, nullable=False, default=1)
# many to many uses association_proxy.
shops = association_proxy("user_shops", "shop", creator=lambda s: UserShop(shop=s))

View file

@ -0,0 +1,30 @@
"""new ux columns
Revision ID: c263b4fba9e7
Revises: cdad5c220c39
Create Date: 2022-01-31 15:45:34.314442
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c263b4fba9e7'
down_revision = 'cdad5c220c39'
branch_labels = None
depends_on = None
from make_post_sell.models.meta import UUIDType
def upgrade():
op.add_column('mps_shop', sa.Column('maint_mode', sa.Boolean(), nullable=True, server_default="0"))
op.add_column('mps_shop', sa.Column('favicon', sa.Boolean(), nullable=True, server_default="0"))
op.add_column('mps_user', sa.Column('theme_id', sa.BigInteger(), nullable=False, server_default="1"))
def downgrade():
op.drop_column('mps_shop', 'maint_mode')
op.drop_column('mps_shop', 'favicon')
op.drop_column('mps_user', 'theme_id')

View file

@ -1,66 +0,0 @@
div.message-ribbon {
color: #ffffff;
background-color: #5871ad;
background: linear-gradient(to right, #5871ad, #82A8FF);
}
#alerts {
animation: fadein 2s;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
/* ALERT START */
#alerts {
z-index: 98;
}
.alert {
opacity: .85;
display: grid;
grid-template-columns: 1fr 50px;
grid-gap: 10px;
}
.alert-message {
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
}
.alert .close {
text-align: right;
margin-top: 10px;
margin-bottom: 10px;
margin-right: 15px;
}
.alert-danger, .alert-error {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeeba;
}
.alert-info, .alert-notice {
color: #0c5460;
background-color: #98b6fa;
border-color: #bee5eb;
}
.alert-success {
color: #155724;
background-color: #a3c765;
border-color: #c3e6cb;
}
/* ALERT END */

View file

@ -7,6 +7,7 @@
*
* * * * * */
body {
margin: 0px;
font-family: helvetica;
@ -32,6 +33,10 @@ input {
box-sizing: border-box;
}
section {
margin-bottom: 0px;
}
input[type="text"],
input[type="tel"],
input[type="password"],
@ -177,6 +182,7 @@ div.message-ribbon {
section.log-in {
text-align: center;
align-self: center;
font-size: .75em;
}
section.search {
@ -199,7 +205,9 @@ section.content {
}
section.logo{
align-self: center;
text-align: center;
vertical-align: middle;
}
img.logo {
@ -736,3 +744,107 @@ textarea.markup-editor-textarea {
padding: 20px;
}
}
div.message-ribbon {
color: #ffffff;
background-color: #5871ad;
background: linear-gradient(to right, #5871ad, #82A8FF);
}
.highlight {
text-shadow:
-6px 0px 6px rgba(255, 231, 13, 0.6),
6px 0px 6px rgba(255, 231, 13, 0.6),
12px 0px 12px rgba(255, 231, 13, 0.6),
-12px 0px 12px rgba(255, 231, 13, 0.6);
}
.avoid-wrap {
display:inline-block;
}
.vertical-text {
display: inline-block;
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
-o-transform: rotate(-90deg);
transform: rotate(-90deg);
}
.shown-on-desktop {
display: none;
}
.not-shown-on-desktop {
display: block;
}
@media screen and (min-width:800px) {
.shown-on-desktop {display: block}
.not-shown-on-desktop {display: none}
}
div.message-ribbon {
color: #ffffff;
background-color: #5871ad;
background: linear-gradient(to right, #5871ad, #82A8FF);
}
#alerts {
animation: fadein 2s;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
/* ALERT START */
#alerts {
z-index: 98;
}
.alert {
opacity: .85;
display: grid;
grid-template-columns: 1fr 50px;
grid-gap: 10px;
}
.alert-message {
text-align: center;
margin-top: 10px;
margin-bottom: 10px;
}
.alert .close {
text-align: right;
margin-top: 10px;
margin-bottom: 10px;
margin-right: 15px;
}
.alert-danger, .alert-error {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-warning {
color: #856404;
background-color: #fff3cd;
border-color: #ffeeba;
}
.alert-info, .alert-notice {
color: #0c5460;
background-color: #98b6fa;
border-color: #bee5eb;
}
.alert-success {
color: #155724;
background-color: #a3c765;
border-color: #c3e6cb;
}
/* ALERT END */

View file

@ -1,25 +0,0 @@
.avoid-wrap {
display:inline-block;
}
.vertical-text {
display: inline-block;
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
-o-transform: rotate(-90deg);
transform: rotate(-90deg);
}
.shown-on-desktop {
display: none;
}
.not-shown-on-desktop {
display: block;
}
@media screen and (min-width:800px) {
.shown-on-desktop {display: block}
.not-shown-on-desktop {display: none}
}

View file

@ -1,13 +0,0 @@
div.message-ribbon {
color: #ffffff;
background-color: #5871ad;
background: linear-gradient(to right, #5871ad, #82A8FF);
}
.highlight {
text-shadow:
-6px 0px 6px rgba(255, 231, 13, 0.6),
6px 0px 6px rgba(255, 231, 13, 0.6),
12px 0px 12px rgba(255, 231, 13, 0.6),
-12px 0px 12px rgba(255, 231, 13, 0.6);
}

View file

@ -5,18 +5,11 @@
{% if request.product -%}
<!--product_id={{ request.product.id }}-->
{%- endif %}
<html lang="en">
<html lang="en" data-theme="{{ request.html_data_theme or 'dark' }}">
<head>
<!--
TODO: merge all these CSS files into a single file during builds.
In prod single concatenated ("streamlined") all.css file.
In dev each CSS file busted out like so:
-->
<!-- stylesheet to-end-all stylesheets, pico.fluid.classless.css ! woot -->
<link rel="stylesheet" href="/static/css/common.css">
<link rel="stylesheet" href="/static/css/mps.css">
<link rel="stylesheet" href="/static/css/alerts.css">
<link rel="stylesheet" href="/static/css/lib.css">
{% include 'snippets/analytics.j2' %}
@ -26,6 +19,10 @@
<meta name="theme-color" content="{{ theme_color }}">
<meta charset="utf-8">
{% if request.shop and request.shop.favicon %}
<link rel="icon" href="{{ request.app['bucket.secure_uploads.get_endpoint'] }}/{{ request.shop.uuid_str }}/meta/shop-favicon" />
{% endif %}
{%- block append_to_head_tag_section %}
<title>{{ request.app_domain }}</title>
{% endblock %}

View file

@ -114,7 +114,7 @@
<form action="/cart/{{ cart.id }}/quantity" method="POST" onsubmit="submit.disabled = true; return true;" style="display: inline;">
<input type="hidden" name="product_id" value="{{ product.id }}">
quantity: <input type="text" name="quantity" class="mps-quantity" value="{{ product_quantity }}">
<input type="submit" id="submit" class="quantity button-small" value="update" />
<input type="submit" id="submit" value="update" role="button" style="width: 80px;"/>
</form>
<br/>

View file

@ -30,7 +30,8 @@
<li>
<form method="post" action="/p/{{ product.id }}/bundle/remove/{{ p.id }}" onsubmit="submit.disabled = true; return true;">
{{p.title}}
<input type="submit" name="submit" class="mps-submit" value="remove" />
<input type="submit" name="submit" class="mps-submit" value="remove" style
="width: 80px;"/>
</form>
</li>
<br>
@ -43,9 +44,12 @@
<h3>Upload or Replace Product File</h3>
People may download this file after making a purchase.
<br>
<br>
{% if "product" in product.originals %}<p style="font-weight: bold">Current File: {{ product.originals["product"] }}</p>{% endif %}
{% set signed_post = signed_posts["product"] %}
<form method="post" action="{{ signed_post["url"] }}" enctype="multipart/form-data" onsubmit="submit.disabled = true; return true;">
{% for field_name, field_value in signed_post["fields"].items() %}
@ -55,19 +59,22 @@
<input type="submit" name="submit" class="mps-upload" value="Upload Product" />
</form>
{% if "product" in product.originals %}<p style="font-weight: bold">Current File: {{ product.originals["product"] }}</p>{% endif %}
{% endif %}
{% if product.is_sellable %}
<br/>
<br/>
<hr/>
<h3>Upload or Replace Preview File</h3>
People may download this file before making a purchase.
<br>
<br>
<br />
<br />
{% if "preview" in product.originals %}<p style="font-weight: bold">Current File: {{ product.originals["preview"] }}</p>{% endif %}
{% set signed_post = signed_posts["preview"] %}
<form method="post" action="{{ signed_post["url"] }}" enctype="multipart/form-data" onsubmit="submit.disabled = true; return true;">
{% for field_name, field_value in signed_post["fields"].items() %}
@ -77,7 +84,7 @@
<input type="file" name="file" class="file-input" /> <br />
<input type="submit" name="submit" class="mps-upload" value="Upload Preview" />
</form>
{% if "preview" in product.originals %}<p style="font-weight: bold">Current File: {{ product.originals["preview"] }}</p>{% endif %}
{% endif %}
@ -97,9 +104,13 @@ Your product cover (<code>thumbnail1</code>) will show up on search pages.
{% set s3_key_thumbnails = product.s3_key_thumbnails %}
{% for thumbnail_key in product.file_thumbnail_keys %}
<div class="upload-thumbnail-item">
<b>{{ thumbnail_key }}</b>
<br />
{% if thumbnail_key in s3_key_thumbnails %}
{% set get_endpoint = request.app["bucket.secure_uploads.get_endpoint"] + "/" + s3_key_thumbnails[thumbnail_key] %}
<a href="{{ get_endpoint }}?ts={{ product.updated_timestamp }}"><img src="{{ get_endpoint }}?ts={{ product.updated_timestamp }}" width="120px" /></a>
{% endif %}
{% set signed_post = signed_posts[thumbnail_key] %}
<form method="post" action="{{ signed_post["url"] }}" enctype="multipart/form-data" onsubmit="submit.disabled = true; return true;">
@ -109,14 +120,14 @@ Your product cover (<code>thumbnail1</code>) will show up on search pages.
<input type="file" name="file" class="file-input" /> <br />
<input type="submit" name="submit" class="mps-upload" value="Upload {{ thumbnail_key }}" />
</form>
<hr/>
<br />
{% if thumbnail_key in s3_key_thumbnails %}
{% set get_endpoint = request.app["bucket.secure_uploads.get_endpoint"] + "/" + s3_key_thumbnails[thumbnail_key] %}
<a href="{{ get_endpoint }}?ts={{ product.updated_timestamp }}"><img src="{{ get_endpoint }}?ts={{ product.updated_timestamp }}" width="120px" /></a>
{% endif %}
</div>
{% endfor %}
</section>

View file

@ -3,9 +3,10 @@
{% block content -%}
<section class="one-column">
<section class="shop-settings well">
<h3>Shop Settings</h3>
<form method="post" action="/s/{{ request.shop.id }}/settings" onsubmit="submit.disabled = true; return true;">
<label for="name_input">Shop Name</label>
@ -57,7 +58,26 @@
<br />
<br />
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
<br />
<br />
</form>
</section>
</section>
<br />
<br />
<section class="one-column">
<section class="shop-settings well">
<h3>Integration Settings</h3>
<form method="post" action="/s/{{ request.shop.id }}/settings" onsubmit="submit.disabled = true; return true;">
<label for="domain_name_input">Custom Shop Domain Name</label>
<input
name = "domain_name"
@ -80,10 +100,10 @@
value = "{% if stripe_public_api_key %}{{ stripe_public_api_key }}{% endif %}"
placeholder = "Stripe Public API Key (pk)"
/>
<br />
<br />
<label for="stripe_secret_api_key_input">Stripe Secret API Key</label>
<input
name = "stripe_secret_api_key"
@ -93,7 +113,7 @@
value = "{% if stripe_secret_api_key %}{{ stripe_secret_api_key }}{% endif %}"
placeholder = "Stripe Secret API Key (sk)"
/>
<br />
<br />
@ -129,15 +149,60 @@
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
<br />
<br />
<br />
<hr />
</form>
</section>
</section>
<br />
<h4>Announcement Ribbon Settings</h4>
<br />
<br />
<section class="one-column">
<section class="shop-settings well">
<h3>Branding Settings</h3>
<label for="logo-banner">Banner Logo</label>
<a href="{{ get_endpoints["shop-logo-banner"] }}"><img src="{{ get_endpoints["shop-logo-banner"] }}" width="420px" /></a>
{% set signed_post = signed_posts["shop-logo-banner"] %}
<form method="post" action="{{ signed_post["url"] }}" enctype="multipart/form-data" onsubmit="submit.disabled = true; return true;">
{% for field_name, field_value in signed_post["fields"].items() %}
<input type="hidden" name="{{ field_name }}" value="{{ field_value }}" />
{% endfor %}
<input type="file" name="file" class="file-input" /> <br />
<input type="submit" name="submit" class="mps-upload" value="Upload Shop Banner" />
</form>
<hr />
<br />
<label for="logo-banner">favicon.ico</label>
<a href="{{ get_endpoints["shop-favicon"] }}"><img src="{{ get_endpoints["shop-favicon"] }}" width="32px" /></a>
{% set signed_post = signed_posts["shop-favicon"] %}
<form method="post" action="{{ signed_post["url"] }}" enctype="multipart/form-data" onsubmit="submit.disabled = true; return true;">
{% for field_name, field_value in signed_post["fields"].items() %}
<input type="hidden" name="{{ field_name }}" value="{{ field_value }}" />
{% endfor %}
<input type="file" name="file" class="file-input" /> <br />
<input type="submit" name="submit" class="mps-upload" value="Upload favicon.ico" />
</form>
</section>
</section>
<br />
<br />
<section class="one-column">
<section class="shop-settings well">
<form method="post" action="/s/{{ request.shop.id }}/settings" onsubmit="submit.disabled = true; return true;">
<h3>Announcement Ribbon Settings 🎀</h3>
<label for="ribbon_text_input">Ribbon Text</label>
<textarea
@ -194,35 +259,44 @@
<br />
<br />
<hr>
<br />
<br />
</form>
<h4>Branding</h4>
</section>
</section>
<br />
<br />
<section class="one-column">
<section class="shop-settings well">
<form method="post" action="/s/{{ request.shop.id }}/settings" onsubmit="submit.disabled = true; return true;">
<h3>Maintenance Mode 🏗️</h3>
<input type="checkbox" name="maint-mode-checkbox" id="maint-mode-checkbox" {% if maint_mode %}checked{% endif %}></input>
<b>If checked, enabled maintenance mode.</b>
<br />
<br />
Temporarily prevent new searches or sales on this shop.
Existing sales honored for download buy purchasers.
<label for="logo-banner">Banner Logo</label>
{% set signed_post = signed_posts["shop-logo-banner"] %}
<form method="post" action="{{ signed_post["url"] }}" enctype="multipart/form-data" onsubmit="submit.disabled = true; return true;">
{% for field_name, field_value in signed_post["fields"].items() %}
<input type="hidden" name="{{ field_name }}" value="{{ field_value }}" />
{% endfor %}
<input type="file" name="file" class="file-input" /> <br />
<input type="submit" name="submit" class="mps-upload" value="Upload Shop Banner" />
</form>
<br />
<br />
<a href="{{ get_endpoints["shop-logo-banner"] }}"><img src="{{ get_endpoints["shop-logo-banner"] }}" width="420px" /></a>
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
<br />
<br />
</form>
</section>
</section>
{%- endblock -%}

View file

@ -1,5 +1,5 @@
{% if request.shop and not search_disabled %}
<form action="/search" id="search" name="search">
<form action="/search" id="search" name="search" style="margin-bottom: 0px;">
{% include 'csrf.j2' %}
{%- if request.shop -%}
@ -12,6 +12,7 @@
class="mps-keywords"
placeholder="Keyword search on {{ request.shop.name }}"
{% if keywords %}value="{{ keywords }}"{% endif %}
style="margin-bottom: 0px;"
required/>
</form>

View file

@ -6,9 +6,9 @@
{% block content -%}
<section class="two-column">
<section class="one-column">
<section class="user-settings well">
<h3>User Settings</h3>
<form method="post" action="/u/settings" onsubmit="submit.disabled = true; return true;">
@ -37,15 +37,36 @@
<br />
<br />
<fieldset>
<legend>User Theme</legend>
<input type="radio" name="user_theme" id="light_mode" value="1" {% if user_theme == 1 %}checked{% endif %} />
<label for="user_theme" style="display: inline">Light mode</label>
<input type="radio" name="user_theme" id="visibility_unlisted" value="0" {% if user_theme == 0 %}checked{% endif %} />
<label for="user_theme" style="display: inline;">Dark mode</label>
</fieldset>
<br />
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
<br />
<br />
</form>
</section>
<section class="button-panel">
</section>
<br />
<br />
<section class="one-column">
<section class="well">
{% if request.is_saas_domain %}
<a href="/u/shops" class="product-edit-button mps-button">My Shops</a>

View file

@ -26,6 +26,13 @@ from pyramid.httpexceptions import HTTPFound
from miscutils.phone_numbers import is_phone_number_valid
def checkbox_to_bool(checkbox):
return True if checkbox == "on" else False
def bool_to_checkbox(boolean):
return "on" if boolean == True else "off"
def get_shop_from_matchdict(request, prefetched_shop=None):
"""
This function uses the shop_id from the url path
@ -308,6 +315,9 @@ def shop_settings(request):
stripe_public_api_key = request.params.get("stripe_public_api_key", shop.stripe_public_api_key)
stripe_secret_api_key = request.params.get("stripe_secret_api_key", shop.stripe_secret_api_key)
maint_mode_checkbox = request.params.get("maint-mode-checkbox", bool_to_checkbox(shop.maint_mode))
maint_mode = checkbox_to_bool(maint_mode_checkbox)
s3_webhook_key = request.params.get("key")
s3_webhook_bucket = request.params.get("bucket")
s3_webhook_etag = request.params.get("etag")
@ -396,6 +406,18 @@ def shop_settings(request):
msg = ("The shop's stripe_secret_api_key must start with 'sk_'.", "error")
request.session.flash(msg)
print("{} {}".format(shop.maint_mode, maint_mode))
if shop.maint_mode != maint_mode:
shop.maint_mode = maint_mode
request.session.flash(
(
"You turned {} maint_mode".format(
maint_mode_checkbox
),
"success",
)
)
# TODO: Dry out this block, it's a copy pasta from views/product.py
if s3_webhook_key and s3_webhook_bucket and s3_webhook_etag:
@ -409,9 +431,9 @@ def shop_settings(request):
# determine content type from extension.
file_ext = s3_webhook_key.split(".")[-1]
print(file_ext)
#print(file_ext)
content_type = mimetypes.types_map.get("." + file_ext)
print(content_type)
#print(content_type)
# copy upload to our system defined s3 location.
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html#S3.Client.copy_object
@ -435,6 +457,9 @@ def shop_settings(request):
Key=s3_webhook_key,
)
if file_key == "shop-favicon":
shop.favicon = True
# redirect back to this page to clear
# the params posted by the s3 webhooks.
return HTTPFound("/s/{}/{}/settings?uploaded={}".format(shop.id, shop.slug, file_key))
@ -444,7 +469,7 @@ def shop_settings(request):
signed_posts = {}
get_endpoints = {}
for file_key in ["shop-logo-banner", "shop-logo-avatar"]:
for file_key in ["shop-logo-banner", "shop-logo-avatar", "shop-favicon"]:
# conditionally sign the ability to POST to the object path when the
# 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
@ -493,9 +518,10 @@ def shop_settings(request):
"ribbon_color_1": ribbon_color_1,
"ribbon_color_2": ribbon_color_2,
"google_analytics_id": google_analytics_id,
"plausble_domain_name": plausible_domain_name,
"plausible_domain_name": plausible_domain_name,
"stripe_public_api_key": stripe_public_api_key,
"stripe_secret_api_key": stripe_secret_api_key,
"maint_mode": maint_mode,
"signed_posts": signed_posts,
"get_endpoints": get_endpoints,
}

View file

@ -1,5 +1,6 @@
# python 2 -> 3 compat.
six
#future
# Pyramid with SQLAlchemy.
plaster_pastedeploy
@ -52,7 +53,7 @@ miniuri
# my miscutils package can sanitize and make HTML safe.
# that said, it requires the following:
# Bleach sanitizes MarkDown (removes HTML/Javascript) to prevent XSS.
bleach>=2.1.4
bleach
#bleach-whitelist
bleach-allowlist
beautifulsoup4