After having added the “custom list” functionality in the last post as a backup for a failed url parse, it was time to add it as an actual feature that the user can access. After all, what if you wanted to use a recipe that wasn’t online, or was in a format that couldn’t be automatically parsed?
A Spot of Refactoring
First, this required a bit of refactoring. I touched on this a few posts ago, but the code that was involved in the creation of a new list and recipe was not packaged in a way that could be easily reused. This wasn’t a problem when there was only one way to create a recipe and a list, but now that I’m adding more than one way, we need to generalize this code.
I did so by first splitting the code that creates a recipe and a list, and transferring that code to my utils.py
file:
# creates a new list
def create_list():
# create new list
random_hex = secrets.token_urlsafe(8)
new_list = CompiledList(hex_name=random_hex)
db.session.add(new_list)
db.session.commit()
# create recipe for user-added lines
user_added_list = RecipeList(name="Additional Ingredients",
hex_name=secrets.token_urlsafe(8),
hex_color="#D3D3D3",
recipe_url="NA") # FIXME: make recipe_url optional
user_added_list.complist = new_list
db.session.add(user_added_list)
db.session.commit()
return new_list
# creates a new recipe
def create_recipe(title):
random_hex = secrets.token_urlsafe(8)
r = lambda: random.randint(0, 255)
hex_color = ('#%02X%02X%02X' % (r(), r(), r()))
rlist = RecipeList(hex_name=random_hex, hex_color=hex_color, name=title, recipe_url="") # FIXME: make recipe_url optional
db.session.add(rlist)
db.session.commit()
return rlist
Note that my current database model requires a recipe_url
, and I plan on changing that, but I didn’t want to fall down the rabbit hole of database changing just yet.
Then, I split off the code that parsed the recipe from a url into its own function:
def create_recipe_from_url(url):
rlist = create_recipe(get_title(url))
rlist.recipe_url = url
recipe_lines = get_recipe_lines(url) # TODO: possibly refactor code so that get_recipe_lines is here too
for num, line in enumerate(recipe_lines):
recipe_colors = color_entities_in_line(line)
recipe_line = RawLine(full_text=line, rlist=rlist, id_in_list=num, text_to_colors=recipe_colors)
db.session.add(recipe_line)
db.session.commit()
return rlist # return the new recipe for use in the route
By splitting the create_recipe()
function and then calling it here, I could then create a second function to create a recipe from provided text:
def create_recipe_from_text(title, recipe_text):
recipe = create_recipe(title)
recipe_lines = recipe_text.splitlines()
def elim_blanks(line): # function to remove blank lines and spaces from list
if not line or line.isspace():
return False
else:
return True
recipe_lines = filter(elim_blanks, recipe_lines)
for num, line in enumerate(recipe_lines): # FIXME: this code is the same as in utils.url_to_recipe
recipe_colors = color_entities_in_line(line)
recipe_line = RawLine(full_text=line, rlist=recipe, id_in_list=num, text_to_colors=recipe_colors)
db.session.add(recipe_line)
db.session.commit()
return recipe
Once this was all finished, the actual code in my routes.py
to create recipes was drastically simplified. Here’s the new backup form:
if not rlist_lines: # we failed to extract any lines from the recipe
form = CustomRecipeForm()
if form.validate_on_submit():
recipe = create_recipe_from_text("Untitled Recipe", form.recipe_lines.data)
recipe.name = form.name.data
return redirect(url_for('main.add_recipe', list_name=list_name, new_recipe=new_recipe))
With this out of the way, it was time to add the new functionality to the app.
Adding the Custom Recipe Option
The first place I wanted to add the feature was in my home
route. I conceived of this as a second option to start a new list: you could enter in the url, or you could type/paste a list of ingredients. First, I added the CustomRecipeForm
to the main.home
route, and wrote the logic for what to do when it was validated. Thanks to the refactoring I did above, this was quite easy:
custom_form = CustomRecipeForm(prefix="custom")
if custom_form.validate_on_submit():
new_list = create_list()
new_recipe = create_recipe_from_text("Untitled Recipe", custom_form.recipe_lines.data)
new_recipe.complist = new_list
return redirect(url_for('main.add_recipe', list_name=new_list.hex_name, new_recipe=new_recipe.hex_name))
Behold, the power of refactoring. With just a few lines of code I was able to create a completely new way to initialize a list. All that I needed to do was add the actual form to the template. I put it in the <jumbotron>
object, below the url form I’d been using thus far:
<p>Or, if you prefer, enter your recipe manually:</p>
<form method="POST" action="">
{{ custom_form.hidden_tag() }}
<div class="form-group">
{{ custom_form.recipe_lines(class="form-control") }}
</div>
<div class="form-group">
{{ custom_form.submit(class="btn btn-primary") }}
</div>
</form>
This created a nice, simple new piece of the UI:
Clicking the “Find Ingredients” button would then redirect the user to the “clean list” page, where the rest of my code would take over.
For my next piece, I wanted to also add the option to paste recipe lines on the actual list page. I again added the form to my main.compiled_list
route, and thanks to my earlier refactoring, the code was again simple to implement:
custom_recipe_form = CustomRecipeForm(prefix="custom-recipe")
if custom_recipe_form.validate_on_submit():
new_recipe = create_recipe_from_text("Untitled Recipe", custom_recipe_form.recipe_lines.data)
new_recipe.complist = comp_list
return redirect(url_for('main.add_recipe', list_name=comp_list.hex_name, new_recipe=new_recipe.hex_name))
I have to be totally honest here: I’m pretty proud of how easy it was to implement these features. It shows that I’ve structured my code well and that the refactoring was a positive use of my time. I don’t want to pat myself on the back too much (who the hell knows if this is even that impressive, tbh), but dammit I’m proud.
Anyway, horn-tooting aside, I still needed to add these features to the list_page.html
template. Following my lead from the homepage, I added it to the already existing modal that was being used to paste a new url:
<p>Or paste/type the Recipe Ingredients below:</p>
<form method="POST" action="">
{{ custom_recipe_form.hidden_tag() }}
<div class="form-group">
{{ custom_recipe_form.recipe_lines(class="form-control") }}
</div>
<div class="form-group">
{{ custom_recipe_form.submit(class="btn btn-primary") }}
</div>
</form>
Checked the modal and it all works:
I pasted in a few recipes, just to make sure everything was working, and so far so good.
Next steps:
- ability to export the list to print/email
- increased list functionality (delete lines, temporarily cancel them out)
- move lines around?
… and plenty more. At this point I’m beginning to realize that I need a roadmap to figure out when I can call this thing “feature complete.” I have a ton of ideas, but I want to have a “v1.0” out first, with a certain bare minimum functionality, before I chase after some more of my more involved ideas. So expect a roadmap post coming in the near future.