Implemented currency formatting and parsing.

This commit is contained in:
s3lph 2018-07-20 14:58:22 +02:00
parent 0ab7d48622
commit 103e06cf12
7 changed files with 64 additions and 11 deletions

View file

@ -17,7 +17,7 @@ import jinja2
from matemat import __version__ as matemat_version from matemat import __version__ as matemat_version
from matemat.exceptions import HttpException from matemat.exceptions import HttpException
from matemat.webserver import RequestArguments, PageletResponse, RedirectResponse, TemplateResponse from matemat.webserver import RequestArguments, PageletResponse, RedirectResponse, TemplateResponse
from matemat.webserver.util import parse_args from matemat.webserver.util import parse_args, format_chf
# #
# Python internal class hacks # Python internal class hacks
@ -117,8 +117,9 @@ class MatematHTTPServer(HTTPServer):
# Set up the Jinja2 environment # Set up the Jinja2 environment
self.jinja_env: jinja2.Environment = jinja2.Environment( self.jinja_env: jinja2.Environment = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.abspath(templateroot)), loader=jinja2.FileSystemLoader(os.path.abspath(templateroot)),
autoescape=jinja2.select_autoescape(default=True) autoescape=jinja2.select_autoescape(default=True),
) )
self.jinja_env.filters['chf'] = format_chf
# Set up logger # Set up logger
self.logger: logging.Logger = logging.getLogger('matemat.webserver') self.logger: logging.Logger = logging.getLogger('matemat.webserver')
self.logger.setLevel(log_level) self.logger.setLevel(log_level)

View file

