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
Goal ↔ BabyStep
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 attributegoal.babystepsthat gives you a list of allBabySteprows pointing at this goal. It's not a database column itself; SQLAlchemy figures out the join using thegoal_idforeign key onBabyStep.'BabyStep'(string, not the class) — the target model, passed as a string becauseBabyStepis defined afterGoalin 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 toBabyStep, so you also getbabystep.goalto walk from a step back to its parent goal, without defining it explicitly on theBabyStepclass.lazy=True— controls when the related rows are fetched.True(a.k.a.'select') means accessinggoal.babystepstriggers a separateSELECTquery at that moment, rather than eagerly joining it in with the original query forGoal.cascade='all, delete-orphan'— controls what happens to child rows when the parent changes:allcascades all operations (save, update, merge, delete, etc.) fromGoalto its babysteps.delete-orphanmeans if aBabyStepis removed fromgoal.babysteps(or the parentGoalis deleted), thatBabySteprow is deleted too, rather than left orphaned with a danglinggoal_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.
BabyStep ↔ Payment
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', ...)— addsbabystep.payments, a list of everyPaymentwhosebabystep_idpoints at this step.'Payment'is a string for the same reason'BabyStep'was above:Paymentis defined further down the file, so SQLAlchemy resolves it lazily.backref='babystep'— adds the reverse pointer,payment.babystep, without writing it explicitly on thePaymentclass.lazy=True— accessingbabystep.paymentsfires a separateSELECTat that moment, rather than joining it in eagerly.cascade='all, delete-orphan'— deleting aBabyStep(or removing aPaymentfrom its list) deletes the orphanedPaymentrows too, instead of leaving a danglingbabystep_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, andimage_list(the@propertyitems in this codebase) aren't real database columns — there's nosaved_amount = db.Column(...)anywhere. They're derived from other data every time you ask for them (sum of payments, parsed JSON, etc.).@propertyis 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.@propertyhides 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.babystepsevery access, you never have a cachedsaved_amountnumber 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.@propertyis 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, notbabystep.saved_amount(). Computed fresh every access, not stored as a column.def saved_amount(self):— takes onlyself, which is required for@property— a property can only takeself.self.payments— comes from thepayments = db.relationship(...)line atapp.py:76. It's the list of allPaymentrows linked to this baby step.payment.amount for payment in self.payments— a generator expression pullingamountout 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 asbabystep.progress_percentinstead ofbabystep.progress_percent().if not self.monthly_amount: return 0— a guard against division by zero.monthly_amountis the target amount for this baby step (app.py:71). If it's0(orNone), the function bails out and returns0%instead of raising aZeroDivisionError.self.saved_amount— calls the property defined right above it (app.py:78-80), which sums up allPaymentamounts 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 is150/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 rawdb.Column(db.Text, nullable=True)atapp.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 againstNone/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 dofor img in goal.image_listinstead 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 fromapp.py:50, everyBabyStepbelonging to thisGoal.- For each step, it accesses
step.saved_amount— the exactBabyStep.saved_amountproperty fromapp.py:78-80, which itself sums that step'sPayment.amountvalues.
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