Add food item management and improve barcode workflow

Implemented routes and forms for adding and viewing food items by barcode, including templates for displaying and entering nutritional information. Enhanced the scan workflow to redirect to food item details or entry form. Added admin ability to delete food items. Improved UI for login and scan pages. Updated FoodItem model and form fields for consistency and accuracy.
This commit is contained in:
2025-06-29 08:39:25 +02:00
parent 0919048cfd
commit a39f54dbb0
10 changed files with 260 additions and 90 deletions

66
app.py
View File

@@ -5,10 +5,11 @@ from flask_login import (
login_user,
current_user,
)
from forms import LoginForm
from forms import LoginForm, FoodItemForm
from models import User, FoodItem
from application import db, app, login_manager
from application.admin.routes import admin_bp
from typing import Optional
# Config
app.config["SECRET_KEY"] = "Iman"
@@ -28,7 +29,16 @@ app.register_blueprint(admin_bp)
# Routes
def default_return(next_page: Optional[str] = None):
if next_page:
return redirect(next_page)
if current_user.is_admin:
return redirect(url_for("admin.food_items"))
return redirect(url_for("dashboard"))
@app.route("/")
@login_required
def index():
return render_template("index.html")
@@ -36,7 +46,7 @@ def index():
@app.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("dashboard"))
return default_return()
form = LoginForm()
if form.validate_on_submit():
@@ -45,7 +55,7 @@ def login():
# User found and password correct
next_page = request.args.get("next") # Get next page if given
login_user(user) # Log in the user
return redirect(next_page or url_for("dashboard"))
return default_return(next_page=next_page)
else:
pass
# invalid user
@@ -81,6 +91,56 @@ def nutri(barcode):
return jsonify({})
@app.route("/food_item/<int:barcode>", methods=["GET"])
@login_required
def food_item(barcode):
food = FoodItem.query.filter_by(barcode=barcode).first()
if food:
return render_template("food_item.html", item=food)
else:
return render_template(
"add_food_item.html",
barcode=barcode,
form=FoodItemForm(barcode=barcode),
)
@app.route("/add_food_item", methods=["POST"])
@login_required
def add_food_item():
form = FoodItemForm()
if form.validate_on_submit():
print("[DEBUG] Valid form")
if FoodItem.query.filter_by(barcode=form.barcode.data).first() is None:
assert form.name.data is not None
assert form.energy.data is not None
assert form.protein.data is not None
assert form.carbs.data is not None
assert form.fat.data is not None
assert form.barcode.data is not None
db.session.add(
FoodItem(
name=form.name.data,
energy=form.energy.data,
protein=form.protein.data,
carbs=form.carbs.data,
fats=form.fat.data,
barcode=form.barcode.data,
saturated_fats=form.saturated_fat.data,
sugar=form.sugar.data,
)
)
db.session.commit()
print("[DEBUG] New item added")
else:
print("[DEBUG] Invalid form")
if form.barcode.data:
return redirect(url_for("food_item", barcode=form.barcode.data))
else:
return redirect(url_for("scan"))
# Run
if __name__ == "__main__":

View File

@@ -1,6 +1,7 @@
from flask import Blueprint, render_template, abort
from flask import Blueprint, render_template, abort, redirect, url_for
from flask_login import current_user
from models import FoodItem
from application import db
admin_bp = Blueprint(
"admin",
@@ -25,3 +26,12 @@ def food_items():
@admin_bp.route("/barcode_test", methods=["GET"])
def barcode_test():
return render_template("barcode_test.html")
@admin_bp.route("/delete_food/<int:id>", methods=["POST"])
def delete_food(id):
item = FoodItem.query.get(id)
if item:
db.session.delete(item)
db.session.commit()
return redirect(url_for("admin.food_items"))

View File

@@ -18,6 +18,7 @@ Food Nutritional Info
<th>Sugars (g)</th>
<th>Carbs (g)</th>
<th>Protein (g)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@@ -30,6 +31,12 @@ Food Nutritional Info
<td>{{ food.sugar_100g }}</td>
<td>{{ food.carbs_100g }}</td>
<td>{{ food.protein_100g }}</td>
<td>
<form method="POST" action="{{ url_for('admin.delete_food', id=food.id) }}"
onsubmit="return confirm('Are you sure you want to delete this item?');">
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block content %}
<form method="POST" action="{{ url_for('add_food_item') }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.barcode.label(class="form-label") }}
{{ form.barcode(class="form-control", readonly=true, style="background-color: #e9ecef; cursor: not-allowed;") }}
</div>
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
</div>
<div class="mb-3">
{{ form.energy.label(class="form-label") }}
{{ form.energy(class="form-control") }}
</div>
<div class="mb-3">
{{ form.protein.label(class="form-label") }}
{{ form.protein(class="form-control") }}
</div>
<div class="mb-3">
{{ form.carbs.label(class="form-label") }}
{{ form.carbs(class="form-control") }}
</div>
<div class="mb-3">
{{ form.sugar.label(class="form-label") }}
{{ form.sugar(class="form-control") }}
</div>
<div class="mb-3">
{{ form.fat.label(class="form-label") }}
{{ form.fat(class="form-control") }}
</div>
<div class="mb-3">
{{ form.saturated_fat.label(class="form-label") }}
{{ form.saturated_fat(class="form-control") }}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
{% endblock%}

