Merge branch 'implement-shop-user-roles' into 'master'
shop user roles: owners, editors, members shop user roles: owners, editors, members Huge pull requests. In this PR we implement role based authorization by extending the "user shop" table to include a new field for tracking a rudimentary role id integer. For now the integers are from 0,1,2 0 == owner (root) 1 == editor (virtual assistant) 2 == member (viewer) For now the member role doesn't provide any additional behavior but could be added to a future enhancement. We have now separated out privileged views into requiring the user to be part of the owner role or editor role depending on the route. We have created a new route called shop_users & it works for adding or removing new users into the active shop. As such for example, only users with owner access may access the new shop_users view and as such only they can add or remove users from the shop.
This commit is contained in:
commit
be73a16228
16 changed files with 402 additions and 105 deletions
|
|
@ -17,6 +17,8 @@ from make_post_sell.lib.mail_messages import (
|
|||
PURCHASE_1_HTML,
|
||||
SALE_1_TEXT,
|
||||
SALE_1_HTML,
|
||||
INVITE_1_TEXT,
|
||||
INVITE_1_HTML,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -33,7 +35,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"
|
||||
subject = "Verification Code | {}".format(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)
|
||||
|
|
@ -126,3 +128,26 @@ def send_sale_email(request, shop, products, total_cost):
|
|||
# send a separate email for each shop owner.
|
||||
for user in shop.users:
|
||||
send_pyramid_email(request, user.email, subject, message_text, message_html)
|
||||
|
||||
|
||||
def send_invite_email(request, to_email, user, shop):
|
||||
"""
|
||||
Send an email to all shop owners regarding the sale.
|
||||
|
||||
request
|
||||
the request (of the successful log in attempt)
|
||||
|
||||
to_email
|
||||
the email address to send the shop invitation to.
|
||||
|
||||
user
|
||||
the user who sent the invite.
|
||||
|
||||
shop
|
||||
the shop the invitation is for.
|
||||
"""
|
||||
subject = "You have been invited to {}".format(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)
|
||||
send_pyramid_email(request, to_email, subject, message_text, message_html)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
WELCOME_1_TEXT = """
|
||||
Hey there!
|
||||
|
||||
Here is the verification code you requested:
|
||||
|
||||
\n{0}\n
|
||||
|
||||
Type code into the challenge input field:
|
||||
Here is the verification code you requested.
|
||||
Type code into the challenge input field.
|
||||
"""
|
||||
|
||||
WELCOME_1_HTML = """
|
||||
|
|
@ -17,13 +16,10 @@ WELCOME_1_HTML = """
|
|||
<body>
|
||||
<h2>Hey there!</h2>
|
||||
|
||||
<p>
|
||||
Here is the verification code you requested:
|
||||
</p>
|
||||
|
||||
<p><b>{1}</b></p>
|
||||
|
||||
<p>
|
||||
Here is the verification code you requested.
|
||||
Type code into the challenge input field.
|
||||
</p>
|
||||
|
||||
|
|
@ -146,3 +142,60 @@ SALE_1_HTML = """
|
|||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
# 0: user.email
|
||||
# 1: shop.name,
|
||||
# 2: log in link.
|
||||
INVITE_1_TEXT = """
|
||||
Hey There!
|
||||
|
||||
{0} has invited you to join the {1} shop on Make Post Sell.
|
||||
|
||||
Click this link to log in.
|
||||
|
||||
{2}
|
||||
|
||||
Make Post Sell is a lightweight, open-source, public domain, software as a service ecommerce web application.
|
||||
We hope you like our ethical approach to doing business online.
|
||||
|
||||
Thanks for sharing! <3
|
||||
"""
|
||||
|
||||
# 0: subject,
|
||||
# 1: user.email
|
||||
# 2: shop.name,
|
||||
# 3: log in link.
|
||||
|
||||
INVITE_1_HTML = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{0}</title>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Hey There!</h2>
|
||||
|
||||
<p>
|
||||
{1} has invited you to join {2} on Make Post Sell.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="{3}">click this link to log in to the shop.</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Make Post Sell is a lightweight, open-source, public domain, software as a service ecommerce web application.
|
||||
We hope you like our ethical approach to doing business online.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Thanks for sharing! <3
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -113,16 +113,30 @@ class Shop(RBase, Base):
|
|||
UserShop.user == user,
|
||||
).one_or_none()
|
||||
|
||||
def add_user_to_shop(self, user, default=None):
|
||||
def add_user_to_shop(self, user, role_id=0):
|
||||
"""Add User object to UserShop."""
|
||||
us = self.get_usershop_for_user(user)
|
||||
if us is None:
|
||||
us = UserShop(user=user, shop=self)
|
||||
us = UserShop(user=user, shop=self, role_id=role_id)
|
||||
self.shop_users.append(us)
|
||||
if default != None:
|
||||
us.default = default
|
||||
return us
|
||||
|
||||
@property
|
||||
def owners(self):
|
||||
"""Returns a list of user objects who have the owner role on this shop."""
|
||||
# us is a user_shop object.
|
||||
return [us.user for us in self.shop_users if us.is_owner]
|
||||
|
||||
@property
|
||||
def editors(self):
|
||||
"""Returns a list of user objects who have the editor role on this shop."""
|
||||
return self.owners + [us.user for us in self.shop_users if us.is_editor]
|
||||
|
||||
@property
|
||||
def members(self):
|
||||
"""Returns a list of user objects who have the member role on this shop."""
|
||||
return self.editors + [us.user for us in self.shop_users if us.is_member]
|
||||
|
||||
@property
|
||||
def slug(self):
|
||||
"""return slug from name"""
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ class User(RBase, Base):
|
|||
|
||||
def can_edit_product(self, product):
|
||||
"""Return True or False"""
|
||||
if product and self in product.shop.users:
|
||||
if product and self in product.shop.editors:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
@ -198,7 +198,7 @@ class User(RBase, Base):
|
|||
return not self.can_download_product(product)
|
||||
|
||||
def can_edit_shop(self, shop):
|
||||
if shop and self in shop.users:
|
||||
if shop and self in shop.editors:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import uuid
|
||||
|
||||
from sqlalchemy import Column, Boolean, BigInteger
|
||||
from sqlalchemy import Column, Integer, BigInteger
|
||||
|
||||
from .meta import Base, RBase, UUIDType, foreign_key, now_timestamp
|
||||
|
||||
from sqlalchemy.orm import relationship, backref
|
||||
|
||||
role_id_to_name = {
|
||||
0: "owner", # root.
|
||||
1: "editor", # virtual assistant.
|
||||
2: "member", # viewer.
|
||||
}
|
||||
|
||||
class UserShop(RBase, Base):
|
||||
"""
|
||||
|
|
@ -17,8 +22,7 @@ class UserShop(RBase, Base):
|
|||
user_id = Column(UUIDType, foreign_key("User", "id"), nullable=False)
|
||||
shop_id = Column(UUIDType, foreign_key("Shop", "id"), nullable=False)
|
||||
created_timestamp = Column(BigInteger, nullable=False)
|
||||
# a user may set a default shop if they have more than 1.
|
||||
default = Column(Boolean, default=False)
|
||||
role_id = Column(Integer, nullable=False, default=0)
|
||||
|
||||
user = relationship(
|
||||
argument="User", backref=backref("user_shops", cascade="all, delete-orphan")
|
||||
|
|
@ -28,8 +32,24 @@ class UserShop(RBase, Base):
|
|||
argument="Shop", backref=backref("shop_users", cascade="all, delete-orphan")
|
||||
)
|
||||
|
||||
def __init__(self, user=None, shop=None):
|
||||
def __init__(self, user=None, shop=None, role_id=0):
|
||||
self.id = uuid.uuid1()
|
||||
self.user = user
|
||||
self.shop = shop
|
||||
self.role_id = role_id
|
||||
self.created_timestamp = now_timestamp()
|
||||
|
||||
def role_name(self):
|
||||
return role_id_to_name[self.role_id]
|
||||
|
||||
@property
|
||||
def is_owner(self):
|
||||
return self.role_id == 0
|
||||
|
||||
@property
|
||||
def is_editor(self):
|
||||
return self.role_id == 1
|
||||
|
||||
@property
|
||||
def is_member(self):
|
||||
return self.role_id == 2
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ def includeme(config):
|
|||
return get_shop_by_id(request.dbsession, shop_id)
|
||||
elif request.product:
|
||||
return request.product.shop
|
||||
elif request.user and request.user.active_shop:
|
||||
return request.user.active_shop
|
||||
else:
|
||||
return get_shop_by_domain_name(request.dbsession, request.domain)
|
||||
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ def includeme(config):
|
|||
|
||||
config.add_route("shop_settings", "/s/{shop_id}/settings")
|
||||
|
||||
config.add_route("shop_users", "/s/{shop_id}/users")
|
||||
config.add_route("shop_user_remove", "/s/{shop_id}/remove-user")
|
||||
|
||||
config.add_route("shop_terms_of_service", "/s/terms")
|
||||
config.add_route("shop_terms_of_service2", "/s/{shop_id}/terms")
|
||||
config.add_route("shop_edit_terms_of_service", "/s/{shop_id}/terms/edit")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
"""role id to user shop table
|
||||
|
||||
Revision ID: 03292288a132
|
||||
Revises: e6ac162d096f
|
||||
Create Date: 2022-06-27 12:31:50.885273
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '03292288a132'
|
||||
down_revision = 'e6ac162d096f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from make_post_sell.models.meta import UUIDType
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('mps_user_shop', sa.Column('role_id', sa.Integer(), nullable=False, server_default="0"))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('mps_user_shop', 'role_id')
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -20,6 +20,10 @@
|
|||
<br/>
|
||||
<a href="/s/{{ request.user.active_shop.id }}/coupons" class="product-edit-button mps-button">  View Coupons</a>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="/s/{{ request.user.active_shop.id }}/users" class="product-edit-button mps-button">👤   Shop Users</a>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<a href="/s/{{ request.user.active_shop.id }}/settings" class="product-edit-button mps-button">⚙   Shop Settings</a>
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@
|
|||
|
||||
<h3>Maintenance Mode 🏗️</h3>
|
||||
|
||||
<input type="checkbox" name="maint-mode-checkbox" id="maint-mode-checkbox" {% if maint_mode %}checked{% endif %}></input>
|
||||
<input type="checkbox" name="maint-mode-checkbox" id="maint-mode-checkbox" {% if request.shop.maint_mode %}checked{% endif %}></input>
|
||||
<b>If checked, enabled maintenance mode.</b>
|
||||
|
||||
<br />
|
||||
|
|
|
|||
105
make_post_sell/templates/shop_users.j2
Normal file
105
make_post_sell/templates/shop_users.j2
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
{% extends "base.j2" -%}
|
||||
|
||||
{% block content -%}
|
||||
|
||||
<section class="one-column">
|
||||
<section class="shop-settings well">
|
||||
|
||||
<h3>Invite</h3>
|
||||
|
||||
<form method="post" action="/s/{{ request.shop.id }}/users" onsubmit="submit.disabled = true; return true;">
|
||||
|
||||
<label for="add_user">Invite a new user to this shop.</label>
|
||||
<input
|
||||
name = "email-to-invite"
|
||||
type = "text"
|
||||
id = "email-to-invite"
|
||||
class = "mps-shop-name"
|
||||
value = ""
|
||||
placeholder = "Email address"
|
||||
required
|
||||
/>
|
||||
|
||||
<style>
|
||||
label.role_id_radio {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
|
||||
<fieldset>
|
||||
|
||||
<legend>role</legend>
|
||||
|
||||
<input type="radio" name="role-id" id="role_id_0" value="0" %} />
|
||||
<label class="role_id_radio" for="role_id_0">owner</label>
|
||||
|
||||
<input type="radio" name="role-id" id="role_id_1" value="1" />
|
||||
<label class="role_id_radio" for="role_id_1">editor</label>
|
||||
|
||||
<input type="radio" name="role-id" id="role_id_2" value="2" />
|
||||
<label class="role_id_radio" for="role_id_2">member</label>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<br />
|
||||
|
||||
<input type="submit" name="submit" class="mps-submit" value="Send Invite" />
|
||||
|
||||
</form>
|
||||
|
||||
<br />
|
||||
|
||||
<h3>owners</h3>
|
||||
|
||||
{% for user in owners %}
|
||||
<ul>
|
||||
<li>{{ user.name }} - {{ user.email }}</li>
|
||||
<form method="post" action="/s/{{ request.shop.id }}/remove-user">
|
||||
<input
|
||||
name = "user_id"
|
||||
type = "hidden"
|
||||
id = "user_id"
|
||||
value = "{{ user.id }}"
|
||||
required />
|
||||
<input type="submit" name="submit" value="remove" />
|
||||
</form>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
<h3>editors</h3>
|
||||
|
||||
{% for user in editors %}
|
||||
<ul>
|
||||
<li>{{ user.name }} - {{ user.email }}</li>
|
||||
<form method="post" action="/s/{{ request.shop.id }}/remove-user">
|
||||
<input
|
||||
name = "user_id"
|
||||
type = "hidden"
|
||||
id = "user_id"
|
||||
value = "{{ user.id }}"
|
||||
required />
|
||||
<input type="submit" name="submit" value="remove" />
|
||||
</form>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
<h3>members</h3>
|
||||
|
||||
{% for user in members %}
|
||||
<ul>
|
||||
<li>{{ user.name }} - {{ user.email }}</li>
|
||||
<form method="post" action="/s/{{ request.shop.id }}/remove-user">
|
||||
<input
|
||||
name = "user_id"
|
||||
type = "hidden"
|
||||
id = "user_id"
|
||||
value = "{{ user.id }}"
|
||||
required />
|
||||
<input type="submit" name="submit" value="remove" />
|
||||
</form>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
</section>
|
||||
</section>
|
||||
{%- endblock -%}
|
||||
|
|
@ -202,7 +202,6 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
|
||||
def log_in_user(self, user_creds):
|
||||
# log in user.
|
||||
#res_login = self.testapp.get(
|
||||
res_login = self.testapp.post(
|
||||
"/verification-challenge?email={}&raw-otp={}&submit".format(*user_creds)
|
||||
)
|
||||
|
|
@ -259,7 +258,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
self.log_in_user(self.user2_creds)
|
||||
redirect_res = self.testapp.get("/p/new", status=302)
|
||||
res = redirect_res.follow()
|
||||
self.assertIn(b"First we must create a shop.", res.body)
|
||||
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):
|
||||
|
||||
|
|
@ -373,7 +372,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
if log_out_user:
|
||||
self.testapp.get("/log-out")
|
||||
|
||||
def test_new_product_when_user_does_not_own_the_shop(self):
|
||||
def test_new_product_when_user_does_not_have_editor_role_on_shop(self):
|
||||
|
||||
# log in user1.
|
||||
self.log_in_user(self.user1_creds)
|
||||
|
|
@ -395,7 +394,7 @@ class AuthenticatedFunctionalTests(FunctionalTests):
|
|||
params = self.product1_params
|
||||
redirect_res = self.testapp.post("/p/new?shop_id={}".format(shop_id), params)
|
||||
res = redirect_res.follow()
|
||||
self.assertIn("You do not own that Shop.", 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(
|
||||
|
|
|
|||
|
|
@ -66,19 +66,52 @@ def shop_is_ready_required(
|
|||
|
||||
|
||||
# view decorator.
|
||||
def super_fly_required(fn):
|
||||
"""This view requires that the request has a super fly admin."""
|
||||
# TODO: don't rely on this hack for super fly admin.
|
||||
def wrapped(request):
|
||||
if (
|
||||
request.user
|
||||
and request.user.authenticated
|
||||
and request.user.name == "Admin"
|
||||
):
|
||||
return fn(request)
|
||||
request.session.flash(
|
||||
("You must be a super fly admin to access that.", "error")
|
||||
)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
def shop_owner_required(
|
||||
flash_msg="You must have a shop owner role to access that.",
|
||||
flash_level="error",
|
||||
):
|
||||
"""This view requires that the user of the request has a shop owner role.
|
||||
for protecting shop settings & shop user pages."""
|
||||
|
||||
def wrapped(fn):
|
||||
def inner(request):
|
||||
if (
|
||||
request.user is not None
|
||||
and request.user.authenticated
|
||||
and request.shop is not None
|
||||
and request.user in request.shop.owners
|
||||
):
|
||||
return fn(request)
|
||||
|
||||
request.session.flash(
|
||||
(flash_msg, flash_level)
|
||||
)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
return inner
|
||||
return wrapped
|
||||
|
||||
|
||||
# view decorator.
|
||||
def shop_editor_required(
|
||||
flash_msg="You must have a shop editor role to access that.",
|
||||
flash_level="error",
|
||||
):
|
||||
"""This view requires that the user of the request has a shop editor role.
|
||||
for protecting product & content edit pages."""
|
||||
|
||||
def wrapped(fn):
|
||||
def inner(request):
|
||||
if (
|
||||
request.user is not None
|
||||
and request.user.authenticated
|
||||
and request.shop is not None
|
||||
and request.user in request.shop.editors
|
||||
):
|
||||
return fn(request)
|
||||
|
||||
request.session.flash(
|
||||
(flash_msg, flash_level)
|
||||
)
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
return inner
|
||||
return wrapped
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from pyramid.view import view_config
|
|||
|
||||
from . import (
|
||||
user_required,
|
||||
shop_owner_required,
|
||||
get_referer_or_home,
|
||||
)
|
||||
|
||||
|
|
@ -52,17 +53,13 @@ def coupon(request):
|
|||
|
||||
@view_config(route_name="coupon_new1", renderer="coupon_new.j2")
|
||||
@view_config(route_name="coupon_new2", renderer="coupon_new.j2")
|
||||
@user_required()
|
||||
@shop_owner_required()
|
||||
def coupon_new(request):
|
||||
|
||||
shop = request.user.active_shop
|
||||
if request.shop:
|
||||
shop = request.shop
|
||||
|
||||
if request.user not in shop.users:
|
||||
request.session.flash(("You do not own that Shop.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
code = request.params.get("code")
|
||||
description = request.params.get("description")
|
||||
action_type = request.params.get("action_type")
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ from pyramid.view import view_config
|
|||
|
||||
from . import (
|
||||
user_required,
|
||||
shop_editor_required,
|
||||
get_referer_or_home,
|
||||
)
|
||||
|
||||
|
|
@ -85,21 +86,13 @@ def product(request):
|
|||
@view_config(route_name="product_new", renderer="product_new.j2")
|
||||
@view_config(route_name="content_new", renderer="content_new.j2")
|
||||
@user_required()
|
||||
@shop_editor_required()
|
||||
def product_new(request):
|
||||
|
||||
if request.shop is None and request.user.active_shop is None:
|
||||
msg = ("First we must create a shop.", "info")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/s/new")
|
||||
|
||||
shop = request.user.active_shop
|
||||
if request.shop:
|
||||
shop = request.shop
|
||||
|
||||
if request.user not in shop.users:
|
||||
request.session.flash(("You do not own that Shop.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
title = request.params.get("title", "")
|
||||
description = request.params.get("description", "")
|
||||
price = request.params.get("price", "")
|
||||
|
|
@ -144,14 +137,10 @@ def product_new(request):
|
|||
|
||||
|
||||
@view_config(route_name="product_description_edit", renderer="markup_editor.j2")
|
||||
@user_required()
|
||||
@shop_editor_required()
|
||||
def product_edit_description(request):
|
||||
product = request.product
|
||||
|
||||
if request.user.can_not_edit_product(product):
|
||||
request.session.flash(("You do not own that Product.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
raw_markup_data = request.params.get(
|
||||
"markup-editor-textarea",
|
||||
# TODO: rename this column to description_raw.
|
||||
|
|
@ -177,16 +166,12 @@ def product_edit_description(request):
|
|||
|
||||
@view_config(route_name="product_edit", renderer="product_edit.j2")
|
||||
@view_config(route_name="product_edit2", renderer="product_edit.j2")
|
||||
@user_required()
|
||||
@shop_editor_required()
|
||||
def product_edit(request):
|
||||
|
||||
product_modified = False
|
||||
product = request.product
|
||||
|
||||
if request.user.can_not_edit_product(product):
|
||||
request.session.flash(("You do not own that Product.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
title = request.params.get("title", product.title)
|
||||
description = request.params.get("description", product.description)
|
||||
price = request.params.get("price", product.price)
|
||||
|
|
@ -325,7 +310,7 @@ def product_edit(request):
|
|||
|
||||
@view_config(route_name="product_bundle_edit")
|
||||
@view_config(route_name="product_bundle_edit2")
|
||||
@user_required()
|
||||
@shop_editor_required()
|
||||
def product_bundle_edit(request):
|
||||
|
||||
from ..models.product import get_products_by_ids
|
||||
|
|
@ -337,10 +322,6 @@ def product_bundle_edit(request):
|
|||
if product.is_bundle == False:
|
||||
request.session.flash(("You may only add products to a Product bundle.", "error"))
|
||||
|
||||
if request.user.can_not_edit_product(product):
|
||||
request.session.flash(("You do not own that Product bundle.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
bundle_data = request.params.get("bundle-data", None)
|
||||
|
||||
if bundle_data:
|
||||
|
|
@ -358,7 +339,7 @@ def product_bundle_edit(request):
|
|||
|
||||
|
||||
@view_config(route_name="product_remove_bundled_product")
|
||||
@user_required()
|
||||
@shop_editor_required()
|
||||
def product_bundle_remove_product(request):
|
||||
|
||||
from ..models.product import get_product_by_id
|
||||
|
|
@ -371,10 +352,6 @@ def product_bundle_remove_product(request):
|
|||
if product.is_bundle == False:
|
||||
request.session.flash(("You may only remove products from a Product bundle.", "error"))
|
||||
|
||||
if request.user.can_not_edit_product(product):
|
||||
request.session.flash(("You do not own that Product bundle.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
if product.remove_product_from_bundle(bundled_product) == False:
|
||||
request.session.flash((product.error_message, "error"))
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import mimetypes
|
|||
|
||||
from pyramid.view import view_config
|
||||
|
||||
from . import user_required
|
||||
from . import (
|
||||
user_required,
|
||||
shop_owner_required,
|
||||
shop_editor_required,
|
||||
)
|
||||
|
||||
from ..models.shop import (
|
||||
Shop,
|
||||
|
|
@ -17,10 +21,17 @@ from ..models.product import (
|
|||
get_products_by_keywords,
|
||||
)
|
||||
|
||||
from ..models.user import (
|
||||
get_user_by_id,
|
||||
get_or_create_user_by_email,
|
||||
)
|
||||
|
||||
from ..models.shop_search_request import (
|
||||
ShopSearchRequest,
|
||||
)
|
||||
|
||||
from ..lib.mail import send_invite_email
|
||||
|
||||
from pyramid.httpexceptions import HTTPFound
|
||||
|
||||
from miscutils.phone_numbers import is_phone_number_valid
|
||||
|
|
@ -72,15 +83,13 @@ def shop(request):
|
|||
|
||||
|
||||
@view_config(route_name="shop_products", renderer="shop_products.j2")
|
||||
@user_required()
|
||||
@shop_editor_required()
|
||||
def shop_products(request):
|
||||
msg = None
|
||||
shop = get_shop_from_matchdict(request)
|
||||
|
||||
if shop is None:
|
||||
msg = ("That shop_id does not exist.", "error")
|
||||
elif request.user.can_not_edit_shop(shop):
|
||||
msg = ("That shop is not owned by you.", "error")
|
||||
|
||||
if msg:
|
||||
request.session.flash(msg)
|
||||
|
|
@ -157,10 +166,7 @@ def shop_new(request):
|
|||
|
||||
else:
|
||||
shop = Shop(name, phone_number, billing_address, description)
|
||||
if len(request.user.shops) == 0:
|
||||
shop.add_user_to_shop(request.user, default=True)
|
||||
else:
|
||||
shop.add_user_to_shop(request.user)
|
||||
shop.add_user_to_shop(request.user)
|
||||
request.user.set_active_shop(shop)
|
||||
request.dbsession.add(shop)
|
||||
request.dbsession.add(request.user)
|
||||
|
|
@ -182,7 +188,7 @@ def shop_new(request):
|
|||
|
||||
|
||||
@view_config(route_name="user_shop_activate")
|
||||
@user_required()
|
||||
@shop_editor_required()
|
||||
def shop_activate(request):
|
||||
shop = get_shop_from_matchdict(
|
||||
request, prefetched_shop=request.user.active_shop
|
||||
|
|
@ -191,9 +197,6 @@ def shop_activate(request):
|
|||
if shop is None:
|
||||
msg = ("That shop_id does not exist.", "error")
|
||||
|
||||
elif request.user.can_not_edit_shop(shop):
|
||||
msg = ("That shop is not owned by you.", "error")
|
||||
|
||||
elif shop == request.user.active_shop:
|
||||
msg = ("This shop is already your active shop.", "success")
|
||||
|
||||
|
|
@ -226,16 +229,12 @@ def shop_privacy_policy(request):
|
|||
|
||||
|
||||
@view_config(route_name="shop_edit_terms_of_service", renderer="markup_editor.j2")
|
||||
@user_required()
|
||||
@shop_owner_required()
|
||||
def shop_edit_terms_of_service(request):
|
||||
shop = request.user.active_shop
|
||||
if request.shop:
|
||||
shop = request.shop
|
||||
|
||||
if request.user.can_not_edit_shop(shop):
|
||||
request.session.flash(("You do not own that Shop.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
raw_markup_data = request.params.get(
|
||||
"markup-editor-textarea",
|
||||
shop.terms_of_service_raw
|
||||
|
|
@ -257,16 +256,12 @@ def shop_edit_terms_of_service(request):
|
|||
|
||||
|
||||
@view_config(route_name="shop_edit_privacy_policy", renderer="markup_editor.j2")
|
||||
@user_required()
|
||||
@shop_owner_required()
|
||||
def shop_edit_privacy_policy(request):
|
||||
shop = request.user.active_shop
|
||||
if request.shop:
|
||||
shop = request.shop
|
||||
|
||||
if request.user.can_not_edit_shop(shop):
|
||||
request.session.flash(("You do not own that Shop.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
|
||||
raw_markup_data = request.params.get(
|
||||
"markup-editor-textarea",
|
||||
shop.privacy_policy_raw
|
||||
|
|
@ -287,17 +282,60 @@ def shop_edit_privacy_policy(request):
|
|||
}
|
||||
|
||||
|
||||
@view_config(route_name="shop_users", renderer="shop_users.j2")
|
||||
@shop_owner_required()
|
||||
def shop_users(request):
|
||||
|
||||
email_to_invite = request.params.get("email-to-invite")
|
||||
role_id = request.params.get("role-id")
|
||||
|
||||
if email_to_invite and role_id:
|
||||
# get or create new user if it doesn't exist.
|
||||
user = get_or_create_user_by_email(request.dbsession, email_to_invite)
|
||||
|
||||
# make new user a shop user of the proper role.
|
||||
request.shop.add_user_to_shop(user, role_id)
|
||||
|
||||
# commit changes to database.
|
||||
request.dbsession.add(user)
|
||||
|
||||
# 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")
|
||||
request.session.flash(msg)
|
||||
|
||||
|
||||
return {
|
||||
"owners": request.shop.owners,
|
||||
"editors": request.shop.editors,
|
||||
"members": request.shop.members,
|
||||
}
|
||||
|
||||
|
||||
@view_config(route_name="shop_user_remove")
|
||||
@shop_owner_required()
|
||||
def shop_user_remove(request):
|
||||
user_id = request.params.get("user_id", None)
|
||||
if user_id:
|
||||
user = get_user_by_id(request.dbsession, user_id)
|
||||
if user:
|
||||
if user == request.user:
|
||||
msg = ("Sheepishly refusing to remove myself!".format(user.email), "error")
|
||||
request.session.flash(msg)
|
||||
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")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/s/{}/users".format(request.shop.id))
|
||||
|
||||
|
||||
@view_config(route_name="shop_settings", renderer="shop_settings.j2")
|
||||
@user_required()
|
||||
@shop_owner_required()
|
||||
def shop_settings(request):
|
||||
|
||||
shop = request.user.active_shop
|
||||
if request.shop:
|
||||
shop = request.shop
|
||||
|
||||
if request.user.can_not_edit_shop(shop):
|
||||
request.session.flash(("You do not own that Shop.", "error"))
|
||||
return HTTPFound(get_referer_or_home(request))
|
||||
shop = request.shop
|
||||
|
||||
name = request.params.get("name", shop.name)
|
||||
phone_number = request.params.get("phone_number", shop.phone_number)
|
||||
|
|
@ -317,7 +355,7 @@ 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 = request.params.get("maint-mode-checkbox", "off")
|
||||
maint_mode = checkbox_to_bool(maint_mode_checkbox)
|
||||
|
||||
s3_webhook_key = request.params.get("key")
|
||||
|
|
@ -408,7 +446,6 @@ 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(
|
||||
|
|
@ -508,7 +545,6 @@ def shop_settings(request):
|
|||
continue
|
||||
signed_posts[file_key]["fields"].update(condition)
|
||||
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
|
|
@ -523,7 +559,6 @@ def shop_settings(request):
|
|||
"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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue