2026-08-12 06:59:50 -05:00
2026-08-09 22:36:10 -05:00
2026-08-09 22:36:10 -05:00
2026-08-09 22:36:10 -05:00
2026-08-09 22:36:10 -05:00
2026-08-12 06:59:50 -05:00
2026-08-09 22:36:10 -05:00

Superior Finance — Notes

Running notes on how the Flask/SQLAlchemy models in app.py fit together.

Database connection

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///info.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

Model relationships

GoalBabyStep

babysteps = db.relationship('BabyStep', backref='goal', lazy=True, cascade='all, delete-orphan')

Lives on the Goal model and defines the one-to-many link between a Goal and its BabyStep records (which have a goal_id foreign key at app.py:74).

  • babysteps = db.relationship('BabyStep', ...) — creates an attribute goal.babysteps that gives you a list of all BabyStep rows pointing at this goal. It's not a database column itself; SQLAlchemy figures out the join using the goal_id foreign key on BabyStep.
  • 'BabyStep' (string, not the class) — the target model, passed as a string because BabyStep is defined after Goal in the file, so the class doesn't exist yet at this point. SQLAlchemy resolves the string lazily once both models are registered.
  • backref='goal' — automatically adds the reverse attribute to BabyStep, so you also get babystep.goal to walk from a step back to its parent goal, without defining it explicitly on the BabyStep class.
  • lazy=True — controls when the related rows are fetched. True (a.k.a. 'select') means accessing goal.babysteps triggers a separate SELECT query at that moment, rather than eagerly joining it in with the original query for Goal.
  • cascade='all, delete-orphan' — controls what happens to child rows when the parent changes:
    • all cascades all operations (save, update, merge, delete, etc.) from Goal to its babysteps.
    • delete-orphan means if a BabyStep is removed from goal.babysteps (or the parent Goal is deleted), that BabyStep row is deleted too, rather than left orphaned with a dangling goal_id.

In short: each Goal owns a list of BabySteps; deleting a goal (or unlinking a step from it) deletes those steps too. It gives goal.babysteps and babystep.goal for navigating both directions, and is used at app.py:58 to sum up saved_amount across all baby steps for a goal's progress calculation.

BabyStepPayment

payments = db.relationship('Payment', backref='babystep', lazy=True, cascade='all, delete-orphan')

The same pattern one rung down. Lives on BabyStep and links each baby step to the Payment rows that fund it, using the babystep_id foreign key at app.py:94.

  • payments = db.relationship('Payment', ...) — adds babystep.payments, a list of every Payment whose babystep_id points at this step. 'Payment' is a string for the same reason 'BabyStep' was above: Payment is defined further down the file, so SQLAlchemy resolves it lazily.
  • backref='babystep' — adds the reverse pointer, payment.babystep, without writing it explicitly on the Payment class.
  • lazy=True — accessing babystep.payments fires a separate SELECT at that moment, rather than joining it in eagerly.
  • cascade='all, delete-orphan' — deleting a BabyStep (or removing a Payment from its list) deletes the orphaned Payment rows too, instead of leaving a dangling babystep_id.

Used at app.py:80 the same way babysteps is used at app.py:58 — summing a child field into a running total. BabyStep.saved_amount sums payment.amount, and that feeds Goal.saved_amount, which sums step.saved_amount across baby steps. Two identical aggregation rungs, stacked.

Both relationships are structurally identical, just one level apart: Goal ⇄ BabyStep at app.py:50 and BabyStep ⇄ Payment at app.py:76. Once one makes sense, the other is a rename.

Diagram of the full cascade: https://claude.ai/code/artifact/57f98b9d-efa0-468e-a81a-614b61134d8a

@property basics

@property is a Python-level feature, not Flask/SQLAlchemy-specific.

What it does: it lets a method be accessed like a plain attribute — no parentheses. Without it, you'd write goal.saved_amount(); with it, you write goal.saved_amount.

Why use it here instead of just a regular method?

  • It's computed, not stored. saved_amount, progress_percent, and image_list (the @property items in this codebase) aren't real database columns — there's no saved_amount = db.Column(...) anywhere. They're derived from other data every time you ask for them (sum of payments, parsed JSON, etc.). @property is Python's way of saying "this looks like a piece of data on the object, but it's actually calculated on demand."
  • Consistency with real columns. In a template like templates/goal.html, you write {{ goal.saved_amount }} right next to {{ goal.title }} or {{ goal.goal_amount }}. Without @property, the calculated ones would need parentheses ({{ goal.saved_amount() }}) while the real columns wouldn't — an inconsistent, easy-to-forget distinction. @property hides that difference so callers don't need to know or care which fields are stored and which are derived.
  • It can't accidentally go stale. Since it recalculates from self.payments/self.babysteps every access, you never have a cached saved_amount number that drifts out of sync after a new payment is added — there's nothing to remember to update. The tradeoff is it recomputes (re-sums, re-queries) every single time it's accessed, which is fine at this scale but wouldn't be free on a huge dataset.
  • The alternative it's avoiding: a plain method get_saved_amount(self) called explicitly everywhere. @property is purely ergonomic — it doesn't add capability, it just lets computed values read exactly like stored attributes, which is idiomatic Python for "this is a value the object has," even when that value is calculated rather than stored.

