Compare commits

...

3 Commits

12 changed files with 160 additions and 60 deletions

View File

@ -9,7 +9,15 @@ import Select
type Tax = Net | Gross type Tax = Net | Gross
ctShow ct = case ct of -- Duplicated from Entry.elm but too lazy to sandwich this out
type alias TaxGroup =
{ id : Int
, description : String
, percentage : Float
}
showTax : Tax -> String
showTax tax = case tax of
Gross -> "Brutto" Gross -> "Brutto"
Net -> "Netto" Net -> "Netto"
@ -19,11 +27,13 @@ type alias Model =
, bundleSize : NumberInput.Model Int , bundleSize : NumberInput.Model Int
} }
init : Float -> Model
init bundlePrice = Model init bundlePrice = Model
(Select.init ctShow ctShow Net [Net, Gross]) (Select.init showTax showTax Net [Net, Gross])
(NumberInput.fromFloat bundlePrice) (NumberInput.fromFloat bundlePrice)
(NumberInput.fromInt 1) (NumberInput.fromInt 1)
getResult : Model -> TaxGroup -> Maybe Float
getResult model taxGroup = getResult model taxGroup =
case (NumberInput.get model.bundlePrice, NumberInput.get model.bundleSize) of case (NumberInput.get model.bundlePrice, NumberInput.get model.bundleSize) of
(Just bundlePrice, Just bundleSize) -> (Just bundlePrice, Just bundleSize) ->
@ -41,6 +51,7 @@ type Msg
| SetBundlePrice String | SetBundlePrice String
| SetBundleSize String | SetBundleSize String
update : Msg -> Model -> Model
update msg model = case msg of update msg model = case msg of
SetTax key -> SetTax key ->
{ model | tax = Select.update key model.tax } { model | tax = Select.update key model.tax }
@ -49,6 +60,7 @@ update msg model = case msg of
SetBundleSize str -> SetBundleSize str ->
{ model | bundleSize = NumberInput.update str model.bundleSize } { model | bundleSize = NumberInput.update str model.bundleSize }
view : Model -> TaxGroup -> Html Msg
view model taxGroup = view model taxGroup =
let let
mainPart = mainPart =
@ -112,4 +124,5 @@ view model taxGroup =
] ]
] ]
roundTo places x = toFloat (round <| x * 10 ^ places) / 10 ^ places roundTo : Int -> Float -> Float
roundTo places x = toFloat (round <| x * 10 ^ toFloat places) / 10 ^ toFloat places

View File

