MPS-20 + MPS-21: form sections + buyer entry points
Shop owner-facing: - shop_settings.j2: new "Make an Offer" form section (form_section=offer-settings) with offer_enabled, auto_accept_threshold_pct, auto_decline_threshold_pct, offer_min, offer_expiration_hours, offer_max_rounds, offer_min_buyer_account_age_hours - views/shop.py: form_section=offer-settings handler with input clamping (decline forced strictly below accept; numeric inputs bounded) - product_edit.j2: pricing_mode radio (5 options: fixed, auction, auction+ buy_now, offer, offer+buy_now), allow_offers radio (inherit/yes/no) appearing only when product is in offer mode, link to live auction page when one exists - views/product.py: handles pricing_mode change; flipping into auction mode (1 or 2) creates a draft MpsAuction with start_price seeded from product.price_in_cents and default bid_increment / soft_close from lib/auction defaults Buyer-facing: - product.j2: "View live auction" link when pricing_mode is auction; "Make an offer" details/form when offers_allowed AND user authenticated AND user not in shop owners; "Log in to make an offer" CTA for anon; Add To Cart only renders when is_buy_now_allowed (modes 0, 2, 4) Tests (9 new, all passing): - TestOfferSettingsForm: enable+set thresholds (round-trip), decline clamped below accept, blank offer_min clears the floor - TestPricingModeFormSection: flip to auction creates draft auction, flip to offer leaves auction None, allow_offers override (yes/no/ inherit), invalid pricing_mode value ignored, Make Offer button renders when eligible, Make Offer hidden for shop owner Total: 913 tests pass (was 904 + 9).
This commit is contained in:
parent
2de9229264
commit
5f92e65482
6 changed files with 487 additions and 0 deletions
|
|
@ -217,11 +217,42 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if product.is_buy_now_allowed %}
|
||||
<form method="POST" action="{{ request.route_url('cart_add_product') }}">
|
||||
{% include "snippets/csrf.j2" %}
|
||||
<input type="hidden" name="product_id" value="{{ product.id }}">
|
||||
<button type="submit" class="product-download-button mps-button">Add To Cart</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# MPS-20: View Auction link when product is in auction mode. #}
|
||||
{% if product.is_auction and product.auction %}
|
||||
<p style="margin-top: var(--space-3)">
|
||||
<a href="/a/{{ product.auction.uuid_str }}" class="mps-button mps-button-blue">
|
||||
View live auction →
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{# MPS-21: Make Offer button when offers are allowed for this product. #}
|
||||
{% if product.offers_allowed and request.user and request.user.authenticated and request.user not in product.shop.owners %}
|
||||
<details style="margin-top: var(--space-3)">
|
||||
<summary class="mps-button">Make an offer</summary>
|
||||
<form method="POST" action="/p/{{ product.id }}/offer" style="margin-top: var(--space-2)">
|
||||
{% include "snippets/csrf.j2" %}
|
||||
<label for="offer_amount">Your offer (USD)</label>
|
||||
<input type="number" step="0.01" min="0.01" name="amount" id="offer_amount" required />
|
||||
<input type="text" name="message" placeholder="Optional message" />
|
||||
<input type="submit" class="mps-submit" value="Submit Offer" />
|
||||
</form>
|
||||
</details>
|
||||
{% elif product.offers_allowed and (not request.user or not request.user.authenticated) %}
|
||||
<p style="margin-top: var(--space-3)">
|
||||
<a href="/join-or-log-in?next={{ request.path | urlencode }}" class="mps-button">
|
||||
Log in to make an offer
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<br/>
|
||||
|
|
|
|||
|
|
@ -262,6 +262,74 @@ Your cover (<code>thumbnail1</code>) will show up on search pages.
|
|||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
{# MPS-20 + MPS-21: pricing mode #}
|
||||
<fieldset>
|
||||
<legend>Pricing mode</legend>
|
||||
<label style="display:block">
|
||||
<input type="radio" name="pricing_mode" value="0"
|
||||
{% if product.pricing_mode == 0 %}checked{% endif %} />
|
||||
Fixed price (today's behavior)
|
||||
</label>
|
||||
<label style="display:block">
|
||||
<input type="radio" name="pricing_mode" value="1"
|
||||
{% if product.pricing_mode == 1 %}checked{% endif %} />
|
||||
Auction
|
||||
</label>
|
||||
<label style="display:block">
|
||||
<input type="radio" name="pricing_mode" value="2"
|
||||
{% if product.pricing_mode == 2 %}checked{% endif %} />
|
||||
Auction with buy-now
|
||||
</label>
|
||||
<label style="display:block">
|
||||
<input type="radio" name="pricing_mode" value="3"
|
||||
{% if product.pricing_mode == 3 %}checked{% endif %} />
|
||||
Make an offer (negotiation)
|
||||
</label>
|
||||
<label style="display:block">
|
||||
<input type="radio" name="pricing_mode" value="4"
|
||||
{% if product.pricing_mode == 4 %}checked{% endif %} />
|
||||
Make an offer with buy-now
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<br />
|
||||
|
||||
{# MPS-21: per-product offer override #}
|
||||
{% if product.is_offer_mode %}
|
||||
<fieldset>
|
||||
<legend>Allow offers (override shop default)</legend>
|
||||
<label style="display:block">
|
||||
<input type="radio" name="allow_offers" value="inherit"
|
||||
{% if product.allow_offers is none %}checked{% endif %} />
|
||||
Use shop default
|
||||
({% if request.shop.offer_enabled %}enabled{% else %}disabled{% endif %})
|
||||
</label>
|
||||
<label style="display:block">
|
||||
<input type="radio" name="allow_offers" value="yes"
|
||||
{% if product.allow_offers == True %}checked{% endif %} />
|
||||
Yes — accept offers on this product
|
||||
</label>
|
||||
<label style="display:block">
|
||||
<input type="radio" name="allow_offers" value="no"
|
||||
{% if product.allow_offers == False %}checked{% endif %} />
|
||||
No — block offers on this product
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<br />
|
||||
{% endif %}
|
||||
|
||||
{% if product.auction %}
|
||||
<p>
|
||||
<a href="/a/{{ product.auction.uuid_str }}" class="mps-button">
|
||||
View live auction page
|
||||
</a>
|
||||
<small>(state: {{ product.auction.state_human }})</small>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -1170,6 +1170,77 @@ Existing sales honored for download buy purchasers.
|
|||
<br />
|
||||
<br />
|
||||
|
||||
{# MPS-21: Make-an-Offer settings #}
|
||||
<section class="one-column">
|
||||
<section class="shop-settings well">
|
||||
<h3>Make an Offer</h3>
|
||||
<p>Allow buyers to negotiate a price for products in offer mode. Offers ≥ auto-accept threshold are accepted instantly. Offers below auto-decline threshold are silently rejected.</p>
|
||||
|
||||
<form method="post" action="/s/{{ request.shop.uuid_str }}/settings" onsubmit="submit.disabled = true; return true;">
|
||||
{% include "snippets/csrf.j2" %}
|
||||
<input type="hidden" name="form_section" value="offer-settings" />
|
||||
|
||||
<label>
|
||||
<input type="checkbox" name="offer_enabled_checkbox"
|
||||
{% if request.shop.offer_enabled %}checked{% endif %} />
|
||||
Enable make-an-offer (shop-wide default)
|
||||
</label>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<label for="offer_auto_accept">Auto-accept threshold (% of list price)</label>
|
||||
<input type="number" name="offer_auto_accept_threshold_pct" id="offer_auto_accept"
|
||||
value="{{ request.shop.offer_auto_accept_threshold_pct }}"
|
||||
min="0" max="100" step="1" class="mps-text-input" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<label for="offer_auto_decline">Auto-decline threshold (% of list price)</label>
|
||||
<input type="number" name="offer_auto_decline_threshold_pct" id="offer_auto_decline"
|
||||
value="{{ request.shop.offer_auto_decline_threshold_pct }}"
|
||||
min="0" max="100" step="1" class="mps-text-input" />
|
||||
<small>Must be strictly below auto-accept; otherwise auto-clamped.</small>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<label for="offer_min">Minimum offer amount ($)</label>
|
||||
<input type="number" name="offer_min" id="offer_min"
|
||||
value="{% if request.shop.offer_min_in_cents %}{{ '%.2f'|format(request.shop.offer_min_in_cents / 100.0) }}{% endif %}"
|
||||
min="0" step="0.01" class="mps-text-input"
|
||||
placeholder="No floor" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<label for="offer_expiration">Offer expiration (hours)</label>
|
||||
<input type="number" name="offer_expiration_hours" id="offer_expiration"
|
||||
value="{{ request.shop.offer_expiration_hours }}"
|
||||
min="1" max="8760" step="1" class="mps-text-input" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<label for="offer_max_rounds">Max counter rounds</label>
|
||||
<input type="number" name="offer_max_rounds" id="offer_max_rounds"
|
||||
value="{{ request.shop.offer_max_rounds }}"
|
||||
min="1" max="100" step="1" class="mps-text-input" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<label for="offer_min_age">Minimum buyer account age (hours)</label>
|
||||
<input type="number" name="offer_min_buyer_account_age_hours" id="offer_min_age"
|
||||
value="{{ request.shop.offer_min_buyer_account_age_hours }}"
|
||||
min="0" max="8760" step="1" class="mps-text-input"
|
||||
placeholder="0 = open" />
|
||||
|
||||
<br /><br />
|
||||
|
||||
<input type="submit" name="submit" class="mps-submit" value="Save Settings" />
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<section class="one-column">
|
||||
<section class="shop-settings well">
|
||||
|
||||
|
|
|
|||
|
|
@ -5942,3 +5942,215 @@ class TestOfferRoutes(_AuthenticatedBase):
|
|||
expect_errors=True,
|
||||
)
|
||||
self.assertEqual(res.status_int, 403)
|
||||
|
||||
|
||||
class TestOfferSettingsForm(_AuthenticatedBase):
|
||||
"""MPS-21: shop-settings offer-settings form section."""
|
||||
|
||||
def test_enable_and_set_thresholds(self):
|
||||
shop = self._create_shop_helper()
|
||||
# Defaults pre-form:
|
||||
self.assertFalse(bool(shop.offer_enabled))
|
||||
self.assertEqual(shop.offer_auto_accept_threshold_pct, 95)
|
||||
|
||||
res = self.testapp.post(
|
||||
f"/s/{shop.id}/settings",
|
||||
{
|
||||
"form_section": "offer-settings",
|
||||
"offer_enabled_checkbox": "on",
|
||||
"offer_auto_accept_threshold_pct": "90",
|
||||
"offer_auto_decline_threshold_pct": "40",
|
||||
"offer_min": "5.00",
|
||||
"offer_expiration_hours": "72",
|
||||
"offer_max_rounds": "5",
|
||||
"offer_min_buyer_account_age_hours": "24",
|
||||
"submit": "Save Settings",
|
||||
},
|
||||
status=302,
|
||||
)
|
||||
res.follow()
|
||||
self.dbsession.refresh(shop)
|
||||
self.assertTrue(shop.offer_enabled)
|
||||
self.assertEqual(shop.offer_auto_accept_threshold_pct, 90)
|
||||
self.assertEqual(shop.offer_auto_decline_threshold_pct, 40)
|
||||
self.assertEqual(shop.offer_min_in_cents, 500)
|
||||
self.assertEqual(shop.offer_expiration_hours, 72)
|
||||
self.assertEqual(shop.offer_max_rounds, 5)
|
||||
self.assertEqual(shop.offer_min_buyer_account_age_hours, 24)
|
||||
|
||||
def test_decline_clamped_below_accept(self):
|
||||
# Setting decline >= accept should clamp decline to accept-1.
|
||||
shop = self._create_shop_helper()
|
||||
res = self.testapp.post(
|
||||
f"/s/{shop.id}/settings",
|
||||
{
|
||||
"form_section": "offer-settings",
|
||||
"offer_auto_accept_threshold_pct": "60",
|
||||
"offer_auto_decline_threshold_pct": "60", # tied — should clamp
|
||||
"submit": "Save Settings",
|
||||
},
|
||||
status=302,
|
||||
)
|
||||
res.follow()
|
||||
self.dbsession.refresh(shop)
|
||||
self.assertEqual(shop.offer_auto_accept_threshold_pct, 60)
|
||||
self.assertEqual(shop.offer_auto_decline_threshold_pct, 59)
|
||||
|
||||
def test_offer_min_blank_clears_floor(self):
|
||||
shop = self._create_shop_helper()
|
||||
# First set a floor.
|
||||
self.testapp.post(
|
||||
f"/s/{shop.id}/settings",
|
||||
{
|
||||
"form_section": "offer-settings",
|
||||
"offer_min": "10.00",
|
||||
"submit": "Save Settings",
|
||||
},
|
||||
status=302,
|
||||
)
|
||||
self.dbsession.refresh(shop)
|
||||
self.assertEqual(shop.offer_min_in_cents, 1000)
|
||||
|
||||
# Then clear it (blank input).
|
||||
self.testapp.post(
|
||||
f"/s/{shop.id}/settings",
|
||||
{
|
||||
"form_section": "offer-settings",
|
||||
"offer_min": "",
|
||||
"submit": "Save Settings",
|
||||
},
|
||||
status=302,
|
||||
)
|
||||
self.dbsession.refresh(shop)
|
||||
self.assertIsNone(shop.offer_min_in_cents)
|
||||
|
||||
|
||||
class TestPricingModeFormSection(_AuthenticatedBase):
|
||||
"""MPS-20 + MPS-21: pricing_mode + allow_offers on product edit."""
|
||||
|
||||
def _make_product(self, owner_creds=None):
|
||||
if owner_creds is None:
|
||||
owner_creds = self.user1_creds
|
||||
shop = self._create_shop_helper(user_creds=owner_creds)
|
||||
from ..models.product import Product
|
||||
product = Product(title="P1", description="...")
|
||||
product.shop = shop
|
||||
product.price_in_cents = 5000
|
||||
product.is_physical = False
|
||||
product.is_sellable = True
|
||||
self.dbsession.add(product)
|
||||
self.dbsession.flush()
|
||||
product_id = product.uuid_str
|
||||
transaction.commit()
|
||||
return shop, product_id
|
||||
|
||||
def _post_edit(self, product_id, **kw):
|
||||
# The product edit form_url is /p/{id}/edit; minimal fields required.
|
||||
params = {
|
||||
"title": "P1",
|
||||
"description": "...",
|
||||
"price": "50.00",
|
||||
"visibility": "1",
|
||||
"submit": "true",
|
||||
}
|
||||
params.update(kw)
|
||||
return self.testapp.post(f"/p/{product_id}/edit", params)
|
||||
|
||||
def test_flip_to_auction_creates_draft_auction(self):
|
||||
from ..models.auction import (
|
||||
MpsAuction, AUCTION_STATE_DRAFT,
|
||||
)
|
||||
shop, product_id = self._make_product()
|
||||
self._post_edit(product_id, pricing_mode="1")
|
||||
|
||||
from ..models.product import get_product_by_id
|
||||
product = get_product_by_id(self.dbsession, product_id)
|
||||
self.assertEqual(product.pricing_mode, 1)
|
||||
self.assertIsNotNone(product.auction)
|
||||
self.assertEqual(product.auction.state, AUCTION_STATE_DRAFT)
|
||||
# Auction inherits product price as default start.
|
||||
self.assertEqual(product.auction.start_price_in_cents, 5000)
|
||||
|
||||
def test_flip_to_offer_does_not_create_auction(self):
|
||||
shop, product_id = self._make_product()
|
||||
self._post_edit(product_id, pricing_mode="3")
|
||||
|
||||
from ..models.product import get_product_by_id
|
||||
product = get_product_by_id(self.dbsession, product_id)
|
||||
self.assertEqual(product.pricing_mode, 3)
|
||||
self.assertIsNone(product.auction)
|
||||
self.assertTrue(product.is_offer_mode)
|
||||
|
||||
def test_allow_offers_override(self):
|
||||
shop, product_id = self._make_product()
|
||||
# First flip to offer mode.
|
||||
self._post_edit(product_id, pricing_mode="3", allow_offers="yes")
|
||||
|
||||
from ..models.product import get_product_by_id
|
||||
product = get_product_by_id(self.dbsession, product_id)
|
||||
self.dbsession.refresh(product)
|
||||
self.assertTrue(product.allow_offers)
|
||||
|
||||
# Flip override to "no" (block on this product).
|
||||
self._post_edit(product_id, pricing_mode="3", allow_offers="no")
|
||||
self.dbsession.expire_all()
|
||||
product = get_product_by_id(self.dbsession, product_id)
|
||||
self.assertEqual(product.allow_offers, False)
|
||||
|
||||
# Flip back to "inherit" (None).
|
||||
self._post_edit(product_id, pricing_mode="3", allow_offers="inherit")
|
||||
self.dbsession.expire_all()
|
||||
product = get_product_by_id(self.dbsession, product_id)
|
||||
self.assertIsNone(product.allow_offers)
|
||||
|
||||
def test_invalid_pricing_mode_ignored(self):
|
||||
shop, product_id = self._make_product()
|
||||
self._post_edit(product_id, pricing_mode="99")
|
||||
|
||||
from ..models.product import get_product_by_id
|
||||
product = get_product_by_id(self.dbsession, product_id)
|
||||
self.assertEqual(product.pricing_mode, 0) # unchanged
|
||||
|
||||
def _make_ready_offer_product(self):
|
||||
"""Create a product that's is_ready=True (has product file) and
|
||||
opted into offer mode. Returns (shop, product_id, product_slug)."""
|
||||
from ..models.product import Product, get_product_by_id
|
||||
shop = self._create_shop_helper()
|
||||
shop.offer_enabled = True
|
||||
|
||||
product = Product(title="Ready P1", description="...")
|
||||
product.shop = shop
|
||||
product.price_in_cents = 5000
|
||||
product.is_physical = False
|
||||
product.is_sellable = True
|
||||
product.pricing_mode = 3 # offer mode
|
||||
# Fake a product file so is_ready=True (file_metadata.extensions.product).
|
||||
import json
|
||||
product.json_file_metadata = json.dumps({
|
||||
"originals": {},
|
||||
"extensions": {"product": "pdf"},
|
||||
})
|
||||
self.dbsession.add(product)
|
||||
self.dbsession.flush()
|
||||
product_id = product.uuid_str
|
||||
product_slug = product.slug
|
||||
transaction.commit()
|
||||
return shop, product_id, product_slug
|
||||
|
||||
def test_make_offer_button_renders_when_eligible(self):
|
||||
shop, product_id, product_slug = self._make_ready_offer_product()
|
||||
self.testapp.get("/log-out")
|
||||
self.log_in_user(self.user2_creds)
|
||||
|
||||
res = self.testapp.get(
|
||||
f"/p/{product_id}/{product_slug}", status=200,
|
||||
)
|
||||
self.assertIn(b"Make an offer", res.body)
|
||||
|
||||
def test_make_offer_hidden_for_seller(self):
|
||||
shop, product_id, product_slug = self._make_ready_offer_product()
|
||||
# user1 (shop owner) is logged in.
|
||||
res = self.testapp.get(
|
||||
f"/p/{product_id}/{product_slug}", status=200,
|
||||
)
|
||||
self.assertNotIn(b"Make an offer", res.body)
|
||||
|
|
|
|||
|
|
@ -311,6 +311,52 @@ def product_edit(request):
|
|||
request.dbsession.add(product.set_price(price))
|
||||
request.session.flash(("You updated the product's price.", "success"))
|
||||
|
||||
# MPS-20 + MPS-21: pricing_mode + allow_offers (per-product overrides).
|
||||
if "submit" in request.params:
|
||||
try:
|
||||
new_mode = int(request.params.get("pricing_mode", product.pricing_mode))
|
||||
except (TypeError, ValueError):
|
||||
new_mode = product.pricing_mode
|
||||
if new_mode in (0, 1, 2, 3, 4) and new_mode != product.pricing_mode:
|
||||
product_modified = True
|
||||
product.pricing_mode = new_mode
|
||||
mode_label = {
|
||||
0: "fixed price", 1: "auction", 2: "auction + buy now",
|
||||
3: "make an offer", 4: "make an offer + buy now",
|
||||
}[new_mode]
|
||||
request.session.flash(
|
||||
(f"Pricing mode set to {mode_label}.", "success")
|
||||
)
|
||||
# If switching INTO auction mode and no auction exists, create one
|
||||
# in DRAFT state with sane defaults from the product price.
|
||||
if new_mode in (1, 2) and product.auction is None:
|
||||
from ..models.auction import (
|
||||
MpsAuction, DEFAULT_BID_INCREMENT_IN_CENTS,
|
||||
DEFAULT_SOFT_CLOSE_SECONDS,
|
||||
)
|
||||
auction = MpsAuction(
|
||||
product=product,
|
||||
shop=product.shop,
|
||||
start_price_in_cents=product.price_in_cents or 100,
|
||||
bid_increment_in_cents=DEFAULT_BID_INCREMENT_IN_CENTS,
|
||||
soft_close_seconds=DEFAULT_SOFT_CLOSE_SECONDS,
|
||||
)
|
||||
request.dbsession.add(auction)
|
||||
request.session.flash((
|
||||
"Auction created in draft state. Configure start/end "
|
||||
"times and reserve price below before scheduling.",
|
||||
"success",
|
||||
))
|
||||
|
||||
# allow_offers — three-state radio: inherit / yes / no.
|
||||
allow_raw = request.params.get("allow_offers", "inherit")
|
||||
new_allow = {
|
||||
"inherit": None, "yes": True, "no": False,
|
||||
}.get(allow_raw, None)
|
||||
if new_allow != product.allow_offers:
|
||||
product_modified = True
|
||||
product.allow_offers = new_allow
|
||||
|
||||
# torrent_opt_in — explicit per-product seeding consent.
|
||||
# MPS-22: gated by global torrent kill switch.
|
||||
if request.torrent_enabled and product.shop.torrent_enabled and "submit" in request.params:
|
||||
|
|
|
|||
|
|
@ -1359,6 +1359,65 @@ def shop_settings(request):
|
|||
# have a product file but no magnet link yet.
|
||||
_torrent_backfill_async(request, shop)
|
||||
|
||||
# MPS-21: make-an-offer settings
|
||||
if form_section == "offer-settings":
|
||||
offer_enabled = checkbox_to_bool(
|
||||
request.params.get("offer_enabled_checkbox", "off")
|
||||
)
|
||||
if shop.offer_enabled != offer_enabled:
|
||||
shop.offer_enabled = offer_enabled
|
||||
status = "enabled" if offer_enabled else "disabled"
|
||||
request.session.flash((f"Make-an-offer {status}.", "success"))
|
||||
|
||||
# Numeric thresholds — silently clamp invalid input rather than
|
||||
# rejecting the whole form.
|
||||
def _int_param(name, default, lo=0, hi=10**9):
|
||||
raw = (request.params.get(name) or "").strip()
|
||||
if not raw:
|
||||
return default
|
||||
try:
|
||||
val = int(float(raw))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return max(lo, min(hi, val))
|
||||
|
||||
new_accept = _int_param(
|
||||
"offer_auto_accept_threshold_pct",
|
||||
shop.offer_auto_accept_threshold_pct, 0, 100,
|
||||
)
|
||||
new_decline = _int_param(
|
||||
"offer_auto_decline_threshold_pct",
|
||||
shop.offer_auto_decline_threshold_pct, 0, 100,
|
||||
)
|
||||
# Auto-decline must be strictly below auto-accept, else thresholds
|
||||
# collide and the queue range disappears. Clamp.
|
||||
if new_decline >= new_accept:
|
||||
new_decline = max(0, new_accept - 1)
|
||||
|
||||
shop.offer_auto_accept_threshold_pct = new_accept
|
||||
shop.offer_auto_decline_threshold_pct = new_decline
|
||||
shop.offer_expiration_hours = _int_param(
|
||||
"offer_expiration_hours", shop.offer_expiration_hours,
|
||||
1, 24 * 365,
|
||||
)
|
||||
shop.offer_max_rounds = _int_param(
|
||||
"offer_max_rounds", shop.offer_max_rounds, 1, 100,
|
||||
)
|
||||
shop.offer_min_buyer_account_age_hours = _int_param(
|
||||
"offer_min_buyer_account_age_hours",
|
||||
shop.offer_min_buyer_account_age_hours, 0, 24 * 365,
|
||||
)
|
||||
|
||||
# offer_min_in_cents — dollar input, optional.
|
||||
offer_min_dollars = (request.params.get("offer_min") or "").strip()
|
||||
if offer_min_dollars:
|
||||
try:
|
||||
shop.offer_min_in_cents = int(round(float(offer_min_dollars) * 100))
|
||||
except (TypeError, ValueError):
|
||||
request.session.flash(("Invalid offer minimum.", "error"))
|
||||
else:
|
||||
shop.offer_min_in_cents = None
|
||||
|
||||
# If we processed any form submission, respond accordingly
|
||||
if form_section:
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue