Initial commit for MVP
This commit is contained in:
53
main.py
53
main.py
@@ -3,5 +3,56 @@ Portfolio Project for CS361 Spring 2022
|
|||||||
Written by Lucas Jensen
|
Written by Lucas Jensen
|
||||||
Last updated 3/29/22 for Assignment 1
|
Last updated 3/29/22 for Assignment 1
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
|
from flask import Flask, redirect, flash, render_template, request, jsonify
|
||||||
|
from markupsafe import escape
|
||||||
|
|
||||||
print("Hello, CS361!")
|
from recipe import Recipe, RecipeBook, open_json
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def home():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/recipes', methods=['GET'])
|
||||||
|
def ReturnJSON():
|
||||||
|
return render_template("recipe.html", content=open_json())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/recipes/<_id>', methods=['GET'])
|
||||||
|
def recipe_page(_id):
|
||||||
|
if request.method == 'GET':
|
||||||
|
# recipe = book.find_by_id(int(_id))
|
||||||
|
# return jsonify(recipe.get_recipe())
|
||||||
|
recipe = open_json()[_id]
|
||||||
|
return recipe
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/add', methods=['GET', 'POST'])
|
||||||
|
def add_recipe():
|
||||||
|
if request.method == "POST":
|
||||||
|
flour = request.form.get("flour_weight")
|
||||||
|
water = request.form.get("water_weight")
|
||||||
|
salt = request.form.get("salt_weight")
|
||||||
|
loaves = request.form.get("yield")
|
||||||
|
name = request.form.get("name")
|
||||||
|
|
||||||
|
new_recipe = Recipe(name, loaves, flour, water, salt)
|
||||||
|
book.add_recipes(new_recipe)
|
||||||
|
|
||||||
|
# flash("Recipe Added!")
|
||||||
|
return redirect("/recipes")
|
||||||
|
return render_template("new.html")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# testing
|
||||||
|
# recipes = default_recipes()
|
||||||
|
book = RecipeBook()
|
||||||
|
# for recipe in recipes:
|
||||||
|
# book.add_recipes(recipe)
|
||||||
|
|
||||||
|
app.run(debug=True)
|
||||||
|
|||||||
179
recipe.py
Normal file
179
recipe.py
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
"""
|
||||||
|
Written by Lucas Jensen
|
||||||
|
Portfolio Project for CS361
|
||||||
|
The main logic behind a recipe
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class RecipeBook:
|
||||||
|
"""
|
||||||
|
Represents a recipe book containing several bread recipes
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._count = 0
|
||||||
|
self._recipes = {}
|
||||||
|
|
||||||
|
def add_recipes(self, recipe: object) -> None:
|
||||||
|
"""
|
||||||
|
Adds a recipe to the book
|
||||||
|
:param recipe: bread recipe object
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
keys = open_json()
|
||||||
|
for key in keys:
|
||||||
|
if int(key) > self._count:
|
||||||
|
self._count = int(key)
|
||||||
|
self._count += 1
|
||||||
|
self._recipes[self._count] = recipe
|
||||||
|
self.save(self._count, recipe)
|
||||||
|
# self._count += 1
|
||||||
|
|
||||||
|
def get_recipes(self) -> list:
|
||||||
|
"""
|
||||||
|
get the dict of recipes
|
||||||
|
:return: the dict of recipes
|
||||||
|
"""
|
||||||
|
return self._recipes
|
||||||
|
|
||||||
|
def find_by_id(self, _id: int) -> object:
|
||||||
|
"""
|
||||||
|
Finds a recipe object by its assigned id
|
||||||
|
:param _id: int, the id of the recipe
|
||||||
|
:return: recipe object
|
||||||
|
"""
|
||||||
|
return self._recipes[_id]
|
||||||
|
|
||||||
|
def save(self, _id: int, recipe: object) -> None:
|
||||||
|
"""
|
||||||
|
Saves a recipe to the JSON file
|
||||||
|
:param _id: int
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
a_dict = {str(_id): recipe.get_recipe()}
|
||||||
|
|
||||||
|
with open('recipes/recipes.json') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
data.update(a_dict)
|
||||||
|
|
||||||
|
with open('recipes/recipes.json', 'w') as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe:
|
||||||
|
"""
|
||||||
|
Represents a bread recipe
|
||||||
|
"""
|
||||||
|
def __init__(self, name, num_loaves, ap_flour, water, salt):
|
||||||
|
self._name = name
|
||||||
|
self._num_loaves = num_loaves
|
||||||
|
self._ingredients = {
|
||||||
|
'ap_flour': ap_flour,
|
||||||
|
'water': water,
|
||||||
|
'salt': salt
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_name(self) -> str:
|
||||||
|
"""
|
||||||
|
Gets the name of a recipe
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def get_recipe(self) -> dict:
|
||||||
|
"""
|
||||||
|
:return: returns the whole recipe as a dict, including quantity
|
||||||
|
"""
|
||||||
|
recipe_dict = {
|
||||||
|
'quantity': self._num_loaves,
|
||||||
|
'name': self._name
|
||||||
|
}
|
||||||
|
|
||||||
|
for ingredient in self._ingredients:
|
||||||
|
recipe_dict[ingredient] = self._ingredients[ingredient]
|
||||||
|
|
||||||
|
return recipe_dict
|
||||||
|
|
||||||
|
def get_num_loaves(self) -> int:
|
||||||
|
"""
|
||||||
|
:return: int: number of loaves the recipe calls for
|
||||||
|
"""
|
||||||
|
return self._num_loaves
|
||||||
|
|
||||||
|
def get_ingredients(self) -> dict:
|
||||||
|
"""
|
||||||
|
:return: dictionary of all ingredients
|
||||||
|
"""
|
||||||
|
return self._ingredients
|
||||||
|
|
||||||
|
def add_ingredient(self, name: str, mass: int) -> None:
|
||||||
|
"""
|
||||||
|
Adds an ingredient to the recipe. Mass in grams.
|
||||||
|
:param name: string, the name of the ingredient
|
||||||
|
:param mass: int, the mass of the ingredient, in grams
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self._ingredients[name] = mass
|
||||||
|
|
||||||
|
def scale(self, new_num: int) -> object:
|
||||||
|
"""
|
||||||
|
Scales the recipe by a given factor
|
||||||
|
:param new_num: integer, the number of desired loaves
|
||||||
|
:return: Recipe object, now scaled
|
||||||
|
"""
|
||||||
|
scale = new_num / self._num_loaves
|
||||||
|
|
||||||
|
ap_flour = self._ingredients['ap_flour'] * scale
|
||||||
|
water = self._ingredients['water'] * scale
|
||||||
|
salt = self._ingredients['salt'] * scale
|
||||||
|
name = f"{self._name} * {scale}"
|
||||||
|
|
||||||
|
scaled_recipe = Recipe(name, new_num, ap_flour, water, salt)
|
||||||
|
|
||||||
|
for ingredient in self._ingredients:
|
||||||
|
if ingredient not in ['ap_flour', 'water', 'salt']:
|
||||||
|
mass = self._ingredients[ingredient] * scale
|
||||||
|
scaled_recipe.add_ingredient(ingredient, mass)
|
||||||
|
|
||||||
|
return scaled_recipe
|
||||||
|
|
||||||
|
|
||||||
|
def build_recipe(infile) -> None:
|
||||||
|
"""
|
||||||
|
Builds a recipe object from a JSON file
|
||||||
|
:param infile: filename
|
||||||
|
:return: nothing
|
||||||
|
"""
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def default_recipes():
|
||||||
|
recipe_1 = Recipe('Country Brown', 1, 600, 300, 13)
|
||||||
|
recipe_2 = Recipe('Conventional White', 2, 1000, 700, 25)
|
||||||
|
recipe_3 = recipe_2.scale(4)
|
||||||
|
|
||||||
|
return [recipe_1, recipe_2, recipe_3]
|
||||||
|
|
||||||
|
|
||||||
|
def open_json():
|
||||||
|
"""TODO"""
|
||||||
|
with open('recipes/recipes.json') as json_file:
|
||||||
|
data = json.load(json_file)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
recipes = default_recipes()
|
||||||
|
book = RecipeBook()
|
||||||
|
for recipe in recipes:
|
||||||
|
book.add_recipes(recipe)
|
||||||
|
|
||||||
|
for recipe in book.get_recipes():
|
||||||
|
print(book.get_recipes()[recipe])
|
||||||
|
|
||||||
|
rec = Recipe('sandwich loaf', 2, 1000, 700, 25)
|
||||||
|
rec.add_ingredient('ww flour', 500)
|
||||||
|
# rec.save('recipes.json')
|
||||||
1
recipes/recipes.json
Normal file
1
recipes/recipes.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"0": {"quantity": "1", "name": "bread", "ap_flour": "1", "water": "1", "salt": "1"}, "1": {"quantity": 2, "name": "Conventional White", "ap_flour": 1000, "water": 700, "salt": 25}, "2": {"quantity": 4, "name": "Conventional White * 2.0", "ap_flour": 2000.0, "water": 1400.0, "salt": 50.0}, "3": {"quantity": "1", "name": "sourdough", "ap_flour": "100", "water": "1001", "salt": "1001"}, "4": {"quantity": "11", "name": "milk", "ap_flour": "10101", "water": "10", "salt": "10"}, "5": {"quantity": "1", "name": "white", "ap_flour": "100", "water": "50", "salt": "2"}}
|
||||||
BIN
static/favicon-16x16.png
Normal file
BIN
static/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 405 B |
62
static/style.css
Normal file
62
static/style.css
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
text-align: left;
|
||||||
|
font-family: 'Akshar', sans-serif;
|
||||||
|
background-color: slategray;
|
||||||
|
color: cornsilk;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: lightblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: lightblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: lightskyblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
margin-left: 10px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a {
|
||||||
|
display: block;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: cornsilk;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Change the link color to #111 (black) on hover */
|
||||||
|
.nav a:hover {
|
||||||
|
background-color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
29
templates/base.html
Normal file
29
templates/base.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Akshar&display=swap" rel="stylesheet">
|
||||||
|
<title>{% block title %}{% endblock %}</title>
|
||||||
|
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon-16x16.png') }}">
|
||||||
|
{% block html_head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li class="nav"><a href="/">Home</a></li>
|
||||||
|
<li class="nav"><a href="/recipes">Recipes</a></li>
|
||||||
|
<li class="nav"><a href="/add">Add</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<body>
|
||||||
|
<div id="content">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="footer">
|
||||||
|
{% block footer %}
|
||||||
|
© Copyright 2022 by <a href="https://github.com/ljensen505">Lucas Jensen</a>.
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
14
templates/index.html
Normal file
14
templates/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Home Page{% endblock %}
|
||||||
|
|
||||||
|
{% block html_head %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>Welcome to the Bread App!</h1>
|
||||||
|
|
||||||
|
<p>Add, scale, and convert all your bread recipes here.</p>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
30
templates/new.html
Normal file
30
templates/new.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Add Recipe{% endblock %}
|
||||||
|
|
||||||
|
{% block html_head %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>New Recipe</h1>
|
||||||
|
|
||||||
|
<form action="{{ url_for("add_recipe")}}" method="post">
|
||||||
|
|
||||||
|
<label for="name">Recipe Name:</label>
|
||||||
|
<input required type="text" id="name" name="name" placeholder="Sourdough">
|
||||||
|
<br>
|
||||||
|
<p>Enter all values grams.</p>
|
||||||
|
|
||||||
|
{% for ing in ['flour', 'water', 'salt'] %}
|
||||||
|
<label for="{{ing}}_weight">{{ing.title()}}:</label>
|
||||||
|
<input required type="number" id="{{ing}}_weight" name="{{ing}}_weight" placeholder="100">
|
||||||
|
<br>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<label for="yield">Loaves:</label>
|
||||||
|
<input required type="number" id="yield" name="yield" placeholder="2">
|
||||||
|
|
||||||
|
<button type="submit">Add!</button>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
19
templates/recipe.html
Normal file
19
templates/recipe.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Recipes{% endblock %}
|
||||||
|
|
||||||
|
{% block html_head %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>Bread Recipes</h1>
|
||||||
|
<ul>
|
||||||
|
{% for recipe in content %}
|
||||||
|
<li>
|
||||||
|
<a href=recipes/{{recipe}}>{{content[recipe]['name']}}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user