"""The brew-process pages: record readings and review the runs.""" from __future__ import annotations from flask import (Blueprint, flash, redirect, render_template, request, session, url_for) from flask_wtf import FlaskForm from wtforms import (BooleanField, DateField, IntegerField, StringField, SubmitField, TextAreaField) from wtforms.validators import DataRequired, Length, NumberRange, Optional from brewing import is_plausible_sg, run_summary bp = Blueprint("brews", __name__) def repo(): """The request-scoped repository (one Mongo client per request).""" from flask import current_app return current_app.extensions["beer_repo"]() def login_required(fn): """Redirect to the login page when there is no session.""" from functools import wraps @wraps(fn) def wrapper(*args, **kwargs): if not session.get("logged_in"): return redirect(url_for("auth.login", next=request.path)) return fn(*args, **kwargs) return wrapper def _last_reading_date(run) -> str | None: """The ISO date of the most recent reading in a run.""" last = run.last_reading() if last is None or last.date is None: return None return last.date.isoformat() class ReadingForm(FlaskForm): """Add or update a gravity reading.""" beer_run_id = IntegerField("Run number", validators=[DataRequired()]) beer_run_type = StringField("Beer / cider name", validators=[Optional(), Length(max=120)]) Date = DateField("Reading date", validators=[DataRequired()]) sg = IntegerField("Gravity reading (points)", validators=[DataRequired(), NumberRange(min=900, max=1300)]) final_reading = BooleanField("This is the final reading") comment = TextAreaField("Comment", validators=[Optional(), Length(max=2000)]) submit = SubmitField("Save reading") @bp.route("/brews", methods=["GET", "POST"]) @login_required def index(): """The brew-process page: record a reading, then review the runs.""" form = ReadingForm() if form.validate_on_submit(): sg_points = int(form.sg.data) if not is_plausible_sg(sg_points): flash( f"{sg_points} is outside the usual hydrometer range " "(990-1200). It was saved anyway — double-check the reading.", "warning", ) repo().upsert_reading( beer_run_id=str(form.beer_run_id.data), beer_type=(form.beer_run_type.data or "").strip(), sg=sg_points, reading_date=form.Date.data.isoformat(), final_reading=bool(form.final_reading.data), comment=(form.comment.data or "").strip(), ) flash("Reading saved.", "success") return redirect(url_for("brews.index")) runs = repo().runs() summaries = [] for run in runs: summary = run_summary(run) summary["canonical_name"] = run.display_name() summary["last_date"] = _last_reading_date(run) summaries.append(summary) # Active runs: most recent activity first — that is what is fermenting now. active = sorted([s for s in summaries if s["active"]], key=lambda s: s["last_date"] or "", reverse=True) # Finished runs: most recently finished first. completed = sorted([s for s in summaries if not s["active"]], key=lambda s: s["final_date"] or "", reverse=True) return render_template( "brews.html", form=form, summaries=summaries, active_runs=active, completed_runs=completed, ) @bp.route("/runs/") @login_required def run_detail(run_id: str): """One run in detail, with all of its readings.""" run = repo().run(run_id) if run is None: flash(f"No run numbered {run_id}.", "error") return redirect(url_for("brews.index")) summary = run_summary(run) summary["canonical_name"] = run.display_name() return render_template("run_detail.html", run=run, summary=summary) @bp.route("/runs//final", methods=["POST"]) @login_required def mark_final(run_id: str): """Flag a reading as the final one for its run.""" reading_date = request.form.get("date", "") if not reading_date: flash("No reading date supplied.", "error") return redirect(url_for("brews.run_detail", run_id=run_id)) changed = repo().set_final_reading(run_id, reading_date, True) if changed: flash("Final reading recorded.", "success") else: flash("That reading could not be updated.", "error") return redirect(url_for("brews.run_detail", run_id=run_id)) @bp.route("/runs//unfinal", methods=["POST"]) @login_required def unfinal(run_id: str): """Un-flag a final reading, putting the run back into the active list.""" reading_date = request.form.get("date", "") if not reading_date: flash("No reading date supplied.", "error") return redirect(url_for("brews.run_detail", run_id=run_id)) repo().set_final_reading(run_id, reading_date, False) flash("Reading is no longer marked final.", "info") return redirect(url_for("brews.run_detail", run_id=run_id))