From 462da47c875a3fa5261d365ef605c1256834f89a Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Tue, 16 Apr 2024 11:04:56 +0100 Subject: [PATCH] Rework price setting So we also list test for the unsponsorable stuff. Also fixes an error which is output when price_levels_pence[s.price] is None --- controllers/manage.py | 66 +++++++++++--------- models/db.py | 4 +- modules/OZfunc.py | 8 ++- views/default/list_sponsorable_children.html | 2 +- views/manage/SET_PRICES.html | 15 +++-- 5 files changed, 55 insertions(+), 40 deletions(-) diff --git a/controllers/manage.py b/controllers/manage.py index b419f222..b2bb3f71 100755 --- a/controllers/manage.py +++ b/controllers/manage.py @@ -108,7 +108,7 @@ def SHOW_SPONSOR_SUMS(): cols['grouped_otts'], cols['sum_paid'], cols['count'], - groupby=groupby, orderby= cols['sum_paid'] + " DESC") + groupby=groupby, orderby=cols['sum_paid'] + " DESC") return dict(rows = rows, cols=cols) @OZfunc.require_https_if_nonlocal() @@ -793,7 +793,14 @@ def SET_PRICES(): """ # Take the initial prices from the previous prices in the DB - bands = {string.ascii_uppercase[i]: row for i, row in enumerate(db().select(db.prices.ALL))} + bands = { + string.ascii_uppercase[i]: row + for i, row in enumerate(db().select( + db.prices.ALL, + orderby="ISNULL(quantile), quantile, ISNULL(price), price" + )) + } + max_band=max((b for b in bands if bands[b].price is not None), default=None) n_leaves = db(db.ordered_leaves).count() bespoke_spp = [ #these are in order highest to lowest @@ -806,8 +813,10 @@ def SET_PRICES(): # Make a form with all the rows in it other_fields = [f for f in db.prices if f.name not in ('id', 'quantile', 'n_leaves')] fields = [] - for i, (band, row) in enumerate(sorted(bands.items(), reverse=True)): - if i > 0: + for band, row in bands.items(): + if band == max_band or row.price is None: + pass # Don't get quantiles for the max band or the unsponsorable + else: field = db.prices.quantile # Don't have a max popularity cutoff for the highest price fields.append(Field("_".join([field.name, band]), type = field.type, default=row[field.name], requires=IS_NOT_EMPTY())) @@ -821,7 +830,7 @@ def SET_PRICES(): cutoffs = {} other_vars = {} prev = None - for band in sorted(bands.keys()): + for band in sorted(bands): if prev is None: # Cheapest category quantile = float(form.vars["_".join(["quantile", band])]) @@ -836,18 +845,22 @@ def SET_PRICES(): (db.ordered_leaves.popularity_rank <= cutoffs[prev][1]) & (db.ordered_leaves.popularity_rank > cutoffs[band][1])) else: - #this is the last one, which does not have a defined top price - queries[band] = db( - (db.ordered_leaves.popularity_rank <= cutoffs[prev][1])) + if bands[band].price is not None: + #this is the last quantile: no upper cutoff + queries[band] = db( + (db.ordered_leaves.popularity_rank <= cutoffs[prev][1])) other_vars[band] = { f.name: form.vars["_".join([f.name, band])] for f in other_fields } prev = band - + if prev is not None: + assert bands[prev].price is None # Check the last has no price set + queries[prev] = db(db.banned.ott) # And set that to the banned list + #save the results db.prices.truncate() - for band in sorted(bands.keys()): + for band in sorted(bands): num=queries[band].count() db.prices.insert( quantile=(float(cutoffs[band][0]) if band in cutoffs else None), @@ -857,34 +870,27 @@ def SET_PRICES(): output = [] revenue = 0 - #also None means 'call us' for band in sorted(bands): + num = queries[band].count() + pc = 0 if n_leaves==0 else (100*num/n_leaves) price = other_vars[band]['price'] - cnt=queries[band].update(price=price) - num=queries[band].count() - revenue += num*price/100 - output.append(f"{OZfunc.fmt_pounds(pence=price)}: {num:>8} species ({100*num/n_leaves:.2f}%) - {cnt} changed") + price_str = OZfunc.fmt_pounds(pence=price) + # Now set all the prices for this band + count = 0 if num == 0 else queries[band].update(price=price) + revenue += num*(price or 0)/100 + output.append(f"{price_str}: {num:>8} species ({pc:.2f}%): {count} changed") output.append(BR()) - response.flash = DIV( f"SET THE FOLLOWING DEFAULT PRICE STRUCTURE for {n_leaves} species:", BR(),PRE(*output), f"Total revenue: {OZfunc.fmt_pounds(revenue)}!\nNow overriding the following special exclusions (and setting banned):", BR(), f"{bespoke_spp}" ) - #override with the bespoke ones - target_band = 0 - for band in sorted(bands, reverse=True): - db(db.ordered_leaves.name.belongs(bespoke_spp[target_band])).update(price=other_vars[band]['price']) - target_band+=1 - if target_band>=len(bespoke_spp): - break - - #make sure the banned ones are NULLified - rows = db().select(db.banned.ott) - for ban in rows: - db(db.ordered_leaves.ott == ban.ott).update(price=None) + #override bespoke species prices: these are ordered by reverse price tag + rev_price_bands = [b for b in sorted(bands, reverse=True) if other_vars[b]['price'] is not None] + for band, spp in zip(rev_price_bands, bespoke_spp): + db(db.ordered_leaves.name.belongs(spp)).update(price=other_vars[band]['price']) elif form.errors: response.flash = 'form has errors' @@ -920,9 +926,9 @@ def SET_PRICES(): pop_max=db(db.ordered_leaves).select(mx) return dict( form=form, - bands={b: bands[b].price for b in sorted(bands.keys())}, + bands={b: bands[b].price for b in sorted(bands)}, other_fields=other_fields, - max_band=max(bands.keys()) if len(bands) else None, + max_band=max_band, pop_min=pop_min[0][mn], pop_max=pop_max[0][mx], example_spp=sorted(example_spp, key=lambda tup: tup[1]), diff --git a/models/db.py b/models/db.py index 9f0e690a..57c69a51 100755 --- a/models/db.py +++ b/models/db.py @@ -649,8 +649,8 @@ # this table defines the current pricing cutoff points db.define_table('prices', - Field('price', type='integer', unique=True, requires=IS_NOT_EMPTY()), - Field('perpetuity_price', type='integer', requires=IS_NOT_EMPTY()), + Field('price', type='integer', unique=True), + Field('perpetuity_price', type='integer'), # Map the "normal" 4 year price to the in-perpetuity price Field('quantile', type='double'), Field('n_leaves', type='integer'), diff --git a/modules/OZfunc.py b/modules/OZfunc.py index 63f5d94d..1658dad0 100644 --- a/modules/OZfunc.py +++ b/modules/OZfunc.py @@ -573,6 +573,8 @@ def otts2ids(ottIntegers): return {"nodes": {}, "leaves": {}, "names": {}} def fmt_pounds(pounds=0, pence=0): - p = pence / 100 + pounds - return '£{:.0f}'.format(p) if float(p).is_integer() else '£{:.2f}'.format(p) - + try: + p = pence / 100 + pounds + return '£{:.0f}'.format(p) if float(p).is_integer() else '£{:.2f}'.format(p) + except: + return current.T('Undefined price') diff --git a/views/default/list_sponsorable_children.html b/views/default/list_sponsorable_children.html index 53dbc722..289a727e 100755 --- a/views/default/list_sponsorable_children.html +++ b/views/default/list_sponsorable_children.html @@ -14,7 +14,7 @@

Alphabetical list of species in this group

{{if sponsorship_enabled:}} £{{if s.price is None:}}{{T('contact us')}}{{else:}}{{=f"{s.price/100:.2f}"}}{{pass}}{{T('or more to sponsor.')}} {{else:}} - {{=T(price_levels_pence.get(s.price, "click for sponsorship details"))}} + {{=T(price_levels_pence.get(s.price) or "click for sponsorship details"))}} {{pass}} {{pass}} diff --git a/views/manage/SET_PRICES.html b/views/manage/SET_PRICES.html index 01cf18ab..ecb23267 100755 --- a/views/manage/SET_PRICES.html +++ b/views/manage/SET_PRICES.html @@ -39,10 +39,12 @@

set cutoffs:

{{for b, price in bands.items():}} -{{ if b != max_band:}} -{{=b}} (currently {{=fmt_pounds(pence=price)}}){{=form.custom.widget['_'.join(['quantile', b])]}} -{{ else:}} +{{ if b == max_band:}} {{=b}} (currently {{=fmt_pounds(pence=price)}})remainder +{{elif price is None:}} +{{=b}} (currently {{=fmt_pounds(pence=price)}}){{=price}} +{{ else:}} +{{=b}} (currently {{=fmt_pounds(pence=price)}}){{=form.custom.widget['_'.join(['quantile', b])]}} {{ pass}} {{ for i, field in enumerate(other_fields):}} {{=form.custom.widget["_".join([field.name, b])]}} @@ -51,6 +53,11 @@

set cutoffs:

Click here to set all the prices in the database (may take some time): {{=form.custom.submit}} -{{=form.custom.end}}

Submitted variables

+{{=form.custom.end}} +
+{{if request.vars:}} +
Click to see submitted variables {{=BEAUTIFY(request.vars)}} +{{pass}} +
\ No newline at end of file