View File

@@ -10,7 +10,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light p-4">
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid">
@@ -20,8 +20,14 @@
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('scan') }}">Scan</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
</li>

View File

@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block content %}
<div class="card mb-4" style="max-width: 600px;">
<div class="card-header">
<h5 class="card-title mb-0">{{ item.name }}</h5>
<small class="text-muted">Barcode: {{ item.barcode }}</small>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-5">Energy per 100g</dt>
<dd class="col-sm-7">{{ item.energy_100g }} kcal</dd>
<dt class="col-sm-5">Protein per 100g</dt>
<dd class="col-sm-7">{{ "%.1f"|format(item.protein_100g) }} g</dd>
<dt class="col-sm-5">Carbohydrates per 100g</dt>
<dd class="col-sm-7">{{ "%.1f"|format(item.carbs_100g) }} g</dd>
<dt class="col-sm-5">Sugar per 100g</dt>
<dd class="col-sm-7">
{% if item.sugar_100g is not none %}
{{ "%.1f"|format(item.sugar_100g) }} g
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</dd>
<dt class="col-sm-5">Fats per 100g</dt>
<dd class="col-sm-7">{{ "%.1f"|format(item.fats_100g) }} g</dd>
<dt class="col-sm-5">Saturated fats per 100g</dt>
<dd class="col-sm-7">
{% if item.saturated_fats_100g is not none %}
{{ "%.1f"|format(item.saturated_fats_100g) }} g
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</dd>
</dl>
</div>
</div>
{% endblock%}

View File

@@ -1,10 +1,36 @@
{% extends "base.html" %}
{% block content %}
<form method="post">
{{form.hidden_tag()}}
{{form.username()}}
{{form.password()}}
{{form.submit()}}
</form>
<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

