11 Commits

Author SHA1 Message Date
Stef
7b84ab980e Merge pull request #6 from StefBuwalda/development
Hotfix
2025-08-14 12:52:56 +02:00
4aca63671b Update scan_barcode_v2.html 2025-08-14 12:52:38 +02:00
bd0468a600 Merge branch 'main' into development 2025-08-14 12:47:16 +02:00
c1f46357f0 Update daily_log2.html 2025-08-14 12:47:08 +02:00
Stef
1680383d47 Merge pull request #5 from StefBuwalda/development
Switch to new daily log as main log
2025-08-14 12:46:41 +02:00
4c76496920 Content aware class fix 2025-08-14 12:46:12 +02:00
c3e9c8631d Remove part of day from remaining code 2025-08-14 12:32:00 +02:00
f7f6d23562 Removed old log for new log 2025-08-14 12:29:02 +02:00
4f1b5a5667 Refactor meal addition flow and improve date handling
Refactored add_meal_v2 routes to simplify and clarify the meal addition process, including renaming endpoints and templates, and introducing decorators for date and item selection. Updated daily_log2 to use user's timezone and display the selected date. Adjusted templates and barcode scan logic to match new routes and improved user experience.
2025-08-14 06:59:29 +02:00
d78f48710e Add meal v2 flow and user timezone support
Introduces a new add_meal_v2 blueprint with barcode scanning, item search, and improved meal logging UI. Adds user timezone support: login form now captures timezone, User model and database schema updated, and timezone is set on login. Refactors templates and forms to support these changes, and removes the old login template.
2025-08-14 06:08:17 +02:00
7ff345d3a2 Do not update pip 2025-08-14 04:07:15 +02:00
20 changed files with 210 additions and 380 deletions

View File

@@ -5,8 +5,6 @@ WORKDIR /app
COPY requirements.txt . COPY requirements.txt .
RUN python.exe -m pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .

6
app.py
View File

@@ -11,8 +11,8 @@ from models import User
from application import db, app, login_manager from application import db, app, login_manager
from application.admin.routes import admin_bp from application.admin.routes import admin_bp
from application.user.routes import user_bp from application.user.routes import user_bp
from application.add_meal.routes import bp as add_meal_bp
from application.auth.routes import bp as auth_bp from application.auth.routes import bp as auth_bp
from application.add_meal_v2.routes import bp as add_meal_v2_bp
from typing import Optional from typing import Optional
# Config # Config
@@ -29,13 +29,13 @@ def load_user(user_id: int):
# Register blueprints # Register blueprints
app.register_blueprint(admin_bp) app.register_blueprint(admin_bp)
app.register_blueprint(user_bp) app.register_blueprint(user_bp)
app.register_blueprint(add_meal_bp)
app.register_blueprint(auth_bp) app.register_blueprint(auth_bp)
app.register_blueprint(add_meal_v2_bp)
# Routes # Routes
def default_return(next_page: Optional[str] = None): def default_return(next_page: Optional[str] = None):
return redirect(url_for("user.daily_log")) return redirect(url_for("user.daily_log2"))
if next_page: if next_page:
return redirect(next_page) return redirect(next_page)
if current_user.is_admin: if current_user.is_admin:

View File

