Compare commits
No commits in common. "05aad26e21df75a4ba668489e0c91b7fcd352d76" and "df8c25a161f8d1090875c2ecbbc2d2f0f6ed3eb0" have entirely different histories.
05aad26e21
...
df8c25a161
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -158,7 +158,7 @@ cython_debug/
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
.idea/
|
#.idea/
|
||||||
|
|
||||||
# ---> JetBrains
|
# ---> JetBrains
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
|
|
||||||
123
MILESTONES.md
123
MILESTONES.md
|
|
@ -1,123 +0,0 @@
|
||||||
### FunkyJuice Recipes — Project Milestones
|
|
||||||
|
|
||||||
This document outlines the sequence of milestones to deliver the FunkyJuice Recipes desktop app using Toga, Peewee (SQLite), and Briefcase. Each milestone is scoped to be achievable in a short iteration and includes clear acceptance criteria.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 1 — Project skeleton and tooling
|
|
||||||
- Set up `src/funkyjuicerecipes/` package layout per README.
|
|
||||||
- Add minimal `app.py` with `FunkyJuiceRecipesApp` and `main()` returning the app.
|
|
||||||
- Configure `pyproject.toml` (already added) and ensure `briefcase dev` can start a blank window.
|
|
||||||
- Add `tests/` folder and test runner config.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- `briefcase dev` launches an empty Toga window titled “FunkyJuice Recipes”.
|
|
||||||
- `pytest` runs (even with zero or smoke tests).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 2 — Database initialization and migrations framework
|
|
||||||
- Implement `data/db.py` using `app.paths.data` and Peewee pragmas (`foreign_keys=1`, WAL).
|
|
||||||
- Create migrations table and a simple migration runner that detects unapplied migration files by filename.
|
|
||||||
- Implement initial migrations for `flavor`, `recipe`, and `recipe_flavor` tables matching README DDL.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- On first launch, DB file is created in the per‑user data directory, tables exist, and migrations table is populated.
|
|
||||||
- Subsequent launches do not re-run applied migrations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 3 — Peewee models and DAOs
|
|
||||||
- Implement `Flavor`, `Recipe`, `RecipeFlavor` models reflecting constraints (UNIQUEs, FKs, CHECKs where applicable in logic).
|
|
||||||
- Provide DAO/helper functions for common ops (create/list/get/update/delete) with soft-delete semantics for flavors.
|
|
||||||
- Enable case-insensitive uniqueness for `Flavor(name, company)`.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- Unit tests can create flavors (no duplicates ignoring case), recipes, and link ingredients; soft-deleted flavors are excluded by default queries.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 4 — Calculation engine
|
|
||||||
- Implement pure functions in `logic/calculations.py` to compute PG/VG/Nic and per-flavor mL from inputs (percents + size).
|
|
||||||
- Include validation (non-negative PG/VG; base_pg + base_vg = 100; percent totals ≈ 100 within tolerance).
|
|
||||||
- Define rounding/display policy (e.g., 1 decimal for View screen).
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- Tests cover the README example (120 mL case) and edge cases (nic in PG vs VG, excessive flavor %).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 5 — Main window and navigation shell
|
|
||||||
- Build `MainWindow`: menu bar (File/New Recipe/Exit; Help/About), buttons for Inventory, recipe list placeholder.
|
|
||||||
- Wire basic screens/routes (View Recipe, Add/Edit Recipe, Inventory) with placeholders.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- App displays main list and can navigate to empty placeholder screens and back.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 6 — Inventory screen (CRUD + soft delete)
|
|
||||||
- Implement `FlavorsTable` with add/edit/delete.
|
|
||||||
- Enforce no duplicates; on delete, soft-delete if referenced by recipes; otherwise hard-delete.
|
|
||||||
- Provide toggle to view soft-deleted flavors and actions to undelete or hard-delete when safe.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- Users can manage flavors per README, with correct soft/hard delete behavior.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 7 — Add/Edit Recipe screens
|
|
||||||
- Implement `RecipeForm` with PG/VG base, Nic % and base, size, and flavor lines (select existing flavors + percentage).
|
|
||||||
- Auto-maintain PG + VG = 100; prevent negatives.
|
|
||||||
- On save: validate, combine duplicate flavors (prompt as specified), persist recipe + ingredients.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- Users can create and edit recipes meeting validation rules and duplicate-combine flow.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 8 — View Recipe screen
|
|
||||||
- Render computed components (PG, VG, Nic, flavors) using calculation engine, rounded to 1 decimal.
|
|
||||||
- Support view mode toggle: Individual Flavors vs Flavor Prep total.
|
|
||||||
- Provide action to open Edit, Delete confirmation, and Inventory.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- A saved recipe displays correct amounts; toggle updates view accordingly.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 9 — Flavor Prep mixer utility
|
|
||||||
- Implement “Mix Flavor Prep” dialog/workflow computing batch amounts from recipe flavor ratios for selectable sizes (10/30/120 mL, etc.).
|
|
||||||
- No persistence for Flavor Prep (ephemeral), but support printing/exporting batch breakdown (optional CSV).
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- Users can choose a batch size and see per-flavor mL for the prep; closing returns to View Recipe.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 10 — Polishing, About, and edge behaviors
|
|
||||||
- Implement About dialog (app name, version, author).
|
|
||||||
- Confirm unsaved-changes prompts on screen change and app exit while editing.
|
|
||||||
- Empty states, error dialogs, and friendly validation messages.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- UX polish complete; no obvious dead-ends; dialogs work across platforms.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 11 — Packaging and distribution
|
|
||||||
- Verify `briefcase dev` on target OS; add icons under `resources/`.
|
|
||||||
- Build installers with Briefcase for target platforms.
|
|
||||||
- Document install/run instructions and DB location.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- Installers build successfully; smoke test of installed app passes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 12 — Testing and CI (optional but recommended)
|
|
||||||
- Add unit tests for models/DAOs and calculations; UI smoke tests using Toga Dummy backend where feasible.
|
|
||||||
- Set up a simple CI (e.g., GitHub Actions) to run tests on push.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
- CI passes on main branch; coverage of calculation and DAO logic acceptable.
|
|
||||||
332
SPECS.md
332
SPECS.md
|
|
@ -1,332 +0,0 @@
|
||||||
# FunkyJuice Recipes
|
|
||||||
A python application for managing e-juice recipes
|
|
||||||
* Uses SQLite3 as database
|
|
||||||
* Uses Toga as GUI
|
|
||||||
* Uses Peewee ORM
|
|
||||||
* Separate files per model
|
|
||||||
* Separate files per migration
|
|
||||||
* Migrations for database schema changes using `playhouse.migrate`
|
|
||||||
* Migrations are versioned via the date they were created (YYYY-MM-DD_migration_name.py)
|
|
||||||
* Migrations state is stored in a Migrations table in the database
|
|
||||||
* Migrations table does not use a migration file of its own
|
|
||||||
* Migrations are run automatically on startup (check migration files vs database entries in migrations table)
|
|
||||||
* At startup
|
|
||||||
* The app creates (if missing) the per‑user data directory at `app.paths.data`
|
|
||||||
* The app stores the SQLite DB at `<app.paths.data>/funkyjuicerecipes.data`.
|
|
||||||
* The connection enables pragmas: `{'foreign_keys': 1, 'journal_mode': 'wal', 'synchronous': 1}`.
|
|
||||||
* Tests use a temporary DB path.
|
|
||||||
* Uses class wrappers for Toga widgets
|
|
||||||
* FunkyJuiceRecipesApp(toga.App)
|
|
||||||
* MainWindow(toga.MainWindow)
|
|
||||||
* RecipesTable(toga.Table)
|
|
||||||
* RecipeForm(toga.Box -> with form elements as needed)
|
|
||||||
* AddRecipeButton(toga.Button)
|
|
||||||
* EditRecipeButton(toga.Button)
|
|
||||||
* DeleteRecipeButton(toga.Button -> with confirmation dialog)
|
|
||||||
* FlavorsTable(toga.Table)
|
|
||||||
* AddFlavorButton(toga.Button)
|
|
||||||
* EditFlavorButton(toga.Button)
|
|
||||||
* DeleteFlavorButton(toga.Button -> with confirmation dialog)
|
|
||||||
* FlavorForm(toga.Box -> with form elements as needed)
|
|
||||||
* Uses briefcase to package app for distribution
|
|
||||||
* Code follows PEP8
|
|
||||||
* Code follows SOLID principles
|
|
||||||
* SRP at method level
|
|
||||||
|
|
||||||
## How to Run
|
|
||||||
* `pip install briefcase`
|
|
||||||
* or `uv add briefcase`
|
|
||||||
* `briefcase dev`
|
|
||||||
* or `python -m funkyjuicerecipes.app`
|
|
||||||
|
|
||||||
## Flavors Soft Delete Policy
|
|
||||||
* When a flavor is deleted
|
|
||||||
* Flavors are Soft Deleted if they are in a recipe.
|
|
||||||
* Flavors are Hard Deleted if they are not in a recipe.
|
|
||||||
* Flavors can be Un-Deleted when viewing the Soft Deleted Flavors list.
|
|
||||||
* Flavors can be Hard Deleted when viewing the Soft Deleted List and they are no longer in a recipe.
|
|
||||||
* UI Queries default to `WHERE is_deleted = 0`
|
|
||||||
* Hard Delete is only allowed when `NOT EXISTS (SELECT 1 FROM recipe_flavor WHERE flavor_id=flavor.id)`
|
|
||||||
|
|
||||||
## Directory/File Structure
|
|
||||||
* project-root/
|
|
||||||
* pyproject.toml
|
|
||||||
* README.md
|
|
||||||
* src/
|
|
||||||
* funkyjuicerecipes/
|
|
||||||
* __init__.py
|
|
||||||
* app.py # Toga App subclass and `main()`
|
|
||||||
* ui/
|
|
||||||
* __init__.py
|
|
||||||
* buttons/
|
|
||||||
* __init__.py
|
|
||||||
* add_recipe_button.py
|
|
||||||
* edit_recipe_button.py
|
|
||||||
* delete_recipe_button.py
|
|
||||||
* add_flavor_button.py
|
|
||||||
* edit_flavor_button.py
|
|
||||||
* delete_flavor_button.py
|
|
||||||
* forms/
|
|
||||||
* __init__.py
|
|
||||||
* recipe_form.py
|
|
||||||
* flavor_form.py
|
|
||||||
* tables/
|
|
||||||
* __init__.py
|
|
||||||
* recipes_table.py
|
|
||||||
* flavors_table.py
|
|
||||||
* dialogs/
|
|
||||||
* __init__.py
|
|
||||||
* delete_recipe_confirmation.py
|
|
||||||
* delete_flavor_confirmation.py
|
|
||||||
* data/
|
|
||||||
* __init__.py
|
|
||||||
* db.py # database init (Peewee SqliteDatabase, pragmas)
|
|
||||||
* models/
|
|
||||||
* __init__.py
|
|
||||||
* flavor.py
|
|
||||||
* recipe.py
|
|
||||||
* recipe_flavor.py
|
|
||||||
* migrations/
|
|
||||||
* __init__.py
|
|
||||||
* 2025-12-18_flavor_table_created.py
|
|
||||||
* 2025-12-18_recipe_table_created.py
|
|
||||||
* 2025-12-18_recipe_flavor_table_created.py
|
|
||||||
* logic/
|
|
||||||
* __init__.py
|
|
||||||
* calculations.py # pure functions for PG/VG/nic/flavor math
|
|
||||||
* resources/ # icons, etc.
|
|
||||||
* tests/
|
|
||||||
* test_calculations.py
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
* Python 3.8+
|
|
||||||
* Toga
|
|
||||||
* Peewee
|
|
||||||
* playhouse.migrate
|
|
||||||
* Briefcase
|
|
||||||
|
|
||||||
## DB Schema
|
|
||||||
* Enable FKs on connection: `SqliteDatabase(path, pragmas={'foreign_keys': 1})`
|
|
||||||
* Flavor Table
|
|
||||||
```
|
|
||||||
flavor(
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name TEXT COLLATE NOCASE NOT NULL,
|
|
||||||
company TEXT COLLATE NOCASE NOT NULL,
|
|
||||||
base TEXT CHECK(base IN ('PG','VG')) NOT NULL,
|
|
||||||
is_deleted INTEGER NOT NULL DEFAULT 0,
|
|
||||||
deleted_at DATETIME NULL,
|
|
||||||
UNIQUE(name, company)
|
|
||||||
)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_flavor_name_company ON flavor(name, company);
|
|
||||||
```
|
|
||||||
* Recipe Table
|
|
||||||
```
|
|
||||||
recipe(
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL UNIQUE,
|
|
||||||
size_ml INTEGER NOT NULL,
|
|
||||||
base_pg_pct REAL NOT NULL,
|
|
||||||
base_vg_pct REAL NOT NULL,
|
|
||||||
nic_pct REAL CHECK(nic_pct >= 0 AND nic_pct <= 100) NOT NULL,
|
|
||||||
nic_base TEXT CHECK(nic_base IN ('PG','VG')) NOT NULL,
|
|
||||||
CHECK (
|
|
||||||
base_pg_pct >= 0 AND
|
|
||||||
base_vg_pct >= 0 AND
|
|
||||||
base_pg_pct + base_vg_pct = 100
|
|
||||||
)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
* RecipeFlavor XREF Table
|
|
||||||
```
|
|
||||||
recipe_flavor(
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
recipe_id INTEGER NOT NULL,
|
|
||||||
flavor_id INTEGER NOT NULL,
|
|
||||||
pct REAL CHECK(pct >= 0 AND pct <= 100) NOT NULL,
|
|
||||||
FOREIGN KEY(recipe_id)
|
|
||||||
REFERENCES recipe(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY(flavor_id) REFERENCES flavor(id) ON DELETE RESTRICT,
|
|
||||||
UNIQUE(recipe_id, flavor_id)
|
|
||||||
)
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_recipe_flavor_recipe ON recipe_flavor(recipe_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_recipe_flavor_flavor ON recipe_flavor(flavor_id);
|
|
||||||
```
|
|
||||||
* Migrations Table
|
|
||||||
```
|
|
||||||
migrations(
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
batch INTEGER NOT NULL,
|
|
||||||
filename TEXT NOT NULL,
|
|
||||||
date_run DATETIME NOT NULL,
|
|
||||||
UNIQUE(filename)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Recipes
|
|
||||||
* Recipes require
|
|
||||||
* Name,
|
|
||||||
* Size (ml),
|
|
||||||
* PG Base %,
|
|
||||||
* VG base %,
|
|
||||||
* Nic Base (PG/VG)
|
|
||||||
* Nic %,
|
|
||||||
* Flavor List
|
|
||||||
* Recipe equations are:
|
|
||||||
* flavor_pg_pct = sum(pct for flavor in flavors if flavor.base == 'PG')
|
|
||||||
* flavor_vg_pct = sum(pct for flavor in flavors if flavor.base == 'VG')
|
|
||||||
* pg_pct = base_pg_pct - flavor_pg_pct - (nic_pct if nic_base == 'PG' else 0)
|
|
||||||
* vg_pct = base_vg_pct - flavor_vg_pct - (nic_pct if nic_base == 'VG' else 0)
|
|
||||||
* PG_ml = pg_pct / 100 * size_ml
|
|
||||||
* VG_ml = vg_pct / 100 * size_ml
|
|
||||||
* Nic_ml = nic_pct / 100 * size_ml
|
|
||||||
* Flavor_i_ml = flavor_i.pct / 100 * size_ml
|
|
||||||
* Recipe output is:
|
|
||||||
* Recipe Name
|
|
||||||
* Size (ml)
|
|
||||||
* PG: PG_ml
|
|
||||||
* VG: VG_ml
|
|
||||||
* Nic: Nic_ml
|
|
||||||
* Flavors
|
|
||||||
* If viewing individual flavors:
|
|
||||||
* Flavor 1 Name: Flavor_1_ml
|
|
||||||
* Flavor 2 Name: Flavor_2_ml
|
|
||||||
* if viewing Flavor Prep:
|
|
||||||
* "Flavor Prep": Flavor_Prep_ml (sum(pct for flavor in flavors)/100 * size_ml)
|
|
||||||
|
|
||||||
## Main Screen
|
|
||||||
* Menu Bar with File, Recipes, and Help
|
|
||||||
* File Menu
|
|
||||||
* New Recipe
|
|
||||||
* Exit
|
|
||||||
* Recipes Menu
|
|
||||||
* Shows a list of saved recipes as menu items
|
|
||||||
* Selecting menu item opens the recipe screen
|
|
||||||
* Help Menu
|
|
||||||
* About screen
|
|
||||||
* Shows Saved Recipes List
|
|
||||||
* Recipe Name
|
|
||||||
* Button to open the recipe screen (View only)
|
|
||||||
* Button to edit the recipe (Edit form)
|
|
||||||
* Button to delete the recipe (Confirmation dialog)
|
|
||||||
* Has button on top to open Inventory Screen.
|
|
||||||
|
|
||||||
## About Screen
|
|
||||||
* Shows
|
|
||||||
* Application Name (FunkyJuice Recipes)
|
|
||||||
* Version Number
|
|
||||||
* Author (Funky Waddle)
|
|
||||||
|
|
||||||
## View Recipe Screen
|
|
||||||
* Needs access to soft deleted flavors for recipes that currently use deleted flavors.
|
|
||||||
* Show components rounded to 1 decimal point
|
|
||||||
* Just convert percentage to decimal number and display, no need to validate sum(amounts) = Size (ml)
|
|
||||||
* We store the percent in the database, not the actual amount, so there is no check that the amount adds to size (ml), only that sum(percentages) = 100
|
|
||||||
* Shows recipe name,
|
|
||||||
* Amount in ml,
|
|
||||||
* PG: PG_ml, (rounded to 1 decimal point)
|
|
||||||
* VG: VG_ml, (rounded to 1 decimal point)
|
|
||||||
* Nic: Nic_ml, (rounded to 1 decimal point)
|
|
||||||
* Flavors
|
|
||||||
* If viewing individual flavors:
|
|
||||||
* Flavor 1 Name: Flavor_1_ml (rounded to 1 decimal point)
|
|
||||||
* Flavor 2 Name: Flavor_2_ml (rounded to 1 decimal point)
|
|
||||||
* if viewing Flavor Prep:
|
|
||||||
* Flavor Prep Amount
|
|
||||||
* Has button to open Edit Recipe Screen
|
|
||||||
* Has button to open Delete Recipe
|
|
||||||
* Has button to open Inventory Screen
|
|
||||||
* Has Drop Down for "View Recipe Using"
|
|
||||||
* Options are "Individual Flavors" or "Flavor Prep"
|
|
||||||
* When Flavor Prep is chosen, it replaces the list of flavors with a total ml of the sum of all the flavors in the recipe.
|
|
||||||
* When Individual Flavors is chosen (default), it replaces the Flavor Prep with the list of individual flavors.
|
|
||||||
* Has button for "Mix Flavor Prep"
|
|
||||||
* Flavor Prep is not stored in database. It's calculated on the fly based on flavors in the recipe.
|
|
||||||
* Shows recipe of Flavors only, no PG, VG, or Nicotine
|
|
||||||
* Flavor Prep is a batch of flavors
|
|
||||||
* Take ratio of each flavor to get total amount of each flavor (12% flavor 1 + 2% flavor 2 = 6:1 ratio)
|
|
||||||
* Flavor Prep batches of different sizes (10ml, 30ml, 120ml)
|
|
||||||
* Flavor Prep is just pre-mixing flavors, much like premixing flour and sugar for use in cookie batter.
|
|
||||||
* You can make 10ml or 1000ml of Sugar Flour, and it won't change the amount used in the cookie recipe.
|
|
||||||
* You can pre-mix 1000ml of flavors, and use 10ml per recipe, giving you 100 batches of Flavor Prep.
|
|
||||||
|
|
||||||
## Edit Recipe Screen
|
|
||||||
* Needs access to soft deleted flavors for recipes that currently use deleted flavors.
|
|
||||||
* Has a form for editing the recipe
|
|
||||||
* Name
|
|
||||||
* Size (mL) (10ml, 30ml, 120ml)
|
|
||||||
* PG Base %
|
|
||||||
* VG Base %
|
|
||||||
* Nic Base (PG or VG)
|
|
||||||
* Nic %
|
|
||||||
* Flavors
|
|
||||||
* Name (with company)
|
|
||||||
* Amount %
|
|
||||||
* If recipe contains a deleted flavor,
|
|
||||||
* Recipe can be saved with the deleted flavor still in the list, as long as it was not changed.
|
|
||||||
* Cannot actively change a flavor to a deleted flavor.
|
|
||||||
* Has button to save changes (Save form, then return to View Recipe screen)
|
|
||||||
* Has button to cancel changes (Discard form changes, return to View Recipe screen)
|
|
||||||
* Has button to delete recipe (Confirmation dialog)
|
|
||||||
* On Save, if recipe contains duplicate flavors,
|
|
||||||
* If duplicate flavor is found:
|
|
||||||
* Show question dialog to ask if user wants to "Combine the duplicate flavors" or "Cancel Save" and return to Edit Recipe screen.
|
|
||||||
* If user chooses to combine duplicate flavors,
|
|
||||||
* Combine flavors with same name and company into one flavor with combined percentage.
|
|
||||||
* If user chooses to cancel save,
|
|
||||||
* DO NOT SAVE and close dialog box, returning user to Edit Recipe screen.
|
|
||||||
* Validate that (PG Base + VG Base) === 100
|
|
||||||
* Validate that neither PG Base nor VG Base is < 0
|
|
||||||
* Validate sum(flavor.pct) + nic.pct + pg.pct + vg.pct ~= 100
|
|
||||||
* On App Exit, if on Edit Recipe screen and data has changed, ask to save changes before exiting.
|
|
||||||
* On Screen Change, if on Edit Recipe screen and data has changed, ask to save changes before changing screens.
|
|
||||||
|
|
||||||
## Add Recipe Screen
|
|
||||||
* Has a form for adding a recipe
|
|
||||||
* Name
|
|
||||||
* Size (mL) (10ml, 30ml, 120ml)
|
|
||||||
* PG Base %
|
|
||||||
* VG Base %
|
|
||||||
* Nic Base (PG or VG)
|
|
||||||
* Nic %
|
|
||||||
* Flavors
|
|
||||||
* Name
|
|
||||||
* Amount %
|
|
||||||
* Has button to save changes (Save form, then return to View Recipe screen)
|
|
||||||
* Has button to cancel changes (Discard form changes, return to View Recipe screen)
|
|
||||||
* On Save, if recipe contains duplicate flavors,
|
|
||||||
* If duplicate flavor is found:
|
|
||||||
* Show question dialog to ask if user wants to "Combine the duplicate flavors" or "Cancel Save" and return to Edit Recipe screen.
|
|
||||||
* If user chooses to combine duplicate flavors,
|
|
||||||
* Combine flavors with same name and company into one flavor with combined percentage.
|
|
||||||
* If user chooses to cancel save,
|
|
||||||
* DO NOT SAVE and close dialog box, returning user to Edit Recipe screen.
|
|
||||||
* Validate that (PG Base + VG Base) === 100
|
|
||||||
* Validate that neither PG Base nor VG Base is < 0
|
|
||||||
* Validate sum(flavor.pct) + nic.pct + pg.pct + vg.pct ~= 100
|
|
||||||
* On App Exit, if on Edit Recipe screen and data has changed, ask to save changes before exiting.
|
|
||||||
* On Screen Change, if on Edit Recipe screen and data has changed, ask to save changes before changing screens.
|
|
||||||
|
|
||||||
## Recipe Form Screens (Add/Edit)
|
|
||||||
* When adding/editing a recipe, you enter the PG, VG, Flavors, and Nic as %
|
|
||||||
* PG and VG are entered as % of total recipe size
|
|
||||||
* When changing PG or VG, the other is updated automatically so that PG + VG is always 100%
|
|
||||||
* Neither PG or VG can be negative
|
|
||||||
|
|
||||||
## Inventory Screen
|
|
||||||
* List of all Non-Deleted Flavors saved in the database
|
|
||||||
* Flavor Name
|
|
||||||
* Company
|
|
||||||
* PG or VG Base
|
|
||||||
* Has button to view Soft Deleted Flavors, and how many recipes they are in
|
|
||||||
* Can Hard Delete Soft Deleted Flavors if they are no longer in a recipe
|
|
||||||
* Can Un-Delete a Soft Deleted Flavor
|
|
||||||
* Each Flavor has a button to edit the flavor (Name, Company, PG or VG Base)
|
|
||||||
* Each Flavor has a button to delete the flavor
|
|
||||||
* When deleting a flavor,
|
|
||||||
* If flavor is in a recipe,
|
|
||||||
* Soft Delete the flavor
|
|
||||||
* Allow flavor data to be viewed in recipe, but with a strikethrough
|
|
||||||
* If flavor is not in a recipe,
|
|
||||||
* Hard Delete the flavor
|
|
||||||
* NO DUPLICATES ALLOWED
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
[project]
|
|
||||||
name = "funkyjuicerecipes"
|
|
||||||
version = "0.1.0"
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
dependencies = [
|
|
||||||
"toga==0.5.3",
|
|
||||||
"peewee>=3.18.3"
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.uv]
|
|
||||||
environments = [
|
|
||||||
"sys_platform == 'linux'",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.briefcase]
|
|
||||||
project_name = "FunkyJuice Recipes"
|
|
||||||
bundle = "com.TargonProducts"
|
|
||||||
version = "0.1.0"
|
|
||||||
url = "https://example.com"
|
|
||||||
author = "Funky Waddle"
|
|
||||||
author_email = "you@example.com"
|
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[tool.briefcase.app.funkyjuicerecipes]
|
|
||||||
formal_name = "FunkyJuice Recipes"
|
|
||||||
description = "A python application for managing e-juice recipes"
|
|
||||||
sources = ["src/funkyjuicerecipes"]
|
|
||||||
requires = ["toga==0.5.3", "peewee>=3.18.3"]
|
|
||||||
icon = "src/funkyjuicerecipes/resources/icon"
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
testpaths = ["tests"]
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
|
||||||
where = ["src"]
|
|
||||||
103
uv.lock
103
uv.lock
|
|
@ -1,103 +0,0 @@
|
||||||
version = 1
|
|
||||||
revision = 2
|
|
||||||
requires-python = ">=3.10"
|
|
||||||
resolution-markers = [
|
|
||||||
"sys_platform == 'linux' and 'freebsd' in sys_platform",
|
|
||||||
"sys_platform == 'linux' and 'freebsd' not in sys_platform",
|
|
||||||
]
|
|
||||||
supported-markers = [
|
|
||||||
"sys_platform == 'linux'",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "funkyjuicerecipes"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = { virtual = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "peewee", marker = "sys_platform == 'linux'" },
|
|
||||||
{ name = "toga", marker = "sys_platform == 'linux'" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "peewee", specifier = ">=3.18.3" },
|
|
||||||
{ name = "toga", specifier = "==0.5.3" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "packaging"
|
|
||||||
version = "25.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "peewee"
|
|
||||||
version = "3.18.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/60/58e7a307a24044e0e982b99042fcd5a58d0cd928d9c01829574d7553ee8d/peewee-3.18.3.tar.gz", hash = "sha256:62c3d93315b1a909360c4b43c3a573b47557a1ec7a4583a71286df2a28d4b72e", size = 3026296, upload-time = "2025-11-03T16:43:46.678Z" }
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pycairo"
|
|
||||||
version = "1.29.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/22/d9/1728840a22a4ef8a8f479b9156aa2943cd98c3907accd3849fb0d5f82bfd/pycairo-1.29.0.tar.gz", hash = "sha256:f3f7fde97325cae80224c09f12564ef58d0d0f655da0e3b040f5807bd5bd3142", size = 665871, upload-time = "2025-11-11T19:13:01.584Z" }
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pygobject"
|
|
||||||
version = "3.54.5"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "pycairo", marker = "sys_platform == 'linux'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/a5/68f883df1d8442e3b267cb92105a4b2f0de819bd64ac9981c2d680d3f49f/pygobject-3.54.5.tar.gz", hash = "sha256:b6656f6348f5245606cf15ea48c384c7f05156c75ead206c1b246c80a22fb585", size = 1274658, upload-time = "2025-10-18T13:45:03.121Z" }
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toga"
|
|
||||||
version = "0.5.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "toga-gtk", marker = "sys_platform == 'linux'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e8/b2/2a671f88bdc00234f86eafaf4990157a76f1252ba82027bf21a58924a1f3/toga-0.5.3.tar.gz", hash = "sha256:4dc555c1cdff459e1224f55e71cb5c44c98c04b665a6d4cc49a7d40c6b0fc071", size = 3899, upload-time = "2025-12-03T06:50:19.357Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/45/ceb4021e46a1dbad10a4101d61f34b1b1d5c8213bbf71071d4f17d27fa88/toga-0.5.3-py3-none-any.whl", hash = "sha256:d535e7829cfbff747349e1932e45b5492845da43998321bac81fd6a19c2f0f72", size = 3371, upload-time = "2025-12-03T06:50:07.774Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toga-core"
|
|
||||||
version = "0.5.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "travertino", marker = "sys_platform == 'linux'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/65/02d6fc7572b09e100791b9053e3e47bb72c0ab97cd90a3ffb069e30fdcf1/toga_core-0.5.3.tar.gz", hash = "sha256:b8a32fb73f53088eec503bcf536e5b1da548147e356ae7c2077943c34af64519", size = 1023659, upload-time = "2025-12-03T06:50:18.656Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/83/7da1a3ca22d67b514ccaecc3342eb6cc2dc1a34852b8c29cc3199f4d2bfd/toga_core-0.5.3-py3-none-any.whl", hash = "sha256:6dc50df6575c18bf34f385167808b027122f16ee25f143907293362178c01fab", size = 147585, upload-time = "2025-12-03T06:50:06.837Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "toga-gtk"
|
|
||||||
version = "0.5.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "packaging", marker = "sys_platform == 'linux'" },
|
|
||||||
{ name = "pycairo", marker = "sys_platform == 'linux'" },
|
|
||||||
{ name = "pygobject", marker = "sys_platform == 'linux'" },
|
|
||||||
{ name = "toga-core", marker = "sys_platform == 'linux'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/52/50/b76b8d74e43b5f9cd690e3a39c816dbfb75cab4209b696e6561c5f31f009/toga_gtk-0.5.3.tar.gz", hash = "sha256:38a566d14e90912c299d38e0a6e3a1ff6596a8db31412a9e37f4d23158b4a87f", size = 82525, upload-time = "2025-12-03T06:50:16.583Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/ca/1f65e926c4369b9a2d366498418b5d5e76b1dfe7b2448448869dc15de86e/toga_gtk-0.5.3-py3-none-any.whl", hash = "sha256:4e48a01b4aace7cd7e653bf82b2c31780e5b7f1d138130bb5ab73bc3f2bb5fa5", size = 75458, upload-time = "2025-12-03T06:50:05.718Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "travertino"
|
|
||||||
version = "0.5.3"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/59/0940b241c3af6939a9c9fe22d218e77970b2fbeef7471dadafaeb3d59531/travertino-0.5.3.tar.gz", hash = "sha256:fdbdf3a2c78335ce6c357c334defe6f15e8315c0e213c43e59d1975e77df7cf3", size = 48923, upload-time = "2025-12-03T06:50:04.638Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/f0/dd7f32b7cb6e2102d5247150a57d74dc31fc09fea8a9212b7c2507e7df77/travertino-0.5.3-py3-none-any.whl", hash = "sha256:9f6f6840760af28295bba76aa175f35678aba33782ff29e78053b3cbdafafabf", size = 26118, upload-time = "2025-12-03T06:50:02.426Z" },
|
|
||||||
]
|
|
||||||
Loading…
Reference in a new issue