mirror of
https://github.com/shouptech/humulus.git
synced 2026-02-03 14:59:43 +00:00
Display recipe specs (#6)
* Add Jinja filters for specifications * Include tests for Jinja filters * Javascript to handle form changes. * Add footer for displaying specs on recipe forms * Don't build docker on pull requests
This commit is contained in:
parent
705f84d3cb
commit
022d36041c
12 changed files with 586 additions and 29 deletions
|
|
@ -46,6 +46,9 @@ steps:
|
||||||
when:
|
when:
|
||||||
branch:
|
branch:
|
||||||
- master
|
- master
|
||||||
|
event:
|
||||||
|
exclude:
|
||||||
|
- pull_request
|
||||||
|
|
||||||
- name: docker-release
|
- name: docker-release
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,8 @@ def create_app(test_config=None):
|
||||||
from . import auth
|
from . import auth
|
||||||
app.register_blueprint(auth.bp)
|
app.register_blueprint(auth.bp)
|
||||||
|
|
||||||
|
# Register custom filters
|
||||||
|
from . import filters
|
||||||
|
filters.create_filters(app)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
|
||||||
118
src/humulus/filters.py
Normal file
118
src/humulus/filters.py
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
"""This module contains filters used in rendering of Jinja templates."""
|
||||||
|
|
||||||
|
# Copyright 2019 Mike Shoup
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_og(recipe):
|
||||||
|
"""Returns a recipe's Original Gravity"""
|
||||||
|
if 'fermentables' not in recipe:
|
||||||
|
return '0.000'
|
||||||
|
points = 0
|
||||||
|
grain_points = 0
|
||||||
|
# Loop through fermentables, adding up points
|
||||||
|
for fermentable in recipe['fermentables']:
|
||||||
|
if fermentable['type'] == 'Grain':
|
||||||
|
grain_points += (
|
||||||
|
float(fermentable['amount']) * float(fermentable['ppg'])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
points += (
|
||||||
|
float(fermentable['amount']) * float(fermentable['ppg'])
|
||||||
|
)
|
||||||
|
points += grain_points * float(recipe['efficiency']) / 100
|
||||||
|
return '{:.3f}'.format(
|
||||||
|
round(1 + points / (1000 * float(recipe['volume'])), 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_fg(recipe):
|
||||||
|
"""Returns a recipe's final gravity"""
|
||||||
|
if 'yeast' not in recipe or 'fermentables' not in recipe:
|
||||||
|
return '0.000'
|
||||||
|
og = float(recipe_og(recipe))
|
||||||
|
og_delta = 0.0
|
||||||
|
# Adjust original gravity by removing nonfermentables (i.e., Lactose)
|
||||||
|
for fermentable in recipe['fermentables']:
|
||||||
|
if fermentable['type'] == 'Non-fermentable':
|
||||||
|
og_delta += (
|
||||||
|
float(fermentable['amount']) * float(fermentable['ppg']) /
|
||||||
|
(1000 * float(recipe['volume']))
|
||||||
|
)
|
||||||
|
attenuation = (
|
||||||
|
(
|
||||||
|
float(recipe['yeast']['low_attenuation']) +
|
||||||
|
float(recipe['yeast']['high_attenuation'])
|
||||||
|
) / 200
|
||||||
|
)
|
||||||
|
return '{:.3f}'.format(
|
||||||
|
round(1 + (og - 1 - og_delta)*(1 - attenuation) + og_delta, 3)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_ibu(recipe):
|
||||||
|
"""Return a recipe's IBU"""
|
||||||
|
if 'hops' not in recipe:
|
||||||
|
return '0'
|
||||||
|
bigness = 1.65 * 0.000125**(float(recipe_og(recipe)) - 1)
|
||||||
|
ibu = 0.0
|
||||||
|
for h in recipe['hops']:
|
||||||
|
mgl = (
|
||||||
|
float(h['alpha']) * float(h['amount']) * 7490.0 /
|
||||||
|
(float(recipe['volume']) * 100.0)
|
||||||
|
)
|
||||||
|
btf = (1 - math.exp(-0.04 * float(h['duration']))) / 4.15
|
||||||
|
ibu += bigness * btf * mgl
|
||||||
|
return '{:.0f}'.format(ibu)
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_ibu_ratio(recipe):
|
||||||
|
"""Return a recipe's IBU ratio"""
|
||||||
|
if 'fermentables' not in recipe or 'hops' not in recipe:
|
||||||
|
return '0'
|
||||||
|
if len(recipe['fermentables']) == 0:
|
||||||
|
return '0' # Otherwise a divide by zero error will occur
|
||||||
|
og = float(recipe_og(recipe))
|
||||||
|
ibu = float(recipe_ibu(recipe))
|
||||||
|
return '{:.2f}'.format(round(0.001 * ibu / (og - 1), 2))
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_abv(recipe):
|
||||||
|
"""Return a recipe's finished ABV"""
|
||||||
|
if 'fermentables' not in recipe or 'yeast' not in recipe:
|
||||||
|
return '0'
|
||||||
|
og = float(recipe_og(recipe))
|
||||||
|
fg = float(recipe_fg(recipe))
|
||||||
|
return '{:.1f}'.format(round((og - fg) * 131.25, 1))
|
||||||
|
|
||||||
|
|
||||||
|
def recipe_srm(recipe):
|
||||||
|
"""Return a recipe's SRM"""
|
||||||
|
if 'fermentables' not in recipe:
|
||||||
|
return '0'
|
||||||
|
mcu = 0
|
||||||
|
for f in recipe['fermentables']:
|
||||||
|
mcu += float(f['amount']) * float(f['color']) / float(recipe['volume'])
|
||||||
|
return '{:.0f}'.format(1.4922 * (mcu**0.6859))
|
||||||
|
|
||||||
|
|
||||||
|
def create_filters(app):
|
||||||
|
app.add_template_filter(recipe_og)
|
||||||
|
app.add_template_filter(recipe_fg)
|
||||||
|
app.add_template_filter(recipe_ibu)
|
||||||
|
app.add_template_filter(recipe_ibu_ratio)
|
||||||
|
app.add_template_filter(recipe_abv)
|
||||||
|
app.add_template_filter(recipe_srm)
|
||||||
|
|
@ -13,6 +13,13 @@
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// unbinds and re-binds the change event
|
||||||
|
function rebindChangeEvents() {
|
||||||
|
$('.ingredient-field').unbind('change');
|
||||||
|
$('.ingredient-field').change(displayAll);
|
||||||
|
}
|
||||||
|
|
||||||
// Correct all the indices for forms matching item.
|
// Correct all the indices for forms matching item.
|
||||||
function adjustIndices(removedIndex, item) {
|
function adjustIndices(removedIndex, item) {
|
||||||
var $forms = $(item);
|
var $forms = $(item);
|
||||||
|
|
@ -51,6 +58,7 @@ function removeForm($remButton, formClass, formsId) {
|
||||||
var $fermsDiv = $(formsId);
|
var $fermsDiv = $(formsId);
|
||||||
$fermsDiv.data('length', $fermsDiv.data('length') - 1);
|
$fermsDiv.data('length', $fermsDiv.data('length') - 1);
|
||||||
adjustIndices(removedIndex, formClass);
|
adjustIndices(removedIndex, formClass);
|
||||||
|
displayAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a fermentable
|
// Remove a fermentable
|
||||||
|
|
@ -75,14 +83,14 @@ function addFerm() {
|
||||||
// Name field
|
// Name field
|
||||||
'<div class="row"><div class="col"><div class="form-group">' +
|
'<div class="row"><div class="col"><div class="form-group">' +
|
||||||
`<label for="fermentables-${fermsLength}-name">Name</label>` +
|
`<label for="fermentables-${fermsLength}-name">Name</label>` +
|
||||||
`<input class="form-control form-control-sm" id="fermentables-${fermsLength}-name"` +
|
`<input class="form-control form-control-sm ingredient-field" id="fermentables-${fermsLength}-name"` +
|
||||||
` name="fermentables-${fermsLength}-name" required type="text" value="">` +
|
` name="fermentables-${fermsLength}-name" required type="text" value="">` +
|
||||||
'</div></div></div>' + // End name field
|
'</div></div></div>' + // End name field
|
||||||
'<div class="row">' +
|
'<div class="row">' +
|
||||||
// Type field
|
// Type field
|
||||||
'<div class="col-sm"><div class="form-group">' +
|
'<div class="col-sm"><div class="form-group">' +
|
||||||
`<label for="fermentables-${fermsLength}-type">Type</label>` +
|
`<label for="fermentables-${fermsLength}-type">Type</label>` +
|
||||||
`<select class="form-control form-control-sm" id="fermentables-${fermsLength}-type"` +
|
`<select class="form-control form-control-sm ingredient-field" id="fermentables-${fermsLength}-type"` +
|
||||||
` name="fermentables-${fermsLength}-type" required>` +
|
` name="fermentables-${fermsLength}-type" required>` +
|
||||||
'<option value="Grain">Grain</option>' +
|
'<option value="Grain">Grain</option>' +
|
||||||
'<option value="LME">LME</option>' +
|
'<option value="LME">LME</option>' +
|
||||||
|
|
@ -94,19 +102,19 @@ function addFerm() {
|
||||||
// Amount field
|
// Amount field
|
||||||
'<div class="col-sm"><div class="form-group">' +
|
'<div class="col-sm"><div class="form-group">' +
|
||||||
`<label for="fermentables-${fermsLength}-amount">Amount (lb)</label>` +
|
`<label for="fermentables-${fermsLength}-amount">Amount (lb)</label>` +
|
||||||
`<input class="form-control form-control-sm" id="fermentables-${fermsLength}-amount"` +
|
`<input class="form-control form-control-sm ingredient-field" id="fermentables-${fermsLength}-amount"` +
|
||||||
` name="fermentables-${fermsLength}-amount" required type="text" value="">` +
|
` name="fermentables-${fermsLength}-amount" required type="text" value="">` +
|
||||||
'</div></div>' + // End amount field
|
'</div></div>' + // End amount field
|
||||||
// PPG field
|
// PPG field
|
||||||
'<div class="col-sm"><div class="form-group">' +
|
'<div class="col-sm"><div class="form-group">' +
|
||||||
`<label for="fermentables-${fermsLength}-ppg">PPG</label>` +
|
`<label for="fermentables-${fermsLength}-ppg">PPG</label>` +
|
||||||
`<input class="form-control form-control-sm" id="fermentables-${fermsLength}-ppg"` +
|
`<input class="form-control form-control-sm ingredient-field" id="fermentables-${fermsLength}-ppg"` +
|
||||||
` name="fermentables-${fermsLength}-ppg" required type="text" value="">` +
|
` name="fermentables-${fermsLength}-ppg" required type="text" value="">` +
|
||||||
'</div></div>' + // End PPG field
|
'</div></div>' + // End PPG field
|
||||||
// Color field
|
// Color field
|
||||||
'<div class="col-sm"><div class="form-group">' +
|
'<div class="col-sm"><div class="form-group">' +
|
||||||
`<label for="fermentables-${fermsLength}-color">Color (°L)</label>` +
|
`<label for="fermentables-${fermsLength}-color">Color (°L)</label>` +
|
||||||
`<input class="form-control form-control-sm" id="fermentables-${fermsLength}-color"` +
|
`<input class="form-control form-control-sm ingredient-field" id="fermentables-${fermsLength}-color"` +
|
||||||
` name="fermentables-${fermsLength}-color" required type="text" value="">` +
|
` name="fermentables-${fermsLength}-color" required type="text" value="">` +
|
||||||
'</div></div>' + // End PPG field
|
'</div></div>' + // End PPG field
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
|
@ -118,10 +126,13 @@ function addFerm() {
|
||||||
'</div>';
|
'</div>';
|
||||||
$fermsDiv.append(newFerm);
|
$fermsDiv.append(newFerm);
|
||||||
$fermsDiv.data('length', fermsLength + 1);
|
$fermsDiv.data('length', fermsLength + 1);
|
||||||
|
// Unbind click events and re-bind them. This is needed to prevent multiple click events
|
||||||
$('.rem-ferm').unbind('click');
|
$('.rem-ferm').unbind('click');
|
||||||
$('.rem-ferm').click(removeFerm);
|
$('.rem-ferm').click(removeFerm);
|
||||||
|
rebindChangeEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a hop
|
||||||
function addHop() {
|
function addHop() {
|
||||||
var $hopsDiv = $('#hops');
|
var $hopsDiv = $('#hops');
|
||||||
var hopsLength = $hopsDiv.data('length');
|
var hopsLength = $hopsDiv.data('length');
|
||||||
|
|
@ -134,14 +145,14 @@ function addHop() {
|
||||||
// Name field
|
// Name field
|
||||||
'<div class="row"><div class="col"><div class="form-group">' +
|
'<div class="row"><div class="col"><div class="form-group">' +
|
||||||
`<label for="hops-${hopsLength}-name">Name</label>` +
|
`<label for="hops-${hopsLength}-name">Name</label>` +
|
||||||
`<input class="form-control form-control-sm" id="hops-${hopsLength}-name"` +
|
`<input class="form-control form-control-sm ingredient-field" id="hops-${hopsLength}-name"` +
|
||||||
` name="hops-${hopsLength}-name" required type="text" value="">` +
|
` name="hops-${hopsLength}-name" required type="text" value="">` +
|
||||||
'</div></div></div>' + // End name field
|
'</div></div></div>' + // End name field
|
||||||
'<div class="row">' +
|
'<div class="row">' +
|
||||||
// Usage field
|
// Usage field
|
||||||
'<div class="col-sm"><div class="form-group">' +
|
'<div class="col-sm"><div class="form-group">' +
|
||||||
`<label for="hops-${hopsLength}-use">Usage</label>` +
|
`<label for="hops-${hopsLength}-use">Usage</label>` +
|
||||||
`<select class="form-control form-control-sm" id="hops-${hopsLength}-use"` +
|
`<select class="form-control form-control-sm ingredient-field" id="hops-${hopsLength}-use"` +
|
||||||
` name="hops-${hopsLength}-use" required>` +
|
` name="hops-${hopsLength}-use" required>` +
|
||||||
'<option value="Boil">Boil</option>' +
|
'<option value="Boil">Boil</option>' +
|
||||||
'<option value="FWH">FWH</option>' +
|
'<option value="FWH">FWH</option>' +
|
||||||
|
|
@ -151,19 +162,19 @@ function addHop() {
|
||||||
// Alpha acid % field
|
// Alpha acid % field
|
||||||
'<div class="col-sm"><div class="form-group">' +
|
'<div class="col-sm"><div class="form-group">' +
|
||||||
`<label for="hops-${hopsLength}-alpha">Alpha Acid %</label>` +
|
`<label for="hops-${hopsLength}-alpha">Alpha Acid %</label>` +
|
||||||
`<input class="form-control form-control-sm" id="hops-${hopsLength}-alpha"` +
|
`<input class="form-control form-control-sm ingredient-field" id="hops-${hopsLength}-alpha"` +
|
||||||
` name="hops-${hopsLength}-alpha" required type="text" value="">` +
|
` name="hops-${hopsLength}-alpha" required type="text" value="">` +
|
||||||
'</div></div>' + // End alpha acid % field
|
'</div></div>' + // End alpha acid % field
|
||||||
// Duration field
|
// Duration field
|
||||||
'<div class="col-sm"><div class="form-group">' +
|
'<div class="col-sm"><div class="form-group">' +
|
||||||
`<label for="hops-${hopsLength}-duration">Duration (min/day)</label>` +
|
`<label for="hops-${hopsLength}-duration">Duration (min/day)</label>` +
|
||||||
`<input class="form-control form-control-sm" id="hops-${hopsLength}-duration"` +
|
`<input class="form-control form-control-sm ingredient-field" id="hops-${hopsLength}-duration"` +
|
||||||
` name="hops-${hopsLength}-duration" required type="text" value="">` +
|
` name="hops-${hopsLength}-duration" required type="text" value="">` +
|
||||||
'</div></div>' + // End duration field
|
'</div></div>' + // End duration field
|
||||||
// Amount field
|
// Amount field
|
||||||
'<div class="col-sm"><div class="form-group">' +
|
'<div class="col-sm"><div class="form-group">' +
|
||||||
`<label for="hops-${hopsLength}-amount">Amount (oz)</label>` +
|
`<label for="hops-${hopsLength}-amount">Amount (oz)</label>` +
|
||||||
`<input class="form-control form-control-sm" id="hops-${hopsLength}-amount"` +
|
`<input class="form-control form-control-sm ingredient-field" id="hops-${hopsLength}-amount"` +
|
||||||
` name="hops-${hopsLength}-amount" required type="text" value="">` +
|
` name="hops-${hopsLength}-amount" required type="text" value="">` +
|
||||||
'</div></div>' + // End amount field
|
'</div></div>' + // End amount field
|
||||||
'</div>' +
|
'</div>' +
|
||||||
|
|
@ -176,13 +187,167 @@ function addHop() {
|
||||||
|
|
||||||
$hopsDiv.append(newHop);
|
$hopsDiv.append(newHop);
|
||||||
$hopsDiv.data('length', hopsLength + 1);
|
$hopsDiv.data('length', hopsLength + 1);
|
||||||
|
// Unbind click events and re-bind them. This is needed to prevent multiple click events
|
||||||
$('.rem-hop').unbind('click');
|
$('.rem-hop').unbind('click');
|
||||||
$('.rem-hop').click(removeHop);
|
$('.rem-hop').click(removeHop);
|
||||||
|
rebindChangeEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate recipe's original gravity
|
||||||
|
function calculateOG() {
|
||||||
|
var fermsLength = $('#ferms').data('length');
|
||||||
|
if (fermsLength == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var points = 0;
|
||||||
|
var grain_points = 0;
|
||||||
|
for (var i = 0; i < fermsLength; i++) {
|
||||||
|
var ppg = parseFloat($(`#fermentables-${i}-ppg`).val()) || 0.0;
|
||||||
|
var amt = parseFloat($(`#fermentables-${i}-amount`).val()) || 0.0;
|
||||||
|
|
||||||
|
// Check if type is grain
|
||||||
|
if ($(`#fermentables-${i}-type`).val() == 'Grain') {
|
||||||
|
grain_points += ppg * amt;
|
||||||
|
} else {
|
||||||
|
points += ppg * amt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add grain_points to points, adjusting for efficiency
|
||||||
|
var efficiency = parseFloat($(`#efficiency`).val()) || 0;
|
||||||
|
var volume = parseFloat($(`#volume`).val()) || 0;
|
||||||
|
points += grain_points * efficiency / 100;
|
||||||
|
return 1 + points / (1000 * volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
// display OG
|
||||||
|
function displayOG() {
|
||||||
|
$('#estimated-og').text(calculateOG().toFixed(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate final gravity
|
||||||
|
function calculateFG() {
|
||||||
|
var fermsLength = $('#ferms').data('length');
|
||||||
|
if (fermsLength == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var og = parseFloat(calculateOG()) || 0.0;
|
||||||
|
var og_delta = 0.0;
|
||||||
|
var volume = parseFloat($(`#volume`).val()) || 0;
|
||||||
|
for (var i = 0; i < fermsLength; i++) {
|
||||||
|
if ($(`#fermentables-${i}-type`).val() == 'Non-fermentable') {
|
||||||
|
var ppg = parseFloat($(`#fermentables-${i}-ppg`).val()) || 0.0;
|
||||||
|
var amt = parseFloat($(`#fermentables-${i}-amount`).val()) || 0.0;
|
||||||
|
og_delta += amt * ppg / (volume * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var low_attenuation = parseFloat($('#yeast-low_attenuation').val()) || 0.0;
|
||||||
|
var high_attenuation = parseFloat($('#yeast-high_attenuation').val()) || 0.0;
|
||||||
|
var attenuation = (low_attenuation + high_attenuation) / 200;
|
||||||
|
return 1 + (og - 1 - og_delta)*(1 - attenuation) + og_delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display FG
|
||||||
|
function displayFG() {
|
||||||
|
$('#estimated-fg').text(calculateFG().toFixed(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate ABV
|
||||||
|
function calculate_abv() {
|
||||||
|
return (calculateOG() - calculateFG()) * 131.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display ABV
|
||||||
|
function displayABV() {
|
||||||
|
var abv = calculate_abv().toFixed(1);
|
||||||
|
$('#estimated-abv').text(`${abv} %/vol.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate IBU
|
||||||
|
function calculateIBU() {
|
||||||
|
var hopsLength = $('#hops').data('length');
|
||||||
|
if (hopsLength == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bigness = 1.65 * Math.pow(0.000125, calculateOG() - 1);
|
||||||
|
var ibu = 0;
|
||||||
|
var volume = parseFloat($(`#volume`).val()) || 0;
|
||||||
|
for (var i = 0; i < hopsLength; i++) {
|
||||||
|
var use = $(`#hops-${i}-use`).val();
|
||||||
|
if (use != 'Boil' && use != 'FWH') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var alpha = parseFloat($(`#hops-${i}-alpha`).val()) || 0.0;
|
||||||
|
var amt = parseFloat($(`#hops-${i}-amount`).val()) || 0.0;
|
||||||
|
var duration = parseFloat($(`#hops-${i}-duration`).val()) || 0.0;
|
||||||
|
var mgl = alpha * amt * 7490 / (volume * 100)
|
||||||
|
var btf = (1 - Math.pow(Math.E, -0.04 * duration)) / 4.15
|
||||||
|
ibu += bigness * btf * mgl;
|
||||||
|
}
|
||||||
|
return ibu;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display IBU
|
||||||
|
function displayIBU() {
|
||||||
|
var ibu = calculateIBU().toFixed();
|
||||||
|
$('#estimated-ibu').text(`${ibu} IBU`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate IBU Ratio
|
||||||
|
function calculateIBURatio() {
|
||||||
|
return 0.001 * calculateIBU() / (calculateOG() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display IBU Ratio
|
||||||
|
function displayIBURatio() {
|
||||||
|
var iburatio = calculateIBURatio().toFixed(3);
|
||||||
|
$('#estimated-iburatio').text(`${iburatio} IBU/OG`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate SRM
|
||||||
|
function calculateSRM() {
|
||||||
|
var fermsLength = $('#ferms').data('length');
|
||||||
|
if (fermsLength == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var mcu = 0;
|
||||||
|
var volume = parseFloat($(`#volume`).val()) || 0;
|
||||||
|
for (var i = 0; i < fermsLength; i++) {
|
||||||
|
var color = parseFloat($(`#fermentables-${i}-color`).val()) || 0.0;
|
||||||
|
var amt = parseFloat($(`#fermentables-${i}-amount`).val()) || 0.0;
|
||||||
|
mcu += amt * color / volume;
|
||||||
|
}
|
||||||
|
return 1.4922 * Math.pow(mcu, 0.6859)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display SRM
|
||||||
|
function displaySRM() {
|
||||||
|
var srm = calculateSRM().toFixed();
|
||||||
|
$('#estimated-srm').text(`${srm} SRM`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display all specifications
|
||||||
|
function displayAll() {
|
||||||
|
displayOG();
|
||||||
|
displayFG();
|
||||||
|
displayABV();
|
||||||
|
displayIBU();
|
||||||
|
displayIBURatio();
|
||||||
|
displaySRM();
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
// Register clicks
|
||||||
$('#add-ferm').click(addFerm);
|
$('#add-ferm').click(addFerm);
|
||||||
$('.rem-ferm').click(removeFerm);
|
$('.rem-ferm').click(removeFerm);
|
||||||
$('#add-hop').click(addHop);
|
$('#add-hop').click(addHop);
|
||||||
$('.rem-hop').click(removeHop);
|
$('.rem-hop').click(removeHop);
|
||||||
|
|
||||||
|
// Register change events
|
||||||
|
rebindChangeEvents();
|
||||||
|
|
||||||
|
// Update specifications
|
||||||
|
displayAll();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,3 +31,7 @@
|
||||||
body {
|
body {
|
||||||
padding-top: 5rem;
|
padding-top: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.above-footer {
|
||||||
|
padding-bottom: 12rem;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</main><!-- /.container -->
|
</main><!-- /.container -->
|
||||||
|
|
||||||
|
<footer class="fixed-bottom">{% block footer %}{% endblock %}</footer>
|
||||||
|
|
||||||
<!-- Bootstrap JS -->
|
<!-- Bootstrap JS -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@
|
||||||
-#}
|
-#}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6">{{ render_field_with_errors(form.name) }}</div>
|
<div class="col-sm-6">{{ render_field_with_errors(form.name) }}</div>
|
||||||
<div class="col-sm-3">{{ render_field_with_errors(form.efficiency) }}</div>
|
<div class="col-sm-3">{{ render_field_with_errors(form.efficiency, 'ingredient-field') }}</div>
|
||||||
<div class="col-sm-3">{{ render_field_with_errors(form.volume) }}</div>
|
<div class="col-sm-3">{{ render_field_with_errors(form.volume, 'ingredient-field') }}</div>
|
||||||
</div>
|
</div>
|
||||||
{#-
|
{#-
|
||||||
Fermentable Ingredients
|
Fermentable Ingredients
|
||||||
|
|
@ -42,10 +42,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">{{ render_field_with_errors(fermentable.form.type, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(fermentable.form.type, 'form-control-sm ingredient-field') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(fermentable.form.amount, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(fermentable.form.amount, 'form-control-sm ingredient-field') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(fermentable.form.ppg, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(fermentable.form.ppg, 'form-control-sm ingredient-field') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(fermentable.form.color, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(fermentable.form.color, 'form-control-sm ingredient-field') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
@ -73,10 +73,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">{{ render_field_with_errors(hop.form.use, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(hop.form.use, 'form-control-sm ingredient-field') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(hop.form.alpha, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(hop.form.alpha, 'form-control-sm ingredient-field') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(hop.form.duration, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(hop.form.duration, 'form-control-sm ingredient-field') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(hop.form.amount, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(hop.form.amount, 'form-control-sm ingredient-field') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
@ -104,8 +104,8 @@
|
||||||
<div class="col-sm-2">{{ render_field_with_errors(form.yeast.form.code, 'form-control-sm') }}</div>
|
<div class="col-sm-2">{{ render_field_with_errors(form.yeast.form.code, 'form-control-sm') }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.low_attenuation, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.low_attenuation, 'form-control-sm ingredient-field') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.high_attenuation, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.high_attenuation, 'form-control-sm ingredient-field') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.flocculation, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.flocculation, 'form-control-sm') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.min_temperature, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.min_temperature, 'form-control-sm') }}</div>
|
||||||
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.max_temperature, 'form-control-sm') }}</div>
|
<div class="col-sm">{{ render_field_with_errors(form.yeast.form.max_temperature, 'form-control-sm') }}</div>
|
||||||
|
|
@ -128,3 +128,32 @@
|
||||||
</form>
|
</form>
|
||||||
<script src="{{ url_for('static', filename='recipes.js') }}"></script>
|
<script src="{{ url_for('static', filename='recipes.js') }}"></script>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro render_footer() %}
|
||||||
|
<div class="bg-info text-white">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<dl>
|
||||||
|
<dt>Original Gravity</dt>
|
||||||
|
<dd id="estimated-og"></dd>
|
||||||
|
<dt>Final Gravity</dt>
|
||||||
|
<dd id="estimated-fg"></dd>
|
||||||
|
<dt>Alcohol</dt>
|
||||||
|
<dd id="estimated-abv"></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<dl>
|
||||||
|
<dt>Bitterness</dt>
|
||||||
|
<dd id="estimated-ibu"></dd>
|
||||||
|
<dt>Bitterness Ratio</dt>
|
||||||
|
<dd id="estimated-iburatio"></dd>
|
||||||
|
<dt>Color</dt>
|
||||||
|
<dd id="estimated-srm"></dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-#}
|
-#}
|
||||||
{% from "recipes/_macros.html" import render_recipe_form %}
|
{% from "recipes/_macros.html" import render_recipe_form, render_footer %}
|
||||||
|
|
||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% block title %}Create Recipe{% endblock %}
|
{% block title %}Create Recipe{% endblock %}
|
||||||
|
|
@ -21,5 +21,13 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="row"><h1>Create a new recipe</h1></div>
|
<div class="row"><h1>Create a new recipe</h1></div>
|
||||||
{{ render_recipe_form(form, url_for('recipes.create'), 'Create recipe') }}
|
{{ render_recipe_form(form, url_for('recipes.create'), 'Create recipe') }}
|
||||||
<a href="{{ url_for('recipes.index')}}" class="mt-2 btn btn-secondary btn-sm">Back to recipe list</a>
|
<div class="row above-footer">
|
||||||
|
<div class="col">
|
||||||
|
<a href="{{ url_for('recipes.index')}}" class="mt-2 btn btn-secondary btn-sm">Back to recipe list</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
{{ render_footer() }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,34 @@
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row"><h3>Estimated Specifications</h3></div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<dl>
|
||||||
|
<dt>Original Gravity</dt>
|
||||||
|
<dd>{{ recipe|recipe_og }}</dd>
|
||||||
|
<dt>Final Gravity</dt>
|
||||||
|
<dd>{{ recipe|recipe_fg }}</dd>
|
||||||
|
<dt>Alcohol</dt>
|
||||||
|
<dd>{{ recipe|recipe_abv }} %/vol.</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<dl>
|
||||||
|
<dt>Bitterness</dt>
|
||||||
|
<dd>{{ recipe|recipe_ibu }} IBU</dd>
|
||||||
|
<dt>Bitterness Ratio</dt>
|
||||||
|
<dd>{{ recipe|recipe_ibu_ratio }} IBU/OG</dd>
|
||||||
|
<dt>Color</dt>
|
||||||
|
<dd>{{ recipe|recipe_srm }} SRM</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{#-
|
{#-
|
||||||
Fermentables
|
Fermentables
|
||||||
-#}
|
-#}
|
||||||
<div class="row border-top"><h2>Fermentables</h2></div>
|
<div class="row border-top"><h2>Fermentables</h2></div>
|
||||||
<div class="row"><div class="col">
|
<div class="row"><div class="table-responsive">
|
||||||
<table class="table table-hover table-sm">
|
<table class="table table-hover table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -75,7 +98,7 @@
|
||||||
Hops
|
Hops
|
||||||
-#}
|
-#}
|
||||||
<div class="row border-top"><h2>Hops</h2></div>
|
<div class="row border-top"><h2>Hops</h2></div>
|
||||||
<div class="row"><div class="col">
|
<div class="row"><div class="table-responsive">
|
||||||
<table class="table table-hover table-sm">
|
<table class="table table-hover table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -102,7 +125,7 @@
|
||||||
-#}
|
-#}
|
||||||
{% if 'yeast' in recipe %}
|
{% if 'yeast' in recipe %}
|
||||||
<div class="row border-top"><h2>Yeast</h2></div></div>
|
<div class="row border-top"><h2>Yeast</h2></div></div>
|
||||||
<div class="row"><div class="col">
|
<div class="row"><div class="table-responsive">
|
||||||
<table class="table table-hover table-sm">
|
<table class="table table-hover table-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-#}
|
-#}
|
||||||
{% from "recipes/_macros.html" import render_recipe_form %}
|
{% from "recipes/_macros.html" import render_recipe_form, render_footer %}
|
||||||
|
|
||||||
{% extends '_base.html' %}
|
{% extends '_base.html' %}
|
||||||
{% block title %}Update | {{ form.name.data }}{% endblock %}
|
{% block title %}Update | {{ form.name.data }}{% endblock %}
|
||||||
|
|
@ -21,5 +21,13 @@
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="row"><h1>{{ form.name.data }}</h1></div>
|
<div class="row"><h1>{{ form.name.data }}</h1></div>
|
||||||
{{ render_recipe_form(form, url_for('recipes.update', id=id, rev=rev), 'Update recipe') }}
|
{{ render_recipe_form(form, url_for('recipes.update', id=id, rev=rev), 'Update recipe') }}
|
||||||
<a href="{{ url_for('recipes.info', id=id)}}" class="mt-2 btn btn-secondary btn-sm">Cancel</a>
|
<div class="row above-footer">
|
||||||
|
<div class="col">
|
||||||
|
<a href="{{ url_for('recipes.info', id=id)}}" class="mt-2 btn btn-secondary btn-sm">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
{{ render_footer() }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -152,3 +152,124 @@ class AuthActions(object):
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def auth(client):
|
def auth(client):
|
||||||
return AuthActions(client)
|
return AuthActions(client)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_recipes():
|
||||||
|
"""These sample recipes are useful for testing filters."""
|
||||||
|
return {
|
||||||
|
'lager': {
|
||||||
|
'efficiency': '72',
|
||||||
|
'fermentables': [
|
||||||
|
{
|
||||||
|
'amount': '9.5',
|
||||||
|
'color': '1.80',
|
||||||
|
'name': 'Pale Malt, 2-row (Rahr) (US)',
|
||||||
|
'ppg': '37.00',
|
||||||
|
'type': 'Grain'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'amount': '1',
|
||||||
|
'color': '0',
|
||||||
|
'name': 'Corn Sugar (Dextrose)',
|
||||||
|
'ppg': '46.00',
|
||||||
|
'type': 'Sugar'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'hops': [
|
||||||
|
{
|
||||||
|
'alpha': '7.0',
|
||||||
|
'amount': '1',
|
||||||
|
'duration': '60',
|
||||||
|
'name': 'Cluster (US)',
|
||||||
|
'use': 'Boil'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'alpha': '2.8',
|
||||||
|
'amount': '1',
|
||||||
|
'duration': '10.00',
|
||||||
|
'name': 'Saaz (CZ)',
|
||||||
|
'use': 'Boil'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'alpha': '2.8',
|
||||||
|
'amount': '1.0',
|
||||||
|
'duration': '5',
|
||||||
|
'name': 'Saaz (CZ)',
|
||||||
|
'use': 'Dry-Hop'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'name': 'Lager',
|
||||||
|
'notes': 'Test simple dry-hopped lager w/ sugar',
|
||||||
|
'volume': '5.50',
|
||||||
|
'yeast': {
|
||||||
|
'abv_tolerance': '15.00',
|
||||||
|
'code': 'WLP940',
|
||||||
|
'flocculation': 'Medium',
|
||||||
|
'high_attenuation': '78.00',
|
||||||
|
'lab': 'White Labs',
|
||||||
|
'low_attenuation': '70.00',
|
||||||
|
'max_temperature': '55.00',
|
||||||
|
'min_temperature': '50.00',
|
||||||
|
'name': 'Mexican Lager',
|
||||||
|
'type': 'Liquid'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'sweetstout': {
|
||||||
|
'efficiency': '72',
|
||||||
|
'fermentables': [
|
||||||
|
{
|
||||||
|
'amount': '2.75',
|
||||||
|
'color': '3',
|
||||||
|
'name': 'Pale Malt, 2-row (UK)',
|
||||||
|
'ppg': '36.00',
|
||||||
|
'type': 'Grain'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'amount': '0.25',
|
||||||
|
'color': '450',
|
||||||
|
'name': 'Chocolate Malt (UK)',
|
||||||
|
'ppg': '34.00',
|
||||||
|
'type': 'Grain'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'amount': '0.5',
|
||||||
|
'color': '0',
|
||||||
|
'name': 'Lactose',
|
||||||
|
'ppg': '35.00',
|
||||||
|
'type': 'Non-fermentable'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'hops': [
|
||||||
|
{
|
||||||
|
'alpha': '5.0',
|
||||||
|
'amount': '0.5',
|
||||||
|
'duration': '60',
|
||||||
|
'name': 'East Kent Goldings (UK)',
|
||||||
|
'use': 'Boil'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'alpha': '5.0',
|
||||||
|
'amount': '0.5',
|
||||||
|
'duration': '30',
|
||||||
|
'name': 'East Kent Goldings (UK)',
|
||||||
|
'use': 'Boil'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'name': 'Sweet Stout',
|
||||||
|
'notes': 'Test stout w/ Lactose',
|
||||||
|
'volume': '2.5',
|
||||||
|
'yeast': {
|
||||||
|
'abv_tolerance': '12.00',
|
||||||
|
'code': '',
|
||||||
|
'flocculation': 'High',
|
||||||
|
'high_attenuation': '77.00',
|
||||||
|
'lab': 'Danstar',
|
||||||
|
'low_attenuation': '73.00',
|
||||||
|
'max_temperature': '70.00',
|
||||||
|
'min_temperature': '57.00',
|
||||||
|
'name': 'Nottingham',
|
||||||
|
'type': 'Dry'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
73
tests/test_filters.py
Normal file
73
tests/test_filters.py
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
# Copyright 2019 Mike Shoup
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from humulus.filters import *
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipe_og(sample_recipes):
|
||||||
|
assert recipe_og(sample_recipes['lager']) == '1.054'
|
||||||
|
assert recipe_og(sample_recipes['sweetstout']) == '1.038'
|
||||||
|
# Remove fermentables, verify 0 is returned
|
||||||
|
sample_recipes['lager'].pop('fermentables')
|
||||||
|
assert recipe_og(sample_recipes['lager']) == '0.000'
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipe_fg(sample_recipes):
|
||||||
|
assert recipe_fg(sample_recipes['lager']) == '1.014'
|
||||||
|
assert recipe_fg(sample_recipes['sweetstout']) == '1.015'
|
||||||
|
# Remove fermentables, verify 0 is returned
|
||||||
|
sample_recipes['lager'].pop('fermentables')
|
||||||
|
assert recipe_fg(sample_recipes['lager']) == '0.000'
|
||||||
|
# Remove yeast, verify 0 is returned
|
||||||
|
sample_recipes['sweetstout'].pop('yeast')
|
||||||
|
assert recipe_fg(sample_recipes['sweetstout']) == '0.000'
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipe_ibu(sample_recipes):
|
||||||
|
assert recipe_ibu(sample_recipes['lager']) == '26'
|
||||||
|
assert recipe_ibu(sample_recipes['sweetstout']) == '34'
|
||||||
|
# Remove hops, verify 0 is returned
|
||||||
|
sample_recipes['lager'].pop('hops')
|
||||||
|
assert recipe_ibu(sample_recipes['lager']) == '0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipe_ibu_ratio(sample_recipes):
|
||||||
|
assert recipe_ibu_ratio(sample_recipes['lager']) == '0.48'
|
||||||
|
assert recipe_ibu_ratio(sample_recipes['sweetstout']) == '0.89'
|
||||||
|
# Remove fermentables, verify 0 is returned
|
||||||
|
sample_recipes['lager'].pop('fermentables')
|
||||||
|
assert recipe_ibu_ratio(sample_recipes['lager']) == '0'
|
||||||
|
# Remove hops, verify 0 is returned
|
||||||
|
sample_recipes['sweetstout'].pop('hops')
|
||||||
|
assert recipe_ibu_ratio(sample_recipes['sweetstout']) == '0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipe_abv(sample_recipes):
|
||||||
|
assert recipe_abv(sample_recipes['lager']) == '5.3'
|
||||||
|
assert recipe_abv(sample_recipes['sweetstout']) == '3.0'
|
||||||
|
# Remove fermentables, verify 0 is returned
|
||||||
|
sample_recipes['lager'].pop('fermentables')
|
||||||
|
assert recipe_abv(sample_recipes['lager']) == '0'
|
||||||
|
# Remove yeast, verify 0 is returned
|
||||||
|
sample_recipes['sweetstout'].pop('yeast')
|
||||||
|
assert recipe_abv(sample_recipes['sweetstout']) == '0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipe_srm(sample_recipes):
|
||||||
|
assert recipe_srm(sample_recipes['lager']) == '3'
|
||||||
|
assert recipe_srm(sample_recipes['sweetstout']) == '21'
|
||||||
|
# Remove fermentables, verify 0 is returned
|
||||||
|
sample_recipes['lager'].pop('fermentables')
|
||||||
|
assert recipe_srm(sample_recipes['lager']) == '0'
|
||||||
Loading…
Add table
Reference in a new issue