@@ -6,50 +6,52 @@ from flask import (
session, session,
request, request,
jsonify, jsonify,
abort,
) )
from flask_login import current_user from flask_login import current_user
from forms import FoodItemForm, FoodLogForm from forms import FoodItemForm, FoodLogForm
from application import db from application import db
from models import FoodItem, FoodLog from models import FoodItem, FoodLog
from sqlalchemy import and_, or_ from sqlalchemy import and_, or_
from datetime import datetime, timedelta, timezone from datetime import datetime
from sqlalchemy.sql.elements import BinaryExpression from sqlalchemy.sql.elements import BinaryExpression
from typing import cast from typing import cast
bp = Blueprint( bp = Blueprint(
"add_meal", "add_meal_v2",
__name__, __name__,
url_prefix="/add_meal", url_prefix="/add_meal_v2",
template_folder="templates", template_folder="templates",
) )
def date_present(func):
def check_date():
if "selected_date" not in session:
return redirect(url_for("user.daily_log2"))
def item_selected(func):
def check_item():
if check_item():
if "item_id" not in session:
return redirect(url_for("add_meal_v2.get_barcode"))
@bp.before_request @bp.before_request
def login_required(): def login_required():
if not current_user.is_authenticated: if not current_user.is_authenticated:
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
@bp.route("/select_meal/<int:meal_type>", methods=["GET"]) @date_present
def step1(meal_type: int):
assert type(meal_type) is int
assert 0 <= meal_type <= 3
session["meal_type"] = meal_type
return redirect(url_for("add_meal.step2"))
@bp.route("/get_barcode", methods=["GET"]) @bp.route("/get_barcode", methods=["GET"])
def step2(): def get_barcode():
return render_template("scan_barcode.html") return render_template("scan_barcode_v2.html")
@bp.route("/step3/<string:input>", methods=["GET"]) @date_present
def step3(input: str): @bp.route("/add_existing/<string:input>", methods=["GET"])
# check if meal_type cookie is set def add_existing(input: str):
if "meal_type" not in session:
return redirect("/")
# Check if input is a barcode # Check if input is a barcode
if input.isdigit(): if input.isdigit():
item = current_user.food_items.filter_by(barcode=input).first() item = current_user.food_items.filter_by(barcode=input).first()
@@ -59,15 +61,16 @@ def step3(input: str):
if item is None: if item is None:
# Does not exist, add item # Does not exist, add item
return redirect(url_for("add_meal.step3_alt1", input=input)) return redirect(url_for("add_meal_v2.add_new", input=input))
# Track item to add and continue to next step # Track item to add and continue to next step
session["item_id"] = item.id session["item_id"] = item.id
return redirect(url_for("add_meal.step4")) return redirect(url_for("add_meal_v2.step4"))
@bp.route("/step3_alt1/<string:input>", methods=["GET"]) @date_present
def step3_alt1(input: str): @bp.route("/add_new/<string:input>", methods=["GET"])
def add_new(input: str):
form = FoodItemForm() form = FoodItemForm()
if input.isdigit(): if input.isdigit():
@@ -77,8 +80,9 @@ def step3_alt1(input: str):
return render_template("add_item.html", form=form) return render_template("add_item.html", form=form)
@bp.route("/step3_alt1/<string:input>", methods=["POST"]) @date_present
def step3_alt1_post(input: str): @bp.route("/add_new/<string:input>", methods=["POST"])
def add_new_post(input: str):
form = FoodItemForm() form = FoodItemForm()
if form.validate_on_submit(): if form.validate_on_submit():
@@ -122,25 +126,20 @@ def step3_alt1_post(input: str):
print(f"Item exists: {item.barcode} {item.name}") print(f"Item exists: {item.barcode} {item.name}")
# Item added or already present, return to step 3. # Item added or already present, return to step 3.
return redirect(url_for("add_meal.step3", input=input)) return redirect(url_for("add_meal_v2.add_existing", input=input))
else: else:
print("[DEBUG] Form Invalid") print("[DEBUG] Form Invalid")
return redirect(url_for("add_meal.step3_alt1", input=input)) return redirect(url_for("add_meal_v2.add_new", input=input))
@date_present
@item_selected
@bp.route("/step4", methods=["GET", "POST"]) @bp.route("/step4", methods=["GET", "POST"])
def step4(): def step4():
if "item_id" not in session:
return redirect(url_for("add_meal.step2"))
form = FoodLogForm() form = FoodLogForm()
item = db.session.get(FoodItem, session["item_id"]) item = db.session.get(FoodItem, session["item_id"])
if not item:
offset = session["offset"] return redirect(url_for("add_meal_v2.get_barcode"))
if offset is None or item is None:
abort(404)
today = datetime.now(timezone.utc).date()
day = today + timedelta(days=offset)
if form.validate_on_submit(): if form.validate_on_submit():
assert form.amount.data assert form.amount.data
@@ -149,29 +148,20 @@ def step4():
food_item_id=item.id, food_item_id=item.id,
user_id=current_user.id, user_id=current_user.id,
amount=form.amount.data, amount=form.amount.data,
part_of_day=session["meal_type"], date_=datetime.strptime(
date_=day, session["selected_date"], "%Y-%m-%d"
).date(),
) )
) )
db.session.commit() db.session.commit()
session.pop("meal_type")
session.pop("item_id") session.pop("item_id")
return redirect(url_for("user.daily_log", offset=offset)) session.pop("selected_date")
return redirect(url_for("user.daily_log2"))
match session["meal_type"]: return render_template("step4.html", tod="idk", item=item, form=form)
case 0:
tod = "Breakfast"
case 1:
tod = "Lunch"
case 2:
tod = "Dinner"
case 3:
tod = "Snack"
case _:
tod = "Unknown"
return render_template("step4.html", tod=tod, item=item, form=form)
@date_present
@bp.route("/query", methods=["GET"]) @bp.route("/query", methods=["GET"])
def query(): def query():
q = request.args.get("q", "").strip().lower() q = request.args.get("q", "").strip().lower()