Computed properties

BabyStep.saved_amount

@property
def saved_amount(self):
    return sum(payment.amount for payment in self.payments)

A companion to Goal.saved_amount (below), one level down.

  • @property — turns the method into something accessed like an attribute rather than called like a function: babystep.saved_amount, not babystep.saved_amount(). Computed fresh every access, not stored as a column.
  • def saved_amount(self): — takes only self, which is required for @property — a property can only take self.
  • self.payments — comes from the payments = db.relationship(...) line at app.py:76. It's the list of all Payment rows linked to this baby step.
  • payment.amount for payment in self.payments — a generator expression pulling amount out of every payment tied to this baby step.
  • sum(...) — adds all those amounts together.

So babystep.saved_amount returns the total dollars paid toward that baby step, by summing all its associated Payment records. This is exactly what Goal.saved_amount (app.py:57-58) then sums again one level up — sum(step.saved_amount for step in self.babysteps) — to get the total saved toward the whole goal. It's a two-level rollup: payments → baby step total → goal total.

BabyStep.progress_percent

@property
def progress_percent(self):
    if not self.monthly_amount:
        return 0
    return min(100, round(self.saved_amount / self.monthly_amount * 100))

The BabyStep counterpart to Goal.progress_percent (app.py:60-64), living right after saved_amount (app.py:82-86).

  • @property — same as above, lets you call it as babystep.progress_percent instead of babystep.progress_percent().
  • if not self.monthly_amount: return 0 — a guard against division by zero. monthly_amount is the target amount for this baby step (app.py:71). If it's 0 (or None), the function bails out and returns 0% instead of raising a ZeroDivisionError.
  • self.saved_amount — calls the property defined right above it (app.py:78-80), which sums up all Payment amounts tied to this baby step.
  • self.saved_amount / self.monthly_amount * 100 — turns the raw saved amount into a percentage of the target. E.g. $150 saved toward a $500 monthly target is 150/500*100 = 30.
  • round(...) — rounds that percentage to the nearest whole number.
  • min(100, ...) — caps the result at 100, so overpaying the baby step doesn't push the displayed progress past 100%.

Mirrors Goal.progress_percent exactly, just one level down — per baby step instead of per whole goal.

Goal.image_list

@property
def image_list(self):
    return json.loads(self.images) if self.images else []
  • self.images — the raw db.Column(db.Text, nullable=True) at app.py:45, a single text column storing a JSON-encoded array of image filenames/paths as a string (e.g. '["a.jpg", "b.jpg"]'), since this schema has no native array/list column type.
  • if self.images else [] — guards against None/empty string. If no images have been set, skip parsing and return an empty list.
  • json.loads(self.images) — otherwise parses that JSON string back into an actual Python list, so templates/code can do for img in goal.image_list instead of dealing with raw JSON text.

Unrelated to the saved-amount math — this one is purely for handling image uploads (ties to allowed_image/UPLOAD_FOLDER near the top of the file), stored as a JSON blob in one column instead of a separate images table.

Goal.saved_amount

@property
def saved_amount(self):
    return sum(step.saved_amount for step in self.babysteps)
  • self.babysteps — the relationship list from app.py:50, every BabyStep belonging to this Goal.
  • For each step, it accesses step.saved_amount — the exact BabyStep.saved_amount property from app.py:78-80, which itself sums that step's Payment.amount values.

A rollup of a rollup: Payment.amount → summed into BabyStep.saved_amount → summed again into Goal.saved_amount. Three levels: payments fund baby steps, baby steps roll up into the goal.

Goal.progress_percent

@property
def progress_percent(self):
    if not self.goal_amount:
        return 0
    return min(100, round(self.saved_amount / self.goal_amount * 100))

Structurally identical to BabyStep.progress_percent (app.py:82-86): guard against a zero goal_amount (the target for the whole goal, app.py:46), divide saved_amount by the target, *100, round, and cap at 100 with min.

The key difference is scale: BabyStep.progress_percent compares its own saved_amount (sum of its payments) against its own monthly_amount. Goal.progress_percent compares the goal's saved_amount (sum across all baby steps) against the goal's goal_amount.

How it all ties together

Payment.amount
   └─ summed by BabyStep.saved_amount
         └─ compared to BabyStep.monthly_amount → BabyStep.progress_percent
   └─ also summed again (via all babysteps) by Goal.saved_amount
         └─ compared to Goal.goal_amount → Goal.progress_percent
Description
No description provided
Readme 37 KiB
Languages
HTML 63.2%
Python 36.8%