12 KiB
12 KiB
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.
- The app creates (if missing) the per‑user data directory at
- 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
- or
briefcase dev- or
python -m funkyjuicerecipes.app
- or
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.
- funkyjuicerecipes/
- 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)
- If viewing individual flavors:
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
- Shows a list of saved recipes as menu items
- Help Menu
- About screen
- File Menu
- 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
- If viewing individual flavors:
- 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
- If duplicate flavor is found:
- 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
- If duplicate flavor is found:
- 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
- If flavor is in a recipe,
- When deleting a flavor,
- NO DUPLICATES ALLOWED