View File

@@ -58,7 +58,7 @@
return; return;
} }
fetch(`{{url_for("add_meal.query")}}?q=${encodeURIComponent(query)}`) fetch(`{{url_for("add_meal_v2.query")}}?q=${encodeURIComponent(query)}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
suggestionsBox.innerHTML = ''; suggestionsBox.innerHTML = '';
@@ -85,7 +85,7 @@
goButton.addEventListener('click', () => { goButton.addEventListener('click', () => {
const value = searchBox.value.trim(); const value = searchBox.value.trim();
if (value) { if (value) {
const baseURL = "{{url_for('add_meal.step3', input='!')}}"; const baseURL = "{{url_for('add_meal_v2.add_existing', input='!')}}";
window.location.href = baseURL.replace("!", encodeURIComponent(value)); window.location.href = baseURL.replace("!", encodeURIComponent(value));
} }
}); });
@@ -121,7 +121,7 @@
if (result) { if (result) {
// Result found, this should post the barcode // Result found, this should post the barcode
const codeText = result.getText(); const codeText = result.getText();
const baseURL = "{{url_for('add_meal.step3', input='!')}}"; const baseURL = "{{url_for('add_meal_v2.add_existing', input='!')}}";
window.location.href = baseURL.replace("!", encodeURIComponent(codeText)); window.location.href = baseURL.replace("!", encodeURIComponent(codeText));
} }
}) })

View File

@@ -1,8 +1,8 @@
from flask import Blueprint, request, render_template, redirect, url_for from flask import Blueprint, render_template, redirect, url_for
from flask_login import current_user, login_user, logout_user from flask_login import current_user, login_user, logout_user
from forms import LoginForm, ChangePasswordForm from forms import LoginForm, ChangePasswordForm
from models import User from models import User
from application.utils import default_return from application.utils import default_return, is_valid_timezone
from application import db from application import db
bp = Blueprint( bp = Blueprint(
@@ -19,12 +19,16 @@ def login():
form = LoginForm() form = LoginForm()
if form.validate_on_submit(): if form.validate_on_submit():
assert form.timezone.data
user = User.query.filter_by(username=form.username.data).first() user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(password=form.password.data): if user and user.check_password(password=form.password.data):
# User found and password correct # User found and password correct
next_page = request.args.get("next") # Get next page if given tz = form.timezone.data
if is_valid_timezone(tz):
user.set_timezone(tz)
db.session.commit()
login_user(user) # Log in the user login_user(user) # Log in the user
return default_return(next_page=next_page) return default_return()
else: else:
pass pass
# invalid user # invalid user

View File

@@ -3,7 +3,10 @@
{% block content %} {% block content %}
<div class="container d-flex justify-content-center align-items-center"> <div class="container d-flex justify-content-center align-items-center">
<div class="card shadow-sm p-4" style="width: 100%; max-width: 400px;"> <div class="card shadow-sm p-4" style="width: 100%; max-width: 400px;">
<h3 class="mb-4 text-center">Login</h3> <h3 class="mb-1 text-center">Login</h3>
<p class="text-center text-muted small mb-4">
Your timezone will be saved to show times correctly.
</p>
<form method="post"> <form method="post">
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
@@ -27,6 +30,8 @@
{% endif %} {% endif %}
</div> </div>
{{ form.timezone(id="timezone") }}
<div class="d-grid"> <div class="d-grid">
{{ form.submit(class="btn btn-primary btn-lg") }} {{ form.submit(class="btn btn-primary btn-lg") }}
</div> </div>
@@ -34,3 +39,12 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', () => {
const tzField = document.getElementById('timezone');
tzField.value = Intl.DateTimeFormat().resolvedOptions().timeZone;
});
</script>
{% endblock %}

View File

@@ -22,9 +22,6 @@
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<div class="d-flex w-100"> <div class="d-flex w-100">
<ul class="navbar-nav flex-grow-1"> <ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('user.daily_log') }}">Daily Log</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('user.daily_log2') }}">Daily Log (new)</a> <a class="nav-link" href="{{ url_for('user.daily_log2') }}">Daily Log (new)</a>
</li> </li>
@@ -83,6 +80,9 @@
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}
{% endblock %}
</body> </body>
</html> </html>

View File

@@ -1,36 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container d-flex justify-content-center align-items-center">
<div class="card shadow-sm p-4" style="width: 100%; max-width: 400px;">
<h3 class="mb-4 text-center">Login</h3>
<form method="post" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control", placeholder="Enter username") }}
{% if form.username.errors %}
<div class="text-danger small">
{{ form.username.errors[0] }}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control", placeholder="Enter password") }}
{% if form.password.errors %}
<div class="text-danger small">
{{ form.password.errors[0] }}
</div>
{% endif %}
</div>
<div class="d-grid">
{{ form.submit(class="btn btn-primary btn-lg") }}
</div>
</form>
</div>
</div>
{% endblock%}

View File

@@ -11,10 +11,10 @@ from flask_login import current_user
from application import db from application import db
from forms import FoodItemForm from forms import FoodItemForm
from models import FoodItem, FoodLog from models import FoodItem, FoodLog
from datetime import datetime, timezone, timedelta from datetime import datetime
from application.utils import login_required from application.utils import login_required
from typing import cast
from numpy import array from numpy import array
from zoneinfo import ZoneInfo
user_bp = Blueprint( user_bp = Blueprint(
"user", "user",
@@ -119,50 +119,33 @@ def edit_food_item(id: int):
return redirect(url_for("user.dashboard")) return redirect(url_for("user.dashboard"))
@user_bp.route("/", methods=["GET"])
@user_bp.route("/<offset>", methods=["GET"])
def daily_log(offset: int = 0):
try:
offset = int(offset)
except ValueError:
abort(400) # or handle invalid input
today = datetime.now(timezone.utc).date()
day = today + timedelta(days=offset)
session["offset"] = offset
logs_today = current_user.food_logs.filter_by(date_=day).all()
logs = [[], [], [], []]
calories: float = 0
protein: float = 0
carbs: float = 0
fat: float = 0
for log in logs_today:
logs[log.part_of_day].append(log)
calories += log.amount * log.food_item.energy_100 / 100
protein += log.amount * log.food_item.protein_100 / 100
carbs += log.amount * log.food_item.carbs_100 / 100
fat += log.amount * log.food_item.fat_100 / 100
return render_template(
"daily_log.html",
date=(day.strftime("%d/%m/%y")),
logs=logs,
calories=calories,
protein=protein,
carbs=carbs,
fat=fat,
offset=offset,
)
@user_bp.route("/daily_log2", methods=["GET"]) @user_bp.route("/daily_log2", methods=["GET"])
def daily_log2(): def daily_log2():
today = datetime.now(timezone.utc).date() # Get today's date according to user's timezone
logs_today = current_user.food_logs.filter_by(date_=today).all() today = datetime.now(ZoneInfo(current_user.timezone)).date()
# Save date in session
session["selected_date"] = today.isoformat()
# Get logs from today
logs_today = current_user.food_logs.filter_by(
date_=today.isoformat()
).all()
# calculate macros
macros = array((0.0, 0.0, 0.0, 0.0)) macros = array((0.0, 0.0, 0.0, 0.0))
for log in logs_today: for log in logs_today:
item = cast(FoodItem, log.food_item) macros += array(log.food_item.macros()) / 100 * log.amount
macros += array(item.macros()) / 100 * log.amount
macros = macro_arr_to_json(macros.tolist()) macros = macro_arr_to_json(macros.tolist())
return render_template("daily_log2.html", macros=macros, logs=logs_today)
# Render HTML
return render_template(
"daily_log2.html",
macros=macros,
logs=logs_today,
today=today.strftime("%d/%m/%Y"),
min=min,
)
@user_bp.route("/remove_log/<int:id>", methods=["POST"]) @user_bp.route("/remove_log/<int:id>", methods=["POST"])
@@ -175,6 +158,4 @@ def remove_log(id: int):
# Delete log # Delete log
db.session.delete(log) db.session.delete(log)
db.session.commit() db.session.commit()
if "offset" in session: return redirect(url_for("user.daily_log2"))
return redirect(url_for("user.daily_log", offset=session["offset"]))
return redirect(url_for("user.daily_log"))

View File

@@ -1,211 +0,0 @@
{% extends "base.html" %}
{% block title %}
Food Nutritional Info
{% endblock %}
{% block content %}
<!-- Daily Overview Section -->
<div class="container">
<div class="mb-4 p-3 border rounded d-flex align-items-center justify-content-between">
<!-- Previous Day Button -->
<form method="get" action="{{url_for('user.daily_log', offset=offset - 1)}}" class="m-0">
<button type="submit" class="btn btn-outline-primary d-flex align-items-center justify-content-center"
style="width: 40px; height: 40px;">
&laquo;
</button>
</form>
<!-- Main Content -->
<div class="flex-grow-1 text-center">
<h2>Daily Overview ({{date}})</h2>
<!-- Row 1 -->
<div class="row justify-content-center text-center mb-2">
<div class="col-auto">
<strong>Calories:</strong> {{ '%.0f' % calories }} kcal
</div>
<div class="col-auto">
<strong>Protein:</strong> {{ '%.1f' % protein }} g
</div>
</div>
<!-- Row 2 -->
<div class="row justify-content-center text-center">
<div class="col-auto">
<strong>Carbs:</strong> {{ '%.1f' % carbs }} g
</div>
<div class="col-auto">
<strong>Fat:</strong> {{ '%.1f' % fat }} g
</div>
</div>
</div>
<!-- Next Day Button -->
<form method="get" action="{{url_for('user.daily_log', offset=offset + 1)}}" class="m-0">
<button type="submit" class="btn btn-outline-primary d-flex align-items-center justify-content-center"
style="width: 40px; height: 40px;">
&raquo;
</button>
</form>
</div>
<div class="p-3 border rounded">
<div class="text-center">
<h2>Eaten today</h2>
</div>
<div class="p-3 mb-2 border rounded">
<!-- Header row (centered vertically for consistency) -->
<div class="row align-items-center mb-2">
<div class="col">
<h4 class="mb-0">Breakfast</h4>
</div>
<div class="col-auto text-end" style="min-width: 80px;">
<h4 class="mb-0">Amount</h4>
</div>
<div class="col-auto text-end" style="min-width: 80px;">
<a href="{{ url_for('add_meal.step1', meal_type=0) }}"
class="btn btn-sm btn-primary px-3 py-1">Add</a>
</div>
</div>
<!-- Data rows -->
<div>
{% for log in logs[0] %}
<div class="row mb-2">
<div class="col text-wrap">
{{ log.food_item.name }}
</div>
<div class="col-auto text-end align-self-start" style="min-width: 80px;">
{{ "{:g}".format(log.amount) }}
</div>
<div class="col-auto text-end align-self-start" style="min-width: 80px;">
<form method="POST" action="{{url_for('user.remove_log', id=log.id)}}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger px-3 py-1"
title="Delete">&times;</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="p-3 mb-2 border rounded">
<!-- Header row (centered vertically for consistency) -->
<div class="row align-items-center mb-2">
<div class="col">
<h4 class="mb-0">Lunch</h4>
</div>
<div class="col-auto text-end" style="min-width: 80px;">
<h4 class="mb-0">Amount</h4>
</div>
<div class="col-auto text-end" style="min-width: 80px;">
<a href="{{ url_for('add_meal.step1', meal_type=1) }}"
class="btn btn-sm btn-primary px-3 py-1">Add</a>
</div>
</div>
<!-- Data rows -->
<div>
{% for log in logs[1] %}
<div class="row mb-2">
<div class="col text-wrap">
{{ log.food_item.name }}
</div>
<div class="col-auto text-end align-self-start" style="min-width: 80px;">
{{ "{:g}".format(log.amount) }}
</div>
<div class="col-auto text-end align-self-start" style="min-width: 80px;">
<form method="POST" action="{{url_for('user.remove_log', id=log.id)}}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger px-3 py-1"
title="Delete">&times;</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="p-3 mb-2 border rounded">
<!-- Header row (centered vertically for consistency) -->
<div class="row align-items-center mb-2">
<div class="col">
<h4 class="mb-0">Dinner</h4>
</div>
<div class="col-auto text-end" style="min-width: 80px;">
<h4 class="mb-0">Amount</h4>
</div>
<div class="col-auto text-end" style="min-width: 80px;">
<a href="{{ url_for('add_meal.step1', meal_type=2) }}"
class="btn btn-sm btn-primary px-3 py-1">Add</a>
</div>
</div>
<!-- Data rows -->
<div>
{% for log in logs[2] %}
<div class="row mb-2">
<div class="col text-wrap">
{{ log.food_item.name }}
</div>
<div class="col-auto text-end align-self-start" style="min-width: 80px;">
{{ "{:g}".format(log.amount) }}
</div>
<div class="col-auto text-end align-self-start" style="min-width: 80px;">
<form method="POST" action="{{url_for('user.remove_log', id=log.id)}}" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger px-3 py-1"
title="Delete">&times;</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
<div class="p-3 mb-2 border rounded">
<!-- Header row (centered vertically for consistency) -->
<div class="row align-items-center mb-2">
<div class="col">
<h4 class="mb-0">Snacks</h4>
</div>
<div class="col-auto text-end" style="min-width: 80px;">
<h4 class="mb-0">Amount</h4>
</div>
<div class="col-auto text-end" style="min-width: 80px;">
<a href="{{ url_for('add_meal.step1', meal_type=3) }}"
class="btn btn-sm btn-primary px-3 py-1">Add</a>
</div>
</div>
<!-- Data rows -->
<div>
{% for log in logs[3] %}
<div class="row mb-2">
<div class="col text-wrap">
{{ log.food_item.name }}
</div>
<div class="col-auto text-end align-self-start" style="min-width: 80px;">
{{ "{:g}".format(log.amount) }}
</div>
<div class="col-auto text-end align-self-start" style="min-width: 80px;">
<form method="POST" action="{{url_for('user.remove_log', id=log.id)}}" class="d-inline"
onsubmit="return confirm('Are you sure you want to delete this item?');">
<button type="submit" class="btn btn-sm btn-danger px-3 py-1"
title="Delete">&times;</button>
</form>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock%}

View File

@@ -3,10 +3,8 @@
{% block title %}Daily Calorie Dashboard{% endblock %} {% block title %}Daily Calorie Dashboard{% endblock %}
{% block content %} {% block content %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/macros.css') }}">
<div class="container my-4"> <div class="container my-4">
<h2 class="mb-3">Daily Calorie Dashboard</h2> <h2 class="mb-3">Daily Calorie Dashboard ({{ today }})</h2>
<!-- Macro Summary --> <!-- Macro Summary -->
<div class="card p-3 mb-3"> <div class="card p-3 mb-3">
@@ -17,10 +15,10 @@
<div class="progress rounded" style="height: 24px;"> <div class="progress rounded" style="height: 24px;">
<div class="progress-bar bg-danger macro-bar" role="progressbar" <div class="progress-bar bg-danger macro-bar" role="progressbar"
style="width: {{ macro.bar_width_overflow }}%"> style="width: {{ macro.bar_width_overflow }}%">
{{ macro.current - macro.target }}{{ macro.unit }} {{ (macro.current - macro.target) }}{{ macro.unit }}
</div> </div>
<div class="progress-bar bg-success macro-bar" role="progressbar" style="width: {{ macro.bar_width }}%"> <div class="progress-bar bg-success macro-bar" role="progressbar" style="width: {{ macro.bar_width }}%">
{{ macro.current }}{{ macro.unit }} {{ min(macro.current, macro.target) }}{{ macro.unit }}
</div> </div>
</div> </div>
</div> </div>
@@ -32,9 +30,9 @@
<h5>Items Eaten Today</h5> <h5>Items Eaten Today</h5>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{% for log in logs %} {% for log in logs %}
<div class="list-group-item item-row d-flex justify-content-between align-items-center bg-dark text-light"> <div class="list-group-item item-row d-flex justify-content-between align-items-center">
<span>({{ log.amount }}g) {{ log.food_item.name }}</span> <span>({{ log.amount }}g) {{ log.food_item.name }}</span>
<span>{{ log.food_item.energy_100 }} kcal</span> <span>{{ log.food_item.energy_100 * log.amount / 100 }} kcal</span>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
@@ -47,18 +45,18 @@
<div class="container-fluid fixed-bottom py-2"> <div class="container-fluid fixed-bottom py-2">
<div class="d-flex p-3"> <div class="d-flex p-3">
<!-- Left Button --> <!-- Left Button -->
<a href="" class="btn btn-outline-light flex-fill me-2 rounded-pill"> <a href="" class="btn card flex-fill me-2 rounded-pill">
Previous Previous
</a> </a>
<!-- Center Button (highlighted) --> <!-- Center Button (highlighted) -->
<a href="{{ url_for('add_meal.step1', meal_type=0) }}" <a id="set_link_date" href="{{ url_for('add_meal_v2.get_barcode') }}"
class="btn btn-success flex-fill mx-2 fw-bold rounded-pill"> class="btn btn-success flex-fill mx-2 fw-bold rounded-pill">
Add Item Add Item
</a> </a>
<!-- Right Button --> <!-- Right Button -->
<a href="" class="btn btn-outline-light flex-fill ms-2 rounded-pill"> <a href="" class="btn card flex-fill ms-2 rounded-pill">
Next Next
</a> </a>
</div> </div>

View File

@@ -1,6 +1,7 @@
from flask_login import current_user from flask_login import current_user
from flask import redirect, url_for, flash from flask import redirect, url_for, flash
from typing import Optional from typing import Optional
from zoneinfo import ZoneInfo
def login_required(): def login_required():
@@ -13,9 +14,18 @@ def login_required():
def default_return(next_page: Optional[str] = None): def default_return(next_page: Optional[str] = None):
return redirect(url_for("user.daily_log")) return redirect(url_for("user.daily_log2"))
if next_page: if next_page:
return redirect(next_page) return redirect(next_page)
if current_user.is_admin: if current_user.is_admin:
return redirect(url_for("admin.food_items")) return redirect(url_for("admin.food_items"))
return redirect(url_for("dashboard")) return redirect(url_for("dashboard"))
def is_valid_timezone(tz: str) -> bool:
try:
ZoneInfo(tz)
except Exception:
print(Exception)
return False
return True

View File

@@ -4,13 +4,20 @@ from wtforms import (
PasswordField, PasswordField,
SubmitField, SubmitField,
FloatField, FloatField,
HiddenField,
) )
from wtforms.validators import DataRequired, InputRequired, Optional from wtforms.validators import DataRequired, InputRequired, Optional
class SelectDateForm(FlaskForm):
date = HiddenField(validators=[DataRequired()])
submit = SubmitField(" Add Item")
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()]) username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()]) password = PasswordField("Password", validators=[DataRequired()])
timezone = HiddenField("Timezone", validators=[DataRequired()])
submit = SubmitField("Log in") submit = SubmitField("Log in")

View File

@@ -0,0 +1,32 @@
"""empty message
Revision ID: 65eaeafb0904
Revises: 9eb23abd5294
Create Date: 2025-08-14 12:28:56.157288
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '65eaeafb0904'
down_revision = '9eb23abd5294'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('food_log', schema=None) as batch_op:
batch_op.drop_column('part_of_day')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('food_log', schema=None) as batch_op:
batch_op.add_column(sa.Column('part_of_day', sa.INTEGER(), nullable=False))
# ### end Alembic commands ###

View File

@@ -0,0 +1,40 @@
"""empty message
Revision ID: 9eb23abd5294
Revises: 101002a6ef17
Create Date: 2025-08-14 05:40:27.342711
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "9eb23abd5294"
down_revision = "101002a6ef17"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"timezone",
sa.String(length=64),
nullable=False,
server_default="UTC",
)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_column("timezone")
# ### end Alembic commands ###

View File

@@ -4,6 +4,7 @@ from application import db
from typing import Optional from typing import Optional
from forms import FoodItemForm from forms import FoodItemForm
from datetime import datetime, timezone, date from datetime import datetime, timezone, date
from application.utils import is_valid_timezone
class User(UserMixin, db.Model): class User(UserMixin, db.Model):
@@ -11,6 +12,7 @@ class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False) username = db.Column(db.String(150), unique=True, nullable=False)
password = db.Column(db.String, nullable=False) password = db.Column(db.String, nullable=False)
timezone = db.Column(db.String(64), nullable=False, default="UTC")
is_admin = db.Column(db.Boolean, nullable=False, default=False) is_admin = db.Column(db.Boolean, nullable=False, default=False)
must_change_password = db.Column(db.Boolean, nullable=False, default=False) must_change_password = db.Column(db.Boolean, nullable=False, default=False)
@@ -39,6 +41,10 @@ class User(UserMixin, db.Model):
def set_pw_change(self, change: bool) -> None: def set_pw_change(self, change: bool) -> None:
self.must_change_password = change self.must_change_password = change
def set_timezone(self, tz: str) -> None:
if is_valid_timezone(tz):
self.timezone = tz
class Unit(db.Model): class Unit(db.Model):
__tablename__ = "unit" __tablename__ = "unit"
@@ -128,7 +134,6 @@ class FoodLog(db.Model):
food_item_id = db.Column( food_item_id = db.Column(
db.Integer, db.ForeignKey("food_item.id"), nullable=False db.Integer, db.ForeignKey("food_item.id"), nullable=False
) )
part_of_day = db.Column(db.Integer, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
amount = db.Column(db.Float, nullable=False) amount = db.Column(db.Float, nullable=False)
@@ -137,7 +142,6 @@ class FoodLog(db.Model):
food_item_id: int, food_item_id: int,
user_id: int, user_id: int,
amount: float, amount: float,
part_of_day: int,
date_: Optional[date] = None, date_: Optional[date] = None,
): ):
super().__init__() super().__init__()
@@ -146,4 +150,3 @@ class FoodLog(db.Model):
self.amount = amount self.amount = amount
if date_ is not None: if date_ is not None:
self.date_ = date_ self.date_ = date_
self.part_of_day = part_of_day

10
seed.py
View File

@@ -22,10 +22,10 @@ with app.app_context():
) )
FoodLog.query.delete() FoodLog.query.delete()
db.session.add(FoodLog(1, 1, 200, 0)) db.session.add(FoodLog(1, 1, 200))
db.session.add(FoodLog(1, 1, 200, 1)) db.session.add(FoodLog(1, 1, 200))
db.session.add(FoodLog(1, 1, 200, 2)) db.session.add(FoodLog(1, 1, 200))
db.session.add(FoodLog(1, 1, 200, 3)) db.session.add(FoodLog(1, 1, 200))
db.session.add(FoodLog(1, 1, 100, 1)) db.session.add(FoodLog(1, 1, 100))
db.session.commit() db.session.commit()