Compare commits
	
		
			4 Commits
		
	
	
		
			5cec28cad1
			...
			1b6d0c40f3
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 1b6d0c40f3 | |||
| 235ef32efd | |||
| 85598adaab | |||
| 70934a2f85 | 
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							| @ -4,6 +4,11 @@ | |||||||
| 
 | 
 | ||||||
| ## Setup | ## Setup | ||||||
| 
 | 
 | ||||||
|  | `jon` is a Python WSGI application written using Flask. | ||||||
|  | This means you'll have to install a bunch of Python packages to get up and running. | ||||||
|  | 
 | ||||||
|  | ### Dependencies | ||||||
|  | 
 | ||||||
| ``` | ``` | ||||||
| pip install -r requirements.txt | pip install -r requirements.txt | ||||||
| ``` | ``` | ||||||
| @ -11,6 +16,14 @@ pip install -r requirements.txt | |||||||
| You should probably use a virtualenv for that. | You should probably use a virtualenv for that. | ||||||
| I develop `jon` using Python 3.10 but it should work with older versions as well. | I develop `jon` using Python 3.10 but it should work with older versions as well. | ||||||
| 
 | 
 | ||||||
|  | #### Arch Linux | ||||||
|  | 
 | ||||||
|  | If you're on Arch, these packages are required for running the server: | ||||||
|  | 
 | ||||||
|  | ``` | ||||||
|  | python python-flask python-flask-login python-psycopg2 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
| ### Building Frontend JS | ### Building Frontend JS | ||||||
| 
 | 
 | ||||||
| Most of jon works without JS but there are some features that require it. | Most of jon works without JS but there are some features that require it. | ||||||
|  | |||||||
| @ -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(): | ||||||
|  | |||||||
							
								
								
									
										63
									
								
								jon/auth.py
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								jon/auth.py
									
									
									
									
									
								
							| @ -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 = [ | |||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | # 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]] | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         self.location = None | ||||||
|  |         self.orders = [] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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(): |     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 "token" in request.args: | ||||||
|             if request.args["token"] == ACCESS_TOKEN: |             if request.args["token"] == ACCESS_TOKEN: | ||||||
|             session["authenticated"] = () |                 login_user(the_one_and_only_user) | ||||||
|             # Reload the page without query parameters |             # Reload the page without query parameters | ||||||
|             return redirect(request.path) |             return redirect(request.path) | ||||||
| 
 | 
 | ||||||
|     # Don't deny any paths in `ALLOWED_PATHS` |         # Never deny any paths in `ALLOWED_PATHS` | ||||||
|         if request.path in ALLOWED_PATHS: |         if request.path in ALLOWED_PATHS: | ||||||
|             return |             return | ||||||
| 
 | 
 | ||||||
|     if not "authenticated" in session: |         if not current_user.is_authenticated: | ||||||
|             return render_template("auth/denied.html"), 403 |             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("/") | ||||||
|  | |||||||
							
								
								
									
										28
									
								
								jon/entry.py
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								jon/entry.py
									
									
									
									
									
								
							| @ -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() | ||||||
|  | |||||||
| @ -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() | ||||||
|  | |||||||
| @ -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() | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -71,3 +71,6 @@ th { | |||||||
|   display: block; |   display: block; | ||||||
|   width: 8em; |   width: 8em; | ||||||
| } | } | ||||||
|  | details { | ||||||
|  |   font-size: 0.8em; | ||||||
|  | } | ||||||
|  | |||||||
| @ -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 %} | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user