@ -14,6 +14,50 @@ import Calculator
import NumberInput import NumberInput
import Select import Select
{-
Elm forces us to use the Elm architecture:
Model view Html
Elm runtime
Model+CmdupdateMsg+Model
This architecture is similar to what React does but its implementation
in Elm is a bit special since it's purely functional and side effects
are isolated into the runtime system.
An Elm component is usually centered around two types, Model and Msg.
Model contains all data the application is concerned with, including the state
of UI elements. Msg encodes all updates to Model that the application supports.
In addition to Msg and Model, we have to provide two functions, view and update.
view : Model -> Html Msg
update : Msg -> Model -> (Model, Cmd Msg)
view maps a Model to a DOM tree. Events in this DOM tree create Msg values.
update maps a Msg and a Model to a new Model. In addition, update can create
a command. Commands are used to make the runtime do side effects, which in
turn create new Msg values.
For example, we have a SetSearchTerm message which simply updates the searchTerm
property in the model. This message is triggered every time the search box input
is changed. Submitting the search box form triggers a SubmitSearch event.
This event leaves the model unchanged but issues a command that sends the search
term to a JSON endpoint. When the request successfully resolves, the runtime
triggers a ReceiveSearchResults messages which updates the list of search results
in the model.
See Calculator.elm for a simpler example of this architecture.
-}
main = Browser.element main = Browser.element
{ init = \globals -> { init = \globals ->
( Context globals <| ItemSearch { searchTerm = "", searchResults = [] } ( Context globals <| ItemSearch { searchTerm = "", searchResults = [] }

View File

@ -22,12 +22,7 @@ def create_app():
app.config.from_file("config.json", load=json.load, silent=True) app.config.from_file("config.json", load=json.load, silent=True)
db.init_app(app) db.init_app(app)
auth.init_app(app)
# This function denies every request until `auth.ACCESS_TOKEN`
# is passed using `?token=` to authenticate the session.
@app.before_request
def before_req_fun():
return auth.before_request()
@app.context_processor @app.context_processor
def utility_processor(): def utility_processor():

View File

@ -1,7 +1,10 @@
import secrets import secrets
import string import string
from flask import Blueprint, request, redirect, render_template, session from flask import Blueprint, request, redirect, render_template
from flask_login import current_user, login_user, logout_user, LoginManager
from typing import Any, Dict, List, Optional
bp = Blueprint("auth", __name__, url_prefix="/auth") bp = Blueprint("auth", __name__, url_prefix="/auth")
@ -15,27 +18,67 @@ ALLOWED_PATHS = [
] ]
def before_request(): # A poor man's replacement for memory-backed session solution.
""" # We keep exactly one User (and the corresponding UserData) in
If the correct token query parameter is passed along with any request, # memory and use that to store session data.
we mark this session authenticated by setting `session["authenticated"]`. class UserData:
Unless the session is authenticated, all requests result in a 403 FORBIDDEN. location: Optional[Dict[str, Any]]
""" orders: List[Dict[str, Any]]
if "token" in request.args:
if request.args["token"] == ACCESS_TOKEN:
session["authenticated"] = ()
# Reload the page without query parameters
return redirect(request.path)
# Don't deny any paths in `ALLOWED_PATHS` def __init__(self):
if request.path in ALLOWED_PATHS: self.location = None
return self.orders = []
if not "authenticated" in session:
return render_template("auth/denied.html"), 403 class User:
is_authenticated: bool
is_active: bool
is_anonymous: bool
data: UserData
def __init__(self):
self.is_authenticated = True
self.is_active = True
self.is_anonymous = False
self.data = UserData()
def get_id(self) -> str:
return ""
def init_app(app):
login_manager = LoginManager(app)
the_one_and_only_user = User()
@login_manager.user_loader
def load_user(user_id: str) -> User:
assert user_id == ""
return the_one_and_only_user
# This function denies every request until `auth.ACCESS_TOKEN`
# is passed using `?token=` to authenticate the user.
# We use this instead of @login_required because otherwise we'd have
# to add that annotation to all routes.
# See also: https://flask-login.readthedocs.io/en/latest/#flask_login.login_required
@app.before_request
def before_request():
if "token" in request.args:
if request.args["token"] == ACCESS_TOKEN:
login_user(the_one_and_only_user)
# Reload the page without query parameters
return redirect(request.path)
# Never deny any paths in `ALLOWED_PATHS`
if request.path in ALLOWED_PATHS:
return
if not current_user.is_authenticated:
return render_template("auth/denied.html"), 403
@bp.get("/logout") @bp.get("/logout")
def logout(): def logout():
session.pop("authenticated", None) logout_user()
return redirect("/") return redirect("/")

View File

@ -1,5 +1,5 @@
from flask import Blueprint, flash, redirect, render_template, request, session from flask import Blueprint, flash, redirect, render_template, request
from flask_login import current_user
from . import db from . import db
@ -9,22 +9,18 @@ bp = Blueprint("entry", __name__, url_prefix="/entry")
@bp.route("/", methods=["GET", "POST"]) @bp.route("/", methods=["GET", "POST"])
def index(): def index():
cart = session.get("cart", default=[])
return render_template( return render_template(
"entry/index.html", "entry/index.html"
cart=cart
) )
@bp.post("/add-new-items") @bp.post("/add-new-items")
def add_new_entries(): def add_new_entries():
print(session)
i_know_what_im_doing = "i-know-what-im-doing" in request.form i_know_what_im_doing = "i-know-what-im-doing" in request.form
if not i_know_what_im_doing: if not i_know_what_im_doing:
return "Du weißt nicht was du tust", 400 return "Du weißt nicht was du tust", 400
orders = session.get("cart", default=[]) orders = current_user.data.orders
if not orders: if not orders:
return "Keine Aufträge", 404 return "Keine Aufträge", 404
@ -36,7 +32,7 @@ def add_new_entries():
db.get_db().commit() db.get_db().commit()
# Reset the cart # Reset the cart
session["cart"] = [] current_user.data.orders = []
return redirect(request.referrer) return redirect(request.referrer)
@ -48,9 +44,7 @@ def delete_order():
except: except:
return "Incomplete or mistyped form", 400 return "Incomplete or mistyped form", 400
cart = session.get("cart", default=[]) del current_user.data.orders[order_index]
del cart[order_index]
session["cart"] = cart
return redirect(request.referrer) return redirect(request.referrer)
@ -76,9 +70,7 @@ def new_order():
except: except:
return f"Incomplete or mistyped form", 400 return f"Incomplete or mistyped form", 400
cart = session.get("cart", default=[]) current_user.data.orders.append({
print(cart)
cart.append({
"barcode": barcode, "barcode": barcode,
"name": name, "name": name,
"sales_units": sales_units, "sales_units": sales_units,
@ -91,7 +83,6 @@ def new_order():
"net_unit_price": net_unit_price, "net_unit_price": net_unit_price,
"gross_unit_price": gross_unit_price "gross_unit_price": gross_unit_price
}) })
session["cart"] = cart
return redirect("/entry") return redirect("/entry")
with db.run_query("entry/get_groups.sql") as cursor: with db.run_query("entry/get_groups.sql") as cursor:
@ -122,10 +113,9 @@ def api_search_items():
except: except:
return {"error": "Missing query parameter `search-term`"}, 400 return {"error": "Missing query parameter `search-term`"}, 400
location = session.get("location", None) location = current_user.data.location
with db.run_query("search_items.sql", { with db.run_query("search_items.sql", {
"location_id": None if location is None else location["location_id"], "location_id": location["location_id"] if location else None,
"search_term": search_term "search_term": search_term
}) as cursor: }) as cursor:
items = cursor.fetchall() items = cursor.fetchall()

View File

@ -1,4 +1,5 @@
from flask import Blueprint, redirect, render_template, request, session from flask import Blueprint, redirect, render_template, request
from flask_login import current_user
from . import db from . import db
@ -8,7 +9,7 @@ bp = Blueprint("inventory", __name__, url_prefix="/inventory")
@bp.get("/") @bp.get("/")
def index(): def index():
location = session.get("location", None) location = current_user.data.location
items = db.run_query("get_inventory_overview.sql", { items = db.run_query("get_inventory_overview.sql", {
"location_id": None if location is None else location["location_id"] "location_id": None if location is None else location["location_id"]
}).fetchall() }).fetchall()
@ -20,7 +21,7 @@ def index():
@bp.get("/report") @bp.get("/report")
def read_report(): def read_report():
location = session.get("location", None) location = current_user.data.location
items = db.run_query("get_inventory_report.sql", { items = db.run_query("get_inventory_report.sql", {
"location_id": None if location is None else location["location_id"] "location_id": None if location is None else location["location_id"]
}).fetchall() }).fetchall()

View File

@ -1,4 +1,5 @@
from flask import Blueprint, render_template, request, session from flask import Blueprint, render_template, request
from flask_login import current_user
from . import db from . import db
@ -11,13 +12,12 @@ def index():
if request.method == "POST": if request.method == "POST":
location_id = request.form.get("location_id", "") location_id = request.form.get("location_id", "")
if location_id == "": if location_id == "":
session.pop("location", None) current_user.data.location = None
else: else:
location = db.run_query("get_location_by_id.sql", { location = db.run_query("get_location_by_id.sql", {
"location_id": location_id} "location_id": location_id
).fetchone() }).fetchone()
session["location"] = location current_user.data.location = location
locations = db.run_query("get_locations.sql").fetchall() locations = db.run_query("get_locations.sql").fetchall()

View File

@ -71,3 +71,6 @@ th {
display: block; display: block;
width: 8em; width: 8em;
} }
details {
font-size: 0.8em;
}

View File

@ -15,10 +15,10 @@
<li{{ " class=current-page" if request.path.startswith("/entry") else "" }}><a href="/entry">Eintragen</a></li> <li{{ " class=current-page" if request.path.startswith("/entry") else "" }}><a href="/entry">Eintragen</a></li>
<li{{ " class=current-page" if request.path.startswith("/location") else "" }}> <li{{ " class=current-page" if request.path.startswith("/location") else "" }}>
<a href="/location"> <a href="/location">
{% if "location" not in session %} {% if not current_user.data.location %}
Raum wählen Raum wählen
{% else %} {% else %}
Raum: {{ session.location.location_name }} Raum: {{ current_user.data.location.location_name }}
{% endif %} {% endif %}
</a> </a>
</li> </li>
@ -30,6 +30,16 @@
<details> <details>
<summary><code>config</code></summary> <summary><code>config</code></summary>
<pre>{% for key, value in config.items() %}{{ key }} = {{ value }} <pre>{% for key, value in config.items() %}{{ key }} = {{ value }}
{% endfor %}</pre>
</details>
<details>
<summary><code>session</code></summary>
<pre>{% for key, value in session.items() %}{{ key }} = {{ value }}
{% endfor %}</pre>
</details>
<details>
<summary><code>current_user.data</code></summary>
<pre>{% for key, value in current_user.data.__dict__.items() %}{{ key }} = {{ value }}
{% endfor %}</pre> {% endfor %}</pre>
</details> </details>
{% endif %} {% endif %}

View File

@ -15,7 +15,7 @@
<th>VK-Preis (Brutto)</th> <th>VK-Preis (Brutto)</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
{% for cart_item in cart %} {% for cart_item in current_user.data.orders %}
<tr> <tr>
<td><code>{{ cart_item.barcode }}</code></td> <td><code>{{ cart_item.barcode }}</code></td>
<td>{{ cart_item.name }}</td> <td>{{ cart_item.name }}</td>

View File

@ -3,9 +3,9 @@
{% block content %} {% block content %}
<form method="POST"> <form method="POST">
<select name="location_id"> <select name="location_id">
<option value="" {{ "selected" if "location" not in session else ""}}>-</option> <option value="" {{ "selected" if not current_user.data.location else ""}}>-</option>
{% for location in locations %} {% for location in locations %}
<option value="{{ location.location_id }}" {{ "selected" if "location" in session and session.location.location_id == location.location_id else "" }}>{{ location.location_name }}</option> <option value="{{ location.location_id }}" {{ "selected" if current_user.data.location.location_id == location.location_id else "" }}>{{ location.location_name }}</option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,6 +1,7 @@
blinker==1.6.2 blinker==1.6.2
click==8.1.3 click==8.1.3
Flask==2.3.2 Flask==2.3.2
Flask-Login==0.6.2
itsdangerous==2.1.2 itsdangerous==2.1.2
Jinja2==3.1.2 Jinja2==3.1.2
MarkupSafe==2.1.2 MarkupSafe==2.1.2