mirror of
https://github.com/StefBuwalda/cal_counter.git
synced 2025-10-29 19:00:00 +00:00
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.
This commit is contained in:
2
app.py
2
app.py
@@ -13,6 +13,7 @@ from application.admin.routes import admin_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.add_meal_v2.routes import bp as add_meal_v2_bp
|
||||
from typing import Optional
|
||||
|
||||
# Config
|
||||
@@ -31,6 +32,7 @@ app.register_blueprint(admin_bp)
|
||||
app.register_blueprint(user_bp)
|
||||
app.register_blueprint(add_meal_bp)
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(add_meal_v2_bp)
|
||||
|
||||
|
||||
# Routes
|
||||
|
||||
187
application/add_meal_v2/routes.py
Normal file
187
application/add_meal_v2/routes.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from flask import (
|
||||
Blueprint,
|
||||
redirect,
|
||||
url_for,
|
||||
render_template,
|
||||
session,
|
||||
request,
|
||||
jsonify,
|
||||
abort,
|
||||
)
|
||||
from flask_login import current_user
|
||||
from forms import FoodItemForm, FoodLogForm
|
||||
from application import db
|
||||
from models import FoodItem, FoodLog
|
||||
from sqlalchemy import and_, or_
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from sqlalchemy.sql.elements import BinaryExpression
|
||||
from typing import cast
|
||||
|
||||
bp = Blueprint(
|
||||
"add_meal_v2",
|
||||
__name__,
|
||||
url_prefix="/add_meal_v2",
|
||||
template_folder="templates",
|
||||
)
|
||||
|
||||
|
||||
@bp.before_request
|
||||
def login_required():
|
||||
if not current_user.is_authenticated:
|
||||
return redirect(url_for("auth.login"))
|
||||
|
||||
|
||||
@bp.route("/select_meal/<int:meal_type>", methods=["GET"])
|
||||
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"])
|
||||
def step2():
|
||||
return render_template("scan_barcode.html")
|
||||
|
||||
|
||||
@bp.route("/step3/<string:input>", methods=["GET"])
|
||||
def step3(input: str):
|
||||
# check if meal_type cookie is set
|
||||
if "meal_type" not in session:
|
||||
return redirect("/")
|
||||
|
||||
# Check if input is a barcode
|
||||
if input.isdigit():
|
||||
item = current_user.food_items.filter_by(barcode=input).first()
|
||||
|
||||
else:
|
||||
item = current_user.food_items.filter_by(name=input).first()
|
||||
|
||||
if item is None:
|
||||
# Does not exist, add item
|
||||
return redirect(url_for("add_meal.step3_alt1", input=input))
|
||||
|
||||
# Track item to add and continue to next step
|
||||
session["item_id"] = item.id
|
||||
return redirect(url_for("add_meal.step4"))
|
||||
|
||||
|
||||
@bp.route("/step3_alt1/<string:input>", methods=["GET"])
|
||||
def step3_alt1(input: str):
|
||||
form = FoodItemForm()
|
||||
|
||||
if input.isdigit():
|
||||
form.barcode.data = input
|
||||
else:
|
||||
form.name.data = input
|
||||
return render_template("add_item.html", form=form)
|
||||
|
||||
|
||||
@bp.route("/step3_alt1/<string:input>", methods=["POST"])
|
||||
def step3_alt1_post(input: str):
|
||||
form = FoodItemForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
# Form has valid input
|
||||
barcode = form.barcode.data
|
||||
name = form.name.data
|
||||
assert name
|
||||
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
|
||||
|
||||
# Check if name or barcode already exists
|
||||
name_filter = cast(BinaryExpression, FoodItem.name == name)
|
||||
barcode_filter = cast(BinaryExpression, FoodItem.barcode == barcode)
|
||||
filter_exp = or_(name_filter, barcode_filter)
|
||||
item = current_user.food_items.filter(filter_exp).first()
|
||||
|
||||
if item is None:
|
||||
# Item does not exist, add to DB
|
||||
barcode = (
|
||||
barcode if barcode else None
|
||||
) # Turn empty strings into None
|
||||
db.session.add(
|
||||
FoodItem(
|
||||
name=name,
|
||||
owner_id=current_user.id,
|
||||
energy=form.energy.data,
|
||||
protein=form.protein.data,
|
||||
carbs=form.carbs.data,
|
||||
fat=form.fat.data,
|
||||
barcode=barcode,
|
||||
saturated_fat=form.saturated_fat.data,
|
||||
sugar=form.sugar.data,
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
print("[DEBUG] New FoodItem Added")
|
||||
input = barcode if barcode else name # update input
|
||||
else:
|
||||
print(f"Item exists: {item.barcode} {item.name}")
|
||||
|
||||
# Item added or already present, return to step 3.
|
||||
return redirect(url_for("add_meal.step3", input=input))
|
||||
else:
|
||||
print("[DEBUG] Form Invalid")
|
||||
return redirect(url_for("add_meal.step3_alt1", input=input))
|
||||
|
||||
|
||||
@bp.route("/step4", methods=["GET", "POST"])
|
||||
def step4():
|
||||
if "item_id" not in session:
|
||||
return redirect(url_for("add_meal.step2"))
|
||||
form = FoodLogForm()
|
||||
item = db.session.get(FoodItem, session["item_id"])
|
||||
|
||||
offset = session["offset"]
|
||||
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():
|
||||
assert form.amount.data
|
||||
db.session.add(
|
||||
FoodLog(
|
||||
food_item_id=item.id,
|
||||
user_id=current_user.id,
|
||||
amount=form.amount.data,
|
||||
part_of_day=session["meal_type"],
|
||||
date_=day,
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
session.pop("meal_type")
|
||||
session.pop("item_id")
|
||||
return redirect(url_for("user.daily_log", offset=offset))
|
||||
|
||||
match session["meal_type"]:
|
||||
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)
|
||||
|
||||
|
||||
@bp.route("/query", methods=["GET"])
|
||||
def query():
|
||||
q = request.args.get("q", "").strip().lower()
|
||||
if not q:
|
||||
return jsonify([])
|
||||
|
||||
words = q.split()
|
||||
filters = [
|
||||
FoodItem.name.ilike(f"%{word}%") for word in words # type: ignore
|
||||
]
|
||||
|
||||
results = current_user.food_items.filter(and_(*filters)).all()
|
||||
return jsonify([item.name for item in results])
|
||||
49
application/add_meal_v2/templates/add_item.html
Normal file
49
application/add_meal_v2/templates/add_item.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.barcode.label(class="form-label") }}
|
||||
{{ form.barcode(class="form-control-plaintext", readonly=true) }}
|
||||
</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.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>
|
||||
|
||||
<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.protein.label(class="form-label") }}
|
||||
{{ form.protein(class="form-control") }}
|
||||
</div>
|
||||
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</form>
|
||||
{% endblock%}
|
||||
134
application/add_meal_v2/templates/scan_barcode.html
Normal file
134
application/add_meal_v2/templates/scan_barcode.html
Normal file
@@ -0,0 +1,134 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-5">
|
||||
<!-- Header -->
|
||||
<div class="text-center mb-4">
|
||||
<h1 class="fw-bold">Barcode Scanner</h1>
|
||||
<p class="text-muted">Use your camera to scan barcodes</p>
|
||||
</div>
|
||||
|
||||
<!-- Video preview -->
|
||||
<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>
|
||||
|
||||
<!-- Start/Stop buttons -->
|
||||
<div class="d-flex justify-content-center gap-3 mb-4">
|
||||
<button id="startButton" class="btn btn-primary px-4">Start Scanning</button>
|
||||
<button id="stopButton" class="btn btn-danger px-4">Stop</button>
|
||||
</div>
|
||||
|
||||
<!-- Search box and suggestions -->
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<div class="w-100 position-relative" style="max-width: 500px;">
|
||||
<!-- Input group (search + go button) -->
|
||||
<div class="input-group">
|
||||
<input type="text" id="search-box" class="form-control" placeholder="Search..." autocomplete="off">
|
||||
<button id="go-button" class="btn btn-success">Go</button>
|
||||
</div>
|
||||
|
||||
<!-- Suggestions -->
|
||||
<ul id="suggestions" class="list-group position-absolute w-100 mt-1" style="z-index: 1000;"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Suggestions list -->
|
||||
<div class="d-flex justify-content-center">
|
||||
<ul id="suggestions" class="list-group position-absolute mt-1 w-100" style="max-width: 500px; z-index: 1000;">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
const searchBox = document.getElementById('search-box');
|
||||
const suggestionsBox = document.getElementById('suggestions');
|
||||
const goButton = document.getElementById('go-button');
|
||||
|
||||
searchBox.addEventListener('input', function () {
|
||||
const query = searchBox.value;
|
||||
if (query.length < 2) {
|
||||
suggestionsBox.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`{{url_for("add_meal.query")}}?q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
suggestionsBox.innerHTML = '';
|
||||
data.forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = item;
|
||||
|
||||
// Apply Bootstrap classes
|
||||
li.classList.add('list-group-item', 'list-group-item-action', 'cursor-pointer');
|
||||
|
||||
// Add click behavior
|
||||
li.addEventListener('click', () => {
|
||||
searchBox.value = item;
|
||||
suggestionsBox.innerHTML = '';
|
||||
});
|
||||
|
||||
// Add to suggestions box
|
||||
suggestionsBox.appendChild(li);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ✅ Redirect when the "Go" button is clicked
|
||||
goButton.addEventListener('click', () => {
|
||||
const value = searchBox.value.trim();
|
||||
if (value) {
|
||||
const baseURL = "{{url_for('add_meal.step3', input='!')}}";
|
||||
window.location.href = baseURL.replace("!", encodeURIComponent(value));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="module">
|
||||
import { BrowserMultiFormatReader } from 'https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/+esm';
|
||||
|
||||
// constants
|
||||
const codeReader = new BrowserMultiFormatReader();
|
||||
const videoElement = document.getElementById('video');
|
||||
|
||||
|
||||
// Start scanning for barcode
|
||||
document.getElementById('startButton').addEventListener('click', async () => {
|
||||
console.log('[DEBUG] Start button clicked')
|
||||
try {
|
||||
await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
} catch (err) {
|
||||
alert("No camera found or no camera permission");
|
||||
console.error("Could not access the camera:", err);
|
||||
return;
|
||||
}
|
||||
console.log('[DEBUG] Permission given and at least one device present');
|
||||
const devices = await codeReader.listVideoInputDevices();
|
||||
console.log('[DEBUG] Cameras found:', devices);
|
||||
const rearCamera = devices.find(device => device.label.toLowerCase().includes('back'))
|
||||
|| devices.find(device => device.label.toLowerCase().includes('rear'))
|
||||
|| devices[0]; // fallback
|
||||
|
||||
const selectedDeviceId = rearCamera?.deviceId;
|
||||
await codeReader.decodeFromVideoDevice(selectedDeviceId, videoElement, async (result, err) => {
|
||||
if (result) {
|
||||
// Result found, this should post the barcode
|
||||
const codeText = result.getText();
|
||||
const baseURL = "{{url_for('add_meal.step3', input='!')}}";
|
||||
window.location.href = baseURL.replace("!", encodeURIComponent(codeText));
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
document.getElementById('stopButton').addEventListener('click', () => {
|
||||
codeReader.reset();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
23
application/add_meal_v2/templates/step4.html
Normal file
23
application/add_meal_v2/templates/step4.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<p>{{ tod }}</p>
|
||||
<p>{{ item.name }}</p>
|
||||
|
||||
<form method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-control-plaintext">
|
||||
{{item_id}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.amount.label(class="form-label") }}
|
||||
{{ form.amount(class="form-control") }}
|
||||
</div>
|
||||
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</form>
|
||||
{% endblock%}
|
||||
@@ -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 forms import LoginForm, ChangePasswordForm
|
||||
from models import User
|
||||
from application.utils import default_return
|
||||
from application.utils import default_return, is_valid_timezone
|
||||
from application import db
|
||||
|
||||
bp = Blueprint(
|
||||
@@ -19,12 +19,16 @@ def login():
|
||||
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
assert form.timezone.data
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user and user.check_password(password=form.password.data):
|
||||
# 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
|
||||
return default_return(next_page=next_page)
|
||||
return default_return()
|
||||
else:
|
||||
pass
|
||||
# invalid user
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
{% 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>
|
||||
<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.hidden_tag() }}
|
||||
|
||||
@@ -27,10 +30,21 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{ form.timezone(id="timezone") }}
|
||||
|
||||
<div class="d-grid">
|
||||
{{ form.submit(class="btn btn-primary btn-lg") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock%}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tzField = document.getElementById('timezone');
|
||||
tzField.value = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -83,6 +83,9 @@
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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%}
|
||||
@@ -52,7 +52,7 @@
|
||||
</a>
|
||||
|
||||
<!-- Center Button (highlighted) -->
|
||||
<a href="{{ url_for('add_meal.step1', meal_type=0) }}"
|
||||
<a id="set_link_date" href="{{ url_for('add_meal.step1', meal_type=0) }}"
|
||||
class="btn btn-success flex-fill mx-2 fw-bold rounded-pill">
|
||||
+ Add Item
|
||||
</a>
|
||||
@@ -64,4 +64,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function formatToday() {
|
||||
const today = new Date();
|
||||
const yyyy = today.getFullYear();
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(today.getDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
|
||||
const link = document.getElementById('set_link_date');
|
||||
const todayString = formatToday();
|
||||
// Set href on page load
|
||||
link.href = `/select_date?date=${todayString}`;
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,7 @@
|
||||
from flask_login import current_user
|
||||
from flask import redirect, url_for, flash
|
||||
from typing import Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
|
||||
def login_required():
|
||||
@@ -19,3 +20,12 @@ def default_return(next_page: Optional[str] = None):
|
||||
if current_user.is_admin:
|
||||
return redirect(url_for("admin.food_items"))
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
|
||||
def is_valid_timezone(tz: str) -> bool:
|
||||
try:
|
||||
ZoneInfo(tz)
|
||||
except Exception:
|
||||
print(Exception)
|
||||
return False
|
||||
return True
|
||||
|
||||
7
forms.py
7
forms.py
@@ -4,13 +4,20 @@ from wtforms import (
|
||||
PasswordField,
|
||||
SubmitField,
|
||||
FloatField,
|
||||
HiddenField,
|
||||
)
|
||||
from wtforms.validators import DataRequired, InputRequired, Optional
|
||||
|
||||
|
||||
class SelectDateForm(FlaskForm):
|
||||
date = HiddenField(validators=[DataRequired()])
|
||||
submit = SubmitField("+ Add Item")
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField("Username", validators=[DataRequired()])
|
||||
password = PasswordField("Password", validators=[DataRequired()])
|
||||
timezone = HiddenField("Timezone", validators=[DataRequired()])
|
||||
submit = SubmitField("Log in")
|
||||
|
||||
|
||||
|
||||
40
migrations/versions/9eb23abd5294_.py
Normal file
40
migrations/versions/9eb23abd5294_.py
Normal 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 ###
|
||||
@@ -4,6 +4,7 @@ from application import db
|
||||
from typing import Optional
|
||||
from forms import FoodItemForm
|
||||
from datetime import datetime, timezone, date
|
||||
from application.utils import is_valid_timezone
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
@@ -11,6 +12,7 @@ class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(150), unique=True, 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)
|
||||
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:
|
||||
self.must_change_password = change
|
||||
|
||||
def set_timezone(self, tz: str) -> None:
|
||||
if is_valid_timezone(tz):
|
||||
self.timezone = tz
|
||||
|
||||
|
||||
class Unit(db.Model):
|
||||
__tablename__ = "unit"
|
||||
|
||||
Reference in New Issue
Block a user