diff --git a/readme.md b/readme.md index 57ebc04..eefc81d 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,154 @@ -The User will need to enter: +# Superior Finance — Notes -Multiple Financial Goals (Each Item will be a Goal_ID) +Running notes on how the Flask/SQLAlchemy models in `app.py` fit together. -Monthly Steps Towards that goal (Baby_Step_ID) +## Database connection -How much they get from each paycheck (Payment_ID) +```python +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///info.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False +``` +## Model relationships -Payment ID relates to Baby Step ID, which updates each Goal ID depending on how the money is allocated. +### `Goal` ↔ `BabyStep` +```python +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. + +### `BabyStep` ↔ `Payment` + +```python +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` + +```python +@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` + +```python +@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` + +```python +@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` + +```python +@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` + +```python +@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 +```