@@ -1,76 +1,32 @@
{% extends "base.html" %}
{% block content %}
<div id="main" class="container text-center">
<div class="container py-5">
<div class="text-center mb-4">
<h1 class="fw-bold">📷 ZXing Barcode Scanner</h1>
<p class="text-muted">Use your camera to scan barcodes in real time.</p>
</div>
<div class="d-flex justify-content-center mb-4">
<video id="video" class="border rounded shadow-sm" style="width: 100%; max-width: 500px;" autoplay
muted></video>
</div>
<div class="d-flex justify-content-center">
<button id="startButton" class="btn btn-primary px-4">Start Scanning</button>
<button id="stopButton" class="btn btn-danger px-4 ms-3">Stop</button>
</div>
</div>
<template id="scan_result">
<div>
<h5>Result:</h5>
<p id="result" class="fw-bold text-success"></p>
</div>
<div id="product-info"></div>
</template>
<template id="template_Reader">
<h1 class="mb-4">📷 ZXing Barcode Scanner</h1>
<div class="mb-3">
<video id="video" class="border rounded shadow-sm" width="100%" style="max-width: 500px;"></video>
</div>
<div class="mb-3">
<button id="startButton" class="btn btn-primary">Start Scanning</button>
<button id="stopButton" class="btn btn-danger ms-2">Stop</button>
</div>
</template>
<script type="module">
import { BrowserMultiFormatReader } from 'https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/+esm';
const mainElement = document.getElementById('main');
const readerTemplate = document.getElementById('template_Reader')
const readerClone = readerTemplate.content.cloneNode(true);
const resultTemplate = document.getElementById('scan_result')
const resultClone = resultTemplate.content.cloneNode(true);
mainElement.appendChild(readerClone);
// constants
const codeReader = new BrowserMultiFormatReader();
const videoElement = document.getElementById('video');
async function fetchProductData(barcode) {
// Step 1: GET Data
const response = await fetch(`/nutri/${barcode}`);
// Step 2: Check if response wasn't ok
if (!response.ok) throw new Error('Network response was not ok');
// Step 3: Convert response to json and check if empty
const nutritionData = await response.json();
mainElement.innerHTML = '';
mainElement.appendChild(resultClone);
const resultElement = document.getElementById('result');
resultElement.textContent = barcode;
const container = document.getElementById('product-info');
if (Object.keys(nutritionData).length === 0) {
// No data, enter new data
}
else {
container.innerHTML = `
<h2>${nutritionData.name}</h2>
<p><strong>Barcode:</strong> ${nutritionData.barcode}</p>
<p><strong>Energy:</strong> ${nutritionData.energy_100g} kcal per 100g</p>
<p><strong>Carbs:</strong> ${nutritionData.carbs_100g} g</p>
<p><strong>Sugar:</strong> ${nutritionData.sugar_100g} g</p>
<p><strong>Fats:</strong> ${nutritionData.fats_100g} g</p>
<p><strong>Saturated Fats:</strong> ${nutritionData.saturated_fats_100g} g</p>
<p><strong>Protein:</strong> ${nutritionData.protein_100g} g</p>
`;
}
}
// Start scanning for barcode
document.getElementById('startButton').addEventListener('click', async () => {
console.log('[DEBUG] Start button clicked')
@@ -89,20 +45,14 @@
await codeReader.decodeFromVideoDevice(selectedDeviceId, videoElement, async (result, err) => {
if (result) {
const codeText = result.getText();
try {
await fetchProductData(codeText)
codeReader.reset();
} catch (error) {
console.error('Error fetching nutrition data:', error);
}
const baseURL = "{{url_for('food_item', barcode='0')}}"
window.location.href = baseURL.replace("0", encodeURIComponent(codeText))
}
});
});
document.getElementById('stopButton').addEventListener('click', () => {
codeReader.reset();
resultElement.textContent = '';
container.innerHTML = ""
});
</script>
{% endblock %}

View File

@@ -1,9 +1,27 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired
from wtforms import (
StringField,
PasswordField,
SubmitField,
IntegerField,
FloatField,
)
from wtforms.validators import DataRequired, InputRequired
class LoginForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
password = PasswordField("Password", validators=[DataRequired()])
submit = SubmitField("Login")
class FoodItemForm(FlaskForm):
barcode = IntegerField("Barcode", validators=[InputRequired()])
name = StringField("Product Name", validators=[DataRequired()])
energy = IntegerField("Energy per 100g", validators=[InputRequired()])
protein = FloatField("protein per 100g", validators=[InputRequired()])
carbs = FloatField("carbs per 100g", validators=[InputRequired()])
sugar = FloatField("sugar per 100g")
fat = FloatField("fat per 100g", validators=[InputRequired()])
saturated_fat = FloatField("saturated_fat per 100g")
submit = SubmitField("Add Item")

View File

@@ -36,26 +36,26 @@ class Unit(db.Model):
class FoodItem(db.Model):
__tablename__ = "food_item"
id = db.Column(db.Integer, primary_key=True)
barcode = db.Column(db.Integer)
barcode = db.Column(db.Integer, nullable=False)
name = db.Column(db.String(150), unique=True, nullable=False)
energy_100g = db.Column(db.Integer, nullable=False)
protein_100g = db.Column(db.Float, nullable=False)
carbs_100g = db.Column(db.Integer, nullable=False)
sugar_100g = db.Column(db.Integer)
fats_100g = db.Column(db.Integer, nullable=False)
saturated_fats_100g = db.Column(db.Integer)
carbs_100g = db.Column(db.Float, nullable=False)
sugar_100g = db.Column(db.Float)
fats_100g = db.Column(db.Float, nullable=False)
saturated_fats_100g = db.Column(db.Float)
def __init__(
self,
name: str,
energy: int,
protein: float,
carbs: int,
fats: int,
sugar: Optional[int] = None,
saturated_fats: Optional[int] = None,
barcode: Optional[int] = None,
carbs: float,
fats: float,
barcode: int,
sugar: Optional[float] = None,
saturated_fats: Optional[float] = None,
):
self.name = name
self.energy_100g = energy