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
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"
Net -> "Netto"
@ -19,11 +27,13 @@ type alias Model =
, bundleSize : NumberInput.Model Int
}
init : Float -> Model
init bundlePrice = Model
(Select.init ctShow ctShow Net [Net, Gross])
(Select.init showTax showTax Net [Net, Gross])
(NumberInput.fromFloat bundlePrice)
(NumberInput.fromInt 1)
getResult : Model -> TaxGroup -> Maybe Float
getResult model taxGroup =
case (NumberInput.get model.bundlePrice, NumberInput.get model.bundleSize) of
(Just bundlePrice, Just bundleSize) ->
@ -41,6 +51,7 @@ type Msg
| SetBundlePrice String
| SetBundleSize String
update : Msg -> Model -> Model
update msg model = case msg of
SetTax key ->
{ model | tax = Select.update key model.tax }
@ -49,6 +60,7 @@ update msg model = case msg of
SetBundleSize str ->
{ model | bundleSize = NumberInput.update str model.bundleSize }
view : Model -> TaxGroup -> Html Msg
view model taxGroup =
let
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 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
{ init = \globals ->
( 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)
db.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()
auth.init_app(app)
@app.context_processor
def utility_processor():

View File

@ -1,7 +1,10 @@
import secrets
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")
@ -15,27 +18,67 @@ ALLOWED_PATHS = [
]
def before_request():
"""
If the correct token query parameter is passed along with any request,
we mark this session authenticated by setting `session["authenticated"]`.
Unless the session is authenticated, all requests result in a 403 FORBIDDEN.
"""
if "token" in request.args:
if request.args["token"] == ACCESS_TOKEN:
session["authenticated"] = ()
# Reload the page without query parameters
return redirect(request.path)
# A poor man's replacement for memory-backed session solution.
# We keep exactly one User (and the corresponding UserData) in
# memory and use that to store session data.
class UserData:
location: Optional[Dict[str, Any]]
orders: List[Dict[str, Any]]
# Don't deny any paths in `ALLOWED_PATHS`
if request.path in ALLOWED_PATHS:
return
def __init__(self):
self.location = None
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")
def logout():
session.pop("authenticated", None)
logout_user()
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
@ -9,22 +9,18 @@ bp = Blueprint("entry", __name__, url_prefix="/entry")
@bp.route("/", methods=["GET", "POST"])
def index():
cart = session.get("cart", default=[])
return render_template(
"entry/index.html",
cart=cart
"entry/index.html"
)
@bp.post("/add-new-items")
def add_new_entries():
print(session)
i_know_what_im_doing = "i-know-what-im-doing" in request.form
if not i_know_what_im_doing:
return "Du weißt nicht was du tust", 400
orders = session.get("cart", default=[])
orders = current_user.data.orders
if not orders:
return "Keine Aufträge", 404
@ -36,7 +32,7 @@ def add_new_entries():
db.get_db().commit()
# Reset the cart
session["cart"] = []
current_user.data.orders = []
return redirect(request.referrer)
@ -48,9 +44,7 @@ def delete_order():
except:
return "Incomplete or mistyped form", 400
cart = session.get("cart", default=[])
del cart[order_index]
session["cart"] = cart
del current_user.data.orders[order_index]
return redirect(request.referrer)
@ -76,9 +70,7 @@ def new_order():
except:
return f"Incomplete or mistyped form", 400
cart = session.get("cart", default=[])
print(cart)
cart.append({
current_user.data.orders.append({
"barcode": barcode,
"name": name,
"sales_units": sales_units,
@ -91,7 +83,6 @@ def new_order():
"net_unit_price": net_unit_price,
"gross_unit_price": gross_unit_price
})
session["cart"] = cart
return redirect("/entry")
with db.run_query("entry/get_groups.sql") as cursor:
@ -122,10 +113,9 @@ def api_search_items():
except:
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", {
"location_id": None if location is None else location["location_id"],
"location_id": location["location_id"] if location else None,
"search_term": search_term
}) as cursor:
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
@ -8,7 +9,7 @@ bp = Blueprint("inventory", __name__, url_prefix="/inventory")
@bp.get("/")
def index():
location = session.get("location", None)
location = current_user.data.location
items = db.run_query("get_inventory_overview.sql", {
"location_id": None if location is None else location["location_id"]
}).fetchall()
@ -20,7 +21,7 @@ def index():
@bp.get("/report")
def read_report():
location = session.get("location", None)
location = current_user.data.location
items = db.run_query("get_inventory_report.sql", {
"location_id": None if location is None else location["location_id"]
}).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
@ -11,13 +12,12 @@ def index():
if request.method == "POST":
location_id = request.form.get("location_id", "")
if location_id == "":
session.pop("location", None)
current_user.data.location = None
else:
location = db.run_query("get_location_by_id.sql", {
"location_id": location_id}
).fetchone()
session["location"] = location
"location_id": location_id
}).fetchone()
current_user.data.location = location
locations = db.run_query("get_locations.sql").fetchall()

View File

@ -71,3 +71,6 @@ th {
display: block;
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("/location") else "" }}>
<a href="/location">
{% if "location" not in session %}
{% if not current_user.data.location %}
Raum wählen
{% else %}
Raum: {{ session.location.location_name }}
Raum: {{ current_user.data.location.location_name }}
{% endif %}
</a>
</li>
@ -30,6 +30,16 @@
<details>
<summary><code>config</code></summary>
<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>
</details>
{% endif %}

View File

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

View File

@ -3,9 +3,9 @@
{% block content %}
<form method="POST">
<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 %}
<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 %}
</select>

View File

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