@ -8,6 +8,7 @@ from matemat.webserver import pagelet, RequestArguments, PageletResponse, Redire
from matemat.db import MatematDatabase from matemat.db import MatematDatabase
from matemat.primitives import Product from matemat.primitives import Product
from matemat.exceptions import DatabaseConsistencyError, HttpException from matemat.exceptions import DatabaseConsistencyError, HttpException
from matemat.webserver.util import parse_chf
@pagelet('/modproduct') @pagelet('/modproduct')
@ -60,8 +61,8 @@ def handle_change(args: RequestArguments, product: Product, db: MatematDatabase,
if 'name' not in args or 'pricemember' not in args or 'pricenonmember' not in args or 'stock' not in args: if 'name' not in args or 'pricemember' not in args or 'pricenonmember' not in args or 'stock' not in args:
return return
name = str(args.name) name = str(args.name)
price_member = int(str(args.pricemember)) price_member = parse_chf(str(args.pricemember))
price_non_member = int(str(args.pricenonmember)) price_non_member = parse_chf(str(args.pricenonmember))
stock = int(str(args.stock)) stock = int(str(args.stock))
try: try:
db.change_product(product, db.change_product(product,

View file

@ -8,6 +8,7 @@ from matemat.webserver import pagelet, RequestArguments, PageletResponse, Redire
from matemat.db import MatematDatabase from matemat.db import MatematDatabase
from matemat.primitives import User from matemat.primitives import User
from matemat.exceptions import DatabaseConsistencyError, HttpException from matemat.exceptions import DatabaseConsistencyError, HttpException
from matemat.webserver.util import parse_chf
@pagelet('/moduser') @pagelet('/moduser')
@ -62,7 +63,7 @@ def handle_change(args: RequestArguments, user: User, db: MatematDatabase, confi
username = str(args.username) username = str(args.username)
email = str(args.email) email = str(args.email)
password = str(args.password) password = str(args.password)
balance = int(str(args.balance)) balance = parse_chf(str(args.balance))
is_member = 'ismember' in args is_member = 'ismember' in args
is_admin = 'isadmin' in args is_admin = 'isadmin' in args
if len(email) == 0: if len(email) == 0:

View file

@ -136,3 +136,53 @@ def parse_args(request: str, postbody: Optional[bytes] = None, enctype: str = 't
raise ValueError(f'Unsupported Content-Type: {enctype}') raise ValueError(f'Unsupported Content-Type: {enctype}')
# Return the path and the parsed arguments # Return the path and the parsed arguments
return tokens.path, args return tokens.path, args
def format_chf(value: int, with_currencysign: bool = True) -> str:
"""
Formats a centime value into a commonly understood representation ("CHF -13.37").
:param value: The value to format, in centimes.
:param with_currencysign: Whether to include the currency prefix ("CHF ") in the output.
:return: A human-readable string representation.
"""
sign: str = ''
if value < 0:
# As // and % round towards -Inf, convert into a positive value and prepend the negative sign
sign = '-'
value = -value
# Split into full francs and fractions (centimes)
full: int = value // 100
frac: int = value % 100
csign: str = 'CHF ' if with_currencysign else ''
# Put it all together; centimes are always padded with 2 zeros
return f'{csign}{sign}{full}.{frac:02}'
def parse_chf(value: str) -> int:
"""
Parse a currency value into machine-readable format (integer centimes). The prefix "CHF", the decimal point, and
digits after the decimal point are optional.
:param value: The value to parse.
:return: An integer representation of the value.
:raises: Value error: If more than two digits after the decimal point are present.
"""
value = value.strip()
if value.startswith('CHF'):
value = value[3:]
value = value.strip()
if '.' not in value:
return int(value) * 100
full, frac = value.split('.', 1)
if len(frac) > 2:
raise ValueError('Needs max. 2 digits after decimal point')
elif len(frac) < 2:
frac = frac + '0' * (2 - len(frac))
ifrac: int = int(frac)
ifull: int = int(full)
if ifrac < 0:
raise ValueError('Fraction part must not be negative.')
if ifull < 0:
ifrac = -ifrac
return ifull * 100 + ifrac

View file

@ -15,10 +15,10 @@
<input id="modproduct-name" type="text" name="name" value="{{ product.name }}" /><br/> <input id="modproduct-name" type="text" name="name" value="{{ product.name }}" /><br/>
<label for="modproduct-price-member">Member price: </label> <label for="modproduct-price-member">Member price: </label>
<input id="modproduct-price-member" type="text" name="pricemember" value="{{ product.price_member }}" /><br/> CHF <input id="modproduct-price-member" type="number" step="0.01" name="pricemember" value="{{ product.price_member|chf(False) }}" /><br/>
<label for="modproduct-price-non-member">Non-member price: </label> <label for="modproduct-price-non-member">Non-member price: </label>
<input id="modproduct-price-non-member" type="text" name="pricenonmember" value="{{ product.price_non_member }}" /><br/> CHF <input id="modproduct-price-non-member" type="number" step="0.01" name="pricenonmember" value="{{ product.price_non_member|chf(False) }}" /><br/>
<label for="modproduct-balance">Stock: </label> <label for="modproduct-balance">Stock: </label>
<input id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/> <input id="modproduct-balance" name="stock" type="text" value="{{ product.stock }}" /><br/>

View file

@ -27,7 +27,7 @@
<input id="moduser-account-isadmin" name="isadmin" type="checkbox" {% if user.is_admin %} checked="checked" {% endif %}/><br/> <input id="moduser-account-isadmin" name="isadmin" type="checkbox" {% if user.is_admin %} checked="checked" {% endif %}/><br/>
<label for="moduser-account-balance">Balance: </label> <label for="moduser-account-balance">Balance: </label>
<input id="moduser-account-balance" name="balance" type="text" value="{{ user.balance }}" /><br/> CHF <input id="moduser-account-balance" name="balance" type="number" step="0.01" value="{{ user.balance|chf(False) }}" /><br/>
<label for="moduser-account-avatar"> <label for="moduser-account-avatar">
<img height="150" src="/upload/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" /> <img height="150" src="/upload/thumbnails/users/{{ user.id }}.png" alt="Avatar of {{ user.name }}" />

View file

@ -9,7 +9,7 @@
{% block main %} {% block main %}
Your balance: {{ authuser.balance }} Your balance: {{ authuser.balance|chf }}
<br/> <br/>
<a href="/deposit?n=100">Deposit CHF 1</a><br/> <a href="/deposit?n=100">Deposit CHF 1</a><br/>
<a href="/deposit?n=1000">Deposit CHF 10</a><br/> <a href="/deposit?n=1000">Deposit CHF 10</a><br/>
@ -20,9 +20,9 @@ Your balance: {{ authuser.balance }}
<span class="thumblist-title">{{ product.name }}</span> <span class="thumblist-title">{{ product.name }}</span>
<span class="thumblist-detail">Price: <span class="thumblist-detail">Price:
{% if authuser.is_member %} {% if authuser.is_member %}
{{ product.price_member }} {{ product.price_member|chf }}
{% else %} {% else %}
{{ product.price_non_member }} {{ product.price_non_member|chf }}
{% endif %} {% endif %}
; Stock: {{ product.stock }}</span><br/> ; Stock: {{ product.stock }}</span><br/>
<div class="imgcontainer"> <div class="imgcontainer">